eslint-plugin-san
Version:
Official ESLint plugin for San
1,328 lines (1,247 loc) • 72.7 kB
JavaScript
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict';
// 源码来自于 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. `=" "`, `="/* */"`).
* @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