diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js index 54ededbdb..a0d508014 100644 --- a/lib/rules/no-side-effects-in-computed-properties.js +++ b/lib/rules/no-side-effects-in-computed-properties.js @@ -8,6 +8,7 @@ const utils = require('../utils') /** * @typedef {import('../utils').VueObjectData} VueObjectData + * @typedef {import('../utils').VueVisitor} VueVisitor * @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty */ @@ -32,8 +33,8 @@ module.exports = { const computedPropertiesMap = new Map() /** @type {Array} */ const computedCallNodes = [] - /** @type {Array} */ - const setupFunctions = [] + /** @type {[number, number][]} */ + const setupRanges = [] /** * @typedef {object} ScopeStack @@ -57,7 +58,114 @@ module.exports = { scopeStack = scopeStack && scopeStack.upper } - return Object.assign( + const nodeVisitor = { + ':function': onFunctionEnter, + ':function:exit': onFunctionExit, + + /** + * @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node + * @param {VueObjectData|undefined} [info] + */ + 'MemberExpression > :matches(Identifier, ThisExpression)'(node, info) { + if (!scopeStack) { + return + } + const targetBody = scopeStack.body + + const computedProperty = ( + info ? computedPropertiesMap.get(info.node) || [] : [] + ).find((cp) => { + return ( + cp.value && + cp.value.range[0] <= node.range[0] && + node.range[1] <= cp.value.range[1] && + targetBody === cp.value + ) + }) + if (computedProperty) { + if (!utils.isThis(node, context)) { + return + } + const mem = node.parent + if (mem.object !== node) { + return + } + + const invalid = utils.findMutating(mem) + if (invalid) { + context.report({ + node: invalid.node, + message: 'Unexpected side effect in "{{key}}" computed property.', + data: { key: computedProperty.key || 'Unknown' } + }) + } + return + } + + // ignore `this` for computed functions + if (node.type === 'ThisExpression') { + return + } + + const computedFunction = computedCallNodes.find( + (c) => + c.range[0] <= node.range[0] && + node.range[1] <= c.range[1] && + targetBody === c.body + ) + if (!computedFunction) { + return + } + + const mem = node.parent + if (mem.object !== node) { + return + } + + const variable = findVariable(context.getScope(), node) + if (!variable || variable.defs.length !== 1) { + return + } + + const def = variable.defs[0] + if ( + def.type === 'ImplicitGlobalVariable' || + def.type === 'TDZ' || + def.type === 'ImportBinding' + ) { + return + } + + const isDeclaredInsideSetup = setupRanges.some( + ([start, end]) => + start <= def.node.range[0] && def.node.range[1] <= end + ) + if (!isDeclaredInsideSetup) { + return + } + + if ( + computedFunction.range[0] <= def.node.range[0] && + def.node.range[1] <= computedFunction.range[1] + ) { + // mutating local variables are accepted + return + } + + const invalid = utils.findMutating(node) + if (invalid) { + context.report({ + node: invalid.node, + message: 'Unexpected side effect in computed function.' + }) + } + } + } + const scriptSetupNode = utils.getScriptSetupElement(context) + if (scriptSetupNode) { + setupRanges.push(scriptSetupNode.range) + } + return utils.compositingVisitors( { Program() { const tracker = new ReferenceTracker(context.getScope()) @@ -80,120 +188,17 @@ module.exports = { } } }, - utils.defineVueVisitor(context, { - onVueObjectEnter(node) { - computedPropertiesMap.set(node, utils.getComputedProperties(node)) - }, - ':function': onFunctionEnter, - ':function:exit': onFunctionExit, - onSetupFunctionEnter(node) { - setupFunctions.push(node) - }, - - /** - * @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node - * @param {VueObjectData} data - */ - 'MemberExpression > :matches(Identifier, ThisExpression)'( - node, - { node: vueNode } - ) { - if (!scopeStack) { - return - } - const targetBody = scopeStack.body - - const computedProperty = /** @type {ComponentComputedProperty[]} */ ( - 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 - ) + scriptSetupNode + ? utils.defineScriptSetupVisitor(context, nodeVisitor) + : utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + computedPropertiesMap.set(node, utils.getComputedProperties(node)) + }, + onSetupFunctionEnter(node) { + setupRanges.push(node.body.range) + }, + ...nodeVisitor }) - if (computedProperty) { - if (!utils.isThis(node, context)) { - return - } - const mem = node.parent - if (mem.object !== node) { - return - } - - const invalid = utils.findMutating(mem) - if (invalid) { - context.report({ - node: invalid.node, - message: - 'Unexpected side effect in "{{key}}" computed property.', - data: { key: computedProperty.key || 'Unknown' } - }) - } - return - } - - // ignore `this` for computed functions - if (node.type === 'ThisExpression') { - return - } - - const computedFunction = computedCallNodes.find( - (c) => - node.loc.start.line >= c.loc.start.line && - node.loc.end.line <= c.loc.end.line && - targetBody === c.body - ) - if (!computedFunction) { - return - } - - const mem = node.parent - if (mem.object !== node) { - return - } - - const variable = findVariable(context.getScope(), node) - if (!variable || variable.defs.length !== 1) { - return - } - - const def = variable.defs[0] - if ( - def.type === 'ImplicitGlobalVariable' || - def.type === 'TDZ' || - def.type === 'ImportBinding' - ) { - return - } - - const isDeclaredInsideSetup = setupFunctions.some( - (setupFn) => - def.node.loc.start.line >= setupFn.loc.start.line && - def.node.loc.end.line <= setupFn.loc.end.line - ) - if (!isDeclaredInsideSetup) { - return - } - - if ( - def.node.loc.start.line >= computedFunction.loc.start.line && - def.node.loc.end.line <= computedFunction.loc.end.line - ) { - // mutating local variables are accepted - return - } - - const invalid = utils.findMutating(node) - if (invalid) { - context.report({ - node: invalid.node, - message: 'Unexpected side effect in computed function.' - }) - } - } - }) ) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index 5ecda2868..0e6bee93a 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -908,6 +908,12 @@ module.exports = { * @param {RuleContext} context The ESLint rule context object. */ isScriptSetup, + /** + * Gets the element of ` + `, + errors: [ + { + line: 10, + message: 'Unexpected side effect in computed function.' + }, + { + line: 11, + message: 'Unexpected side effect in computed function.' + }, + { + line: 15, + message: 'Unexpected side effect in computed function.' + }, + { + line: 16, + message: 'Unexpected side effect in computed function.' + }, + { + line: 19, + message: 'Unexpected side effect in computed function.' + }, + { + line: 21, + message: 'Unexpected side effect in computed function.' + }, + { + line: 25, + message: 'Unexpected side effect in computed function.' + }, + { + line: 28, + message: 'Unexpected side effect in computed function.' + }, + { + line: 31, + message: 'Unexpected side effect in computed function.' + }, + { + line: 35, + message: 'Unexpected side effect in computed function.' + }, + { + line: 38, + message: 'Unexpected side effect in computed function.' + }, + { + line: 40, + message: 'Unexpected side effect in computed function.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + line: 6, + message: 'Unexpected side effect in computed function.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + line: 6, + message: 'Unexpected side effect in computed function.' + } + ] } ] })