/** * @author Toru Nagashima * @copyright 2017 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ 'use strict' const { getScope } = require('./scope') /** * @typedef {import('eslint').Rule.RuleModule} RuleModule * @typedef {import('estree').Position} Position * @typedef {import('eslint').Rule.CodePath} CodePath * @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment */ /** * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentArrayProp} ComponentArrayProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentObjectProp} ComponentObjectProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeProp} ComponentInferTypeProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownProp} ComponentUnknownProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentProp} ComponentProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentArrayEmit} ComponentArrayEmit * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentObjectEmit} ComponentObjectEmit * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentEmit} ComponentEmit * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModelName} ComponentModelName * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModel} ComponentModel */ /** * @typedef { {key: string | null, value: BlockStatement | null} } ComponentComputedProperty */ /** * @typedef { 'props' | 'asyncData' | 'data' | 'computed' | 'setup' | 'watch' | 'methods' | 'provide' | 'inject' | 'expose' } GroupName * @typedef { { type: 'array', name: string, groupName: GroupName, node: Literal | TemplateLiteral } } ComponentArrayPropertyData * @typedef { { type: 'object', name: string, groupName: GroupName, node: Identifier | Literal | TemplateLiteral, property: Property } } ComponentObjectPropertyData * @typedef { ComponentArrayPropertyData | ComponentObjectPropertyData } ComponentPropertyData */ /** * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').VueObjectType} VueObjectType * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').VueObjectData} VueObjectData * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').VueVisitor} VueVisitor * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ScriptSetupVisitor} ScriptSetupVisitor */ // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json')) const SVG_ELEMENT_NAMES = new Set(require('./svg-elements.json')) const MATH_ELEMENT_NAMES = new Set(require('./math-elements.json')) const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json')) const VUE2_BUILTIN_COMPONENT_NAMES = new Set( require('./vue2-builtin-components') ) const VUE3_BUILTIN_COMPONENT_NAMES = new Set( require('./vue3-builtin-components') ) const VUE_BUILTIN_ELEMENT_NAMES = new Set(require('./vue-builtin-elements')) const path = require('path') const vueEslintParser = require('vue-eslint-parser') const { traverseNodes, getFallbackKeys, NS } = vueEslintParser.AST const { findVariable, ReferenceTracker } = require('@eslint-community/eslint-utils') const { getComponentPropsFromTypeDefine, getComponentEmitsFromTypeDefine, isTypeNode } = require('./ts-utils') /** * @type { WeakMap } */ const componentComments = new WeakMap() /** @type { Map | null } */ let coreRuleMap = null /** @type { Map } */ const stylisticRuleMap = new Map() /** * Get the core rule implementation from the rule name * @param {string} name * @returns {RuleModule | null} */ function getCoreRule(name) { const eslint = require('eslint') try { const map = coreRuleMap || (coreRuleMap = new eslint.Linter().getRules()) return map.get(name) || null } catch { // getRules() is no longer available in flat config. } const { builtinRules } = require('eslint/use-at-your-own-risk') return /** @type {any} */ (builtinRules.get(name) || null) } /** * Get ESLint Stylistic rule implementation from the rule name * @param {string} name * @param {'@stylistic/eslint-plugin' | '@stylistic/eslint-plugin-ts' | '@stylistic/eslint-plugin-js'} [preferModule] * @returns {RuleModule | null} */ function getStylisticRule(name, preferModule) { if (!preferModule) { const cached = stylisticRuleMap.get(name) if (cached) { return cached } } const stylisticPluginNames = [ '@stylistic/eslint-plugin', '@stylistic/eslint-plugin-ts', '@stylistic/eslint-plugin-js' ] if (preferModule) { stylisticPluginNames.unshift(preferModule) } for (const stylisticPluginName of stylisticPluginNames) { try { const plugin = createRequire(`${process.cwd()}/__placeholder__.js`)( stylisticPluginName ) const rule = plugin?.rules?.[name] if (!preferModule) stylisticRuleMap.set(name, rule) return rule } catch { // ignore } } return null } /** * @template {object} T * @param {T} target * @param {Partial[]} propsArray * @returns {T} */ function newProxy(target, ...propsArray) { const result = new Proxy( {}, { get(_object, key) { for (const props of propsArray) { if (key in props) { // @ts-expect-error return props[key] } } // @ts-expect-error return target[key] }, has(_object, key) { return key in target }, ownKeys(_object) { return Reflect.ownKeys(target) }, getPrototypeOf(_object) { return Reflect.getPrototypeOf(target) } } ) return /** @type {T} */ (result) } /** * Wrap the rule context object to override methods which access to tokens (such as getTokenAfter). * @param {RuleContext} context The rule context object. * @param {ParserServices.TokenStore} tokenStore The token store object for template. * @param {Object} options The option of this rule. * @param {boolean} [options.applyDocument] If `true`, apply check to document fragment. * @returns {RuleContext} */ function wrapContextToOverrideTokenMethods(context, tokenStore, options) { const eslintSourceCode = context.getSourceCode() const rootNode = options.applyDocument ? eslintSourceCode.parserServices.getDocumentFragment && eslintSourceCode.parserServices.getDocumentFragment() : eslintSourceCode.ast.templateBody /** @type {Token[] | null} */ let tokensAndComments = null function getTokensAndComments() { if (tokensAndComments) { return tokensAndComments } tokensAndComments = rootNode ? tokenStore.getTokens(rootNode, { includeComments: true }) : [] return tokensAndComments } /** @param {number} index */ function getNodeByRangeIndex(index) { if (!rootNode) { return eslintSourceCode.ast } /** @type {ASTNode} */ let result = eslintSourceCode.ast /** @type {ASTNode[]} */ const skipNodes = [] let breakFlag = false traverseNodes(rootNode, { enterNode(node, parent) { if (breakFlag) { return } if (skipNodes[0] === parent) { skipNodes.unshift(node) return } if (node.range[0] <= index && index < node.range[1]) { result = node } else { skipNodes.unshift(node) } }, leaveNode(node) { if (breakFlag) { return } if (result === node) { breakFlag = true } else if (skipNodes[0] === node) { skipNodes.shift() } } }) return result } const sourceCode = newProxy( eslintSourceCode, { get tokensAndComments() { return getTokensAndComments() }, getNodeByRangeIndex, // @ts-expect-error -- Added in ESLint v8.38.0 getDeclaredVariables }, tokenStore ) /** @type {WeakMap} */ const containerScopes = new WeakMap() /** * @param {ASTNode} node * @returns {import('eslint').Scope.ScopeManager|null} */ function getContainerScope(node) { const exprContainer = getVExpressionContainer(node) if (!exprContainer) { return null } const cache = containerScopes.get(exprContainer) if (cache) { return cache } const programNode = eslintSourceCode.ast const parserOptions = context.languageOptions?.parserOptions ?? context.parserOptions ?? {} const ecmaFeatures = parserOptions.ecmaFeatures || {} const ecmaVersion = context.languageOptions?.ecmaVersion ?? parserOptions.ecmaVersion ?? 2020 const sourceType = programNode.sourceType try { const eslintScope = createRequire(require.resolve('eslint'))( 'eslint-scope' ) const expStmt = newProxy(exprContainer, { // @ts-expect-error type: 'ExpressionStatement' }) const scopeProgram = newProxy(programNode, { // @ts-expect-error body: [expStmt] }) const scope = eslintScope.analyze(scopeProgram, { ignoreEval: true, nodejsScope: false, impliedStrict: ecmaFeatures.impliedStrict, ecmaVersion, sourceType, fallback: getFallbackKeys }) containerScopes.set(exprContainer, scope) return scope } catch (error) { // ignore // console.log(error) } return null } return newProxy(context, { getSourceCode() { return sourceCode }, get sourceCode() { return sourceCode }, getDeclaredVariables }) /** * @param {ESNode} node * @returns {Variable[]} */ function getDeclaredVariables(node) { const scope = getContainerScope(node) return ( scope?.getDeclaredVariables?.(node) ?? context.getDeclaredVariables?.(node) ?? [] ) } } /** * Wrap the rule context object to override report method to skip the dynamic argument. * @param {RuleContext} context The rule context object. * @returns {RuleContext} */ function wrapContextToOverrideReportMethodToSkipDynamicArgument(context) { const sourceCode = context.getSourceCode() const templateBody = sourceCode.ast.templateBody if (!templateBody) { return context } /** @type {Range[]} */ const directiveKeyRanges = [] traverseNodes(templateBody, { enterNode(node, parent) { if ( parent && parent.type === 'VDirectiveKey' && node.type === 'VExpressionContainer' ) { directiveKeyRanges.push(node.range) } }, leaveNode() {} }) return newProxy(context, { report(descriptor, ...args) { let range = null if (descriptor.loc) { const startLoc = descriptor.loc.start || descriptor.loc const endLoc = descriptor.loc.end || startLoc range = [ sourceCode.getIndexFromLoc(startLoc), sourceCode.getIndexFromLoc(endLoc) ] } else if (descriptor.node) { range = descriptor.node.range } if (range) { for (const directiveKeyRange of directiveKeyRanges) { if ( range[0] < directiveKeyRange[1] && directiveKeyRange[0] < range[1] ) { return } } } context.report(descriptor, ...args) } }) } /** * @callback WrapRuleCreate * @param {RuleContext} ruleContext * @param {WrapRuleCreateContext} wrapContext * @returns {TemplateListener} * * @typedef {object} WrapRuleCreateContext * @property {RuleListener} baseHandlers */ /** * @callback WrapRulePreprocess * @param {RuleContext} ruleContext * @param {WrapRulePreprocessContext} wrapContext * @returns {void} * * @typedef {object} WrapRulePreprocessContext * @property { (override: Partial) => RuleContext } wrapContextToOverrideProperties Wrap the rule context object to override * @property { (visitor: TemplateListener) => void } defineVisitor Define template body visitor */ /** * @typedef {object} WrapRuleOptions * @property {string[]} [categories] The categories of this rule. * @property {boolean} [skipDynamicArguments] If `true`, skip validation within dynamic arguments. * @property {boolean} [skipDynamicArgumentsReport] If `true`, skip report within dynamic arguments. * @property {boolean} [applyDocument] If `true`, apply check to document fragment. * @property {boolean} [skipBaseHandlers] If `true`, skip base rule handlers. * @property {WrapRulePreprocess} [preprocess] Preprocess to calling create of base rule. * @property {WrapRuleCreate} [create] If define, extend base rule. */ /** * Wrap a given core rule to apply it to Vue.js template. * @param {string} coreRuleName The name of the core rule implementation to wrap. * @param {WrapRuleOptions} [options] The option of this rule. * @returns {RuleModule} The wrapped rule implementation. */ function wrapCoreRule(coreRuleName, options) { const coreRule = getCoreRule(coreRuleName) if (!coreRule) { return { meta: { type: 'problem', docs: { url: `https://eslint.vuejs.org/rules/${coreRuleName}.html` } }, create(context) { return defineTemplateBodyVisitor(context, { "VElement[name='template'][parent.type='VDocumentFragment']"(node) { context.report({ node, message: `Failed to extend ESLint core rule "${coreRuleName}". You may be able to use this rule by upgrading the version of ESLint. If you cannot upgrade it, turn off this rule.` }) } }) } } } const rule = wrapRuleModule(coreRule, coreRuleName, options) const meta = { ...rule.meta, docs: { ...rule.meta.docs, extensionSource: { url: coreRule.meta.docs.url, name: 'ESLint core' } } } return { ...rule, meta } } /** * @typedef {object} RuleNames * @property {string} core The name of the core rule implementation to wrap. * @property {string} stylistic The name of ESLint Stylistic rule implementation to wrap. * @property {string} vue The name of the wrapped rule */ /** * Wrap a core rule or ESLint Stylistic rule to apply it to Vue.js template. * @param {RuleNames|string} ruleNames The names of the rule implementation to wrap. * @param {WrapRuleOptions} [options] The option of this rule. * @returns {RuleModule} The wrapped rule implementation. */ function wrapStylisticOrCoreRule(ruleNames, options) { const stylisticRuleName = typeof ruleNames === 'string' ? ruleNames : ruleNames.stylistic const coreRuleName = typeof ruleNames === 'string' ? ruleNames : ruleNames.core const vueRuleName = typeof ruleNames === 'string' ? ruleNames : ruleNames.vue const stylisticRule = getStylisticRule(stylisticRuleName) const baseRule = stylisticRule || getCoreRule(coreRuleName) if (!baseRule) { return { meta: { type: 'problem', docs: { url: `https://eslint.vuejs.org/rules/${vueRuleName}.html` } }, create(context) { return defineTemplateBodyVisitor(context, { "VElement[name='template'][parent.type='VDocumentFragment']"(node) { context.report({ node, message: `Failed to extend ESLint Stylistic rule "${stylisticRule}". You may be able to use this rule by installing ESLint Stylistic plugin (https://eslint.style/). If you cannot install it, turn off this rule.` }) } }) } } } const rule = wrapRuleModule(baseRule, vueRuleName, options) const jsRule = getStylisticRule( stylisticRuleName, '@stylistic/eslint-plugin-js' ) const description = stylisticRule ? `${jsRule?.meta.docs.description} in \`