You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3472 lines
101 KiB
3472 lines
101 KiB
1 month ago
|
/**
|
||
|
* @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').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<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.
|
||
|
* @returns {boolean} `true` if the node is a custom component.
|
||
|
*/
|
||
|
isCustomComponent(node) {
|
||
|
return (
|
||
|
(this.isHtmlElementNode(node) &&
|
||
|
!this.isHtmlWellKnownElementName(node.rawName)) ||
|
||
|
(this.isSvgElementNode(node) &&
|
||
|
!this.isSvgWellKnownElementName(node.rawName)) ||
|
||
|
(this.isMathElementNode(node) &&
|
||
|
!this.isMathWellKnownElementName(node.rawName)) ||
|
||
|
hasAttribute(node, 'is') ||
|
||
|
hasDirective(node, 'bind', 'is') ||
|
||
|
hasDirective(node, 'is')
|
||
|
)
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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,
|
||
|
() => undefined
|
||
|
),
|
||
|
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,
|
||
|
|
||
|
getVueObjectType,
|
||
|
/**
|
||
|
* Get the Vue component definition type from given node
|
||
|
* Vue.component('xxx', {}) || component('xxx', {})
|
||
|
* @param {ObjectExpression} node Node to check
|
||
|
* @returns {'component' | 'mixin' | 'extend' | 'createApp' | 'defineComponent' | null}
|
||
|
*/
|
||
|
getVueComponentDefinitionType,
|
||
|
/**
|
||
|
* Checks whether the given object is an SFC definition.
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
* @param {ObjectExpression} node Node to check
|
||
|
* @returns { boolean } `true`, the given object is an SFC definition.
|
||
|
*/
|
||
|
isSFCObject,
|
||
|
compositingVisitors,
|
||
|
|
||
|
/**
|
||
|
* Check if current file is a Vue instance (new Vue) and call callback
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
* @param { (node: ObjectExpression, type: VueObjectType) => void } cb Callback function
|
||
|
*/
|
||
|
executeOnVueInstance(context, cb) {
|
||
|
return {
|
||
|
/** @param {ObjectExpression} node */
|
||
|
'ObjectExpression:exit'(node) {
|
||
|
const type = getVueObjectType(context, node)
|
||
|
if (!type || type !== 'instance') return
|
||
|
cb(node, type)
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Check if current file is a Vue component and call callback
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
* @param { (node: ObjectExpression, type: VueObjectType) => void } cb Callback function
|
||
|
*/
|
||
|
executeOnVueComponent(context, cb) {
|
||
|
return {
|
||
|
/** @param {ObjectExpression} node */
|
||
|
'ObjectExpression:exit'(node) {
|
||
|
const type = getVueObjectType(context, node)
|
||
|
if (
|
||
|
!type ||
|
||
|
(type !== 'mark' && type !== 'export' && type !== 'definition')
|
||
|
)
|
||
|
return
|
||
|
cb(node, type)
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Check call `Vue.component` and call callback.
|
||
|
* @param {RuleContext} _context The ESLint rule context object.
|
||
|
* @param { (node: CallExpression) => void } cb Callback function
|
||
|
*/
|
||
|
executeOnCallVueComponent(_context, cb) {
|
||
|
return {
|
||
|
/** @param {Identifier & { parent: MemberExpression & { parent: CallExpression } } } node */
|
||
|
"CallExpression > MemberExpression > Identifier[name='component']": (
|
||
|
node
|
||
|
) => {
|
||
|
const callExpr = node.parent.parent
|
||
|
const callee = callExpr.callee
|
||
|
|
||
|
if (callee.type === 'MemberExpression') {
|
||
|
const calleeObject = skipTSAsExpression(callee.object)
|
||
|
|
||
|
if (
|
||
|
calleeObject.type === 'Identifier' &&
|
||
|
// calleeObject.name === 'Vue' && // Any names can be used in Vue.js 3.x. e.g. app.component()
|
||
|
callee.property === node &&
|
||
|
callExpr.arguments.length > 0
|
||
|
) {
|
||
|
cb(callExpr)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Return generator with all properties
|
||
|
* @param {ObjectExpression} node Node to check
|
||
|
* @param {Set<GroupName>} groups Name of parent group
|
||
|
* @returns {IterableIterator<ComponentPropertyData>}
|
||
|
*/
|
||
|
*iterateProperties(node, groups) {
|
||
|
for (const item of node.properties) {
|
||
|
if (item.type !== 'Property') {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
const name = /** @type {GroupName | null} */ (getStaticPropertyName(item))
|
||
|
if (!name || !groups.has(name)) continue
|
||
|
|
||
|
switch (item.value.type) {
|
||
|
case 'ArrayExpression': {
|
||
|
yield* this.iterateArrayExpression(item.value, name)
|
||
|
break
|
||
|
}
|
||
|
case 'ObjectExpression': {
|
||
|
yield* this.iterateObjectExpression(item.value, name)
|
||
|
break
|
||
|
}
|
||
|
case 'FunctionExpression': {
|
||
|
yield* this.iterateFunctionExpression(item.value, name)
|
||
|
break
|
||
|
}
|
||
|
case 'ArrowFunctionExpression': {
|
||
|
yield* this.iterateArrowFunctionExpression(item.value, name)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Return generator with all elements inside ArrayExpression
|
||
|
* @param {ArrayExpression} node Node to check
|
||
|
* @param {GroupName} groupName Name of parent group
|
||
|
* @returns {IterableIterator<ComponentArrayPropertyData>}
|
||
|
*/
|
||
|
*iterateArrayExpression(node, groupName) {
|
||
|
for (const item of node.elements) {
|
||
|
if (
|
||
|
item &&
|
||
|
(item.type === 'Literal' || item.type === 'TemplateLiteral')
|
||
|
) {
|
||
|
const name = getStringLiteralValue(item)
|
||
|
if (name) {
|
||
|
yield { type: 'array', name, groupName, node: item }
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Return generator with all elements inside ObjectExpression
|
||
|
* @param {ObjectExpression} node Node to check
|
||
|
* @param {GroupName} groupName Name of parent group
|
||
|
* @returns {IterableIterator<ComponentObjectPropertyData>}
|
||
|
*/
|
||
|
*iterateObjectExpression(node, groupName) {
|
||
|
/** @type {Set<Property> | undefined} */
|
||
|
let usedGetter
|
||
|
for (const item of node.properties) {
|
||
|
if (item.type === 'Property') {
|
||
|
const key = item.key
|
||
|
if (
|
||
|
key.type === 'Identifier' ||
|
||
|
key.type === 'Literal' ||
|
||
|
key.type === 'TemplateLiteral'
|
||
|
) {
|
||
|
const name = getStaticPropertyName(item)
|
||
|
if (name) {
|
||
|
// find getter pair
|
||
|
if (
|
||
|
item.kind === 'set' &&
|
||
|
node.properties.some((item2) => {
|
||
|
if (item2.type === 'Property' && item2.kind === 'get') {
|
||
|
if (!usedGetter) {
|
||
|
usedGetter = new Set()
|
||
|
}
|
||
|
if (usedGetter.has(item2)) {
|
||
|
return false
|
||
|
}
|
||
|
const getterName = getStaticPropertyName(item2)
|
||
|
if (getterName === name) {
|
||
|
usedGetter.add(item2)
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
})
|
||
|
) {
|
||
|
// has getter pair
|
||
|
continue
|
||
|
}
|
||
|
yield {
|
||
|
type: 'object',
|
||
|
name,
|
||
|
groupName,
|
||
|
node: key,
|
||
|
property: item
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Return generator with all elements inside FunctionExpression
|
||
|
* @param {FunctionExpression} node Node to check
|
||
|
* @param {GroupName} groupName Name of parent group
|
||
|
* @returns {IterableIterator<ComponentObjectPropertyData>}
|
||
|
*/
|
||
|
*iterateFunctionExpression(node, groupName) {
|
||
|
if (node.body.type === 'BlockStatement') {
|
||
|
for (const item of node.body.body) {
|
||
|
if (
|
||
|
item.type === 'ReturnStatement' &&
|
||
|
item.argument &&
|
||
|
item.argument.type === 'ObjectExpression'
|
||
|
) {
|
||
|
yield* this.iterateObjectExpression(item.argument, groupName)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Return generator with all elements inside ArrowFunctionExpression
|
||
|
* @param {ArrowFunctionExpression} node Node to check
|
||
|
* @param {GroupName} groupName Name of parent group
|
||
|
* @returns {IterableIterator<ComponentObjectPropertyData>}
|
||
|
*/
|
||
|
*iterateArrowFunctionExpression(node, groupName) {
|
||
|
const body = node.body
|
||
|
if (body.type === 'BlockStatement') {
|
||
|
for (const item of body.body) {
|
||
|
if (
|
||
|
item.type === 'ReturnStatement' &&
|
||
|
item.argument &&
|
||
|
item.argument.type === 'ObjectExpression'
|
||
|
) {
|
||
|
yield* this.iterateObjectExpression(item.argument, groupName)
|
||
|
}
|
||
|
}
|
||
|
} else if (body.type === 'ObjectExpression') {
|
||
|
yield* this.iterateObjectExpression(body, groupName)
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Find all functions which do not always return values
|
||
|
* @param {boolean} treatUndefinedAsUnspecified
|
||
|
* @param { (node: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration) => void } cb Callback function
|
||
|
* @returns {RuleListener}
|
||
|
*/
|
||
|
executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, cb) {
|
||
|
/**
|
||
|
* @typedef {object} FuncInfo
|
||
|
* @property {FuncInfo | null} funcInfo
|
||
|
* @property {CodePath} codePath
|
||
|
* @property {boolean} hasReturn
|
||
|
* @property {boolean} hasReturnValue
|
||
|
* @property {ArrowFunctionExpression | FunctionExpression | FunctionDeclaration} node
|
||
|
* @property {CodePathSegment[]} currentSegments
|
||
|
*/
|
||
|
|
||
|
/** @type {FuncInfo | null} */
|
||
|
let funcInfo = null
|
||
|
|
||
|
function isValidReturn() {
|
||
|
if (!funcInfo) {
|
||
|
return true
|
||
|
}
|
||
|
if (funcInfo.currentSegments.some((segment) => segment.reachable)) {
|
||
|
return false
|
||
|
}
|
||
|
return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
/**
|
||
|
* @param {CodePath} codePath
|
||
|
* @param {ESNode} node
|
||
|
*/
|
||
|
onCodePathStart(codePath, node) {
|
||
|
if (
|
||
|
node.type === 'ArrowFunctionExpression' ||
|
||
|
node.type === 'FunctionExpression' ||
|
||
|
node.type === 'FunctionDeclaration'
|
||
|
) {
|
||
|
funcInfo = {
|
||
|
codePath,
|
||
|
currentSegments: [],
|
||
|
funcInfo,
|
||
|
hasReturn: false,
|
||
|
hasReturnValue: false,
|
||
|
node
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
onCodePathSegmentStart(segment) {
|
||
|
funcInfo?.currentSegments.push(segment)
|
||
|
},
|
||
|
onCodePathSegmentEnd() {
|
||
|
funcInfo?.currentSegments.pop()
|
||
|
},
|
||
|
onCodePathEnd() {
|
||
|
funcInfo = funcInfo && funcInfo.funcInfo
|
||
|
},
|
||
|
/** @param {ReturnStatement} node */
|
||
|
ReturnStatement(node) {
|
||
|
if (funcInfo) {
|
||
|
funcInfo.hasReturn = true
|
||
|
funcInfo.hasReturnValue = Boolean(node.argument)
|
||
|
}
|
||
|
},
|
||
|
/** @param {ArrowFunctionExpression} node */
|
||
|
'ArrowFunctionExpression:exit'(node) {
|
||
|
if (funcInfo && !isValidReturn() && !node.expression) {
|
||
|
cb(funcInfo.node)
|
||
|
}
|
||
|
},
|
||
|
'FunctionExpression:exit'() {
|
||
|
if (funcInfo && !isValidReturn()) {
|
||
|
cb(funcInfo.node)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Check whether the component is declared in a single line or not.
|
||
|
* @param {ASTNode} node
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
isSingleLine(node) {
|
||
|
return node.loc.start.line === node.loc.end.line
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Check whether the templateBody of the program has invalid EOF or not.
|
||
|
* @param {Program} node The program node to check.
|
||
|
* @returns {boolean} `true` if it has invalid EOF.
|
||
|
*/
|
||
|
hasInvalidEOF(node) {
|
||
|
const body = node.templateBody
|
||
|
if (body == null || body.errors == null) {
|
||
|
return false
|
||
|
}
|
||
|
return body.errors.some(
|
||
|
(error) => typeof error.code === 'string' && error.code.startsWith('eof-')
|
||
|
)
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get the chaining nodes of MemberExpression.
|
||
|
*
|
||
|
* @param {ESNode} node The node to parse
|
||
|
* @return {[ESNode, ...MemberExpression[]]} The chaining nodes
|
||
|
*/
|
||
|
getMemberChaining(node) {
|
||
|
/** @type {MemberExpression[]} */
|
||
|
const nodes = []
|
||
|
let n = skipChainExpression(node)
|
||
|
|
||
|
while (n.type === 'MemberExpression') {
|
||
|
nodes.push(n)
|
||
|
n = skipChainExpression(n.object)
|
||
|
}
|
||
|
|
||
|
return [n, ...nodes.reverse()]
|
||
|
},
|
||
|
/**
|
||
|
* return two string editdistance
|
||
|
* @param {string} a string a to compare
|
||
|
* @param {string} b string b to compare
|
||
|
* @returns {number}
|
||
|
*/
|
||
|
editDistance(a, b) {
|
||
|
if (a === b) {
|
||
|
return 0
|
||
|
}
|
||
|
const alen = a.length
|
||
|
const blen = b.length
|
||
|
const dp = Array.from({ length: alen + 1 }).map((_) =>
|
||
|
Array.from({ length: blen + 1 }).fill(0)
|
||
|
)
|
||
|
for (let i = 0; i <= alen; i++) {
|
||
|
dp[i][0] = i
|
||
|
}
|
||
|
for (let j = 0; j <= blen; j++) {
|
||
|
dp[0][j] = j
|
||
|
}
|
||
|
for (let i = 1; i <= alen; i++) {
|
||
|
for (let j = 1; j <= blen; j++) {
|
||
|
dp[i][j] =
|
||
|
a[i - 1] === b[j - 1]
|
||
|
? dp[i - 1][j - 1]
|
||
|
: Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
|
||
|
}
|
||
|
}
|
||
|
return dp[alen][blen]
|
||
|
},
|
||
|
/**
|
||
|
* Checks whether the target node is within the given range.
|
||
|
* @param { [number, number] } range
|
||
|
* @param {ASTNode | Token} target
|
||
|
*/
|
||
|
inRange(range, target) {
|
||
|
return range[0] <= target.range[0] && target.range[1] <= range[1]
|
||
|
},
|
||
|
/**
|
||
|
* Checks whether the given node is Property.
|
||
|
*/
|
||
|
isProperty,
|
||
|
/**
|
||
|
* Checks whether the given node is AssignmentProperty.
|
||
|
*/
|
||
|
isAssignmentProperty,
|
||
|
/**
|
||
|
* Checks whether the given node is VElement.
|
||
|
*/
|
||
|
isVElement,
|
||
|
/**
|
||
|
* Finds the property with the given name from the given ObjectExpression node.
|
||
|
*/
|
||
|
findProperty,
|
||
|
/**
|
||
|
* Finds the assignment property with the given name from the given ObjectPattern node.
|
||
|
*/
|
||
|
findAssignmentProperty,
|
||
|
/**
|
||
|
* Checks if the given node is a property value.
|
||
|
* @param {Property} prop
|
||
|
* @param {Expression} node
|
||
|
*/
|
||
|
isPropertyChain,
|
||
|
/**
|
||
|
* Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it.
|
||
|
*/
|
||
|
skipTSAsExpression,
|
||
|
/**
|
||
|
* Retrieve `AssignmentPattern#left` value if the given node a `AssignmentPattern` node. Otherwise, pass through it.
|
||
|
*/
|
||
|
skipDefaultParamValue,
|
||
|
/**
|
||
|
* Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
|
||
|
*/
|
||
|
skipChainExpression,
|
||
|
/**
|
||
|
* Checks whether the given node is in a type annotation.
|
||
|
*/
|
||
|
withinTypeNode,
|
||
|
findVariableByIdentifier,
|
||
|
getScope,
|
||
|
/**
|
||
|
* Marks a variable with the given name in the current scope as used. This affects the no-unused-vars rule.
|
||
|
*/
|
||
|
markVariableAsUsed,
|
||
|
/**
|
||
|
* Checks whether the given node is in export default.
|
||
|
* @param {ASTNode} node
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
isInExportDefault,
|
||
|
|
||
|
/**
|
||
|
* Check whether the given node is `this` or variable that stores `this`.
|
||
|
* @param {ESNode} node The node to check
|
||
|
* @param {RuleContext} context The rule context to use parser services.
|
||
|
* @returns {boolean} `true` if the given node is `this`.
|
||
|
*/
|
||
|
isThis(node, context) {
|
||
|
if (node.type === 'ThisExpression') {
|
||
|
return true
|
||
|
}
|
||
|
if (node.type !== 'Identifier') {
|
||
|
return false
|
||
|
}
|
||
|
const parent = node.parent
|
||
|
if (
|
||
|
(parent.type === 'MemberExpression' && parent.property === node) ||
|
||
|
(parent.type === 'Property' && parent.key === node && !parent.computed)
|
||
|
) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
const variable = findVariable(getScope(context, node), node)
|
||
|
|
||
|
if (variable != null && variable.defs.length === 1) {
|
||
|
const def = variable.defs[0]
|
||
|
if (
|
||
|
def.type === 'Variable' &&
|
||
|
def.parent.kind === 'const' &&
|
||
|
def.node.id.type === 'Identifier'
|
||
|
) {
|
||
|
return Boolean(
|
||
|
def.node && def.node.init && def.node.init.type === 'ThisExpression'
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* @param {MemberExpression|Identifier} props
|
||
|
* @returns { { kind: 'assignment' | 'update' | 'call' , node: ESNode, pathNodes: MemberExpression[] } | null }
|
||
|
*/
|
||
|
findMutating(props) {
|
||
|
/** @type {MemberExpression[]} */
|
||
|
const pathNodes = []
|
||
|
/** @type {MemberExpression | Identifier | ChainExpression} */
|
||
|
let node = props
|
||
|
let target = node.parent
|
||
|
while (true) {
|
||
|
switch (target.type) {
|
||
|
case 'AssignmentExpression': {
|
||
|
if (target.left === node) {
|
||
|
// this.xxx <=|+=|-=>
|
||
|
return {
|
||
|
kind: 'assignment',
|
||
|
node: target,
|
||
|
pathNodes
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
case 'UpdateExpression': {
|
||
|
// this.xxx <++|-->
|
||
|
return {
|
||
|
kind: 'update',
|
||
|
node: target,
|
||
|
pathNodes
|
||
|
}
|
||
|
}
|
||
|
case 'UnaryExpression': {
|
||
|
if (target.operator === 'delete') {
|
||
|
return {
|
||
|
kind: 'update',
|
||
|
node: target,
|
||
|
pathNodes
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
case 'CallExpression': {
|
||
|
if (pathNodes.length > 0 && target.callee === node) {
|
||
|
const mem = pathNodes[pathNodes.length - 1]
|
||
|
const callName = getStaticPropertyName(mem)
|
||
|
if (
|
||
|
callName &&
|
||
|
/^(?:push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)$/u.test(
|
||
|
callName
|
||
|
)
|
||
|
) {
|
||
|
// this.xxx.push()
|
||
|
pathNodes.pop()
|
||
|
return {
|
||
|
kind: 'call',
|
||
|
node: target,
|
||
|
pathNodes
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
case 'MemberExpression': {
|
||
|
if (target.object === node) {
|
||
|
pathNodes.push(target)
|
||
|
node = target
|
||
|
target = target.parent
|
||
|
continue // loop
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
case 'ChainExpression': {
|
||
|
node = target
|
||
|
target = target.parent
|
||
|
continue // loop
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Return generator with the all handler nodes defined in the given watcher property.
|
||
|
* @param {Property|Expression} property
|
||
|
* @returns {IterableIterator<Expression>}
|
||
|
*/
|
||
|
iterateWatchHandlerValues,
|
||
|
|
||
|
/**
|
||
|
* Wraps composition API trace map in both 'vue' and '@vue/composition-api' imports, or '#imports' from unimport
|
||
|
* @param {import('@eslint-community/eslint-utils').TYPES.TraceMap} map
|
||
|
*/
|
||
|
createCompositionApiTraceMap: (map) => ({
|
||
|
vue: map,
|
||
|
'@vue/composition-api': map,
|
||
|
'#imports': map
|
||
|
}),
|
||
|
|
||
|
/**
|
||
|
* Iterates all references in the given trace map.
|
||
|
* Take the third argument option to detect auto-imported references.
|
||
|
*
|
||
|
* @param {import('@eslint-community/eslint-utils').ReferenceTracker} tracker
|
||
|
* @param {import('@eslint-community/eslint-utils').TYPES.TraceMap} map
|
||
|
* @returns {ReturnType<import('@eslint-community/eslint-utils').ReferenceTracker['iterateEsmReferences']>}
|
||
|
*/
|
||
|
*iterateReferencesTraceMap(tracker, map) {
|
||
|
const esmTraceMap = this.createCompositionApiTraceMap({
|
||
|
...map,
|
||
|
[ReferenceTracker.ESM]: true
|
||
|
})
|
||
|
|
||
|
for (const ref of tracker.iterateEsmReferences(esmTraceMap)) {
|
||
|
yield ref
|
||
|
}
|
||
|
|
||
|
for (const ref of tracker.iterateGlobalReferences(map)) {
|
||
|
yield ref
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Checks whether or not the tokens of two given nodes are same.
|
||
|
* @param {ASTNode} left A node 1 to compare.
|
||
|
* @param {ASTNode} right A node 2 to compare.
|
||
|
* @param {ParserServices.TokenStore | SourceCode} sourceCode The ESLint source code object.
|
||
|
* @returns {boolean} the source code for the given node.
|
||
|
*/
|
||
|
equalTokens(left, right, sourceCode) {
|
||
|
const tokensL = sourceCode.getTokens(left)
|
||
|
const tokensR = sourceCode.getTokens(right)
|
||
|
|
||
|
if (tokensL.length !== tokensR.length) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return tokensL.every(
|
||
|
(token, i) =>
|
||
|
token.type === tokensR[i].type && token.value === tokensR[i].value
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Standard Helpers
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* Checks whether the given value is defined.
|
||
|
* @template T
|
||
|
* @param {T | null | undefined} v
|
||
|
* @returns {v is T}
|
||
|
*/
|
||
|
function isDef(v) {
|
||
|
return v != null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Flattens arrays, objects and iterable objects.
|
||
|
* @template T
|
||
|
* @param {T | Iterable<T> | null | undefined} v
|
||
|
* @returns {T[]}
|
||
|
*/
|
||
|
function flatten(v) {
|
||
|
/** @type {T[]} */
|
||
|
const result = []
|
||
|
if (v) {
|
||
|
if (isIterable(v)) {
|
||
|
result.push(...v)
|
||
|
} else {
|
||
|
result.push(v)
|
||
|
}
|
||
|
}
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {*} v
|
||
|
* @returns {v is Iterable<any>}
|
||
|
*/
|
||
|
function isIterable(v) {
|
||
|
return v && Symbol.iterator in v
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Nodejs Helpers
|
||
|
// ------------------------------------------------------------------------------
|
||
|
/**
|
||
|
* @param {String} filename
|
||
|
*/
|
||
|
function createRequire(filename) {
|
||
|
const Module = require('module')
|
||
|
const moduleCreateRequire =
|
||
|
// Added in v12.2.0
|
||
|
Module.createRequire ||
|
||
|
// Added in v10.12.0, but deprecated in v12.2.0.
|
||
|
Module.createRequireFromPath ||
|
||
|
// Polyfill - This is not executed on the tests on node@>=10.
|
||
|
/**
|
||
|
* @param {string} filename
|
||
|
*/
|
||
|
function (filename) {
|
||
|
const mod = new Module(filename)
|
||
|
|
||
|
mod.filename = filename
|
||
|
// @ts-ignore
|
||
|
mod.paths = Module._nodeModulePaths(path.dirname(filename))
|
||
|
// @ts-ignore
|
||
|
mod._compile('module.exports = require;', filename)
|
||
|
return mod.exports
|
||
|
}
|
||
|
return moduleCreateRequire(filename)
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Rule Helpers
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function defineTemplateBodyVisitor(
|
||
|
context,
|
||
|
templateBodyVisitor,
|
||
|
scriptVisitor,
|
||
|
options
|
||
|
) {
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
if (sourceCode.parserServices.defineTemplateBodyVisitor == null) {
|
||
|
const filename = context.getFilename()
|
||
|
if (path.extname(filename) === '.vue') {
|
||
|
context.report({
|
||
|
loc: { line: 1, column: 0 },
|
||
|
message:
|
||
|
'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
|
||
|
})
|
||
|
}
|
||
|
return {}
|
||
|
}
|
||
|
return sourceCode.parserServices.defineTemplateBodyVisitor(
|
||
|
templateBodyVisitor,
|
||
|
scriptVisitor,
|
||
|
options
|
||
|
)
|
||
|
}
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function defineDocumentVisitor(context, documentVisitor, options) {
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
if (sourceCode.parserServices.defineDocumentVisitor == null) {
|
||
|
const filename = context.getFilename()
|
||
|
if (path.extname(filename) === '.vue') {
|
||
|
context.report({
|
||
|
loc: { line: 1, column: 0 },
|
||
|
message:
|
||
|
'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
|
||
|
})
|
||
|
}
|
||
|
return {}
|
||
|
}
|
||
|
return sourceCode.parserServices.defineDocumentVisitor(
|
||
|
documentVisitor,
|
||
|
options
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
* @param {T} visitor
|
||
|
* @param {...(TemplateListener | RuleListener | NodeListener)} visitors
|
||
|
* @returns {T}
|
||
|
*/
|
||
|
function compositingVisitors(visitor, ...visitors) {
|
||
|
for (const v of visitors) {
|
||
|
for (const key in v) {
|
||
|
// @ts-expect-error
|
||
|
if (visitor[key]) {
|
||
|
// @ts-expect-error
|
||
|
const o = visitor[key]
|
||
|
// @ts-expect-error
|
||
|
visitor[key] = (...args) => {
|
||
|
o(...args)
|
||
|
// @ts-expect-error
|
||
|
v[key](...args)
|
||
|
}
|
||
|
} else {
|
||
|
// @ts-expect-error
|
||
|
visitor[key] = v[key]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return visitor
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// AST Helpers
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* Find the variable of a given identifier.
|
||
|
* @param {RuleContext} context The rule context
|
||
|
* @param {Identifier} node The variable name to find.
|
||
|
* @returns {Variable|null} The found variable or null.
|
||
|
*/
|
||
|
function findVariableByIdentifier(context, node) {
|
||
|
return findVariable(getScope(context, node), node)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Finds the property with the given name from the given ObjectExpression node.
|
||
|
* @param {ObjectExpression} node
|
||
|
* @param {string} name
|
||
|
* @param { (p: Property) => boolean } [filter]
|
||
|
* @returns { (Property) | null}
|
||
|
*/
|
||
|
function findProperty(node, name, filter) {
|
||
|
const predicate = filter
|
||
|
? /**
|
||
|
* @param {Property | SpreadElement} prop
|
||
|
* @returns {prop is Property}
|
||
|
*/
|
||
|
(prop) =>
|
||
|
isProperty(prop) && getStaticPropertyName(prop) === name && filter(prop)
|
||
|
: /**
|
||
|
* @param {Property | SpreadElement} prop
|
||
|
* @returns {prop is Property}
|
||
|
*/
|
||
|
(prop) => isProperty(prop) && getStaticPropertyName(prop) === name
|
||
|
return node.properties.find(predicate) || null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Finds the assignment property with the given name from the given ObjectPattern node.
|
||
|
* @param {ObjectPattern} node
|
||
|
* @param {string} name
|
||
|
* @param { (p: AssignmentProperty) => boolean } [filter]
|
||
|
* @returns { (AssignmentProperty) | null}
|
||
|
*/
|
||
|
function findAssignmentProperty(node, name, filter) {
|
||
|
const predicate = filter
|
||
|
? /**
|
||
|
* @param {AssignmentProperty | RestElement} prop
|
||
|
* @returns {prop is AssignmentProperty}
|
||
|
*/
|
||
|
(prop) =>
|
||
|
isAssignmentProperty(prop) &&
|
||
|
getStaticPropertyName(prop) === name &&
|
||
|
filter(prop)
|
||
|
: /**
|
||
|
* @param {AssignmentProperty | RestElement} prop
|
||
|
* @returns {prop is AssignmentProperty}
|
||
|
*/
|
||
|
(prop) =>
|
||
|
isAssignmentProperty(prop) && getStaticPropertyName(prop) === name
|
||
|
return node.properties.find(predicate) || null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether the given node is Property.
|
||
|
* @param {Property | SpreadElement | AssignmentProperty} node
|
||
|
* @returns {node is Property}
|
||
|
*/
|
||
|
function isProperty(node) {
|
||
|
if (node.type !== 'Property') {
|
||
|
return false
|
||
|
}
|
||
|
return !node.parent || node.parent.type === 'ObjectExpression'
|
||
|
}
|
||
|
/**
|
||
|
* Checks whether the given node is AssignmentProperty.
|
||
|
* @param {AssignmentProperty | RestElement} node
|
||
|
* @returns {node is AssignmentProperty}
|
||
|
*/
|
||
|
function isAssignmentProperty(node) {
|
||
|
return node.type === 'Property'
|
||
|
}
|
||
|
/**
|
||
|
* Checks whether the given node is VElement.
|
||
|
* @param {VElement | VExpressionContainer | VText} node
|
||
|
* @returns {node is VElement}
|
||
|
*/
|
||
|
function isVElement(node) {
|
||
|
return node.type === 'VElement'
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether the given node is in export default.
|
||
|
* @param {ASTNode} node
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
function isInExportDefault(node) {
|
||
|
/** @type {ASTNode | null} */
|
||
|
let parent = node.parent
|
||
|
while (parent) {
|
||
|
if (parent.type === 'ExportDefaultDeclaration') {
|
||
|
return true
|
||
|
}
|
||
|
parent = parent.parent
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it.
|
||
|
* @template T Node type
|
||
|
* @param {T | TSAsExpression} node The node to address.
|
||
|
* @returns {T} The `TSAsExpression#expression` value if the node is a `TSAsExpression` node. Otherwise, the node.
|
||
|
*/
|
||
|
function skipTSAsExpression(node) {
|
||
|
if (!node) {
|
||
|
return node
|
||
|
}
|
||
|
// @ts-expect-error
|
||
|
if (node.type === 'TSAsExpression') {
|
||
|
// @ts-expect-error
|
||
|
return skipTSAsExpression(node.expression)
|
||
|
}
|
||
|
// @ts-expect-error
|
||
|
return node
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the parent node of the given node. This method returns a value ignoring `X as F`.
|
||
|
* @param {Expression} node
|
||
|
* @returns {ASTNode}
|
||
|
*/
|
||
|
function getParent(node) {
|
||
|
let parent = node.parent
|
||
|
while (parent.type === 'TSAsExpression') {
|
||
|
parent = parent.parent
|
||
|
}
|
||
|
return parent
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks if the given node is a property value.
|
||
|
* @param {Property} prop
|
||
|
* @param {Expression} node
|
||
|
*/
|
||
|
function isPropertyChain(prop, node) {
|
||
|
let value = node
|
||
|
while (value.parent.type === 'TSAsExpression') {
|
||
|
value = value.parent
|
||
|
}
|
||
|
return prop === value.parent && prop.value === value
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieve `AssignmentPattern#left` value if the given node a `AssignmentPattern` node. Otherwise, pass through it.
|
||
|
* @template T Node type
|
||
|
* @param {T | AssignmentPattern} node The node to address.
|
||
|
* @return {T} The `AssignmentPattern#left` value if the node is a `AssignmentPattern` node. Otherwise, the node.
|
||
|
*/
|
||
|
function skipDefaultParamValue(node) {
|
||
|
if (!node) {
|
||
|
return node
|
||
|
}
|
||
|
// @ts-expect-error
|
||
|
if (node.type === 'AssignmentPattern') {
|
||
|
// @ts-expect-error
|
||
|
return skipDefaultParamValue(node.left)
|
||
|
}
|
||
|
// @ts-expect-error
|
||
|
return node
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
|
||
|
* @template T Node type
|
||
|
* @param {T | ChainExpression} node The node to address.
|
||
|
* @returns {T} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node.
|
||
|
*/
|
||
|
function skipChainExpression(node) {
|
||
|
if (!node) {
|
||
|
return node
|
||
|
}
|
||
|
// @ts-expect-error
|
||
|
if (node.type === 'ChainExpression') {
|
||
|
// @ts-expect-error
|
||
|
return skipChainExpression(node.expression)
|
||
|
}
|
||
|
// @ts-expect-error
|
||
|
return node
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether the given node is in a type annotation.
|
||
|
* @param {ESNode} node
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
function withinTypeNode(node) {
|
||
|
/** @type {ASTNode | null} */
|
||
|
let target = node
|
||
|
while (target) {
|
||
|
if (isTypeNode(target)) {
|
||
|
return true
|
||
|
}
|
||
|
target = target.parent
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function getStaticPropertyName(node) {
|
||
|
if (node.type === 'Property' || node.type === 'MethodDefinition') {
|
||
|
if (!node.computed) {
|
||
|
const key = node.key
|
||
|
if (key.type === 'Identifier') {
|
||
|
return key.name
|
||
|
}
|
||
|
}
|
||
|
const key = node.key
|
||
|
// @ts-expect-error
|
||
|
return getStringLiteralValue(key)
|
||
|
} else if (node.type === 'MemberExpression') {
|
||
|
if (!node.computed) {
|
||
|
const property = node.property
|
||
|
if (property.type === 'Identifier') {
|
||
|
return property.name
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
const property = node.property
|
||
|
// @ts-expect-error
|
||
|
return getStringLiteralValue(property)
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the string of a given node.
|
||
|
* @param {Literal|TemplateLiteral} node - The node to get.
|
||
|
* @param {boolean} [stringOnly]
|
||
|
* @return {string|null} The string if static. Otherwise, null.
|
||
|
*/
|
||
|
function getStringLiteralValue(node, stringOnly) {
|
||
|
if (node.type === 'Literal') {
|
||
|
if (node.value == null) {
|
||
|
if (!stringOnly && node.bigint != null) {
|
||
|
return node.bigint
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
if (typeof node.value === 'string') {
|
||
|
return node.value
|
||
|
}
|
||
|
if (!stringOnly) {
|
||
|
return String(node.value)
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
if (
|
||
|
node.type === 'TemplateLiteral' &&
|
||
|
node.expressions.length === 0 &&
|
||
|
node.quasis.length === 1
|
||
|
) {
|
||
|
return node.quasis[0].value.cooked
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
/**
|
||
|
* Gets the VExpressionContainer of a given node.
|
||
|
* @param {ASTNode} node - The node to get.
|
||
|
* @return {VExpressionContainer|null}
|
||
|
*/
|
||
|
function getVExpressionContainer(node) {
|
||
|
/** @type {ASTNode | null} */
|
||
|
let n = node
|
||
|
while (n && n.type !== 'VExpressionContainer') {
|
||
|
n = n.parent
|
||
|
}
|
||
|
return n
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} path
|
||
|
*/
|
||
|
function isTypeScriptFile(path) {
|
||
|
return path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.mts')
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// ESLint Helpers
|
||
|
// ------------------------------------------------------------------------------
|
||
|
/**
|
||
|
* Marks a variable with the given name in the current scope as used. This affects the no-unused-vars rule.
|
||
|
* @param {RuleContext} context
|
||
|
* @param {string} name
|
||
|
* @param {ASTNode} node The node to get the current scope.
|
||
|
*/
|
||
|
function markVariableAsUsed(context, name, node) {
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
if (sourceCode.markVariableAsUsed) {
|
||
|
sourceCode.markVariableAsUsed(name, node)
|
||
|
} else {
|
||
|
// This function does not use the given node, but the currently visited node.
|
||
|
// If we need to determine the scope of a given node, we need to implement it yourself.
|
||
|
context.markVariableAsUsed?.(name)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Vue Helpers
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* @param {string} path
|
||
|
*/
|
||
|
function isVueFile(path) {
|
||
|
return path.endsWith('.vue') || path.endsWith('.jsx')
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether the current file is uses `<script setup>`
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
*/
|
||
|
function isScriptSetup(context) {
|
||
|
return Boolean(getScriptSetupElement(context))
|
||
|
}
|
||
|
/**
|
||
|
* Gets the element of `<script setup>`
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
* @returns {VElement | null} the element of `<script setup>`
|
||
|
*/
|
||
|
function getScriptSetupElement(context) {
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
const df =
|
||
|
sourceCode.parserServices.getDocumentFragment &&
|
||
|
sourceCode.parserServices.getDocumentFragment()
|
||
|
if (!df) {
|
||
|
return null
|
||
|
}
|
||
|
const scripts = df.children.filter(
|
||
|
/** @returns {e is VElement} */
|
||
|
(e) => isVElement(e) && e.name === 'script'
|
||
|
)
|
||
|
if (scripts.length === 2) {
|
||
|
return scripts.find((e) => hasAttribute(e, 'setup')) || null
|
||
|
} else {
|
||
|
const script = scripts[0]
|
||
|
if (script && hasAttribute(script, 'setup')) {
|
||
|
return script
|
||
|
}
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check whether the given node is a Vue component based
|
||
|
* on the filename and default export type
|
||
|
* export default {} in .vue || .jsx
|
||
|
* @param {ESNode} node Node to check
|
||
|
* @param {string} path File name with extension
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
function isVueComponentFile(node, path) {
|
||
|
return (
|
||
|
isVueFile(path) &&
|
||
|
node.type === 'ExportDefaultDeclaration' &&
|
||
|
node.declaration.type === 'ObjectExpression'
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the Vue component definition type from given node
|
||
|
* Vue.component('xxx', {}) || component('xxx', {})
|
||
|
* @param {ObjectExpression} node Node to check
|
||
|
* @returns {'component' | 'mixin' | 'extend' | 'createApp' | 'defineComponent' | null}
|
||
|
*/
|
||
|
function getVueComponentDefinitionType(node) {
|
||
|
const parent = getParent(node)
|
||
|
if (parent.type === 'CallExpression') {
|
||
|
const callee = parent.callee
|
||
|
|
||
|
if (callee.type === 'MemberExpression') {
|
||
|
const calleeObject = skipTSAsExpression(callee.object)
|
||
|
|
||
|
if (calleeObject.type === 'Identifier') {
|
||
|
const propName = getStaticPropertyName(callee)
|
||
|
if (calleeObject.name === 'Vue') {
|
||
|
// for Vue.js 2.x
|
||
|
// Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {})
|
||
|
const maybeFullVueComponentForVue2 =
|
||
|
propName && isObjectArgument(parent)
|
||
|
|
||
|
return maybeFullVueComponentForVue2 &&
|
||
|
(propName === 'component' ||
|
||
|
propName === 'mixin' ||
|
||
|
propName === 'extend')
|
||
|
? propName
|
||
|
: null
|
||
|
}
|
||
|
|
||
|
// for Vue.js 3.x
|
||
|
// app.component('xxx', {}) || app.mixin({})
|
||
|
const maybeFullVueComponent = propName && isObjectArgument(parent)
|
||
|
|
||
|
return maybeFullVueComponent &&
|
||
|
(propName === 'component' || propName === 'mixin')
|
||
|
? propName
|
||
|
: null
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (callee.type === 'Identifier') {
|
||
|
if (callee.name === 'component') {
|
||
|
// for Vue.js 2.x
|
||
|
// component('xxx', {})
|
||
|
const isDestructedVueComponent = isObjectArgument(parent)
|
||
|
return isDestructedVueComponent ? 'component' : null
|
||
|
}
|
||
|
if (callee.name === 'createApp') {
|
||
|
// for Vue.js 3.x
|
||
|
// createApp({})
|
||
|
const isAppVueComponent = isObjectArgument(parent)
|
||
|
return isAppVueComponent ? 'createApp' : null
|
||
|
}
|
||
|
if (callee.name === 'defineComponent') {
|
||
|
// for Vue.js 3.x
|
||
|
// defineComponent({})
|
||
|
const isDestructedVueComponent = isObjectArgument(parent)
|
||
|
return isDestructedVueComponent ? 'defineComponent' : null
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null
|
||
|
}
|
||
|
|
||
|
/** @param {CallExpression} node */
|
||
|
function isObjectArgument(node) {
|
||
|
return (
|
||
|
node.arguments.length > 0 &&
|
||
|
skipTSAsExpression(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check whether given node is new Vue instance
|
||
|
* new Vue({})
|
||
|
* @param {NewExpression} node Node to check
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
function isVueInstance(node) {
|
||
|
const callee = node.callee
|
||
|
return Boolean(
|
||
|
node.type === 'NewExpression' &&
|
||
|
callee.type === 'Identifier' &&
|
||
|
callee.name === 'Vue' &&
|
||
|
node.arguments.length > 0 &&
|
||
|
skipTSAsExpression(node.arguments[0]).type === 'ObjectExpression'
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If the given object is a Vue component or instance, returns the Vue definition type.
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
* @param {ObjectExpression} node Node to check
|
||
|
* @returns { VueObjectType | null } The Vue definition type.
|
||
|
*/
|
||
|
function getVueObjectType(context, node) {
|
||
|
if (node.type !== 'ObjectExpression') {
|
||
|
return null
|
||
|
}
|
||
|
const parent = getParent(node)
|
||
|
switch (parent.type) {
|
||
|
case 'ExportDefaultDeclaration': {
|
||
|
// export default {} in .vue || .jsx
|
||
|
const filePath = context.getFilename()
|
||
|
if (
|
||
|
isVueComponentFile(parent, filePath) &&
|
||
|
skipTSAsExpression(parent.declaration) === node
|
||
|
) {
|
||
|
const scriptSetup = getScriptSetupElement(context)
|
||
|
if (
|
||
|
scriptSetup &&
|
||
|
scriptSetup.range[0] <= parent.range[0] &&
|
||
|
parent.range[1] <= scriptSetup.range[1]
|
||
|
) {
|
||
|
// `export default` in `<script setup>`
|
||
|
return null
|
||
|
}
|
||
|
return 'export'
|
||
|
}
|
||
|
|
||
|
break
|
||
|
}
|
||
|
case 'CallExpression': {
|
||
|
// Vue.component('xxx', {}) || component('xxx', {})
|
||
|
if (
|
||
|
getVueComponentDefinitionType(node) != null &&
|
||
|
skipTSAsExpression(parent.arguments.slice(-1)[0]) === node
|
||
|
) {
|
||
|
return 'definition'
|
||
|
}
|
||
|
|
||
|
break
|
||
|
}
|
||
|
case 'NewExpression': {
|
||
|
// new Vue({})
|
||
|
if (
|
||
|
isVueInstance(parent) &&
|
||
|
skipTSAsExpression(parent.arguments[0]) === node
|
||
|
) {
|
||
|
return 'instance'
|
||
|
}
|
||
|
|
||
|
break
|
||
|
}
|
||
|
// No default
|
||
|
}
|
||
|
if (
|
||
|
getComponentComments(context).some(
|
||
|
(el) => el.loc.end.line === node.loc.start.line - 1
|
||
|
)
|
||
|
) {
|
||
|
return 'mark'
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether the given object is an SFC definition.
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
* @param {ObjectExpression} node Node to check
|
||
|
* @returns { boolean } `true`, the given object is an SFC definition.
|
||
|
*/
|
||
|
function isSFCObject(context, node) {
|
||
|
if (node.type !== 'ObjectExpression') {
|
||
|
return false
|
||
|
}
|
||
|
const filePath = context.getFilename()
|
||
|
const ext = path.extname(filePath)
|
||
|
if (ext !== '.vue' && ext) {
|
||
|
return false
|
||
|
}
|
||
|
return isSFC(node)
|
||
|
|
||
|
/**
|
||
|
* @param {Expression} node
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
function isSFC(node) {
|
||
|
const parent = getParent(node)
|
||
|
switch (parent.type) {
|
||
|
case 'ExportDefaultDeclaration': {
|
||
|
// export default {}
|
||
|
if (skipTSAsExpression(parent.declaration) !== node) {
|
||
|
return false
|
||
|
}
|
||
|
const scriptSetup = getScriptSetupElement(context)
|
||
|
if (
|
||
|
scriptSetup &&
|
||
|
scriptSetup.range[0] <= parent.range[0] &&
|
||
|
parent.range[1] <= scriptSetup.range[1]
|
||
|
) {
|
||
|
// `export default` in `<script setup>`
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
case 'CallExpression': {
|
||
|
if (parent.arguments.every((arg) => skipTSAsExpression(arg) !== node)) {
|
||
|
return false
|
||
|
}
|
||
|
const { callee } = parent
|
||
|
if (
|
||
|
(callee.type === 'Identifier' && callee.name === 'defineComponent') ||
|
||
|
(callee.type === 'MemberExpression' &&
|
||
|
callee.object.type === 'Identifier' &&
|
||
|
callee.object.name === 'Vue' &&
|
||
|
callee.property.type === 'Identifier' &&
|
||
|
callee.property.name === 'extend')
|
||
|
) {
|
||
|
return isSFC(parent)
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
case 'VariableDeclarator': {
|
||
|
if (
|
||
|
skipTSAsExpression(parent.init) !== node ||
|
||
|
parent.id.type !== 'Identifier'
|
||
|
) {
|
||
|
return false
|
||
|
}
|
||
|
const variable = findVariable(getScope(context, node), parent.id)
|
||
|
if (!variable) {
|
||
|
return false
|
||
|
}
|
||
|
return variable.references.some((ref) => isSFC(ref.identifier))
|
||
|
}
|
||
|
// No default
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the component comments of a given context.
|
||
|
* @param {RuleContext} context The ESLint rule context object.
|
||
|
* @return {Token[]} The the component comments.
|
||
|
*/
|
||
|
function getComponentComments(context) {
|
||
|
let tokens = componentComments.get(context)
|
||
|
if (tokens) {
|
||
|
return tokens
|
||
|
}
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
tokens = sourceCode
|
||
|
.getAllComments()
|
||
|
.filter((comment) => /@vue\/component/g.test(comment.value))
|
||
|
componentComments.set(context, tokens)
|
||
|
return tokens
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return generator with the all handler nodes defined in the given watcher property.
|
||
|
* @param {Property|Expression} property
|
||
|
* @returns {IterableIterator<Expression>}
|
||
|
*/
|
||
|
function* iterateWatchHandlerValues(property) {
|
||
|
const value = property.type === 'Property' ? property.value : property
|
||
|
if (value.type === 'ObjectExpression') {
|
||
|
const handler = findProperty(value, 'handler')
|
||
|
if (handler) {
|
||
|
yield handler.value
|
||
|
}
|
||
|
} else if (value.type === 'ArrayExpression') {
|
||
|
for (const element of value.elements.filter(isDef)) {
|
||
|
if (element.type !== 'SpreadElement') {
|
||
|
yield* iterateWatchHandlerValues(element)
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
yield value
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function getAttribute(node, name, value) {
|
||
|
return (
|
||
|
node.startTag.attributes.find(
|
||
|
/**
|
||
|
* @param {VAttribute | VDirective} node
|
||
|
* @returns {node is VAttribute}
|
||
|
*/
|
||
|
(node) =>
|
||
|
!node.directive &&
|
||
|
node.key.name === name &&
|
||
|
(value === undefined ||
|
||
|
(node.value != null && node.value.value === value))
|
||
|
) || null
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function getDirectives(node, name) {
|
||
|
const attributes =
|
||
|
node.type === 'VElement' ? node.startTag.attributes : node.attributes
|
||
|
return attributes.filter(
|
||
|
/**
|
||
|
* @param {VAttribute | VDirective} node
|
||
|
* @returns {node is VDirective}
|
||
|
*/
|
||
|
(node) => node.directive && node.key.name.name === name
|
||
|
)
|
||
|
}
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function getDirective(node, name, argument) {
|
||
|
return (
|
||
|
node.startTag.attributes.find(
|
||
|
/**
|
||
|
* @param {VAttribute | VDirective} node
|
||
|
* @returns {node is VDirective}
|
||
|
*/
|
||
|
(node) =>
|
||
|
node.directive &&
|
||
|
node.key.name.name === name &&
|
||
|
(argument === undefined ||
|
||
|
(node.key.argument &&
|
||
|
node.key.argument.type === 'VIdentifier' &&
|
||
|
node.key.argument.name) === argument)
|
||
|
) || null
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function hasAttribute(node, name, value) {
|
||
|
return Boolean(getAttribute(node, name, value))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function hasDirective(node, name, argument) {
|
||
|
return Boolean(getDirective(node, name, argument))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check whether the given directive node is v-bind same-name shorthand.
|
||
|
* @param {VAttribute | VDirective} node The directive node to check.
|
||
|
* @returns {node is VDirective & { value: VExpressionContainer & { expression: Identifier } }} `true` if the directive node is v-bind same-name shorthand.
|
||
|
*/
|
||
|
function isVBindSameNameShorthand(node) {
|
||
|
return (
|
||
|
node.directive &&
|
||
|
node.key.name.name === 'bind' &&
|
||
|
node.value?.expression?.type === 'Identifier' &&
|
||
|
node.key.range[0] <= node.value.range[0] &&
|
||
|
node.value.range[1] <= node.key.range[1]
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether given defineProps call node has withDefaults.
|
||
|
* @param {CallExpression} node The node of defineProps
|
||
|
* @returns {node is CallExpression & { parent: CallExpression }}
|
||
|
*/
|
||
|
function hasWithDefaults(node) {
|
||
|
return (
|
||
|
node.parent &&
|
||
|
node.parent.type === 'CallExpression' &&
|
||
|
node.parent.arguments[0] === node &&
|
||
|
node.parent.callee.type === 'Identifier' &&
|
||
|
node.parent.callee.name === 'withDefaults'
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the withDefaults call node from given defineProps call node.
|
||
|
* @param {CallExpression} node The node of defineProps
|
||
|
* @returns {CallExpression | null}
|
||
|
*/
|
||
|
function getWithDefaults(node) {
|
||
|
return hasWithDefaults(node) ? node.parent : null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a map of the property nodes defined in withDefaults.
|
||
|
* @param {CallExpression} node The node of defineProps
|
||
|
* @returns { { [key: string]: Property | undefined } }
|
||
|
*/
|
||
|
function getWithDefaultsProps(node) {
|
||
|
if (!hasWithDefaults(node)) {
|
||
|
return {}
|
||
|
}
|
||
|
const param = node.parent.arguments[1]
|
||
|
if (!param || param.type !== 'ObjectExpression') {
|
||
|
return {}
|
||
|
}
|
||
|
|
||
|
/** @type {Record<string, Property>} */
|
||
|
const result = {}
|
||
|
|
||
|
for (const prop of param.properties) {
|
||
|
if (prop.type !== 'Property') {
|
||
|
continue
|
||
|
}
|
||
|
const name = getStaticPropertyName(prop)
|
||
|
if (name != null) {
|
||
|
result[name] = prop
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all props from component options object.
|
||
|
* @param {ObjectExpression} componentObject Object with component definition
|
||
|
* @return {(ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp)[]} Array of component props
|
||
|
*/
|
||
|
function getComponentPropsFromOptions(componentObject) {
|
||
|
const propsNode = componentObject.properties.find(
|
||
|
/**
|
||
|
* @param {ESNode} p
|
||
|
* @returns {p is (Property & { key: Identifier & {name: 'props'} })}
|
||
|
*/
|
||
|
(p) => p.type === 'Property' && getStaticPropertyName(p) === 'props'
|
||
|
)
|
||
|
|
||
|
if (!propsNode) {
|
||
|
return []
|
||
|
}
|
||
|
if (
|
||
|
propsNode.value.type !== 'ObjectExpression' &&
|
||
|
propsNode.value.type !== 'ArrayExpression'
|
||
|
) {
|
||
|
return [
|
||
|
{
|
||
|
type: 'unknown',
|
||
|
propName: null,
|
||
|
node: propsNode.value
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
|
||
|
return getComponentPropsFromDefine(propsNode.value)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all emits from component options object.
|
||
|
* @param {ObjectExpression} componentObject Object with component definition
|
||
|
* @return {(ComponentArrayEmit | ComponentObjectEmit | ComponentUnknownEmit)[]} Array of component emits
|
||
|
*/
|
||
|
function getComponentEmitsFromOptions(componentObject) {
|
||
|
const emitsNode = componentObject.properties.find(
|
||
|
/**
|
||
|
* @param {ESNode} p
|
||
|
* @returns {p is (Property & { key: Identifier & {name: 'emits'} })}
|
||
|
*/
|
||
|
(p) => p.type === 'Property' && getStaticPropertyName(p) === 'emits'
|
||
|
)
|
||
|
|
||
|
if (!emitsNode) {
|
||
|
return []
|
||
|
}
|
||
|
if (
|
||
|
emitsNode.value.type !== 'ObjectExpression' &&
|
||
|
emitsNode.value.type !== 'ArrayExpression'
|
||
|
) {
|
||
|
return [
|
||
|
{
|
||
|
type: 'unknown',
|
||
|
emitName: null,
|
||
|
node: emitsNode.value
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
|
||
|
return getComponentEmitsFromDefine(emitsNode.value)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all props from `defineProps` call expression.
|
||
|
* @param {RuleContext} context The rule context object.
|
||
|
* @param {CallExpression} node `defineProps` call expression
|
||
|
* @return {ComponentProp[]} Array of component props
|
||
|
*/
|
||
|
function getComponentPropsFromDefineProps(context, node) {
|
||
|
if (node.arguments.length > 0) {
|
||
|
const defNode = getObjectOrArray(context, node.arguments[0])
|
||
|
if (defNode) {
|
||
|
return getComponentPropsFromDefine(defNode)
|
||
|
}
|
||
|
return [
|
||
|
{
|
||
|
type: 'unknown',
|
||
|
propName: null,
|
||
|
node: node.arguments[0]
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
const typeArguments =
|
||
|
'typeArguments' in node ? node.typeArguments : node.typeParameters
|
||
|
if (typeArguments && typeArguments.params.length > 0) {
|
||
|
return getComponentPropsFromTypeDefine(context, typeArguments.params[0])
|
||
|
}
|
||
|
return [
|
||
|
{
|
||
|
type: 'unknown',
|
||
|
propName: null,
|
||
|
node: null
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all emits from `defineEmits` call expression.
|
||
|
* @param {RuleContext} context The rule context object.
|
||
|
* @param {CallExpression} node `defineEmits` call expression
|
||
|
* @return {ComponentEmit[]} Array of component emits
|
||
|
*/
|
||
|
function getComponentEmitsFromDefineEmits(context, node) {
|
||
|
if (node.arguments.length > 0) {
|
||
|
const defNode = getObjectOrArray(context, node.arguments[0])
|
||
|
if (defNode) {
|
||
|
return getComponentEmitsFromDefine(defNode)
|
||
|
}
|
||
|
return [
|
||
|
{
|
||
|
type: 'unknown',
|
||
|
emitName: null,
|
||
|
node: node.arguments[0]
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
const typeArguments =
|
||
|
'typeArguments' in node ? node.typeArguments : node.typeParameters
|
||
|
if (typeArguments && typeArguments.params.length > 0) {
|
||
|
return getComponentEmitsFromTypeDefine(context, typeArguments.params[0])
|
||
|
}
|
||
|
return [
|
||
|
{
|
||
|
type: 'unknown',
|
||
|
emitName: null,
|
||
|
node: null
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
/**
|
||
|
* Get model info from `defineModel` call expression.
|
||
|
* @param {RuleContext} _context The rule context object.
|
||
|
* @param {CallExpression} node `defineModel` call expression
|
||
|
* @return {ComponentModel} Object of component model
|
||
|
*/
|
||
|
function getComponentModelFromDefineModel(_context, node) {
|
||
|
/** @type {ComponentModelName} */
|
||
|
let name = {
|
||
|
modelName: 'modelValue',
|
||
|
node: null
|
||
|
}
|
||
|
/** @type {Expression|null} */
|
||
|
let options =
|
||
|
node.arguments[0]?.type === 'SpreadElement' ? null : node.arguments[0]
|
||
|
if (node.arguments.length > 0) {
|
||
|
const nameNodeCandidate = skipTSAsExpression(node.arguments[0])
|
||
|
if (nameNodeCandidate.type === 'Literal') {
|
||
|
name = {
|
||
|
modelName: String(nameNodeCandidate.value),
|
||
|
node: nameNodeCandidate
|
||
|
}
|
||
|
options =
|
||
|
node.arguments[1]?.type === 'SpreadElement' ? null : node.arguments[1]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const typeArguments =
|
||
|
'typeArguments' in node ? node.typeArguments : node.typeParameters
|
||
|
if (typeArguments && typeArguments.params.length > 0) {
|
||
|
return {
|
||
|
name,
|
||
|
options,
|
||
|
typeNode: typeArguments.params[0]
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
name,
|
||
|
options,
|
||
|
typeNode: null
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Get all props by looking at all component's properties
|
||
|
* @param {ObjectExpression|ArrayExpression} propsNode Object with props definition
|
||
|
* @return {(ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp)[]} Array of component props
|
||
|
*/
|
||
|
function getComponentPropsFromDefine(propsNode) {
|
||
|
if (propsNode.type === 'ObjectExpression') {
|
||
|
return propsNode.properties.map(
|
||
|
/** @returns {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp} */
|
||
|
(prop) => {
|
||
|
if (!isProperty(prop)) {
|
||
|
return {
|
||
|
type: 'unknown',
|
||
|
propName: null,
|
||
|
node: prop
|
||
|
}
|
||
|
}
|
||
|
const propName = getStaticPropertyName(prop)
|
||
|
if (propName != null) {
|
||
|
return {
|
||
|
type: 'object',
|
||
|
key: prop.key,
|
||
|
propName,
|
||
|
value: skipTSAsExpression(prop.value),
|
||
|
node: prop
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
type: 'object',
|
||
|
key: null,
|
||
|
propName: null,
|
||
|
value: skipTSAsExpression(prop.value),
|
||
|
node: prop
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return propsNode.elements.filter(isDef).map((prop) => {
|
||
|
if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') {
|
||
|
const propName = getStringLiteralValue(prop)
|
||
|
if (propName != null) {
|
||
|
return {
|
||
|
type: 'array',
|
||
|
key: prop,
|
||
|
propName,
|
||
|
value: null,
|
||
|
node: prop
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
type: 'array',
|
||
|
key: null,
|
||
|
propName: null,
|
||
|
value: null,
|
||
|
node: prop
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all emits by looking at all component's properties
|
||
|
* @param {ObjectExpression|ArrayExpression} emitsNode Object with emits definition
|
||
|
* @return {(ComponentArrayEmit | ComponentObjectEmit | ComponentUnknownEmit)[]} Array of component emits.
|
||
|
*/
|
||
|
function getComponentEmitsFromDefine(emitsNode) {
|
||
|
if (emitsNode.type === 'ObjectExpression') {
|
||
|
return emitsNode.properties.map((prop) => {
|
||
|
if (!isProperty(prop)) {
|
||
|
return {
|
||
|
type: 'unknown',
|
||
|
key: null,
|
||
|
emitName: null,
|
||
|
value: null,
|
||
|
node: prop
|
||
|
}
|
||
|
}
|
||
|
const emitName = getStaticPropertyName(prop)
|
||
|
if (emitName != null) {
|
||
|
return {
|
||
|
type: 'object',
|
||
|
key: prop.key,
|
||
|
emitName,
|
||
|
value: skipTSAsExpression(prop.value),
|
||
|
node: prop
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
type: 'object',
|
||
|
key: null,
|
||
|
emitName: null,
|
||
|
value: skipTSAsExpression(prop.value),
|
||
|
node: prop
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return emitsNode.elements.filter(isDef).map((emit) => {
|
||
|
if (emit.type === 'Literal' || emit.type === 'TemplateLiteral') {
|
||
|
const emitName = getStringLiteralValue(emit)
|
||
|
if (emitName != null) {
|
||
|
return {
|
||
|
type: 'array',
|
||
|
key: emit,
|
||
|
emitName,
|
||
|
value: null,
|
||
|
node: emit
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
type: 'array',
|
||
|
key: null,
|
||
|
emitName: null,
|
||
|
value: null,
|
||
|
node: emit
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {RuleContext} context The rule context object.
|
||
|
* @param {ESNode} node
|
||
|
* @returns {ObjectExpression | ArrayExpression | null}
|
||
|
*/
|
||
|
function getObjectOrArray(context, node) {
|
||
|
if (node.type === 'ObjectExpression') {
|
||
|
return node
|
||
|
}
|
||
|
if (node.type === 'ArrayExpression') {
|
||
|
return node
|
||
|
}
|
||
|
if (node.type === 'Identifier') {
|
||
|
const variable = findVariable(getScope(context, node), node)
|
||
|
|
||
|
if (variable != null && variable.defs.length === 1) {
|
||
|
const def = variable.defs[0]
|
||
|
if (
|
||
|
def.type === 'Variable' &&
|
||
|
def.parent.kind === 'const' &&
|
||
|
def.node.id.type === 'Identifier' &&
|
||
|
def.node.init
|
||
|
) {
|
||
|
return getObjectOrArray(context, def.node.init)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return null
|
||
|
}
|