From dabbc13472a8069126d300398d3d77c57063fca4 Mon Sep 17 00:00:00 2001 From: chouchouji <70570907+chouchouji@users.noreply.github.com> Date: Mon, 10 Feb 2025 05:17:37 +0000 Subject: [PATCH 1/8] feat(no-ref-as-operand): adapt to emit --- lib/rules/no-ref-as-operand.js | 350 +++++++++++++++++++++++++-------- 1 file changed, 270 insertions(+), 80 deletions(-) diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index 02b438631..24fa2cc83 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,7 @@ module.exports = { create(context) { /** @type {RefObjectReferences} */ let refReferences + const setupContexts = new Map() /** * @param {Identifier} node @@ -64,90 +100,244 @@ 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) { - 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) { + + 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 } - const name = utils.getStaticPropertyName(node.parent) - if ( - name === 'value' || - name == null || - // WritableComputedRef - name === 'effect' - ) { - return + + // verify setup context + const setupContext = setupContexts.get(info ? info.node : programNode) + if (setupContext) { + const { contextReferenceIds, emitReferenceIds } = setupContext + if ( + node.callee.type === 'Identifier' && + emitReferenceIds.has(node.callee) + ) { + // verify setup(props,{emit}) {emit()} + node.arguments + .filter( + (node) => + node.type === 'Identifier' && + isRefInit(refReferences.get(node)) + ) + .forEach((node) => { + reportIfRefWrapped(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()} + emit.member.parent.arguments + .filter( + (node) => + node.type === 'Identifier' && + isRefInit(refReferences.get(node)) + ) + .forEach((node) => { + reportIfRefWrapped(node) + }) + } + } } - reportIfRefWrapped(node) } } + + 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) + } + }, + utils.compositingVisitors( + 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 variable = findVariable( + utils.getScope(context, emitParam), + emitParam + ) + if (!variable) { + return + } + const emitReferenceIds = new Set() + for (const reference of variable.references) { + emitReferenceIds.add(reference.identifier) + } + 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 + } + const emitParam = emitProperty.value + // `setup(props, {emit})` + const variable = findVariable( + utils.getScope(context, emitParam), + emitParam + ) + if (!variable) { + return + } + for (const reference of variable.references) { + emitReferenceIds.add(reference.identifier) + } + } else { + // `setup(props, context)` + const variable = findVariable( + utils.getScope(context, contextParam), + contextParam + ) + if (!variable) { + return + } + for (const reference of variable.references) { + contextReferenceIds.add(reference.identifier) + } + } + setupContexts.set(vueNode, { + contextReferenceIds, + emitReferenceIds + }) + }, + ...callVisitor, + onVueObjectExit(node) { + setupContexts.delete(node) + } + }) + ) + ) } } From 7cae716aee79c9c8c30165b76f65fb2f28f0cf05 Mon Sep 17 00:00:00 2001 From: chouchouji <70570907+chouchouji@users.noreply.github.com> Date: Mon, 10 Feb 2025 06:06:44 +0000 Subject: [PATCH 2/8] test(no-ref-as-operand): add test cases --- tests/lib/rules/no-ref-as-operand.js | 170 +++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index d57356356..2bed15ecf 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -191,7 +191,53 @@ tester.run('no-ref-as-operand', rule, { model.value = value; } + `, + ` + + `, ` + + `, + ` + + `, ], invalid: [ { @@ -823,6 +869,130 @@ 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, + }, + ] + }, // Auto-import { code: ` From 4e137053d06831607fb300ceff80e6de07d1d17b Mon Sep 17 00:00:00 2001 From: chouchouji <70570907+chouchouji@users.noreply.github.com> Date: Mon, 10 Feb 2025 06:19:36 +0000 Subject: [PATCH 3/8] refactor: optimize code --- lib/rules/no-ref-as-operand.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index 24fa2cc83..1a0d6ac4f 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -125,11 +125,7 @@ module.exports = { ) { // verify setup(props,{emit}) {emit()} node.arguments - .filter( - (node) => - node.type === 'Identifier' && - isRefInit(refReferences.get(node)) - ) + .filter((node) => node.type === 'Identifier') .forEach((node) => { reportIfRefWrapped(node) }) @@ -143,11 +139,7 @@ module.exports = { ) { // verify setup(props,context) {context.emit()} emit.member.parent.arguments - .filter( - (node) => - node.type === 'Identifier' && - isRefInit(refReferences.get(node)) - ) + .filter((node) => node.type === 'Identifier') .forEach((node) => { reportIfRefWrapped(node) }) From c9bbc183816638c4bf0df66950d38bb674fce77c Mon Sep 17 00:00:00 2001 From: chouchouji <70570907+chouchouji@users.noreply.github.com> Date: Mon, 10 Feb 2025 06:31:51 +0000 Subject: [PATCH 4/8] refactor: optimize code --- lib/rules/no-ref-as-operand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index 1a0d6ac4f..f3b42eaa9 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -138,7 +138,7 @@ module.exports = { contextReferenceIds.has(emit.member.object) ) { // verify setup(props,context) {context.emit()} - emit.member.parent.arguments + node.arguments .filter((node) => node.type === 'Identifier') .forEach((node) => { reportIfRefWrapped(node) From 8be196179cbc92ad50f0ee5c3b7186d41f7ce9e6 Mon Sep 17 00:00:00 2001 From: chouchouji <70570907+chouchouji@users.noreply.github.com> Date: Mon, 10 Feb 2025 06:37:46 +0000 Subject: [PATCH 5/8] test(no-ref-as-operand): add test cases --- tests/lib/rules/no-ref-as-operand.js | 100 +++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index 2bed15ecf..f348e9b3a 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -238,6 +238,60 @@ tester.run('no-ref-as-operand', rule, { }) `, + ` + + `, + ` + + `, + ` + + `, ], invalid: [ { @@ -993,6 +1047,52 @@ tester.run('no-ref-as-operand', rule, { }, ] }, + { + code: ` + + `, + output:` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 10, + endLine: 10, + }, + ] + }, // Auto-import { code: ` From 78c21d263fd345adf18a700cf888538902af402f Mon Sep 17 00:00:00 2001 From: chouchouji <70570907+chouchouji@users.noreply.github.com> Date: Mon, 10 Feb 2025 06:50:04 +0000 Subject: [PATCH 6/8] docs(no-ref-as-operand): update docs --- docs/rules/no-ref-as-operand.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From c2be9f1aee5a4aecd816dd237562879ec639c9a6 Mon Sep 17 00:00:00 2001 From: chouchouji <1305974212@qq.com> Date: Thu, 13 Feb 2025 22:16:02 +0800 Subject: [PATCH 7/8] refactor: optimize code --- lib/rules/no-ref-as-operand.js | 176 +++++++++++++-------------- tests/lib/rules/no-ref-as-operand.js | 24 ++-- 2 files changed, 100 insertions(+), 100 deletions(-) diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index f3b42eaa9..e7c9a4a76 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -124,11 +124,12 @@ module.exports = { emitReferenceIds.has(node.callee) ) { // verify setup(props,{emit}) {emit()} - node.arguments - .filter((node) => node.type === 'Identifier') - .forEach((node) => { - reportIfRefWrapped(node) - }) + const nodes = node.arguments.filter( + (node) => node.type === 'Identifier' + ) + for (const node of nodes) { + reportIfRefWrapped(node) + } } else { const emit = getCalleeMemberNode(node) if ( @@ -138,11 +139,12 @@ module.exports = { contextReferenceIds.has(emit.member.object) ) { // verify setup(props,context) {context.emit()} - node.arguments - .filter((node) => node.type === 'Identifier') - .forEach((node) => { - reportIfRefWrapped(node) - }) + const nodes = node.arguments.filter( + (node) => node.type === 'Identifier' + ) + for (const node of nodes) { + reportIfRefWrapped(node) + } } } } @@ -235,23 +237,66 @@ module.exports = { reportIfRefWrapped(node) } }, - utils.compositingVisitors( - utils.defineScriptSetupVisitor(context, { - onDefineEmitsEnter(node) { - if ( - !node.parent || - node.parent.type !== 'VariableDeclarator' || - node.parent.init !== node - ) { - 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 emitParam = node.parent.id - if (emitParam.type !== 'Identifier') { + // const emit = defineEmits() + const variable = findVariable( + utils.getScope(context, emitParam), + emitParam + ) + if (!variable) { + return + } + const emitReferenceIds = new Set() + for (const reference of variable.references) { + emitReferenceIds.add(reference.identifier) + } + 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 } - - // const emit = defineEmits() + const emitParam = emitProperty.value + // `setup(props, {emit})` const variable = findVariable( utils.getScope(context, emitParam), emitParam @@ -259,77 +304,32 @@ module.exports = { if (!variable) { return } - const emitReferenceIds = new Set() for (const reference of variable.references) { emitReferenceIds.add(reference.identifier) } - 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 + } else { + // `setup(props, context)` + const variable = findVariable( + utils.getScope(context, contextParam), + contextParam + ) + if (!variable) { 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 - } - const emitParam = emitProperty.value - // `setup(props, {emit})` - const variable = findVariable( - utils.getScope(context, emitParam), - emitParam - ) - if (!variable) { - return - } - for (const reference of variable.references) { - emitReferenceIds.add(reference.identifier) - } - } else { - // `setup(props, context)` - const variable = findVariable( - utils.getScope(context, contextParam), - contextParam - ) - if (!variable) { - return - } - for (const reference of variable.references) { - contextReferenceIds.add(reference.identifier) - } + for (const reference of variable.references) { + contextReferenceIds.add(reference.identifier) } - setupContexts.set(vueNode, { - contextReferenceIds, - emitReferenceIds - }) - }, - ...callVisitor, - onVueObjectExit(node) { - setupContexts.delete(node) } - }) - ) + setupContexts.set(vueNode, { + contextReferenceIds, + emitReferenceIds + }) + }, + ...callVisitor, + onVueObjectExit(node) { + setupContexts.delete(node) + } + }) ) } } diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index f348e9b3a..04234eff7 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -291,7 +291,7 @@ tester.run('no-ref-as-operand', rule, { } }) - `, + ` ], invalid: [ { @@ -951,8 +951,8 @@ tester.run('no-ref-as-operand', rule, { message: 'Must use `.value` to read or write the value wrapped by `ref()`.', line: 8, - endLine: 8, - }, + endLine: 8 + } ] }, { @@ -974,7 +974,7 @@ tester.run('no-ref-as-operand', rule, { }) `, - output:` + output: ` `, - output:` + output: ` `, - output:` + output: `