You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
231 lines
5.8 KiB
231 lines
5.8 KiB
3 years ago
|
/**
|
||
|
* @fileoverview prevent variables defined in `<script setup>` to be marked as undefined
|
||
|
* @author Yosuke Ota
|
||
|
*/
|
||
|
'use strict'
|
||
|
|
||
|
const Module = require('module')
|
||
|
const path = require('path')
|
||
|
const utils = require('../utils')
|
||
|
const AST = require('vue-eslint-parser').AST
|
||
|
|
||
|
const ecmaVersion = 2020
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Rule Definition
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
module.exports = {
|
||
|
meta: {
|
||
|
type: 'problem',
|
||
|
docs: {
|
||
|
description:
|
||
|
'prevent variables defined in `<script setup>` to be marked as undefined', // eslint-disable-line eslint-plugin/require-meta-docs-description
|
||
|
categories: undefined,
|
||
|
url: 'https://eslint.vuejs.org/rules/experimental-script-setup-vars.html'
|
||
|
},
|
||
|
deprecated: true,
|
||
|
schema: []
|
||
|
},
|
||
|
/**
|
||
|
* @param {RuleContext} context - The rule context.
|
||
|
* @returns {RuleListener} AST event handlers.
|
||
|
*/
|
||
|
create(context) {
|
||
|
const documentFragment =
|
||
|
context.parserServices.getDocumentFragment &&
|
||
|
context.parserServices.getDocumentFragment()
|
||
|
if (!documentFragment) {
|
||
|
return {}
|
||
|
}
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
const scriptElement = documentFragment.children
|
||
|
.filter(utils.isVElement)
|
||
|
.find(
|
||
|
(element) =>
|
||
|
element.name === 'script' &&
|
||
|
element.range[0] <= sourceCode.ast.range[0] &&
|
||
|
sourceCode.ast.range[1] <= element.range[1]
|
||
|
)
|
||
|
if (!scriptElement) {
|
||
|
return {}
|
||
|
}
|
||
|
const setupAttr = utils.getAttribute(scriptElement, 'setup')
|
||
|
if (!setupAttr || !setupAttr.value) {
|
||
|
return {}
|
||
|
}
|
||
|
const value = setupAttr.value.value
|
||
|
|
||
|
let eslintScope
|
||
|
try {
|
||
|
eslintScope = getESLintModule('eslint-scope', () =>
|
||
|
// @ts-ignore
|
||
|
require('eslint-scope')
|
||
|
)
|
||
|
} catch (_e) {
|
||
|
context.report({
|
||
|
node: setupAttr,
|
||
|
message: 'Can not be resolved eslint-scope.'
|
||
|
})
|
||
|
return {}
|
||
|
}
|
||
|
let espree
|
||
|
try {
|
||
|
espree = getESLintModule('espree', () =>
|
||
|
// @ts-ignore
|
||
|
require('espree')
|
||
|
)
|
||
|
} catch (_e) {
|
||
|
context.report({
|
||
|
node: setupAttr,
|
||
|
message: 'Can not be resolved espree.'
|
||
|
})
|
||
|
return {}
|
||
|
}
|
||
|
|
||
|
const globalScope = sourceCode.scopeManager.scopes[0]
|
||
|
|
||
|
/** @type {string[]} */
|
||
|
let vars
|
||
|
try {
|
||
|
vars = parseSetup(value, espree, eslintScope)
|
||
|
} catch (_e) {
|
||
|
context.report({
|
||
|
node: setupAttr.value,
|
||
|
message: 'Parsing error.'
|
||
|
})
|
||
|
return {}
|
||
|
}
|
||
|
|
||
|
// Define configured global variables.
|
||
|
for (const id of vars) {
|
||
|
const tempVariable = globalScope.set.get(id)
|
||
|
|
||
|
/** @type {Variable} */
|
||
|
let variable
|
||
|
if (!tempVariable) {
|
||
|
variable = new eslintScope.Variable(id, globalScope)
|
||
|
|
||
|
globalScope.variables.push(variable)
|
||
|
globalScope.set.set(id, variable)
|
||
|
} else {
|
||
|
variable = tempVariable
|
||
|
}
|
||
|
|
||
|
variable.eslintImplicitGlobalSetting = 'readonly'
|
||
|
variable.eslintExplicitGlobal = undefined
|
||
|
variable.eslintExplicitGlobalComments = undefined
|
||
|
variable.writeable = false
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* "through" contains all references which definitions cannot be found.
|
||
|
* Since we augment the global scope using configuration, we need to update
|
||
|
* references and remove the ones that were added by configuration.
|
||
|
*/
|
||
|
globalScope.through = globalScope.through.filter((reference) => {
|
||
|
const name = reference.identifier.name
|
||
|
const variable = globalScope.set.get(name)
|
||
|
|
||
|
if (variable) {
|
||
|
/*
|
||
|
* Links the variable and the reference.
|
||
|
* And this reference is removed from `Scope#through`.
|
||
|
*/
|
||
|
reference.resolved = variable
|
||
|
variable.references.push(reference)
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
})
|
||
|
|
||
|
return {}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} code
|
||
|
* @param {any} espree
|
||
|
* @param {any} eslintScope
|
||
|
* @returns {string[]}
|
||
|
*/
|
||
|
function parseSetup(code, espree, eslintScope) {
|
||
|
/** @type {Program} */
|
||
|
const ast = espree.parse(`(${code})=>{}`, { ecmaVersion })
|
||
|
const result = eslintScope.analyze(ast, {
|
||
|
ignoreEval: true,
|
||
|
nodejsScope: false,
|
||
|
ecmaVersion,
|
||
|
sourceType: 'script',
|
||
|
fallback: AST.getFallbackKeys
|
||
|
})
|
||
|
|
||
|
const variables = /** @type {Variable[]} */ (
|
||
|
result.globalScope.childScopes[0].variables
|
||
|
)
|
||
|
|
||
|
return variables.map((v) => v.name)
|
||
|
}
|
||
|
|
||
|
const createRequire =
|
||
|
// Added in v12.2.0
|
||
|
Module.createRequire ||
|
||
|
// Added in v10.12.0, but deprecated in v12.2.0.
|
||
|
Module.createRequireFromPath ||
|
||
|
// Polyfill - This is not executed on the tests on node@>=10.
|
||
|
/**
|
||
|
* @param {string} filename
|
||
|
*/
|
||
|
function (filename) {
|
||
|
const mod = new Module(filename)
|
||
|
|
||
|
mod.filename = filename
|
||
|
// @ts-ignore
|
||
|
mod.paths = Module._nodeModulePaths(path.dirname(filename))
|
||
|
// @ts-ignore
|
||
|
mod._compile('module.exports = require;', filename)
|
||
|
return mod.exports
|
||
|
}
|
||
|
|
||
|
/** @type { { 'espree'?: any, 'eslint-scope'?: any } } */
|
||
|
const modulesCache = {}
|
||
|
|
||
|
/**
|
||
|
* @param {string} p
|
||
|
*/
|
||
|
function isLinterPath(p) {
|
||
|
return (
|
||
|
// ESLint 6 and above
|
||
|
p.includes(`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`) ||
|
||
|
// ESLint 5
|
||
|
p.includes(`eslint${path.sep}lib${path.sep}linter.js`)
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load module from the loaded ESLint.
|
||
|
* If the loaded ESLint was not found, just returns `fallback()`.
|
||
|
* @param {'espree' | 'eslint-scope'} name
|
||
|
* @param { () => any } fallback
|
||
|
*/
|
||
|
function getESLintModule(name, fallback) {
|
||
|
if (!modulesCache[name]) {
|
||
|
// Lookup the loaded eslint
|
||
|
const linterPath = Object.keys(require.cache).find(isLinterPath)
|
||
|
if (linterPath) {
|
||
|
try {
|
||
|
modulesCache[name] = createRequire(linterPath)(name)
|
||
|
} catch (_e) {
|
||
|
// ignore
|
||
|
}
|
||
|
}
|
||
|
if (!modulesCache[name]) {
|
||
|
modulesCache[name] = fallback()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return modulesCache[name]
|
||
|
}
|