diff --git a/docs/rules/no-ref-as-operand.md b/docs/rules/no-ref-as-operand.md index 506fb2b95..a195fdb1b 100644 --- a/docs/rules/no-ref-as-operand.md +++ b/docs/rules/no-ref-as-operand.md @@ -25,7 +25,7 @@ You must use `.value` to access the `Ref` value. import { ref } from 'vue' export default { - setup() { + setup(_props, { emit }) { const count = ref(0) const ok = ref(true) @@ -34,12 +34,14 @@ export default { count.value + 1 1 + count.value var msg = ok.value ? 'yes' : 'no' + emit('increment', count.value) /* ✗ BAD */ count++ count + 1 1 + count var msg = ok ? 'yes' : 'no' + emit('increment', count) return { count diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index 02b438631..db92f19e4 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -4,6 +4,7 @@ */ 'use strict' +const { findVariable } = require('@eslint-community/eslint-utils') const { extractRefObjectReferences } = require('../utils/ref-object-references') const utils = require('../utils') @@ -24,6 +25,40 @@ function isRefInit(data) { } return data.defineChain.includes(/** @type {any} */ (init)) } + +/** + * Get the callee member node from the given CallExpression + * @param {CallExpression} node CallExpression + */ +function getNameParamNode(node) { + const nameLiteralNode = node.arguments[0] + if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) { + const name = utils.getStringLiteralValue(nameLiteralNode) + if (name != null) { + return { name, loc: nameLiteralNode.loc } + } + } + + // cannot check + return null +} + +/** + * Get the callee member node from the given CallExpression + * @param {CallExpression} node CallExpression + */ +function getCalleeMemberNode(node) { + const callee = utils.skipChainExpression(node.callee) + + if (callee.type === 'MemberExpression') { + const name = utils.getStaticPropertyName(callee) + if (name) { + return { name, member: callee } + } + } + return null +} + module.exports = { meta: { type: 'suggestion', @@ -44,6 +79,22 @@ module.exports = { create(context) { /** @type {RefObjectReferences} */ let refReferences + const setupContexts = new Map() + + /** + * Collect identifier id + * @param {Identifier} node + * @param {Set} referenceIds + */ + function collectReferenceIds(node, referenceIds) { + const variable = findVariable(utils.getScope(context, node), node) + if (!variable) { + return + } + for (const reference of variable.references) { + referenceIds.add(reference.identifier) + } + } /** * @param {Identifier} node @@ -64,90 +115,213 @@ module.exports = { } }) } - return { - Program() { - refReferences = extractRefObjectReferences(context) - }, - // if (refValue) - /** @param {Identifier} node */ - 'IfStatement>Identifier'(node) { - reportIfRefWrapped(node) - }, - // switch (refValue) - /** @param {Identifier} node */ - 'SwitchStatement>Identifier'(node) { - reportIfRefWrapped(node) - }, - // -refValue, +refValue, !refValue, ~refValue, typeof refValue - /** @param {Identifier} node */ - 'UnaryExpression>Identifier'(node) { - reportIfRefWrapped(node) - }, - // refValue++, refValue-- - /** @param {Identifier} node */ - 'UpdateExpression>Identifier'(node) { - reportIfRefWrapped(node) - }, - // refValue+1, refValue-1 - /** @param {Identifier} node */ - 'BinaryExpression>Identifier'(node) { + + /** + * @param {CallExpression} node + */ + function reportWrappedIdentifiers(node) { + const nodes = node.arguments.filter((node) => node.type === 'Identifier') + for (const node of nodes) { reportIfRefWrapped(node) - }, - // refValue+=1, refValue-=1, foo+=refValue, foo-=refValue - /** @param {Identifier & {parent: AssignmentExpression}} node */ - 'AssignmentExpression>Identifier'(node) { - if (node.parent.operator === '=' && node.parent.left !== node) { + } + } + + const programNode = context.getSourceCode().ast + + const callVisitor = { + /** + * @param {CallExpression} node + * @param {import('../utils').VueObjectData} [info] + */ + CallExpression(node, info) { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { + // cannot check return } - reportIfRefWrapped(node) - }, - // refValue || other, refValue && other. ignore: other || refValue - /** @param {Identifier & {parent: LogicalExpression}} node */ - 'LogicalExpression>Identifier'(node) { - if (node.parent.left !== node) { + + // verify setup context + const setupContext = setupContexts.get(info ? info.node : programNode) + if (!setupContext) { return } - // Report only constants. - const data = refReferences.get(node) + + const { contextReferenceIds, emitReferenceIds } = setupContext if ( - !data || - !data.variableDeclaration || - data.variableDeclaration.kind !== 'const' + node.callee.type === 'Identifier' && + emitReferenceIds.has(node.callee) ) { - return + // verify setup(props,{emit}) {emit()} + reportWrappedIdentifiers(node) + } else { + const emit = getCalleeMemberNode(node) + if ( + emit && + emit.name === 'emit' && + emit.member.object.type === 'Identifier' && + contextReferenceIds.has(emit.member.object) + ) { + // verify setup(props,context) {context.emit()} + reportWrappedIdentifiers(node) + } } - reportIfRefWrapped(node) - }, - // refValue ? x : y - /** @param {Identifier & {parent: ConditionalExpression}} node */ - 'ConditionalExpression>Identifier'(node) { - if (node.parent.test !== node) { - return + } + } + + return utils.compositingVisitors( + { + Program() { + refReferences = extractRefObjectReferences(context) + }, + // if (refValue) + /** @param {Identifier} node */ + 'IfStatement>Identifier'(node) { + reportIfRefWrapped(node) + }, + // switch (refValue) + /** @param {Identifier} node */ + 'SwitchStatement>Identifier'(node) { + reportIfRefWrapped(node) + }, + // -refValue, +refValue, !refValue, ~refValue, typeof refValue + /** @param {Identifier} node */ + 'UnaryExpression>Identifier'(node) { + reportIfRefWrapped(node) + }, + // refValue++, refValue-- + /** @param {Identifier} node */ + 'UpdateExpression>Identifier'(node) { + reportIfRefWrapped(node) + }, + // refValue+1, refValue-1 + /** @param {Identifier} node */ + 'BinaryExpression>Identifier'(node) { + reportIfRefWrapped(node) + }, + // refValue+=1, refValue-=1, foo+=refValue, foo-=refValue + /** @param {Identifier & {parent: AssignmentExpression}} node */ + 'AssignmentExpression>Identifier'(node) { + if (node.parent.operator === '=' && node.parent.left !== node) { + return + } + reportIfRefWrapped(node) + }, + // refValue || other, refValue && other. ignore: other || refValue + /** @param {Identifier & {parent: LogicalExpression}} node */ + 'LogicalExpression>Identifier'(node) { + if (node.parent.left !== node) { + return + } + // Report only constants. + const data = refReferences.get(node) + if ( + !data || + !data.variableDeclaration || + data.variableDeclaration.kind !== 'const' + ) { + return + } + reportIfRefWrapped(node) + }, + // refValue ? x : y + /** @param {Identifier & {parent: ConditionalExpression}} node */ + 'ConditionalExpression>Identifier'(node) { + if (node.parent.test !== node) { + return + } + reportIfRefWrapped(node) + }, + // `${refValue}` + /** @param {Identifier} node */ + 'TemplateLiteral>Identifier'(node) { + reportIfRefWrapped(node) + }, + // refValue.x + /** @param {Identifier & {parent: MemberExpression}} node */ + 'MemberExpression>Identifier'(node) { + if (node.parent.object !== node) { + return + } + const name = utils.getStaticPropertyName(node.parent) + if ( + name === 'value' || + name == null || + // WritableComputedRef + name === 'effect' + ) { + return + } + reportIfRefWrapped(node) } - reportIfRefWrapped(node) }, - // `${refValue}` - /** @param {Identifier} node */ - 'TemplateLiteral>Identifier'(node) { - reportIfRefWrapped(node) - }, - // refValue.x - /** @param {Identifier & {parent: MemberExpression}} node */ - 'MemberExpression>Identifier'(node) { - if (node.parent.object !== node) { - return - } - const name = utils.getStaticPropertyName(node.parent) - if ( - name === 'value' || - name == null || - // WritableComputedRef - name === 'effect' - ) { - return + utils.defineScriptSetupVisitor(context, { + onDefineEmitsEnter(node) { + if ( + !node.parent || + node.parent.type !== 'VariableDeclarator' || + node.parent.init !== node + ) { + return + } + + const emitParam = node.parent.id + if (emitParam.type !== 'Identifier') { + return + } + + // const emit = defineEmits() + const emitReferenceIds = new Set() + collectReferenceIds(emitParam, emitReferenceIds) + + setupContexts.set(programNode, { + contextReferenceIds: new Set(), + emitReferenceIds + }) + }, + ...callVisitor + }), + utils.defineVueVisitor(context, { + onSetupFunctionEnter(node, { node: vueNode }) { + const contextParam = utils.skipDefaultParamValue(node.params[1]) + if (!contextParam) { + // no arguments + return + } + if ( + contextParam.type === 'RestElement' || + contextParam.type === 'ArrayPattern' + ) { + // cannot check + return + } + + const contextReferenceIds = new Set() + const emitReferenceIds = new Set() + if (contextParam.type === 'ObjectPattern') { + const emitProperty = utils.findAssignmentProperty( + contextParam, + 'emit' + ) + if (!emitProperty || emitProperty.value.type !== 'Identifier') { + return + } + + // `setup(props, {emit})` + collectReferenceIds(emitProperty.value, emitReferenceIds) + } else { + // `setup(props, context)` + collectReferenceIds(contextParam, contextReferenceIds) + } + setupContexts.set(vueNode, { + contextReferenceIds, + emitReferenceIds + }) + }, + ...callVisitor, + onVueObjectExit(node) { + setupContexts.delete(node) } - reportIfRefWrapped(node) - } - } + }) + ) } } diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index d57356356..04234eff7 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -191,6 +191,106 @@ tester.run('no-ref-as-operand', rule, { model.value = value; } + `, + ` + + `, + ` + + `, + ` + + `, + ` + + `, + ` + + `, + ` + ` ], invalid: [ @@ -823,6 +923,176 @@ tester.run('no-ref-as-operand', rule, { } ] }, + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 8, + endLine: 8 + } + ] + }, + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 10, + endLine: 10 + } + ] + }, + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 10, + endLine: 10 + } + ] + }, + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 10, + endLine: 10 + } + ] + }, // Auto-import { code: `