experimental-script-setup-vars.js 5.8 KB

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