UNPKG

eslint-plugin-san

Version:

Official ESLint plugin for San

1,328 lines (1,247 loc) 72.7 kB
/** * @author Toru Nagashima <https://github.com/mysticatea> * @copyright 2017 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ 'use strict'; // 源码来自于 eslint-plugin-san,所以直接关闭eslint /* eslint-disable */ /** * @typedef {import('eslint').Rule.RuleModule} RuleModule * @typedef {import('estree').Position} Position * @typedef {import('eslint').Rule.CodePath} CodePath * @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment */ /** * @typedef {object} ComponentArrayPropDetectName * @property {'array'} type * @property {Literal | TemplateLiteral} key * @property {string} propName * @property {null} value * @property {Expression | SpreadElement} node * * @typedef {object} ComponentArrayPropUnknownName * @property {'array'} type * @property {null} key * @property {null} propName * @property {null} value * @property {Expression | SpreadElement} node * * @typedef {ComponentArrayPropDetectName | ComponentArrayPropUnknownName} ComponentArrayProp * * @typedef {object} ComponentObjectPropDetectName * @property {'object'} type * @property {Expression} key * @property {string} propName * @property {Expression} value * @property {Property} node * * @typedef {object} ComponentObjectPropUnknownName * @property {'object'} type * @property {null} key * @property {null} propName * @property {Expression} value * @property {Property} node * * @typedef {ComponentObjectPropDetectName | ComponentObjectPropUnknownName} ComponentObjectProp */ /** * @typedef {object} ComponentArrayEmitDetectName * @property {'array'} type * @property {Literal | TemplateLiteral} key * @property {string} emitName * @property {null} value * @property {Expression | SpreadElement} node * * @typedef {object} ComponentArrayEmitUnknownName * @property {'array'} type * @property {null} key * @property {null} emitName * @property {null} value * @property {Expression | SpreadElement} node * * @typedef {ComponentArrayEmitDetectName | ComponentArrayEmitUnknownName} ComponentArrayEmit * * @typedef {object} ComponentObjectEmitDetectName * @property {'object'} type * @property {Expression} key * @property {string} emitName * @property {Expression} value * @property {Property} node * * @typedef {object} ComponentObjectEmitUnknownName * @property {'object'} type * @property {null} key * @property {null} emitName * @property {Expression} value * @property {Property} node * * @typedef {ComponentObjectEmitDetectName | ComponentObjectEmitUnknownName} ComponentObjectEmit */ /** * @typedef { {key: string | null, value: BlockStatement | null} } ComponentComputedProperty */ /** * @typedef { 'props' | 'data' | 'computed' | 'setup' | 'watch' | 'methods' } 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-san/util-types/utils').SanObjectType} SanObjectType * @typedef {import('../../typings/eslint-plugin-san/util-types/utils').SanObjectData} SanObjectData * @typedef {import('../../typings/eslint-plugin-san/util-types/utils').SanVisitor} SanVisitor */ // ------------------------------------------------------------------------------ // 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 path = require('path'); const sanEslintParser = require('san-eslint-parser'); const {findVariable} = require('eslint-utils'); /** * @type { WeakMap<RuleContext, Token[]> } */ const componentComments = new WeakMap(); /** * 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. * @returns {RuleContext} */ function wrapContextToOverrideTokenMethods(context, tokenStore) { const eslintSourceCode = context.getSourceCode(); /** @type {Token[] | null} */ let tokensAndComments = null; function getTokensAndComments() { if (tokensAndComments) { return tokensAndComments; } const templateBody = eslintSourceCode.ast.templateBody; tokensAndComments = templateBody ? tokenStore.getTokens(templateBody, { includeComments: true }) : []; return tokensAndComments; } const sourceCode = new Proxy(Object.assign({}, eslintSourceCode), { get(_object, key) { if (key === 'tokensAndComments') { return getTokensAndComments(); } // @ts-expect-error return key in tokenStore ? tokenStore[key] : eslintSourceCode[key]; } }); return { // @ts-expect-error __proto__: context, getSourceCode() { return sourceCode; } }; } /** * 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 = sanEslintParser.AST.traverseNodes; traverseNodes(templateBody, { enterNode(node, parent) { if (parent && parent.type === 'VDirectiveKey' && node.type === 'VExpressionContainer') { directiveKeyRanges.push(node.range); } }, leaveNode() {} }); return { // @ts-expect-error __proto__: 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 `san-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. * @returns {RuleListener} The merged visitor. */ defineTemplateBodyVisitor, /** * Wrap a given core rule to apply it to San.js template. * @param {RuleModule} coreRule 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 { (context: RuleContext, options: { coreHandlers: RuleListener }) => TemplateListener } [options.create] If define, extend core rule. * @returns {RuleModule} The wrapped rule implementation. */ wrapCoreRule(coreRule, options) { const {categories, skipDynamicArguments, skipDynamicArgumentsReport, 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); } if (skipDynamicArgumentsReport) { context = wrapContextToOverrideReportMethodToSkipDynamicArgument(context); } // Move `Program` handlers to `VElement[parent.type!='VElement']` const coreHandlers = coreRule.create(context); const handlers = /** @type {TemplateListener} */ (Object.assign({}, coreHandlers)); if (handlers.Program) { handlers["VElement[parent.type!='VElement']"] = handlers.Program; delete handlers.Program; } if (handlers['Program:exit']) { handlers["VElement[parent.type!='VElement']:exit"] = 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})); } // Apply the handlers to templates. return defineTemplateBodyVisitor(context, handlers); }, meta: Object.assign({}, coreRule.meta, { docs: Object.assign({}, coreRule.meta.docs, { category: null, categories, url: `https://ecomfe.github.io/eslint-plugin-san/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, /** * 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 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(node, name, value) { return Boolean(this.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. */ hasDirective(node, name, argument) { return Boolean(this.getDirective(node, name, argument)); }, /** * Check whether the given directive attribute has their empty value (`=""`). * @param {VDirective} node The directive attribute node to check. * @param {RuleContext} context The rule context to use parser services. * @returns {boolean} `true` if the directive attribute has their empty value (`=""`). */ isEmptyValueDirective(node, context) { if (node.value == null) { return false; } if (node.value.expression != null) { return false; } let valueText = context.getSourceCode().getText(node.value); if ((valueText[0] === '"' || valueText[0] === "'") && valueText[0] === valueText[valueText.length - 1]) { // quoted valueText = valueText.slice(1, -1); } if (!valueText) { // empty return true; } return false; }, /** * Check whether the given directive attribute has their empty expression value (e.g. `=" "`, `="/* &ast;/"`). * @param {VDirective} node The directive attribute node to check. * @param {RuleContext} context The rule context to use parser services. * @returns {boolean} `true` if the directive attribute has their empty expression value. */ isEmptyExpressionValueDirective(node, context) { if (node.value == null) { return false; } if (node.value.expression != null) { return false; } const 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(node, name, value) { if (node.startTag && node.startTag.attributes && typeof node.startTag.attributes.find === 'function') { 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 ); } return 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. */ 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. */ 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 ); }, /** * Returns the list of all registered components * @param {ObjectExpression} componentObject * @returns { { node: Property, name: string }[] } Array of ASTNodes */ getRegisteredComponents(componentObject) { if (!componentObject) { return []; } let componentsNode = null; if (componentObject.type === 'ClassBody' && componentObject.body) { componentsNode = componentObject.body.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'components'}, value: ObjectExpression })} */ p => { return ( isClassProperty(p) && p.key.type === 'Identifier' && p.key.name === 'components' && (p.value && p.value.type === 'ObjectExpression') ); } ); } else if (componentObject.properties) { componentsNode = componentObject.properties.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'components'}, value: ObjectExpression })} */ p => { return ( p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === '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') ) ); }, /** * 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.hasAttribute(node, 'is') || this.hasDirective(node, 'bind', 'is') || this.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 === sanEslintParser.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 === sanEslintParser.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 === sanEslintParser.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); }, /** * 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)[]} Array of component props in format: [{key?: String, value?: ASTNode, node: ASTNod}] */ getComponentProps(componentObject) { const propsNode = componentObject.properties.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'props'}, value: ObjectExpression | ArrayExpression })} */ p => { return ( p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'props' && (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression') ); } ); if (!propsNode) { return []; } if (propsNode.value.type === 'ObjectExpression') { return propsNode.value.properties.filter(isProperty).map(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.value.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} componentObject Object with component definition * @return {(ComponentArrayEmit | ComponentObjectEmit)[]} Array of component emits in format: [{key?: String, value?: ASTNode, node: ASTNod}] */ getComponentEmits(componentObject) { const emitsNode = componentObject.properties.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'emits'}, value: ObjectExpression | ArrayExpression })} */ p => { return ( p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'emits' && p.value && (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression') ); } ); if (!emitsNode) { return []; } if (emitsNode.value.type === 'ObjectExpression') { return emitsNode.value.properties.filter(isProperty).map(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.value.elements.filter(isDef).map(prop => { if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') { const emitName = getStringLiteralValue(prop); if (emitName != null) { return { type: 'array', key: prop, emitName, value: null, node: prop }; } } return { type: 'array', key: null, emitName: null, value: null, node: prop }; }); } }, /** * 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) { if (!componentObject) { return []; } let computedPropertiesNode = null; if (componentObject.type === 'ClassBody' && componentObject.body) { computedPropertiesNode = componentObject.body.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'computed'}, value: ObjectExpression })} */ p => { return ( isClassProperty(p) && p.key.type === 'Identifier' && p.key.name === 'computed' && p.value.type === 'ObjectExpression' ); } ); } else if (componentObject.properties) { computedPropertiesNode = componentObject.properties.find( /** * @param {ESNode} p * @returns {p is (Property & { key: Identifier & {name: 'computed'}, value: ObjectExpression })} */ p => { return ( p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === '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 = propValue.properties.find( /** * @param {ESNode} p * @returns { p is (Property & { value: FunctionExpression }) } */ p => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'get' && p.value.type === 'FunctionExpression' ); value = get ? get.value.body : null; } return {key, value}; }); }, isSanFile, /** * Check if current file is a San instance or component and call callback * @param {RuleContext} context The ESLint rule context object. * @param { (node: ObjectExpression, type: SanObjectType) => void } cb Callback function */ executeOnSan(context, cb) { return compositingVisitors(this.executeOnSanComponent(context, cb), this.executeOnSanInstance(context, cb)); }, /** * Define handlers to traverse the San Objects. * Some special events are available to visitor. * * - `onSanObjectEnter` ... Event when San Object is found. * - `onSanObjectExit` ... Event when San Object visit ends. * - `onSetupFunctionEnter` ... Event when setup function found. * - `onRenderFunctionEnter` ... Event when render function found. * * @param {RuleContext} context The ESLint rule context object. * @param {SanVisitor} visitor The visitor to traverse the San Objects. */ defineSanVisitor(context, visitor) { /** @type {SanObjectData | null} */ let sanStack = null; /** * @param {string} key * @param {ESNode} node */ function callVisitor(key, node) { if (visitor[key] && sanStack) { // @ts-expect-error visitor[key](node, sanStack); } } /** @type {NodeListener} */ const sanVisitor = {}; for (const key in visitor) { sanVisitor[key] = node => callVisitor(key, node); } /** * @param {ObjectExpression} node */ sanVisitor.ObjectExpression = sanVisitor.ClassBody =node => { const type = getSanObjectType(context, node); if (type) { sanStack = { node, type, parent: sanStack, 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('onSanObjectEnter', node); } callVisitor('ObjectExpression', node); }; sanVisitor['ObjectExpression:exit'] = sanVisitor['ClassBody:exit'] = node => { callVisitor('ObjectExpression:exit', node); if (sanStack && sanStack.node === node) { callVisitor('onSanObjectExit', node); sanStack = sanStack.parent; } }; if (visitor.onSetupFunctionEnter || visitor.onRenderFunctionEnter) { /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property } } node */ sanVisitor['Property[value.type=/^(Arrow)?FunctionExpression$/] > :function'] = node => { /** @type {Property} */ const prop = node.parent; if (sanStack && prop.parent === sanStack.node && prop.value === node) { const name = getStaticPropertyName(prop); if (name === 'setup') { callVisitor('onSetupFunctionEnter', node); } else if (name === 'render') { callVisitor('onRenderFunctionEnter', node); } } callVisitor('Property[value.type=/^(Arrow)?FunctionExpression$/] > :function', node); }; } return sanVisitor; }, getSanObjectType, compositingVisitors, /** * Check if current file is a San instance (new San) and call callback * @param {RuleContext} context The ESLint rule context object. * @param { (node: ObjectExpression, type: SanObjectType) => void } cb Callback function */ executeOnSanInstance(context, cb) { return { /** @param {ObjectExpression} node */ 'ObjectExpression:exit'(node) { const type = getSanObjectType(context, node); if (!type || type !== 'instance') return; cb(node, type); } }; }, /** * Check if current file is a San component and call callback * @param {RuleContext} context The ESLint rule context object. * @param { (node: ObjectExpression, type: SanObjectType) => void } cb Callback function */ executeOnSanComponent(context, cb) { return { /** @param {ObjectExpression} node */ 'ObjectExpression:exit'(node) { const type = getSanObjectType(context, node); if (!type || (type !== 'mark' && type !== 'export' && type !== 'definition')) return; cb(node, type); }, 'ClassBody:exit'(node) { const type = getSanObjectType(context, node); if (!type || (type !== 'mark' && type !== 'export' && type !== 'definition')) return; cb(node, type); } }; }, /** * Check call `San.component` and call callback. * @param {RuleContext} _context The ESLint rule context object. * @param { (node: CallExpression) => void } cb Callback function */ executeOnCallSanComponent(_context, cb) { return { /** @param {Identifier & { parent: MemberExpression & { parent: CallExpression } } } node */ "CallExpression > MemberExpression > Identifier[name='defineComponent']": 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 === 'San' && // Any names can be used in San.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) { if (node.type === 'ObjectExpression') { const props = node.properties; for (const item of props) { 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); } } } else if (node.type === 'ClassBody') { const body = node.body; for (const item of body) { const name = /** @type {GroupName | null} */ (getStaticPropertyName(item)); if (!name || !groups.has(name) || !item.value) 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: ESNode) => void } cb Callback function * @returns {RuleListener} */ executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, cb) { /** * @typedef {object} FuncInfo * @property {FuncInfo} funcInfo * @property {CodePath} codePath * @property {boolean} hasReturn * @property {boolean} hasReturnValue * @property {ESNode} node */ /** @type {FuncInfo} */ let funcInfo; /** @param {CodePathSegment} segment */ function isReachable(segment) { return segment.reachable; } function isValidReturn() { if (funcInfo.codePath && funcInfo.codePath.currentSegments.some(isReachable)) { return false; } return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue; } return { /** * @param {CodePath} codePath * @param {ESNode} node */ onCodePathStart(codePath, node) { funcInfo = { codePath, funcInfo, hasReturn: false, hasReturnValue: false, node }; }, onCodePathEnd() { funcInfo = funcInfo.funcInfo; }, /** @param {ReturnStatement} node */ ReturnStatement(node) { funcInfo.hasReturn = true; funcInfo.hasReturnValue = Boolean(node.argument); }, /** @param {ArrowFunctionExpression} node */ 'ArrowFunctionExpression:exit'(node) { if (!isValidReturn() && !node.expression) { cb(funcInfo.node); } }, 'FunctionExpression:exit'() { if (!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]; }, isClassProperty, isMethodDefinition, /** * 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, /** * F