From 6f34b25b04145a8968b176ac8f76f86808edddbc Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 11 Mar 2024 13:08:16 +0100 Subject: [PATCH 1/5] wip: support `detectAutoImport` option --- lib/rules/no-async-in-computed-properties.js | 30 +++++++--- lib/rules/no-lifecycle-after-await.js | 15 +++-- lib/rules/no-ref-as-operand.js | 15 ++++- lib/rules/no-ref-object-reactivity-loss.js | 18 +++++- .../no-side-effects-in-computed-properties.js | 32 ++++++++--- lib/rules/no-watch-after-await.js | 26 ++++++--- lib/rules/return-in-computed-property.js | 14 ++++- lib/utils/index.js | 24 ++++++++ lib/utils/property-references.js | 17 ++++-- lib/utils/ref-object-references.js | 56 +++++++++++-------- tests/lib/rules/no-ref-as-operand.js | 46 +++++++++++++++ 11 files changed, 226 insertions(+), 67 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 6ee4373f1..d6f0eeb43 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -85,7 +85,17 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html' }, fixable: null, - schema: [], + schema: [ + { + type: 'object', + properties: { + detectAutoImport: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], messages: { unexpectedInFunction: 'Unexpected {{expressionName}} in computed function.', @@ -263,14 +273,18 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const traceMap = utils.createCompositionApiTraceMap({ - [ReferenceTracker.ESM]: true, - computed: { - [ReferenceTracker.CALL]: true - } - }) + const detectAutoImport = !!context.options?.[0]?.detectAutoImport - for (const { node } of tracker.iterateEsmReferences(traceMap)) { + for (const { node } of utils.iterateReferencesTraceMap( + tracker, + { + [ReferenceTracker.ESM]: true, + computed: { + [ReferenceTracker.CALL]: true + } + }, + detectAutoImport + )) { if (node.type !== 'CallExpression') { continue } diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js index 21416f893..18999dc67 100644 --- a/lib/rules/no-lifecycle-after-await.js +++ b/lib/rules/no-lifecycle-after-await.js @@ -63,19 +63,22 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) + const detectAutoImport = !!context.options?.[0]?.detectAutoImport + /** @type {TraceMap} */ const traceMap = { - /** @type {TraceMap} */ - vue: { - [ReferenceTracker.ESM]: true - } + [ReferenceTracker.ESM]: true } for (const lifecycleHook of LIFECYCLE_HOOKS) { - traceMap.vue[lifecycleHook] = { + traceMap[lifecycleHook] = { [ReferenceTracker.CALL]: true } } - for (const { node } of tracker.iterateEsmReferences(traceMap)) { + for (const { node } of utils.iterateReferencesTraceMap( + tracker, + traceMap, + detectAutoImport + )) { lifecycleHookCallNodes.add(node) } } diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index 9661b2b60..ac69dc167 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -34,7 +34,17 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-ref-as-operand.html' }, fixable: 'code', - schema: [], + schema: [ + { + type: 'object', + properties: { + detectAutoImport: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], messages: { requireDotValue: 'Must use `.value` to read or write the value wrapped by `{{method}}()`.' @@ -66,7 +76,8 @@ module.exports = { } return { Program() { - refReferences = extractRefObjectReferences(context) + const autoImport = !!context.options?.[0]?.detectAutoImport + refReferences = extractRefObjectReferences(context, autoImport) }, // if (refValue) /** @param {Identifier} node */ diff --git a/lib/rules/no-ref-object-reactivity-loss.js b/lib/rules/no-ref-object-reactivity-loss.js index 8f61804b9..e1d85ef1e 100644 --- a/lib/rules/no-ref-object-reactivity-loss.js +++ b/lib/rules/no-ref-object-reactivity-loss.js @@ -54,7 +54,17 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-ref-object-reactivity-loss.html' }, fixable: null, - schema: [], + schema: [ + { + type: 'object', + properties: { + detectAutoImport: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], messages: { getValueInSameScope: 'Getting a value from the ref object in the same scope will cause the value to lose reactivity.', @@ -76,8 +86,12 @@ module.exports = { let scopeStack = { upper: null, node: context.getSourceCode().ast } /** @type {Map} */ const scopes = new Map() + const detectAutoImport = !!context.options?.[0]?.detectAutoImport - const refObjectReferences = extractRefObjectReferences(context) + const refObjectReferences = extractRefObjectReferences( + context, + detectAutoImport + ) const reactiveVariableReferences = extractReactiveVariableReferences(context) diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js index 76127c37f..81964f87f 100644 --- a/lib/rules/no-side-effects-in-computed-properties.js +++ b/lib/rules/no-side-effects-in-computed-properties.js @@ -24,7 +24,17 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-side-effects-in-computed-properties.html' }, fixable: null, - schema: [], + schema: [ + { + type: 'object', + properties: { + detectAutoImport: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], messages: { unexpectedSideEffectInFunction: 'Unexpected side effect in computed function.', @@ -183,14 +193,18 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const traceMap = utils.createCompositionApiTraceMap({ - [ReferenceTracker.ESM]: true, - computed: { - [ReferenceTracker.CALL]: true - } - }) - - for (const { node } of tracker.iterateEsmReferences(traceMap)) { + const detectAutoImport = !!context.options?.[0]?.detectAutoImport + + for (const { node } of utils.iterateReferencesTraceMap( + tracker, + { + [ReferenceTracker.ESM]: true, + computed: { + [ReferenceTracker.CALL]: true + } + }, + detectAutoImport + )) { if (node.type !== 'CallExpression') { continue } diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js index cbe1772bd..6f77ee40c 100644 --- a/lib/rules/no-watch-after-await.js +++ b/lib/rules/no-watch-after-await.js @@ -49,7 +49,17 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-watch-after-await.html' }, fixable: null, - schema: [], + schema: [ + { + type: 'object', + properties: { + detectAutoImport: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], messages: { forbidden: '`watch` is forbidden after an `await` expression.' } @@ -78,8 +88,11 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const traceMap = { - vue: { + const detectAutoImport = !!context.options?.[0]?.detectAutoImport + + for (const { node } of utils.iterateReferencesTraceMap( + tracker, + { [ReferenceTracker.ESM]: true, watch: { [ReferenceTracker.CALL]: true @@ -87,10 +100,9 @@ module.exports = { watchEffect: { [ReferenceTracker.CALL]: true } - } - } - - for (const { node } of tracker.iterateEsmReferences(traceMap)) { + }, + detectAutoImport + )) { watchCallNodes.add(node) } } diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js index 54d2553a1..10ffc27b7 100644 --- a/lib/rules/return-in-computed-property.js +++ b/lib/rules/return-in-computed-property.js @@ -26,6 +26,9 @@ module.exports = { properties: { treatUndefinedAsUnspecified: { type: 'boolean' + }, + detectAutoImport: { + type: 'boolean' } }, additionalProperties: false @@ -57,14 +60,19 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const traceMap = utils.createCompositionApiTraceMap({ + const map = { [ReferenceTracker.ESM]: true, computed: { [ReferenceTracker.CALL]: true } - }) + } - for (const { node } of tracker.iterateEsmReferences(traceMap)) { + const detectAutoImport = !!context.options?.[0]?.detectAutoImport + for (const { node } of utils.iterateReferencesTraceMap( + tracker, + map, + detectAutoImport + )) { if (node.type !== 'CallExpression') { continue } diff --git a/lib/utils/index.js b/lib/utils/index.js index 6773611cb..03a6d9a5c 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -2112,6 +2112,30 @@ module.exports = { '@vue/composition-api': map }), + /** + * Iterates all references in the given trace map. + * Take the third argument option to detect auto-imported references. + * + * @param {import('@eslint-community/eslint-utils').ReferenceTracker} tracker + * @param {import('@eslint-community/eslint-utils').TYPES.TraceMap} map + * @param {boolean} detectAutoImport + * @returns {ReturnType} + */ + *iterateReferencesTraceMap(tracker, map, detectAutoImport) { + const traceMap = this.createCompositionApiTraceMap(map) + + if (detectAutoImport) { + for (const ref of tracker.iterateGlobalReferences(traceMap)) { + yield ref + } + for (const ref of tracker.iterateEsmReferences(map)) { + yield ref + } + } else { + return tracker.iterateEsmReferences(traceMap) + } + }, + /** * Checks whether or not the tokens of two given nodes are same. * @param {ASTNode} left A node 1 to compare. diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js index 1ed26640f..effad9684 100644 --- a/lib/utils/property-references.js +++ b/lib/utils/property-references.js @@ -119,24 +119,29 @@ function definePropertyReferenceExtractor( context.getSourceCode().scopeManager.scopes[0] ) const toRefNodes = new Set() - for (const { node } of tracker.iterateEsmReferences( - utils.createCompositionApiTraceMap({ + const detectAutoImport = !!context.options?.[0]?.detectAutoImport + for (const { node } of utils.iterateReferencesTraceMap( + tracker, + { [eslintUtils.ReferenceTracker.ESM]: true, toRef: { [eslintUtils.ReferenceTracker.CALL]: true } - }) + }, + detectAutoImport )) { toRefNodes.add(node) } const toRefsNodes = new Set() - for (const { node } of tracker.iterateEsmReferences( - utils.createCompositionApiTraceMap({ + for (const { node } of utils.iterateReferencesTraceMap( + tracker, + { [eslintUtils.ReferenceTracker.ESM]: true, toRefs: { [eslintUtils.ReferenceTracker.CALL]: true } - }) + }, + detectAutoImport )) { toRefsNodes.add(node) } diff --git a/lib/utils/ref-object-references.js b/lib/utils/ref-object-references.js index 538fbb08f..dc21a9005 100644 --- a/lib/utils/ref-object-references.js +++ b/lib/utils/ref-object-references.js @@ -78,32 +78,36 @@ const cacheForReactiveVariableReferences = new WeakMap() /** * Iterate the call expressions that define the ref object. * @param {import('eslint').Scope.Scope} globalScope + * @param {boolean} detectAutoImport * @returns {Iterable<{ node: CallExpression, name: string }>} */ -function* iterateDefineRefs(globalScope) { +function* iterateDefineRefs(globalScope, detectAutoImport) { const tracker = new ReferenceTracker(globalScope) - const traceMap = utils.createCompositionApiTraceMap({ - [ReferenceTracker.ESM]: true, - ref: { - [ReferenceTracker.CALL]: true - }, - computed: { - [ReferenceTracker.CALL]: true - }, - toRef: { - [ReferenceTracker.CALL]: true - }, - customRef: { - [ReferenceTracker.CALL]: true - }, - shallowRef: { - [ReferenceTracker.CALL]: true + for (const { node, path } of utils.iterateReferencesTraceMap( + tracker, + { + [ReferenceTracker.ESM]: true, + ref: { + [ReferenceTracker.CALL]: true + }, + computed: { + [ReferenceTracker.CALL]: true + }, + toRef: { + [ReferenceTracker.CALL]: true + }, + customRef: { + [ReferenceTracker.CALL]: true + }, + shallowRef: { + [ReferenceTracker.CALL]: true + }, + toRefs: { + [ReferenceTracker.CALL]: true + } }, - toRefs: { - [ReferenceTracker.CALL]: true - } - }) - for (const { node, path } of tracker.iterateEsmReferences(traceMap)) { + detectAutoImport + )) { const expr = /** @type {CallExpression} */ (node) yield { node: expr, @@ -531,9 +535,10 @@ class RefObjectReferenceExtractor { /** * Extracts references of all ref objects. * @param {RuleContext} context The rule context. + * @param {boolean} detectAutoImport * @returns {RefObjectReferences} */ -function extractRefObjectReferences(context) { +function extractRefObjectReferences(context, detectAutoImport = false) { const sourceCode = context.getSourceCode() const cachedReferences = cacheForRefObjectReferences.get(sourceCode.ast) if (cachedReferences) { @@ -542,7 +547,10 @@ function extractRefObjectReferences(context) { const references = new RefObjectReferenceExtractor(context) const globalScope = getGlobalScope(context) - for (const { node, name } of iterateDefineRefs(globalScope)) { + for (const { node, name } of iterateDefineRefs( + globalScope, + detectAutoImport + )) { references.processDefineRef(node, name) } for (const { node } of iterateDefineModels(globalScope)) { diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index f08a9e1c2..477461bc3 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -822,6 +822,52 @@ tester.run('no-ref-as-operand', rule, { line: 9 } ] + }, + // Auto-import + { + code: ` + let count = ref(0) + + count++ // error + console.log(count + 1) // error + console.log(1 + count) // error + `, + output: ` + let count = ref(0) + + count.value++ // error + console.log(count.value + 1) // error + console.log(1 + count.value) // error + `, + options: { + detectAutoImport: true + }, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 4, + column: 7, + endLine: 4, + endColumn: 12 + }, + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 5, + column: 19, + endLine: 5, + endColumn: 24 + }, + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 6, + column: 23, + endLine: 6, + endColumn: 28 + } + ] } ] }) From 9e2a93a9725d2ce63ecd40a34a7a1c9f41e9429b Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 11 Mar 2024 13:51:04 +0100 Subject: [PATCH 2/5] chore: update --- lib/rules/no-async-in-computed-properties.js | 29 +++------- lib/rules/no-lifecycle-after-await.js | 8 +-- lib/rules/no-ref-as-operand.js | 15 +----- lib/rules/no-ref-object-reactivity-loss.js | 18 +------ .../no-side-effects-in-computed-properties.js | 30 +++-------- lib/rules/no-watch-after-await.js | 32 +++-------- lib/rules/return-in-computed-property.js | 8 +-- lib/utils/index.js | 33 ++++++------ lib/utils/property-references.js | 33 +++++------- lib/utils/ref-object-references.js | 54 ++++++++----------- tests/lib/rules/no-ref-as-operand.js | 4 +- 11 files changed, 82 insertions(+), 182 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index d6f0eeb43..afe9cd350 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -85,17 +85,7 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html' }, fixable: null, - schema: [ - { - type: 'object', - properties: { - detectAutoImport: { - type: 'boolean' - } - }, - additionalProperties: false - } - ], + schema: [], messages: { unexpectedInFunction: 'Unexpected {{expressionName}} in computed function.', @@ -273,18 +263,11 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const detectAutoImport = !!context.options?.[0]?.detectAutoImport - - for (const { node } of utils.iterateReferencesTraceMap( - tracker, - { - [ReferenceTracker.ESM]: true, - computed: { - [ReferenceTracker.CALL]: true - } - }, - detectAutoImport - )) { + for (const { node } of utils.iterateReferencesTraceMap(tracker, { + computed: { + [ReferenceTracker.CALL]: true + } + })) { if (node.type !== 'CallExpression') { continue } diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js index 18999dc67..c695ae420 100644 --- a/lib/rules/no-lifecycle-after-await.js +++ b/lib/rules/no-lifecycle-after-await.js @@ -63,11 +63,8 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const detectAutoImport = !!context.options?.[0]?.detectAutoImport /** @type {TraceMap} */ - const traceMap = { - [ReferenceTracker.ESM]: true - } + const traceMap = {} for (const lifecycleHook of LIFECYCLE_HOOKS) { traceMap[lifecycleHook] = { [ReferenceTracker.CALL]: true @@ -76,8 +73,7 @@ module.exports = { for (const { node } of utils.iterateReferencesTraceMap( tracker, - traceMap, - detectAutoImport + traceMap )) { lifecycleHookCallNodes.add(node) } diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index ac69dc167..9661b2b60 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -34,17 +34,7 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-ref-as-operand.html' }, fixable: 'code', - schema: [ - { - type: 'object', - properties: { - detectAutoImport: { - type: 'boolean' - } - }, - additionalProperties: false - } - ], + schema: [], messages: { requireDotValue: 'Must use `.value` to read or write the value wrapped by `{{method}}()`.' @@ -76,8 +66,7 @@ module.exports = { } return { Program() { - const autoImport = !!context.options?.[0]?.detectAutoImport - refReferences = extractRefObjectReferences(context, autoImport) + refReferences = extractRefObjectReferences(context) }, // if (refValue) /** @param {Identifier} node */ diff --git a/lib/rules/no-ref-object-reactivity-loss.js b/lib/rules/no-ref-object-reactivity-loss.js index e1d85ef1e..8f61804b9 100644 --- a/lib/rules/no-ref-object-reactivity-loss.js +++ b/lib/rules/no-ref-object-reactivity-loss.js @@ -54,17 +54,7 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-ref-object-reactivity-loss.html' }, fixable: null, - schema: [ - { - type: 'object', - properties: { - detectAutoImport: { - type: 'boolean' - } - }, - additionalProperties: false - } - ], + schema: [], messages: { getValueInSameScope: 'Getting a value from the ref object in the same scope will cause the value to lose reactivity.', @@ -86,12 +76,8 @@ module.exports = { let scopeStack = { upper: null, node: context.getSourceCode().ast } /** @type {Map} */ const scopes = new Map() - const detectAutoImport = !!context.options?.[0]?.detectAutoImport - const refObjectReferences = extractRefObjectReferences( - context, - detectAutoImport - ) + const refObjectReferences = extractRefObjectReferences(context) const reactiveVariableReferences = extractReactiveVariableReferences(context) diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js index 81964f87f..b0fbfc3d9 100644 --- a/lib/rules/no-side-effects-in-computed-properties.js +++ b/lib/rules/no-side-effects-in-computed-properties.js @@ -24,17 +24,7 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-side-effects-in-computed-properties.html' }, fixable: null, - schema: [ - { - type: 'object', - properties: { - detectAutoImport: { - type: 'boolean' - } - }, - additionalProperties: false - } - ], + schema: [], messages: { unexpectedSideEffectInFunction: 'Unexpected side effect in computed function.', @@ -193,18 +183,12 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const detectAutoImport = !!context.options?.[0]?.detectAutoImport - - for (const { node } of utils.iterateReferencesTraceMap( - tracker, - { - [ReferenceTracker.ESM]: true, - computed: { - [ReferenceTracker.CALL]: true - } - }, - detectAutoImport - )) { + + for (const { node } of utils.iterateReferencesTraceMap(tracker, { + computed: { + [ReferenceTracker.CALL]: true + } + })) { if (node.type !== 'CallExpression') { continue } diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js index 6f77ee40c..7c4e852f2 100644 --- a/lib/rules/no-watch-after-await.js +++ b/lib/rules/no-watch-after-await.js @@ -49,17 +49,7 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-watch-after-await.html' }, fixable: null, - schema: [ - { - type: 'object', - properties: { - detectAutoImport: { - type: 'boolean' - } - }, - additionalProperties: false - } - ], + schema: [], messages: { forbidden: '`watch` is forbidden after an `await` expression.' } @@ -88,21 +78,15 @@ module.exports = { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) - const detectAutoImport = !!context.options?.[0]?.detectAutoImport - for (const { node } of utils.iterateReferencesTraceMap( - tracker, - { - [ReferenceTracker.ESM]: true, - watch: { - [ReferenceTracker.CALL]: true - }, - watchEffect: { - [ReferenceTracker.CALL]: true - } + for (const { node } of utils.iterateReferencesTraceMap(tracker, { + watch: { + [ReferenceTracker.CALL]: true }, - detectAutoImport - )) { + watchEffect: { + [ReferenceTracker.CALL]: true + } + })) { watchCallNodes.add(node) } } diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js index 10ffc27b7..74fff9d1f 100644 --- a/lib/rules/return-in-computed-property.js +++ b/lib/rules/return-in-computed-property.js @@ -26,9 +26,6 @@ module.exports = { properties: { treatUndefinedAsUnspecified: { type: 'boolean' - }, - detectAutoImport: { - type: 'boolean' } }, additionalProperties: false @@ -61,17 +58,14 @@ module.exports = { Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) const map = { - [ReferenceTracker.ESM]: true, computed: { [ReferenceTracker.CALL]: true } } - const detectAutoImport = !!context.options?.[0]?.detectAutoImport for (const { node } of utils.iterateReferencesTraceMap( tracker, - map, - detectAutoImport + map )) { if (node.type !== 'CallExpression') { continue diff --git a/lib/utils/index.js b/lib/utils/index.js index 03a6d9a5c..f16bcef2c 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -62,7 +62,10 @@ const VUE_BUILTIN_ELEMENT_NAMES = new Set(require('./vue-builtin-elements')) const path = require('path') const vueEslintParser = require('vue-eslint-parser') const { traverseNodes, getFallbackKeys, NS } = vueEslintParser.AST -const { findVariable } = require('@eslint-community/eslint-utils') +const { + findVariable, + ReferenceTracker +} = require('@eslint-community/eslint-utils') const { getComponentPropsFromTypeDefine, getComponentEmitsFromTypeDefine, @@ -2104,12 +2107,13 @@ module.exports = { iterateWatchHandlerValues, /** - * Wraps composition API trace map in both 'vue' and '@vue/composition-api' imports + * Wraps composition API trace map in both 'vue' and '@vue/composition-api' imports, or '#imports' from unimport * @param {import('@eslint-community/eslint-utils').TYPES.TraceMap} map */ createCompositionApiTraceMap: (map) => ({ vue: map, - '@vue/composition-api': map + '@vue/composition-api': map, + '#imports': map }), /** @@ -2118,21 +2122,20 @@ module.exports = { * * @param {import('@eslint-community/eslint-utils').ReferenceTracker} tracker * @param {import('@eslint-community/eslint-utils').TYPES.TraceMap} map - * @param {boolean} detectAutoImport * @returns {ReturnType} */ - *iterateReferencesTraceMap(tracker, map, detectAutoImport) { - const traceMap = this.createCompositionApiTraceMap(map) + *iterateReferencesTraceMap(tracker, map) { + const esmTraceMap = this.createCompositionApiTraceMap({ + ...map, + [ReferenceTracker.ESM]: true + }) - if (detectAutoImport) { - for (const ref of tracker.iterateGlobalReferences(traceMap)) { - yield ref - } - for (const ref of tracker.iterateEsmReferences(map)) { - yield ref - } - } else { - return tracker.iterateEsmReferences(traceMap) + for (const ref of tracker.iterateEsmReferences(esmTraceMap)) { + yield ref + } + + for (const ref of tracker.iterateGlobalReferences(map)) { + yield ref } }, diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js index effad9684..57fe59975 100644 --- a/lib/utils/property-references.js +++ b/lib/utils/property-references.js @@ -119,30 +119,21 @@ function definePropertyReferenceExtractor( context.getSourceCode().scopeManager.scopes[0] ) const toRefNodes = new Set() - const detectAutoImport = !!context.options?.[0]?.detectAutoImport - for (const { node } of utils.iterateReferencesTraceMap( - tracker, - { - [eslintUtils.ReferenceTracker.ESM]: true, - toRef: { - [eslintUtils.ReferenceTracker.CALL]: true - } - }, - detectAutoImport - )) { + for (const { node } of utils.iterateReferencesTraceMap(tracker, { + [eslintUtils.ReferenceTracker.ESM]: true, + toRef: { + [eslintUtils.ReferenceTracker.CALL]: true + } + })) { toRefNodes.add(node) } const toRefsNodes = new Set() - for (const { node } of utils.iterateReferencesTraceMap( - tracker, - { - [eslintUtils.ReferenceTracker.ESM]: true, - toRefs: { - [eslintUtils.ReferenceTracker.CALL]: true - } - }, - detectAutoImport - )) { + for (const { node } of utils.iterateReferencesTraceMap(tracker, { + [eslintUtils.ReferenceTracker.ESM]: true, + toRefs: { + [eslintUtils.ReferenceTracker.CALL]: true + } + })) { toRefsNodes.add(node) } diff --git a/lib/utils/ref-object-references.js b/lib/utils/ref-object-references.js index dc21a9005..fe6f26117 100644 --- a/lib/utils/ref-object-references.js +++ b/lib/utils/ref-object-references.js @@ -78,36 +78,30 @@ const cacheForReactiveVariableReferences = new WeakMap() /** * Iterate the call expressions that define the ref object. * @param {import('eslint').Scope.Scope} globalScope - * @param {boolean} detectAutoImport * @returns {Iterable<{ node: CallExpression, name: string }>} */ -function* iterateDefineRefs(globalScope, detectAutoImport) { +function* iterateDefineRefs(globalScope) { const tracker = new ReferenceTracker(globalScope) - for (const { node, path } of utils.iterateReferencesTraceMap( - tracker, - { - [ReferenceTracker.ESM]: true, - ref: { - [ReferenceTracker.CALL]: true - }, - computed: { - [ReferenceTracker.CALL]: true - }, - toRef: { - [ReferenceTracker.CALL]: true - }, - customRef: { - [ReferenceTracker.CALL]: true - }, - shallowRef: { - [ReferenceTracker.CALL]: true - }, - toRefs: { - [ReferenceTracker.CALL]: true - } + for (const { node, path } of utils.iterateReferencesTraceMap(tracker, { + ref: { + [ReferenceTracker.CALL]: true }, - detectAutoImport - )) { + computed: { + [ReferenceTracker.CALL]: true + }, + toRef: { + [ReferenceTracker.CALL]: true + }, + customRef: { + [ReferenceTracker.CALL]: true + }, + shallowRef: { + [ReferenceTracker.CALL]: true + }, + toRefs: { + [ReferenceTracker.CALL]: true + } + })) { const expr = /** @type {CallExpression} */ (node) yield { node: expr, @@ -535,10 +529,9 @@ class RefObjectReferenceExtractor { /** * Extracts references of all ref objects. * @param {RuleContext} context The rule context. - * @param {boolean} detectAutoImport * @returns {RefObjectReferences} */ -function extractRefObjectReferences(context, detectAutoImport = false) { +function extractRefObjectReferences(context) { const sourceCode = context.getSourceCode() const cachedReferences = cacheForRefObjectReferences.get(sourceCode.ast) if (cachedReferences) { @@ -547,10 +540,7 @@ function extractRefObjectReferences(context, detectAutoImport = false) { const references = new RefObjectReferenceExtractor(context) const globalScope = getGlobalScope(context) - for (const { node, name } of iterateDefineRefs( - globalScope, - detectAutoImport - )) { + for (const { node, name } of iterateDefineRefs(globalScope)) { references.processDefineRef(node, name) } for (const { node } of iterateDefineModels(globalScope)) { diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index 477461bc3..52880eaee 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -839,8 +839,8 @@ tester.run('no-ref-as-operand', rule, { console.log(count.value + 1) // error console.log(1 + count.value) // error `, - options: { - detectAutoImport: true + globals: { + ref: 'readonly' }, errors: [ { From 82a0a525e9c41142c88452e111bf84c34fec4410 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 11 Mar 2024 14:00:52 +0100 Subject: [PATCH 3/5] docs: update --- docs/user-guide/index.md | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 8acad1f54..051b6bc27 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -1,3 +1,7 @@ +--- +outline: deep +--- + # User Guide ## :cd: Installation @@ -386,3 +390,41 @@ Try searching for existing issues. If it does not exist, you should open a new issue and share your repository to reproduce the issue. [vue-eslint-parser]: https://github.com/vuejs/vue-eslint-parser + +### Auto Imports Support + +In [Nuxt 3](https://nuxt.com/) or with [`unplugin-auto-import`](https://github.com/unplugin/unplugin-auto-import), Vue APIs can be auto imported. To make rules like [`no-ref-as-operand`](/rules/no-ref-as-operand.html) or [`no-watch-after-await`](/rules/no-watch-after-await.html) work correctly with them, you can specify them in ESLint's [`globals`](https://eslint.org/docs/latest/use/configure/configuration-files-new#configuring-global-variables) options: + +::: code-group + +```json [Legacy Config] +// .eslintrc +{ + "globals": { + "ref": "readonly", + "computed": "readonly", + "watch": "readonly", + "watchEffect": "readonly", + // ...more APIs + } +} +``` + +```js [Flat Config] +// eslint.config.js +export default [ + { + languageOptions: { + globals: { + ref: 'readonly', + computed: 'readonly', + watch: 'readonly', + watchEffect: 'readonly', + // ...more APIs + } + } + } +] +``` + +::: From c6edf0fc67c3f06f1d13a108cb2453d67404b3b4 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 11 Mar 2024 14:01:24 +0100 Subject: [PATCH 4/5] chore: update --- tests/lib/rules/no-ref-as-operand.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index 52880eaee..62e29275c 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -839,9 +839,6 @@ tester.run('no-ref-as-operand', rule, { console.log(count.value + 1) // error console.log(1 + count.value) // error `, - globals: { - ref: 'readonly' - }, errors: [ { message: @@ -867,7 +864,12 @@ tester.run('no-ref-as-operand', rule, { endLine: 6, endColumn: 28 } - ] + ], + languageOptions: { + globals: { + ref: 'readonly' + } + } } ] }) From b182346a75fbabb875a7504e9ac225e5c84888e6 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 11 Mar 2024 15:13:58 +0100 Subject: [PATCH 5/5] Update docs/user-guide/index.md Co-authored-by: Flo Edelmann --- docs/user-guide/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 051b6bc27..1567d6e60 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -393,7 +393,7 @@ If it does not exist, you should open a new issue and share your repository to r ### Auto Imports Support -In [Nuxt 3](https://nuxt.com/) or with [`unplugin-auto-import`](https://github.com/unplugin/unplugin-auto-import), Vue APIs can be auto imported. To make rules like [`no-ref-as-operand`](/rules/no-ref-as-operand.html) or [`no-watch-after-await`](/rules/no-watch-after-await.html) work correctly with them, you can specify them in ESLint's [`globals`](https://eslint.org/docs/latest/use/configure/configuration-files-new#configuring-global-variables) options: +In [Nuxt 3](https://nuxt.com/) or with [`unplugin-auto-import`](https://github.com/unplugin/unplugin-auto-import), Vue APIs can be auto imported. To make rules like [`vue/no-ref-as-operand`](/rules/no-ref-as-operand.html) or [`vue/no-watch-after-await`](/rules/no-watch-after-await.html) work correctly with them, you can specify them in ESLint's [`globals`](https://eslint.org/docs/latest/use/configure/configuration-files-new#configuring-global-variables) options: ::: code-group