eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
1,564 lines (1,469 loc) • 108 kB
JavaScript
/**
* @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. `=" "`, `="/* */"`).
* @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