experimental-script-setup-vars.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /**
  2. * @fileoverview prevent variables defined in `<script setup>` to be marked as undefined
  3. * @author Yosuke Ota
  4. */
  5. 'use strict'
  6. const Module = require('module')
  7. const path = require('path')
  8. const utils = require('../utils')
  9. const AST = require('vue-eslint-parser').AST
  10. const ecmaVersion = 2020
  11. // ------------------------------------------------------------------------------
  12. // Rule Definition
  13. // ------------------------------------------------------------------------------
  14. module.exports = {
  15. meta: {
  16. type: 'problem',
  17. docs: {
  18. description:
  19. 'prevent variables defined in `<script setup>` to be marked as undefined', // eslint-disable-line eslint-plugin/require-meta-docs-description
  20. categories: undefined,
  21. url: 'https://eslint.vuejs.org/rules/experimental-script-setup-vars.html'
  22. },
  23. deprecated: true,
  24. schema: []
  25. },
  26. /**
  27. * @param {RuleContext} context - The rule context.
  28. * @returns {RuleListener} AST event handlers.
  29. */
  30. create(context) {
  31. const documentFragment =
  32. context.parserServices.getDocumentFragment &&
  33. context.parserServices.getDocumentFragment()
  34. if (!documentFragment) {
  35. return {}
  36. }
  37. const sourceCode = context.getSourceCode()
  38. const scriptElement = documentFragment.children
  39. .filter(utils.isVElement)
  40. .find(
  41. (element) =>
  42. element.name === 'script' &&
  43. element.range[0] <= sourceCode.ast.range[0] &&
  44. sourceCode.ast.range[1] <= element.range[1]
  45. )
  46. if (!scriptElement) {
  47. return {}
  48. }
  49. const setupAttr = utils.getAttribute(scriptElement, 'setup')
  50. if (!setupAttr || !setupAttr.value) {
  51. return {}
  52. }
  53. const value = setupAttr.value.value
  54. let eslintScope
  55. try {
  56. eslintScope = getESLintModule('eslint-scope', () =>
  57. // @ts-ignore
  58. require('eslint-scope')
  59. )
  60. } catch (_e) {
  61. context.report({
  62. node: setupAttr,
  63. message: 'Can not be resolved eslint-scope.'
  64. })
  65. return {}
  66. }
  67. let espree
  68. try {
  69. espree = getESLintModule('espree', () =>
  70. // @ts-ignore
  71. require('espree')
  72. )
  73. } catch (_e) {
  74. context.report({
  75. node: setupAttr,
  76. message: 'Can not be resolved espree.'
  77. })
  78. return {}
  79. }
  80. const globalScope = sourceCode.scopeManager.scopes[0]
  81. /** @type {string[]} */
  82. let vars
  83. try {
  84. vars = parseSetup(value, espree, eslintScope)
  85. } catch (_e) {
  86. context.report({
  87. node: setupAttr.value,
  88. message: 'Parsing error.'
  89. })
  90. return {}
  91. }
  92. // Define configured global variables.
  93. for (const id of vars) {
  94. const tempVariable = globalScope.set.get(id)
  95. /** @type {Variable} */
  96. let variable
  97. if (!tempVariable) {
  98. variable = new eslintScope.Variable(id, globalScope)
  99. globalScope.variables.push(variable)
  100. globalScope.set.set(id, variable)
  101. } else {
  102. variable = tempVariable
  103. }
  104. variable.eslintImplicitGlobalSetting = 'readonly'
  105. variable.eslintExplicitGlobal = undefined
  106. variable.eslintExplicitGlobalComments = undefined
  107. variable.writeable = false
  108. }
  109. /*
  110. * "through" contains all references which definitions cannot be found.
  111. * Since we augment the global scope using configuration, we need to update
  112. * references and remove the ones that were added by configuration.
  113. */
  114. globalScope.through = globalScope.through.filter((reference) => {
  115. const name = reference.identifier.name
  116. const variable = globalScope.set.get(name)
  117. if (variable) {
  118. /*
  119. * Links the variable and the reference.
  120. * And this reference is removed from `Scope#through`.
  121. */
  122. reference.resolved = variable
  123. variable.references.push(reference)
  124. return false
  125. }
  126. return true
  127. })
  128. return {}
  129. }
  130. }
  131. /**
  132. * @param {string} code
  133. * @param {any} espree
  134. * @param {any} eslintScope
  135. * @returns {string[]}
  136. */
  137. function parseSetup(code, espree, eslintScope) {
  138. /** @type {Program} */
  139. const ast = espree.parse(`(${code})=>{}`, { ecmaVersion })
  140. const result = eslintScope.analyze(ast, {
  141. ignoreEval: true,
  142. nodejsScope: false,
  143. ecmaVersion,
  144. sourceType: 'script',
  145. fallback: AST.getFallbackKeys
  146. })
  147. const variables = /** @type {Variable[]} */ (
  148. result.globalScope.childScopes[0].variables
  149. )
  150. return variables.map((v) => v.name)
  151. }
  152. const createRequire =
  153. // Added in v12.2.0
  154. Module.createRequire ||
  155. // Added in v10.12.0, but deprecated in v12.2.0.
  156. Module.createRequireFromPath ||
  157. // Polyfill - This is not executed on the tests on node@>=10.
  158. /**
  159. * @param {string} filename
  160. */
  161. function (filename) {
  162. const mod = new Module(filename)
  163. mod.filename = filename
  164. // @ts-ignore
  165. mod.paths = Module._nodeModulePaths(path.dirname(filename))
  166. // @ts-ignore
  167. mod._compile('module.exports = require;', filename)
  168. return mod.exports
  169. }
  170. /** @type { { 'espree'?: any, 'eslint-scope'?: any } } */
  171. const modulesCache = {}
  172. /**
  173. * @param {string} p
  174. */
  175. function isLinterPath(p) {
  176. return (
  177. // ESLint 6 and above
  178. p.includes(`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`) ||
  179. // ESLint 5
  180. p.includes(`eslint${path.sep}lib${path.sep}linter.js`)
  181. )
  182. }
  183. /**
  184. * Load module from the loaded ESLint.
  185. * If the loaded ESLint was not found, just returns `fallback()`.
  186. * @param {'espree' | 'eslint-scope'} name
  187. * @param { () => any } fallback
  188. */
  189. function getESLintModule(name, fallback) {
  190. if (!modulesCache[name]) {
  191. // Lookup the loaded eslint
  192. const linterPath = Object.keys(require.cache).find(isLinterPath)
  193. if (linterPath) {
  194. try {
  195. modulesCache[name] = createRequire(linterPath)(name)
  196. } catch (_e) {
  197. // ignore
  198. }
  199. }
  200. if (!modulesCache[name]) {
  201. modulesCache[name] = fallback()
  202. }
  203. }
  204. return modulesCache[name]
  205. }