UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

1,564 lines (1,469 loc) 108 kB
/** * @author Toru Nagashima <https://github.com/mysticatea> * @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').ComponentTypeSlot} ComponentTypeSlot * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeSlot} ComponentInferTypeSlot * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownSlot} ComponentUnknownSlot * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentSlot} ComponentSlot * @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, getComponentSlotsFromTypeDefine, isTypeNode } = require('./ts-utils') /** * @type { WeakMap<RuleContext, Token[]> } */ const componentComments = new WeakMap() /** @type { Map<string, RuleModule> | null } */ let coreRuleMap = null /** @type { Map<string, RuleModule> } */ 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<T>[]} 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<ASTNode, import('eslint').Scope.ScopeManager>} */ 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>) => 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 \`<template>\`` : rule.meta.docs.description const meta = { ...rule.meta, docs: { ...rule.meta.docs, description, extensionSource: { url: baseRule.meta.docs.url, name: stylisticRule ? 'ESLint Stylistic' : 'ESLint core' } }, deprecated: undefined, replacedBy: undefined } return { ...rule, meta } } /** * Wrap a given rule to apply it to Vue.js template. * @param {RuleModule} baseRule The rule implementation to wrap. * @param {string} ruleName The name of the wrapped rule. * @param {WrapRuleOptions} [options] The option of this rule. * @returns {RuleModule} The wrapped rule implementation. */ function wrapRuleModule(baseRule, ruleName, options) { let description = baseRule.meta.docs.description if (description) { description += ' in `<template>`' } const { categories, skipDynamicArguments, skipDynamicArgumentsReport, skipBaseHandlers, applyDocument, preprocess, create } = options || {} return { create(context) { const sourceCode = context.getSourceCode() const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore && sourceCode.parserServices.getTemplateBodyTokenStore() // The `context.getSourceCode()` cannot access the tokens of templates. // So override the methods which access to tokens by the `tokenStore`. if (tokenStore) { context = wrapContextToOverrideTokenMethods(context, tokenStore, { applyDocument }) } if (skipDynamicArgumentsReport) { context = wrapContextToOverrideReportMethodToSkipDynamicArgument(context) } /** @type {TemplateListener} */ const handlers = {} if (preprocess) { preprocess(context, { wrapContextToOverrideProperties(override) { context = newProxy(context, override) return context }, defineVisitor(visitor) { compositingVisitors(handlers, visitor) } }) } const baseHandlers = baseRule.create(context) if (!skipBaseHandlers) { compositingVisitors(handlers, baseHandlers) } // Move `Program` handlers to `VElement[parent.type!='VElement']` if (handlers.Program) { handlers[ applyDocument ? 'VDocumentFragment' : "VElement[parent.type!='VElement']" ] = /** @type {any} */ (handlers.Program) delete handlers.Program } if (handlers['Program:exit']) { handlers[ applyDocument ? 'VDocumentFragment:exit' : "VElement[parent.type!='VElement']:exit" ] = /** @type {any} */ (handlers['Program:exit']) delete handlers['Program:exit'] } if (skipDynamicArguments) { let withinDynamicArguments = false for (const name of Object.keys(handlers)) { const original = handlers[name] /** @param {any[]} args */ handlers[name] = (...args) => { if (withinDynamicArguments) return // @ts-expect-error original(...args) } } handlers['VDirectiveKey > VExpressionContainer'] = () => { withinDynamicArguments = true } handlers['VDirectiveKey > VExpressionContainer:exit'] = () => { withinDynamicArguments = false } } if (create) { compositingVisitors(handlers, create(context, { baseHandlers })) } if (applyDocument) { // Apply the handlers to document. return defineDocumentVisitor(context, handlers) } // Apply the handlers to templates. return defineTemplateBodyVisitor(context, handlers) }, meta: Object.assign({}, baseRule.meta, { docs: Object.assign({}, baseRule.meta.docs, { description, category: null, categories, url: `https://eslint.vuejs.org/rules/${ruleName}.html` }) }) } } // ------------------------------------------------------------------------------ // Exports // ------------------------------------------------------------------------------ module.exports = { /** * Register the given visitor to parser services. * If the parser service of `vue-eslint-parser` was not found, * this generates a warning. * * @param {RuleContext} context The rule context to use parser services. * @param {TemplateListener} templateBodyVisitor The visitor to traverse the template body. * @param {RuleListener} [scriptVisitor] The visitor to traverse the script. * @param { { templateBodyTriggerSelector: "Program" | "Program:exit" } } [options] The options. * @returns {RuleListener} The merged visitor. */ defineTemplateBodyVisitor, /** * Register the given visitor to parser services. * If the parser service of `vue-eslint-parser` was not found, * this generates a warning. * * @param {RuleContext} context The rule context to use parser services. * @param {TemplateListener} documentVisitor The visitor to traverse the document. * @param { { triggerSelector: "Program" | "Program:exit" } } [options] The options. * @returns {RuleListener} The merged visitor. */ defineDocumentVisitor, /** * Wrap a given core rule to apply it to Vue.js template. * @type {typeof wrapCoreRule} */ wrapCoreRule, wrapStylisticOrCoreRule, getCoreRule, /** * Checks whether the given value is defined. * @template T * @param {T | null | undefined} v * @returns {v is T} */ isDef, /** * Flattens arrays, objects and iterable objects. * @template T * @param {T | Iterable<T> | null | undefined} v * @returns {T[]} */ flatten, /** * Get the previous sibling element of the given element. * @param {VElement} node The element node to get the previous sibling element. * @returns {VElement|null} The previous sibling element. */ prevSibling(node) { let prevElement = null for (const siblingNode of (node.parent && node.parent.children) || []) { if (siblingNode === node) { return prevElement } if (siblingNode.type === 'VElement') { prevElement = siblingNode } } return null }, /** * Check whether the given directive attribute has their empty value (`=""`). * @param {VDirective} node The directive attribute node to check. * @param {RuleContext} context The rule context to use parser services. * @returns {boolean} `true` if the directive attribute has their empty value (`=""`). */ isEmptyValueDirective(node, context) { if (node.value == null) { return false } if (node.value.expression != null) { return false } let valueText = context.getSourceCode().getText(node.value) if ( (valueText[0] === '"' || valueText[0] === "'") && valueText[0] === valueText[valueText.length - 1] ) { // quoted valueText = valueText.slice(1, -1) } if (!valueText) { // empty return true } return false }, /** * Check whether the given directive attribute has their empty expression value (e.g. `=" "`, `="/* &ast;/"`). * @param {VDirective} node The directive attribute node to check. * @param {RuleContext} context The rule context to use parser services. * @returns {boolean} `true` if the directive attribute has their empty expression value. */ isEmptyExpressionValueDirective(node, context) { if (node.value == null) { return false } if (node.value.expression != null) { return false } const sourceCode = context.getSourceCode() const valueNode = node.value const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore() let quote1 = null let quote2 = null // `node.value` may be only comments, so cannot get the correct tokens with `tokenStore.getTokens(node.value)`. for (const token of tokenStore.getTokens(node)) { if (token.range[1] <= valueNode.range[0]) { continue } if (valueNode.range[1] <= token.range[0]) { // empty return true } if ( !quote1 && token.type === 'Punctuator' && (token.value === '"' || token.value === "'") ) { quote1 = token continue } if ( !quote2 && quote1 && token.type === 'Punctuator' && token.value === quote1.value ) { quote2 = token continue } // not empty return false } // empty return true }, /** * Get the attribute which has the given name. * @param {VElement} node The start tag node to check. * @param {string} name The attribute name to check. * @param {string} [value] The attribute value to check. * @returns {VAttribute | null} The found attribute. */ getAttribute, /** * Check whether the given start tag has specific directive. * @param {VElement} node The start tag node to check. * @param {string} name The attribute name to check. * @param {string} [value] The attribute value to check. * @returns {boolean} `true` if the start tag has the attribute. */ hasAttribute, /** * Get the directive list which has the given name. * @param {VElement | VStartTag} node The start tag node to check. * @param {string} name The directive name to check. * @returns {VDirective[]} The array of `v-slot` directives. */ getDirectives, /** * Get the directive which has the given name. * @param {VElement} node The start tag node to check. * @param {string} name The directive name to check. * @param {string} [argument] The directive argument to check. * @returns {VDirective | null} The found directive. */ getDirective, /** * Check whether the given start tag has specific directive. * @param {VElement} node The start tag node to check. * @param {string} name The directive name to check. * @param {string} [argument] The directive argument to check. * @returns {boolean} `true` if the start tag has the directive. */ hasDirective, isVBindSameNameShorthand, /** * Returns the list of all registered components * @param {ObjectExpression} componentObject * @returns { { node: Property, name: string }[] } Array of ASTNodes */ getRegisteredComponents(componentObject) { const componentsNode = componentObject.properties.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'components'}, value: ObjectExpression })} */ (p) => p.type === 'Property' && getStaticPropertyName(p) === 'components' && p.value.type === 'ObjectExpression' ) if (!componentsNode) { return [] } return componentsNode.value.properties .filter(isProperty) .map((node) => { const name = getStaticPropertyName(node) return name ? { node, name } : null }) .filter(isDef) }, /** * Check whether the previous sibling element has `if` or `else-if` directive. * @param {VElement} node The element node to check. * @returns {boolean} `true` if the previous sibling element has `if` or `else-if` directive. */ prevElementHasIf(node) { const prev = this.prevSibling(node) return ( prev != null && prev.startTag.attributes.some( (a) => a.directive && (a.key.name.name === 'if' || a.key.name.name === 'else-if') ) ) }, /** * Returns a generator with all child element v-if chains of the given element. * @param {VElement} node The element node to check. * @returns {IterableIterator<VElement[]>} */ *iterateChildElementsChains(node) { let vIf = false /** @type {VElement[]} */ let elementChain = [] for (const childNode of node.children) { if (childNode.type === 'VElement') { let connected if (hasDirective(childNode, 'if')) { connected = false vIf = true } else if (hasDirective(childNode, 'else-if')) { connected = vIf vIf = true } else if (hasDirective(childNode, 'else')) { connected = vIf vIf = false } else { connected = false vIf = false } if (connected) { elementChain.push(childNode) } else { if (elementChain.length > 0) { yield elementChain } elementChain = [childNode] } } else if (childNode.type !== 'VText' || childNode.value.trim() !== '') { vIf = false } } if (elementChain.length > 0) { yield elementChain } }, /** * @param {ASTNode} node * @returns {node is Literal | TemplateLiteral} */ isStringLiteral(node) { return ( (node.type === 'Literal' && typeof node.value === 'string') || (node.type === 'TemplateLiteral' && node.expressions.length === 0) ) }, /** * Check whether the given node is a custom component or not. * @param {VElement} node The start tag node to check. * @param {boolean} [ignoreElementNamespaces=false] If `true`, ignore element namespaces. * @returns {boolean} `true` if the node is a custom component. */ isCustomComponent(node, ignoreElementNamespaces = false) { if ( hasAttribute(node, 'is') || hasDirective(node, 'bind', 'is') || hasDirective(node, 'is') ) { return true } const isHtmlName = this.isHtmlWellKnownElementName(node.rawName) const isSvgName = this.isSvgWellKnownElementName(node.rawName) const isMathName = this.isMathWellKnownElementName(node.rawName) if (ignoreElementNamespaces) { return !isHtmlName && !isSvgName && !isMathName } return ( (this.isHtmlElementNode(node) && !isHtmlName) || (this.isSvgElementNode(node) && !isSvgName) || (this.isMathElementNode(node) && !isMathName) ) }, /** * Check whether the given node is a HTML element or not. * @param {VElement} node The node to check. * @returns {boolean} `true` if the node is a HTML element. */ isHtmlElementNode(node) { return node.namespace === NS.HTML }, /** * Check whether the given node is a SVG element or not. * @param {VElement} node The node to check. * @returns {boolean} `true` if the name is a SVG element. */ isSvgElementNode(node) { return node.namespace === NS.SVG }, /** * Check whether the given name is a MathML element or not. * @param {VElement} node The node to check. * @returns {boolean} `true` if the node is a MathML element. */ isMathElementNode(node) { return node.namespace === NS.MathML }, /** * Check whether the given name is an well-known element or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is an well-known element name. */ isHtmlWellKnownElementName(name) { return HTML_ELEMENT_NAMES.has(name) }, /** * Check whether the given name is an well-known SVG element or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is an well-known SVG element name. */ isSvgWellKnownElementName(name) { return SVG_ELEMENT_NAMES.has(name) }, /** * Check whether the given name is a well-known MathML element or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is a well-known MathML element name. */ isMathWellKnownElementName(name) { return MATH_ELEMENT_NAMES.has(name) }, /** * Check whether the given name is a void element name or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is a void element name. */ isHtmlVoidElementName(name) { return VOID_ELEMENT_NAMES.has(name) }, /** * Check whether the given name is Vue builtin component name or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is a builtin component name */ isBuiltInComponentName(name) { return ( VUE3_BUILTIN_COMPONENT_NAMES.has(name) || VUE2_BUILTIN_COMPONENT_NAMES.has(name) ) }, /** * Check whether the given name is Vue builtin element name or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is a builtin Vue element name */ isVueBuiltInElementName(name) { return VUE_BUILTIN_ELEMENT_NAMES.has(name.toLowerCase()) }, /** * Check whether the given name is Vue builtin directive name or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is a builtin Directive name */ isBuiltInDirectiveName(name) { return ( name === 'bind' || name === 'on' || name === 'text' || name === 'html' || name === 'show' || name === 'if' || name === 'else' || name === 'else-if' || name === 'for' || name === 'model' || name === 'slot' || name === 'pre' || name === 'cloak' || name === 'once' || name === 'memo' || name === 'is' ) }, /** * Gets the property name of a given node. * @param {Property|AssignmentProperty|MethodDefinition|MemberExpression} node - The node to get. * @return {string|null} The property name if static. Otherwise, null. */ getStaticPropertyName, /** * Gets the string of a given node. * @param {Literal|TemplateLiteral} node - The node to get. * @return {string|null} The string if static. Otherwise, null. */ getStringLiteralValue, /** * Get all props by looking at all component's properties * @param {ObjectExpression} componentObject Object with component definition * @return {(ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp)[]} Array of component props */ getComponentPropsFromOptions, /** * Get all emits by looking at all component's properties * @param {ObjectExpression} componentObject Object with component definition * @return {(ComponentArrayEmit | ComponentObjectEmit | ComponentUnknownEmit)[]} Array of component emits */ getComponentEmitsFromOptions, /** * Get all computed properties by looking at all component's properties * @param {ObjectExpression} componentObject Object with component definition * @return {ComponentComputedProperty[]} Array of computed properties in format: [{key: String, value: ASTNode}] */ getComputedProperties(componentObject) { const computedPropertiesNode = componentObject.properties.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'computed'}, value: ObjectExpression })} */ (p) => p.type === 'Property' && getStaticPropertyName(p) === 'computed' && p.value.type === 'ObjectExpression' ) if (!computedPropertiesNode) { return [] } return computedPropertiesNode.value.properties .filter(isProperty) .map((cp) => { const key = getStaticPropertyName(cp) /** @type {Expression} */ const propValue = skipTSAsExpression(cp.value) /** @type {BlockStatement | null} */ let value = null if (propValue.type === 'FunctionExpression') { value = propValue.body } else if (propValue.type === 'ObjectExpression') { const get = /** @type {(Property & { value: FunctionExpression }) | null} */ ( findProperty( propValue, 'get', (p) => p.value.type === 'FunctionExpression' ) ) value = get ? get.value.body : null } return { key, value } }) }, /** * Get getter body from computed function * @param {CallExpression} callExpression call of computed function * @return {FunctionExpression | ArrowFunctionExpression | null} getter function */ getGetterBodyFromComputedFunction(callExpression) { if (callExpression.arguments.length <= 0) { return null } const arg = callExpression.arguments[0] if ( arg.type === 'FunctionExpression' || arg.type === 'ArrowFunctionExpression' ) { return arg } if (arg.type === 'ObjectExpression') { const getProperty = /** @type {(Property & { value: FunctionExpression | ArrowFunctionExpression }) | null} */ ( findProperty( arg, 'get', (p) => p.value.type === 'FunctionExpression' || p.value.type === 'ArrowFunctionExpression' ) ) return getProperty ? getProperty.value : null } return null }, isTypeScriptFile, isVueFile, /** * Checks whether the current file is uses `<script setup>` * @param {RuleContext} context The ESLint rule context object. */ isScriptSetup, /** * Gets the element of `<script setup>` * @param {RuleContext} context The ESLint rule context object. * @returns {VElement | null} the element of `<script setup>` */ getScriptSetupElement, /** * Check if current file is a Vue instance or component and call callback * @param {RuleContext} context The ESLint rule context object. * @param { (node: ObjectExpression, type: VueObjectType) => void } cb Callback function */ executeOnVue(context, cb) { return compositingVisitors( this.executeOnVueComponent(context, cb), this.executeOnVueInstance(context, cb) ) }, /** * Define handlers to traverse the Vue Objects. * Some special events are available to visitor. * * - `onVueObjectEnter` ... Event when Vue Object is found. * - `onVueObjectExit` ... Event when Vue Object visit ends. * - `onSetupFunctionEnter` ... Event when setup function found. * - `onRenderFunctionEnter` ... Event when render function found. * * @param {RuleContext} context The ESLint rule context object. * @param {VueVisitor} visitor The visitor to traverse the Vue Objects. */ defineVueVisitor(context, visitor) { /** @type {VueObjectData | null} */ let vueStack = null /** * @param {string} key * @param {ESNode} node */ function callVisitor(key, node) { if (visitor[key] && vueStack) { // @ts-expect-error visitor[key](node, vueStack) } } /** @type {NodeListener} */ const vueVisitor = {} for (const key in visitor) { vueVisitor[key] = (node) => callVisitor(key, node) } /** * @param {ObjectExpression} node */ vueVisitor.ObjectExpression = (node) => { const type = getVueObjectType(context, node) if (type) { vueStack = { node, type, parent: vueStack, get functional() { const functional = node.properties.find( /** * @param {Property | SpreadElement} p * @returns {p is Property} */ (p) => p.type === 'Property' && getStaticPropertyName(p) === 'functional' ) if (!functional) { return false } if ( functional.value.type === 'Literal' && functional.value.value === false ) { return false } return true } } callVisitor('onVueObjectEnter', node) } callVisitor('ObjectExpression', node) } vueVisitor['ObjectExpression:exit'] = (node) => { callVisitor('ObjectExpression:exit', node) if (vueStack && vueStack.node === node) { callVisitor('onVueObjectExit', node) vueStack = vueStack.parent } } if ( visitor.onSetupFunctionEnter || visitor.onSetupFunctionExit || visitor.onRenderFunctionEnter ) { const setups = new Set() /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property } } node */ vueVisitor[ 'Property[value.type=/^(Arrow)?FunctionExpression$/] > :function' ] = (node) => { /** @type {Property} */ const prop = node.parent if (vueStack && prop.parent === vueStack.node && prop.value === node) { const name = getStaticPropertyName(prop) if (name === 'setup') { callVisitor('onSetupFunctionEnter', node) setups.add(node) } else if (name === 'render') { callVisitor('onRenderFunctionEnter', node) } } callVisitor( 'Property[value.type=/^(Arrow)?FunctionExpression$/] > :function', node ) } if (visitor.onSetupFunctionExit) { /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property } } node */ vueVisitor[ 'Property[value.type=/^(Arrow)?FunctionExpression$/] > :function:exit' ] = (node) => { if (setups.has(node)) { callVisitor('onSetupFunctionExit', node) setups.delete(node) } } } } return vueVisitor }, /** * Define handlers to traverse the AST nodes in `<script setup>`. * Some special events are available to visitor. * * - `onDefinePropsEnter` ... Event when defineProps is found. * - `onDefinePropsExit` ... Event when defineProps visit ends. * - `onDefineEmitsEnter` ... Event when defineEmits is found. * - `onDefineEmitsExit` ... Event when defineEmits visit ends. * - `onDefineOptionsEnter` ... Event when defineOptions is found. * - `onDefineOptionsExit` ... Event when defineOptions visit ends. * - `onDefineSlotsEnter` ... Event when defineSlots is found. * - `onDefineSlotsExit` ... Event when defineSlots visit ends. * - `onDefineExposeEnter` ... Event when defineExpose is found. * - `onDefineExposeExit` ... Event when defineExpose visit ends. * - `onDefineModelEnter` ... Event when defineModel is found. * - `onDefineModelExit` ... Event when defineModel visit ends. * * @param {RuleContext} context The ESLint rule context object. * @param {ScriptSetupVisitor} visitor The visitor to traverse the AST nodes. */ defineScriptSetupVisitor(context, visitor) { const scriptSetup = getScriptSetupElement(context) if (scriptSetup == null) { return {} } const scriptSetupRange = scriptSetup.range /** * @param {ESNode} node */ function inScriptSetup(node) { return ( scriptSetupRange[0] <= node.range[0] && node.range[1] <= scriptSetupRange[1] ) } /** * @param {string} key * @param {ESNode} node * @param {any[]} args */ function callVisitor(key, node, ...args) { if (visitor[key] && inScriptSetup(node)) { // @ts-expect-error visitor[key](node, ...args) } } /** @type {NodeListener} */ const scriptSetupVisitor = {} for (const key in visitor) { scriptSetupVisitor[key] = (node) => callVisitor(key, node) } class MacroListener { /** * @param {string} name * @param {string} enterName * @param {string} exitName * @param {(candidateMacro: Expression | null, node: CallExpression) => boolean} isMacroNode * @param {(context: RuleContext, node: CallExpression) => unknown} buildParam */ constructor(name, enterName, exitName, isMacroNode, buildParam) { this.name = name this.enterName = enterName this.exitName = exitName this.isMacroNode = isMacroNode this.buildParam = buildParam this.hasListener = Boolean( visitor[this.enterName] || visitor[this.exitName] ) this.paramsMap = new Map() } } const macroListenerList = [ new MacroListener( 'defineProps', 'onDefinePropsEnter', 'onDefinePropsExit', (candidateMacro, node) => candidateMacro === node || candidateMacro === getWithDefaults(node), getComponentPropsFromDefineProps ), new MacroListener( 'defineEmits', 'onDefineEmitsEnter', 'onDefineEmitsExit', (candidateMacro, node) => candidateMacro === node, getComponentEmitsFromDefineEmits ), new MacroListener( 'defineOptions', 'onDefineOptionsEnter', 'onDefineOptionsExit', (candidateMacro, node) => candidateMacro === node, () => undefined ), new MacroListener( 'defineSlots', 'onDefineSlotsEnter', 'onDefineSlotsExit', (candidateMacro, node) => candidateMacro === node, getComponentSlotsFromDefineSlots ), new MacroListener( 'defineExpose', 'onDefineExposeEnter', 'onDefineExposeExit', (candidateMacro, node) => candidateMacro === node, () => undefined ), new MacroListener( 'defineModel', 'onDefineModelEnter', 'onDefineModelExit', (candidateMacro, node) => candidateMacro === node, getComponentModelFromDefineModel ) ].filter((m) => m.hasListener) if (macroListenerList.length > 0) { /** @type {Expression | null} */ let candidateMacro = null /** @param {VariableDeclarator|ExpressionStatement} node */ scriptSetupVisitor[ 'Program > VariableDeclaration > VariableDeclarator, Program > ExpressionStatement' ] = (node) => { if (!candidateMacro) { candidateMacro = node.type === 'VariableDeclarator' ? node.init : node.expression } } /** @param {VariableDeclarator|ExpressionStatement} node */ scriptSetupVisitor[ 'Program > VariableDeclaration > VariableDeclarator, Program > ExpressionStatement:exit' ] = (node) => { if ( candidateMacro === (node.type === 'VariableDeclarator' ? node.init : node.expression) ) { candidateMacro = null } } /** * @param {CallExpression} node */ scriptSetupVisitor.CallExpression = (node) => { if ( candidateMacro && inScriptSetup(node) && node.callee.type === 'Identifier' ) { for (const macroListener of macroListenerList) { if ( node.callee.name !== macroListener.name || !macroListener.isMacroNode(candidateMacro, node) ) { continue } const param = macroListener.buildParam(context, node) callVisitor(macroListener.enterName, node, param) macroListener.paramsMap.set(node, param) break } } callVisitor('CallExpression', node) } scriptSetupVisitor['CallExpression:exit'] = (node) => { callVisitor('CallExpression:exit', node) for (const macroListener of macroListenerList) { if (macroListener.paramsMap.has(node)) { callVisitor( macroListener.exitName, node, macroListener.paramsMap.get(node) ) macroListener.paramsMap.delete(node) } } } } return scriptSetupVisitor }, /** * Checks whether given defineProps call node has withDefaults. * @param {CallExpression} node The node of defineProps * @returns {node is CallExpression & { parent: CallExpression }} */ hasWithDefaults, /** * Gets a map of the expressions defined in withDefaults. * @param {CallExpression} node The node of defineProps * @returns { { [key: string]: Expression | undefined } } */ getWithDefaultsPropExpressions(node) { const map = getWithDefaultsProps(node) /** @type {Record<string, Expression | undefined>} */ const result = {} for (const key of Object.keys(map)) { const prop = map[key] result[key] = prop && prop.value } return result }, /** * Gets a map of the property nodes defined in withDefaults. * @param {CallExpression} node The node of defineProps * @returns { { [key: string]: Property | undefined } } */ getWithDefaultsProps, /** * Gets the default definition nodes for defineProp * using the props destructure with assignment pattern. * @param {CallExpression} node The node of defineProps * @returns { Record<string, {prop: AssignmentProperty , expression: Expression} | undefined> } */ getDefaultPropExpressionsForPropsDestructure, /** * Checks whether the given defineProps node is using Props Destructu