-
-
Notifications
You must be signed in to change notification settings - Fork 680
Add composition api's computed function support to vue/no-side-effects-in-computed-properties close #1393 #1407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ | |
* @author Michał Sajnóg | ||
*/ | ||
'use strict' | ||
|
||
const { ReferenceTracker, findVariable } = require('eslint-utils') | ||
const utils = require('../utils') | ||
|
||
/** | ||
|
@@ -31,6 +31,8 @@ module.exports = { | |
create(context) { | ||
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */ | ||
const computedPropertiesMap = new Map() | ||
/** @type {Array<FunctionExpression | ArrowFunctionExpression>} */ | ||
const computedCallNodes = [] | ||
|
||
/** | ||
* @typedef {object} ScopeStack | ||
|
@@ -54,56 +56,130 @@ module.exports = { | |
scopeStack = scopeStack && scopeStack.upper | ||
} | ||
|
||
return utils.defineVueVisitor(context, { | ||
onVueObjectEnter(node) { | ||
computedPropertiesMap.set(node, utils.getComputedProperties(node)) | ||
}, | ||
':function': onFunctionEnter, | ||
':function:exit': onFunctionExit, | ||
|
||
/** | ||
* @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 | ||
) | ||
}) | ||
if (!computedProperty) { | ||
return | ||
} | ||
return Object.assign( | ||
{ | ||
Program() { | ||
const tracker = new ReferenceTracker(context.getScope()) | ||
const traceMap = utils.createCompositionApiTraceMap({ | ||
[ReferenceTracker.ESM]: true, | ||
computed: { | ||
[ReferenceTracker.CALL]: true | ||
} | ||
}) | ||
|
||
if (!utils.isThis(node, context)) { | ||
return | ||
} | ||
const mem = node.parent | ||
if (mem.object !== node) { | ||
return | ||
for (const { node } of tracker.iterateEsmReferences(traceMap)) { | ||
if (node.type !== 'CallExpression') { | ||
continue | ||
} | ||
|
||
const getterBody = utils.getGetterBodyFromComputedFunction(node) | ||
if (getterBody) { | ||
computedCallNodes.push(getterBody) | ||
} | ||
} | ||
} | ||
}, | ||
utils.defineVueVisitor(context, { | ||
onVueObjectEnter(node) { | ||
computedPropertiesMap.set(node, utils.getComputedProperties(node)) | ||
}, | ||
':function': onFunctionEnter, | ||
':function:exit': onFunctionExit, | ||
|
||
/** | ||
* @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node | ||
* @param {VueObjectData} data | ||
*/ | ||
'MemberExpression > :matches(Identifier, ThisExpression)'( | ||
node, | ||
{ node: vueNode } | ||
) { | ||
if (!scopeStack) { | ||
return | ||
} | ||
const targetBody = scopeStack.body | ||
|
||
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' } | ||
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 | ||
) | ||
}) | ||
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 | ||
sapphi-red marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
if (!computedFunction) { | ||
return | ||
} | ||
|
||
const mem = node.parent | ||
if (mem.object !== node) { | ||
return | ||
} | ||
|
||
const variable = findVariable(context.getScope(), node) | ||
if (variable) { | ||
if (variable.defs.length !== 1) { | ||
return | ||
} | ||
|
||
const def = variable.defs[0] | ||
if ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's better to report only the variables defined in the e.g. const myUtils = { ... } // My utilities
export default {
setup(props) {
const array = reactive([])
const foo = computed(() => myUtils.reverse(array)) // OK Build an array in reverse order.
const bar = computed(() => array.reverse()) // NG
const baz = computed(() => myUtils.reverse(props.array)) // OK Build an array in reverse order.
const qux = computed(() => props.array.reverse()) // NG
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the problem only lies with the CallExpressions. const myUtils = {}
myUtils.reverse() // this is ok, since it is unknown whether it is destructive
myUtils = null // this is a mutation
myUtils.count++ // this is a mutation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main purpose of this rule is to prevent a reactive loop due to mutation. Therefore, I don't think variables that are unlikely to be reactive should be reported. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. Thank you for answering! |
||
def.type !== 'ImplicitGlobalVariable' && | ||
def.type !== 'TDZ' && | ||
def.type !== 'ImportBinding' | ||
) { | ||
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(mem) | ||
if (invalid) { | ||
context.report({ | ||
node: invalid.node, | ||
message: 'Unexpected side effect in computed function.' | ||
}) | ||
} | ||
} | ||
} | ||
}) | ||
}) | ||
) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.