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.
3136 lines
91 KiB
3136 lines
91 KiB
/** |
|
* @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' |
|
|
|
/** |
|
* @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').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').ComponentUnknownEmit} ComponentUnknownEmit |
|
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentEmit} ComponentEmit |
|
*/ |
|
/** |
|
* @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 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 path = require('path') |
|
const vueEslintParser = require('vue-eslint-parser') |
|
const { traverseNodes, getFallbackKeys } = vueEslintParser.AST |
|
const { findVariable } = require('eslint-utils') |
|
const { |
|
getComponentPropsFromTypeDefine, |
|
getComponentEmitsFromTypeDefine, |
|
isTypeNode |
|
} = require('./ts-ast-utils') |
|
|
|
/** |
|
* @type { WeakMap<RuleContext, Token[]> } |
|
*/ |
|
const componentComments = new WeakMap() |
|
|
|
/** @type { Map<string, RuleModule> | null } */ |
|
let ruleMap = null |
|
/** |
|
* Get the core rule implementation from the rule name |
|
* @param {string} name |
|
* @returns {RuleModule | null} |
|
*/ |
|
function getCoreRule(name) { |
|
const map = ruleMap || (ruleMap = new (require('eslint').Linter)().getRules()) |
|
return map.get(name) || 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 |
|
? context.parserServices.getDocumentFragment && |
|
context.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 |
|
}, |
|
tokenStore |
|
) |
|
|
|
const containerScopes = new WeakMap() |
|
|
|
/** |
|
* @param {ASTNode} node |
|
*/ |
|
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.parserOptions || {} |
|
const ecmaFeatures = parserOptions.ecmaFeatures || {} |
|
const 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 (e) { |
|
// ignore |
|
// console.log(e) |
|
} |
|
|
|
return null |
|
} |
|
return newProxy(context, { |
|
getSourceCode() { |
|
return sourceCode |
|
}, |
|
getDeclaredVariables(node) { |
|
const scope = getContainerScope(node) |
|
if (scope) { |
|
return scope.getDeclaredVariables(node) |
|
} |
|
|
|
return 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 = [] |
|
const traverseNodes = vueEslintParser.AST.traverseNodes |
|
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) |
|
} |
|
}) |
|
} |
|
|
|
// ------------------------------------------------------------------------------ |
|
// 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, |
|
|
|
/** |
|
* @callback WrapCoreRuleCreate |
|
* @param {RuleContext} ruleContext |
|
* @param {WrapCoreRuleCreateContext} wrapContext |
|
* @returns {TemplateListener} |
|
* |
|
* @typedef {object} WrapCoreRuleCreateContext |
|
* @property {RuleListener} coreHandlers |
|
*/ |
|
/** |
|
* @callback WrapCoreRulePreprocess |
|
* @param {RuleContext} ruleContext |
|
* @param {WrapCoreRulePreprocessContext} wrapContext |
|
* @returns {void} |
|
* |
|
* @typedef {object} WrapCoreRulePreprocessContext |
|
* @property { (override: Partial<RuleContext>) => RuleContext } wrapContextToOverrideProperties Wrap the rule context object to override |
|
* @property { (visitor: TemplateListener) => void } defineVisitor Define template body visitor |
|
*/ |
|
/** |
|
* 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 {Object} [options] The option of this rule. |
|
* @param {string[]} [options.categories] The categories of this rule. |
|
* @param {boolean} [options.skipDynamicArguments] If `true`, skip validation within dynamic arguments. |
|
* @param {boolean} [options.skipDynamicArgumentsReport] If `true`, skip report within dynamic arguments. |
|
* @param {boolean} [options.applyDocument] If `true`, apply check to document fragment. |
|
* @param {WrapCoreRulePreprocess} [options.preprocess] Preprocess to calling create of core rule. |
|
* @param {WrapCoreRuleCreate} [options.create] If define, extend core rule. |
|
* @returns {RuleModule} The wrapped rule implementation. |
|
*/ |
|
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.` |
|
}) |
|
} |
|
}) |
|
} |
|
} |
|
} |
|
|
|
let description = coreRule.meta.docs.description |
|
if (description) { |
|
description += ' in `<template>`' |
|
} |
|
|
|
const { |
|
categories, |
|
skipDynamicArguments, |
|
skipDynamicArgumentsReport, |
|
applyDocument, |
|
preprocess, |
|
create |
|
} = options || {} |
|
return { |
|
create(context) { |
|
const tokenStore = |
|
context.parserServices.getTemplateBodyTokenStore && |
|
context.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 coreHandlers = coreRule.create(context) |
|
compositingVisitors(handlers, coreHandlers) |
|
|
|
// 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, { coreHandlers })) |
|
} |
|
|
|
if (applyDocument) { |
|
// Apply the handlers to document. |
|
return defineDocumentVisitor(context, handlers) |
|
} |
|
// Apply the handlers to templates. |
|
return defineTemplateBodyVisitor(context, handlers) |
|
}, |
|
|
|
meta: Object.assign({}, coreRule.meta, { |
|
docs: Object.assign({}, coreRule.meta.docs, { |
|
description, |
|
category: null, |
|
categories, |
|
url: `https://eslint.vuejs.org/rules/${path.basename( |
|
coreRule.meta.docs.url || '' |
|
)}.html`, |
|
extensionRule: true, |
|
coreRuleUrl: coreRule.meta.docs.url |
|
}) |
|
}) |
|
} |
|
}, |
|
/** |
|
* 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 valueNode = node.value |
|
const tokenStore = context.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, |
|
|
|
/** |
|
* 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) => { |
|
return ( |
|
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) { |
|
yield elementChain |
|
} |
|
elementChain = [childNode] |
|
} |
|
} else if (childNode.type !== 'VText' || childNode.value.trim() !== '') { |
|
vIf = false |
|
} |
|
} |
|
if (elementChain.length) { |
|
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)) || |
|
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 === vueEslintParser.AST.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 === vueEslintParser.AST.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. |
|
*/ |
|
isMathMLElementNode(node) { |
|
return node.namespace === vueEslintParser.AST.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 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 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, |
|
// TODO Since `utils` is used in some popular packages, we will remove it in the major version. |
|
/** |
|
* For backward compatibility. |
|
* @deprecated Use getComponentPropsFromOptions instead. |
|
*/ |
|
getComponentProps: 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, |
|
// TODO Since `utils` is used in some popular packages, we will remove it in the major version. |
|
/** |
|
* For backward compatibility. |
|
* @deprecated Use getComponentEmitsFromOptions instead. |
|
*/ |
|
getComponentEmits: 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) => { |
|
return ( |
|
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 |
|
}, |
|
|
|
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. |
|
* |
|
* @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]) { |
|
if (inScriptSetup(node)) { |
|
// @ts-expect-error |
|
visitor[key](node, ...args) |
|
} |
|
} |
|
} |
|
|
|
/** @type {NodeListener} */ |
|
const scriptSetupVisitor = {} |
|
for (const key in visitor) { |
|
scriptSetupVisitor[key] = (node) => callVisitor(key, node) |
|
} |
|
|
|
const hasPropsEvent = |
|
visitor.onDefinePropsEnter || visitor.onDefinePropsExit |
|
const hasEmitsEvent = |
|
visitor.onDefineEmitsEnter || visitor.onDefineEmitsExit |
|
if (hasPropsEvent || hasEmitsEvent) { |
|
/** @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 |
|
} |
|
} |
|
const definePropsMap = new Map() |
|
const defineEmitsMap = new Map() |
|
/** |
|
* @param {CallExpression} node |
|
*/ |
|
scriptSetupVisitor.CallExpression = (node) => { |
|
if ( |
|
candidateMacro && |
|
inScriptSetup(node) && |
|
node.callee.type === 'Identifier' |
|
) { |
|
if ( |
|
hasPropsEvent && |
|
(candidateMacro === node || |
|
candidateMacro === getWithDefaults(node)) && |
|
node.callee.name === 'defineProps' |
|
) { |
|
/** @type {ComponentProp[]} */ |
|
const props = getComponentPropsFromDefineProps(context, node) |
|
|
|
callVisitor('onDefinePropsEnter', node, props) |
|
definePropsMap.set(node, props) |
|
} else if ( |
|
hasEmitsEvent && |
|
candidateMacro === node && |
|
node.callee.name === 'defineEmits' |
|
) { |
|
/** @type {ComponentEmit[]} */ |
|
const emits = getComponentEmitsFromDefineEmits(context, node) |
|
|
|
callVisitor('onDefineEmitsEnter', node, emits) |
|
defineEmitsMap.set(node, emits) |
|
} |
|
} |
|
callVisitor('CallExpression', node) |
|
} |
|
scriptSetupVisitor['CallExpression:exit'] = (node) => { |
|
callVisitor('CallExpression:exit', node) |
|
if (definePropsMap.has(node)) { |
|
callVisitor('onDefinePropsExit', node, definePropsMap.get(node)) |
|
definePropsMap.delete(node) |
|
} |
|
if (defineEmitsMap.has(node)) { |
|
callVisitor('onDefineEmitsExit', node, defineEmitsMap.get(node)) |
|
defineEmitsMap.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 >= 1 |
|
) { |
|
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 |
|
|
|
if (item.value.type === 'ArrayExpression') { |
|
yield* this.iterateArrayExpression(item.value, name) |
|
} else if (item.value.type === 'ObjectExpression') { |
|
yield* this.iterateObjectExpression(item.value, name) |
|
} else if (item.value.type === 'FunctionExpression') { |
|
yield* this.iterateFunctionExpression(item.value, name) |
|
} else if (item.value.type === 'ArrowFunctionExpression') { |
|
yield* this.iterateArrowFunctionExpression(item.value, name) |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* 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) { |
|
if (item.kind === 'set') { |
|
// find getter pair |
|
if ( |
|
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 |
|
*/ |
|
|
|
/** @type {FuncInfo | null} */ |
|
let funcInfo = null |
|
|
|
/** @param {CodePathSegment} segment */ |
|
function isReachable(segment) { |
|
return segment.reachable |
|
} |
|
|
|
function isValidReturn() { |
|
if (!funcInfo) { |
|
return true |
|
} |
|
if ( |
|
funcInfo.codePath && |
|
funcInfo.codePath.currentSegments.some(isReachable) |
|
) { |
|
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, |
|
funcInfo, |
|
hasReturn: false, |
|
hasReturnValue: false, |
|
node |
|
} |
|
} |
|
}, |
|
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++) { |
|
if (a[i - 1] === b[j - 1]) { |
|
dp[i][j] = dp[i - 1][j - 1] |
|
} else { |
|
dp[i][j] = 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, |
|
/** |
|
* 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') { |
|
if (parent.property === node) { |
|
return false |
|
} |
|
} else if (parent.type === 'Property') { |
|
if (parent.key === node && !parent.computed) { |
|
return false |
|
} |
|
} |
|
|
|
const variable = findVariable(context.getScope(), 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) { |
|
if (target.type === 'AssignmentExpression') { |
|
if (target.left === node) { |
|
// this.xxx <=|+=|-=> |
|
return { |
|
kind: 'assignment', |
|
node: target, |
|
pathNodes |
|
} |
|
} |
|
} else if (target.type === 'UpdateExpression') { |
|
// this.xxx <++|--> |
|
return { |
|
kind: 'update', |
|
node: target, |
|
pathNodes |
|
} |
|
} else if (target.type === '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.exec( |
|
callName |
|
) |
|
) { |
|
// this.xxx.push() |
|
pathNodes.pop() |
|
return { |
|
kind: 'call', |
|
node: target, |
|
pathNodes |
|
} |
|
} |
|
} |
|
} else if (target.type === 'MemberExpression') { |
|
if (target.object === node) { |
|
pathNodes.push(target) |
|
node = target |
|
target = target.parent |
|
continue // loop |
|
} |
|
} else if (target.type === '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 |
|
* @param {import('eslint-utils').TYPES.TraceMap} map |
|
*/ |
|
createCompositionApiTraceMap: (map) => ({ |
|
vue: map, |
|
'@vue/composition-api': map |
|
}), |
|
|
|
/** |
|
* 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 |
|
} |
|
for (let i = 0; i < tokensL.length; ++i) { |
|
if ( |
|
tokensL[i].type !== tokensR[i].type || |
|
tokensL[i].value !== tokensR[i].value |
|
) { |
|
return false |
|
} |
|
} |
|
|
|
return true |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------------------ |
|
// 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 |
|
) { |
|
if (context.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 context.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) { |
|
if (context.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 context.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) |
|
} |
|
|
|
/** |
|
* Gets the scope for the current node |
|
* @param {RuleContext} context The rule context |
|
* @param {ESNode} currentNode The node to get the scope of |
|
* @returns { import('eslint').Scope.Scope } The scope information for this node |
|
*/ |
|
function getScope(context, currentNode) { |
|
// On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. |
|
const inner = currentNode.type !== 'Program' |
|
const scopeManager = context.getSourceCode().scopeManager |
|
|
|
/** @type {ESNode | null} */ |
|
let node = currentNode |
|
for (; node; node = /** @type {ESNode | null} */ (node.parent)) { |
|
const scope = scopeManager.acquire(node, inner) |
|
|
|
if (scope) { |
|
if (scope.type === 'function-expression-name') { |
|
return scope.childScopes[0] |
|
} |
|
return scope |
|
} |
|
} |
|
|
|
return scopeManager.scopes[0] |
|
} |
|
|
|
/** |
|
* 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') { |
|
if (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 |
|
} |
|
|
|
// ------------------------------------------------------------------------------ |
|
// 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 df = |
|
context.parserServices.getDocumentFragment && |
|
context.parserServices.getDocumentFragment() |
|
if (!df) { |
|
return null |
|
} |
|
const scripts = df.children |
|
.filter(isVElement) |
|
.filter((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 && |
|
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) |
|
if (parent.type === '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' |
|
} |
|
} else if (parent.type === 'CallExpression') { |
|
// Vue.component('xxx', {}) || component('xxx', {}) |
|
if ( |
|
getVueComponentDefinitionType(node) != null && |
|
skipTSAsExpression(parent.arguments.slice(-1)[0]) === node |
|
) { |
|
return 'definition' |
|
} |
|
} else if (parent.type === 'NewExpression') { |
|
// new Vue({}) |
|
if ( |
|
isVueInstance(parent) && |
|
skipTSAsExpression(parent.arguments[0]) === node |
|
) { |
|
return 'instance' |
|
} |
|
} |
|
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) |
|
if (parent.type === '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 |
|
} else if (parent.type === '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 |
|
} else if (parent.type === 'VariableDeclarator') { |
|
if ( |
|
skipTSAsExpression(parent.init) !== node || |
|
parent.id.type !== 'Identifier' |
|
) { |
|
return false |
|
} |
|
const variable = findVariable(context.getScope(), parent.id) |
|
if (!variable) { |
|
return false |
|
} |
|
return variable.references.some((ref) => isSFC(ref.identifier)) |
|
} |
|
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) => { |
|
return ( |
|
!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) => { |
|
return 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) => { |
|
return ( |
|
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)) |
|
} |
|
|
|
/** |
|
* 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') { |
|
return {} |
|
} |
|
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) => { |
|
return p.type === 'Property' && getStaticPropertyName(p) === 'props' |
|
} |
|
) |
|
|
|
if (!propsNode) { |
|
return [] |
|
} |
|
if ( |
|
propsNode.value.type !== 'ObjectExpression' && |
|
propsNode.value.type !== 'ArrayExpression' |
|
) { |
|
return [ |
|
{ |
|
type: 'unknown', |
|
key: null, |
|
propName: null, |
|
value: 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) => { |
|
return p.type === 'Property' && getStaticPropertyName(p) === 'emits' |
|
} |
|
) |
|
|
|
if (!emitsNode) { |
|
return [] |
|
} |
|
if ( |
|
emitsNode.value.type !== 'ObjectExpression' && |
|
emitsNode.value.type !== 'ArrayExpression' |
|
) { |
|
return [ |
|
{ |
|
type: 'unknown', |
|
key: null, |
|
emitName: null, |
|
value: 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 {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp | ComponentUnknownProp)[]} Array of component props |
|
*/ |
|
function getComponentPropsFromDefineProps(context, node) { |
|
if (node.arguments.length >= 1) { |
|
const defNode = getObjectOrArray(context, node.arguments[0]) |
|
if (defNode) { |
|
return getComponentPropsFromDefine(defNode) |
|
} |
|
return [ |
|
{ |
|
type: 'unknown', |
|
key: null, |
|
propName: null, |
|
value: null, |
|
node: node.arguments[0] |
|
} |
|
] |
|
} |
|
if (node.typeParameters && node.typeParameters.params.length >= 1) { |
|
return getComponentPropsFromTypeDefine( |
|
context, |
|
node.typeParameters.params[0] |
|
) |
|
} |
|
return [ |
|
{ |
|
type: 'unknown', |
|
key: null, |
|
propName: null, |
|
value: null, |
|
node: null |
|
} |
|
] |
|
} |
|
|
|
/** |
|
* Get all emits from `defineEmits` call expression. |
|
* @param {RuleContext} context The rule context object. |
|
* @param {CallExpression} node `defineEmits` call expression |
|
* @return {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit | ComponentUnknownEmit)[]} Array of component emits |
|
*/ |
|
function getComponentEmitsFromDefineEmits(context, node) { |
|
if (node.arguments.length >= 1) { |
|
const defNode = getObjectOrArray(context, node.arguments[0]) |
|
if (defNode) { |
|
return getComponentEmitsFromDefine(defNode) |
|
} |
|
return [ |
|
{ |
|
type: 'unknown', |
|
key: null, |
|
emitName: null, |
|
value: null, |
|
node: node.arguments[0] |
|
} |
|
] |
|
} |
|
if (node.typeParameters && node.typeParameters.params.length >= 1) { |
|
return getComponentEmitsFromTypeDefine( |
|
context, |
|
node.typeParameters.params[0] |
|
) |
|
} |
|
return [ |
|
{ |
|
type: 'unknown', |
|
key: null, |
|
emitName: null, |
|
value: null, |
|
node: 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((prop) => { |
|
if (!isProperty(prop)) { |
|
return { |
|
type: 'unknown', |
|
key: null, |
|
propName: null, |
|
value: 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 |
|
} |
|
}) |
|
} else { |
|
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 |
|
} |
|
}) |
|
} else { |
|
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(context.getScope(), 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 |
|
}
|
|
|