diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 35a785b48..96a29350f 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -115,10 +115,7 @@ module.exports = { }) } return utils.defineVueVisitor(context, { - ObjectExpression (node, { node: vueNode }) { - if (node !== vueNode) { - return - } + onVueObjectEnter (node) { computedPropertiesMap.set(node, utils.getComputedProperties(node)) }, ':function': onFunctionEnter, diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js index a009f0e42..9ed65ce16 100644 --- a/lib/rules/no-lifecycle-after-await.js +++ b/lib/rules/no-lifecycle-after-await.js @@ -50,22 +50,15 @@ module.exports = { }, utils.defineVueVisitor(context, { - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) { - if (node.parent !== vueNode) { - return - } - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } - - setupFunctions.set(node.value, { - setupProperty: node, - afterAwait: false - }) - }, ':function' (node) { scopeStack = { upper: scopeStack, functionNode: node } }, + onSetupFunctionEnter (node) { + setupFunctions.set(node, { + setupProperty: node.parent, + afterAwait: false + }) + }, 'AwaitExpression' () { const setupFunctionData = setupFunctions.get(scopeStack.functionNode) if (!setupFunctionData) { diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js index 90e614b48..341f1b22a 100644 --- a/lib/rules/no-mutating-props.js +++ b/lib/rules/no-mutating-props.js @@ -124,64 +124,12 @@ module.exports = { * @param {string} name */ function verifyMutating (props, name) { - const invalid = findMutating(props) + const invalid = utils.findMutating(props) if (invalid) { report(invalid.node, name) } } - /** - * @param {MemberExpression|Identifier} props - * @returns { { kind: 'assignment' | 'update' | 'call' , node: Node, pathNodes: MemberExpression[] } } - */ - function findMutating (props) { - /** @type {MemberExpression[]} */ - const pathNodes = [] - 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 (node !== props && target.callee === node) { - const callName = utils.getStaticPropertyName(node) - 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 - } - } - - return null - } - } - /** * @param {Pattern} param * @param {string[]} path @@ -220,16 +168,10 @@ module.exports = { return Object.assign({}, utils.defineVueVisitor(context, { - ObjectExpression (node, { node: vueNode }) { - if (node !== vueNode) { - return - } + onVueObjectEnter (node) { propsMap.set(node, new Set(utils.getComponentProps(node).map(p => p.propName))) }, - 'ObjectExpression:exit' (node, { node: vueNode, type }) { - if (node !== vueNode) { - return - } + onVueObjectExit (node, { type }) { if (!vueObjectData || vueObjectData.type !== 'export') { vueObjectData = { type, @@ -237,15 +179,9 @@ module.exports = { } } }, - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) { - if (node.parent !== vueNode) { - return - } - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } + onSetupFunctionEnter (node) { /** @type {Pattern} */ - const propsParam = node.value.params[0] + const propsParam = node.params[0] if (!propsParam) { // no arguments return @@ -268,7 +204,7 @@ module.exports = { /** @type {Identifier} */ const id = reference.identifier - const invalid = findMutating(id) + const invalid = utils.findMutating(id) if (!invalid) { continue } diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js index f167357da..fb992980c 100644 --- a/lib/rules/no-setup-props-destructure.js +++ b/lib/rules/no-setup-props-destructure.js @@ -52,14 +52,11 @@ module.exports = { let scopeStack = null return utils.defineVueVisitor(context, { - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) { - if (node.parent !== vueNode) { - return - } - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } - const propsParam = node.value.params[0] + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + onSetupFunctionEnter (node) { + const propsParam = node.params[0] if (!propsParam) { // no arguments return @@ -85,10 +82,7 @@ module.exports = { propsReferenceIds.add(reference.identifier) } - setupScopePropsReferenceIds.set(node.value, propsReferenceIds) - }, - ':function' (node) { - scopeStack = { upper: scopeStack, functionNode: node } + setupScopePropsReferenceIds.set(node, propsReferenceIds) }, 'VariableDeclarator' (node) { const propsReferenceIds = setupScopePropsReferenceIds.get(scopeStack.functionNode) diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js index eab6e0fae..d1bcc3a99 100644 --- a/lib/rules/no-side-effects-in-computed-properties.js +++ b/lib/rules/no-side-effects-in-computed-properties.js @@ -6,6 +6,12 @@ const utils = require('../utils') +/** + * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression + * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression + * @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty + */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -23,6 +29,7 @@ module.exports = { }, create (context) { + /** @type {Map} */ const computedPropertiesMap = new Map() let scopeStack = { upper: null, body: null } @@ -34,53 +41,43 @@ module.exports = { scopeStack = scopeStack.upper } - function verify (node, targetBody, computedProperties) { - computedProperties.forEach(cp => { - if ( - cp.value && - node.loc.start.line >= cp.value.loc.start.line && - node.loc.end.line <= cp.value.loc.end.line && - targetBody === cp.value - ) { - context.report({ - node: node, - message: 'Unexpected side effect in "{{key}}" computed property.', - data: { key: cp.key } - }) - } - }) - } - return utils.defineVueVisitor(context, { - ObjectExpression (node, { node: vueNode }) { - if (node !== vueNode) { - return - } + onVueObjectEnter (node) { computedPropertiesMap.set(node, utils.getComputedProperties(node)) }, ':function': onFunctionEnter, ':function:exit': onFunctionExit, - // this.xxx <=|+=|-=> - 'AssignmentExpression' (node, { node: vueNode }) { - if (node.left.type !== 'MemberExpression') return - if (utils.parseMemberExpression(node.left)[0] === 'this') { - verify(node, scopeStack.body, computedPropertiesMap.get(vueNode)) + 'MemberExpression > :matches(Identifier, ThisExpression)' (node, { node: vueNode }) { + const targetBody = scopeStack.body + const computedProperty = computedPropertiesMap.get(vueNode).find(cp => { + return ( + cp.value && + node.loc.start.line >= cp.value.loc.start.line && + node.loc.end.line <= cp.value.loc.end.line && + targetBody === cp.value + ) + }) + if (!computedProperty) { + return } - }, - // this.xxx <++|--> - 'UpdateExpression > MemberExpression' (node, { node: vueNode }) { - if (utils.parseMemberExpression(node)[0] === 'this') { - verify(node, scopeStack.body, computedPropertiesMap.get(vueNode)) + + if (!utils.isThis(node, context)) { + return + } + /** @type {MemberExpression} */ + const mem = node.parent + if (mem.object !== node) { + return } - }, - // this.xxx.func() - 'CallExpression' (node, { node: vueNode }) { - const code = utils.parseMemberOrCallExpression(node) - const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g - if (MUTATION_REGEX.test(code)) { - verify(node, scopeStack.body, computedPropertiesMap.get(vueNode)) + const invalid = utils.findMutating(mem) + if (invalid) { + context.report({ + node: invalid.node, + message: 'Unexpected side effect in "{{key}}" computed property.', + data: { key: computedProperty.key } + }) } } } diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 2462446e4..cbfcddaff 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -412,12 +412,8 @@ module.exports = { const scriptVisitor = Object.assign( {}, utils.defineVueVisitor(context, { - ObjectExpression (node, vueData) { - if (node !== vueData.node) { - return - } - - const container = getVueComponentPropertiesContainer(vueData.node) + onVueObjectEnter (node) { + const container = getVueComponentPropertiesContainer(node) const watcherNames = new Set() for (const watcher of utils.iterateProperties(node, new Set([GROUP_WATCHER]))) { watcherNames.add(watcher.name) @@ -429,20 +425,14 @@ module.exports = { container.properties.push(prop) } }, - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, vueData) { - if (node.parent !== vueData.node) { - return - } - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } + onSetupFunctionEnter (node, vueData) { const container = getVueComponentPropertiesContainer(vueData.node) - const propsParam = node.value.params[0] + const propsParam = node.params[0] if (!propsParam) { // no arguments return } - const paramsUsedProps = getParamsUsedProps(node.value) + const paramsUsedProps = getParamsUsedProps(node) const paramUsedProps = paramsUsedProps.getParam(0) for (const { usedNames, unknown } of iterateUsedProps(paramUsedProps)) { diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js index e666b8616..cbbd02061 100644 --- a/lib/rules/no-watch-after-await.js +++ b/lib/rules/no-watch-after-await.js @@ -76,22 +76,15 @@ module.exports = { }, utils.defineVueVisitor(context, { - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) { - if (node.parent !== vueNode) { - return - } - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } - - setupFunctions.set(node.value, { - setupProperty: node, - afterAwait: false - }) - }, ':function' (node) { scopeStack = { upper: scopeStack, functionNode: node } }, + onSetupFunctionEnter (node) { + setupFunctions.set(node, { + setupProperty: node.parent, + afterAwait: false + }) + }, 'AwaitExpression' () { const setupFunctionData = setupFunctions.get(scopeStack.functionNode) if (!setupFunctionData) { diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js index 8bfe481b2..decf07277 100644 --- a/lib/rules/require-explicit-emits.js +++ b/lib/rules/require-explicit-emits.js @@ -164,20 +164,11 @@ module.exports = { } }, utils.defineVueVisitor(context, { - ObjectExpression (node, { node: vueNode }) { - if (node !== vueNode) { - return - } + onVueObjectEnter (node) { vueEmitsDeclarations.set(node, utils.getComponentEmits(node)) - - const setupProperty = node.properties.find(p => utils.getStaticPropertyName(p) === 'setup') - if (!setupProperty) { - return - } - if (!/^(Arrow)?FunctionExpression$/.test(setupProperty.value.type)) { - return - } - const contextParam = setupProperty.value.params[1] + }, + onSetupFunctionEnter (node, { node: vueNode }) { + const contextParam = node.params[1] if (!contextParam) { // no arguments return @@ -224,7 +215,7 @@ module.exports = { contextReferenceIds.add(reference.identifier) } } - setupContexts.set(node, { + setupContexts.set(vueNode, { contextReferenceIds, emitReferenceIds }) @@ -267,10 +258,7 @@ module.exports = { } } }, - 'ObjectExpression:exit' (node, { node: vueNode, type }) { - if (node !== vueNode) { - return - } + onVueObjectExit (node, { type }) { if (!vueObjectData || vueObjectData.type !== 'export') { vueObjectData = { type, diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js index 17cfcb355..6b7310663 100644 --- a/lib/rules/require-render-return.js +++ b/lib/rules/require-render-return.js @@ -32,10 +32,7 @@ module.exports = { return Object.assign({}, utils.defineVueVisitor(context, { - ObjectExpression (obj, { node: vueNode }) { - if (obj !== vueNode) { - return - } + onVueObjectEnter (obj) { const node = obj.properties.find(item => item.type === 'Property' && utils.getStaticPropertyName(item) === 'render' && (item.value.type === 'ArrowFunctionExpression' || item.value.type === 'FunctionExpression') diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js index c62f59846..c0cc9a12c 100644 --- a/lib/rules/return-in-computed-property.js +++ b/lib/rules/return-in-computed-property.js @@ -45,10 +45,7 @@ module.exports = { return Object.assign({}, utils.defineVueVisitor(context, { - ObjectExpression (obj, { node: vueNode }) { - if (obj !== vueNode) { - return - } + onVueObjectEnter (obj, { node: vueNode }) { for (const computedProperty of utils.getComputedProperties(obj)) { computedProperties.add(computedProperty) } diff --git a/lib/rules/return-in-emits-validator.js b/lib/rules/return-in-emits-validator.js index 64e62b8c4..3fb95fe1a 100644 --- a/lib/rules/return-in-emits-validator.js +++ b/lib/rules/return-in-emits-validator.js @@ -57,10 +57,7 @@ module.exports = { return Object.assign({}, utils.defineVueVisitor(context, { - ObjectExpression (obj, { node: vueNode }) { - if (obj !== vueNode) { - return - } + onVueObjectEnter (obj) { for (const emits of utils.getComponentEmits(obj)) { const emitsValue = emits.value if (!emitsValue) { diff --git a/lib/utils/index.js b/lib/utils/index.js index dc883e026..698a9a047 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -15,6 +15,8 @@ * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression * @typedef {import('vue-eslint-parser').AST.ESLintProperty} Property * @typedef {import('vue-eslint-parser').AST.ESLintTemplateLiteral} TemplateLiteral + * @typedef {import('vue-eslint-parser').AST.ESLintFunctionExpression} FunctionExpression + * @typedef {import('vue-eslint-parser').AST.ESLintBlockStatement} BlockStatement * @typedef {import('vue-eslint-parser').AST.ESLintNode} ESLintNode */ @@ -31,6 +33,9 @@ * @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], emitName: string} } ComponentArrayEmit * @typedef { {key: Property['key'], value: Property['value'], node: Property, emitName: string} } ComponentObjectEmit */ +/** + * @typedef { {key: string, value: BlockStatement} } ComponentComputedProperty + */ // ------------------------------------------------------------------------------ // Helpers @@ -561,7 +566,7 @@ module.exports = { /** * Get all computed properties by looking at all component's properties * @param {ObjectExpression} componentObject Object with component definition - * @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}] + * @return {ComponentComputedProperty[]} Array of computed properties in format: [{key: String, value: ASTNode}] */ getComputedProperties (componentObject) { const computedPropertiesNode = componentObject.properties @@ -577,20 +582,24 @@ module.exports = { return computedPropertiesNode.value.properties .filter(cp => cp.type === 'Property') .map(cp => { - const key = cp.key.name - let value - - if (cp.value.type === 'FunctionExpression') { - value = cp.value.body - } else if (cp.value.type === 'ObjectExpression') { - value = cp.value.properties - .filter(p => + const key = getStaticPropertyName(cp) + /** @type {Expression} */ + const propValue = cp.value + /** @type {BlockStatement | null} */ + let value = null + + if (propValue.type === 'FunctionExpression') { + value = propValue.body + } else if (propValue.type === 'ObjectExpression') { + /** @type { (Property & { value: FunctionExpression }) | undefined} */ + const get = propValue.properties + .find(p => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'get' && p.value.type === 'FunctionExpression' ) - .map(p => p.value.body)[0] + value = get ? get.value.body : null } return { key, value } @@ -613,9 +622,14 @@ module.exports = { /** * 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. + * * @param {RuleContext} context The ESLint rule context object. * @param {Object} visitor The visitor to traverse the Vue Objects. - * @param {Function} cb Callback function */ defineVueVisitor (context, visitor) { let vueStack = null @@ -625,34 +639,39 @@ module.exports = { visitor[key](node, vueStack) } } - function objectEnter (node) { + + const vueVisitor = {} + for (const key in visitor) { + vueVisitor[key] = (node) => callVisitor(key, node) + } + + vueVisitor['ObjectExpression'] = (node) => { const type = getVueObjectType(context, node) if (type) { vueStack = { node, type, parent: vueStack } + callVisitor('onVueObjectEnter', node) } + callVisitor('ObjectExpression', node) } - function objectExit (node) { + vueVisitor['ObjectExpression:exit'] = (node) => { + callVisitor('ObjectExpression:exit', node) if (vueStack && vueStack.node === node) { + callVisitor('onVueObjectExit', node) vueStack = vueStack.parent } } - - const vueVisitor = {} - for (const key in visitor) { - vueVisitor[key] = (node) => callVisitor(key, node) + vueVisitor['Property[value.type=/^(Arrow)?FunctionExpression$/] > :function'] = (node) => { + /** @type {Property} */ + const prop = node.parent + if (vueStack && prop.parent === vueStack.node) { + if (getStaticPropertyName(prop) === 'setup' && prop.value === node) { + callVisitor('onSetupFunctionEnter', node) + } + } + callVisitor('Property[value.type=/^(Arrow)?FunctionExpression$/] > :function', node) } - return { - ...vueVisitor, - ObjectExpression: vueVisitor.ObjectExpression ? (node) => { - objectEnter(node) - vueVisitor.ObjectExpression(node) - } : objectEnter, - 'ObjectExpression:exit': vueVisitor['ObjectExpression:exit'] ? (node) => { - vueVisitor['ObjectExpression:exit'](node) - objectExit(node) - } : objectExit - } + return vueVisitor }, getVueObjectType, @@ -1009,6 +1028,58 @@ module.exports = { } } return false + }, + + /** + * @param {MemberExpression|Identifier} props + * @returns { { kind: 'assignment' | 'update' | 'call' , node: Node, pathNodes: MemberExpression[] } } + */ + findMutating (props) { + /** @type {MemberExpression[]} */ + const pathNodes = [] + 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 (node !== props && target.callee === node) { + const callName = getStaticPropertyName(node) + 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 + } + } + + return null + } } }