Skip to content

Commit 3abc04c

Browse files
antfuFloEdelmann
andauthored
feat: support Vue APIs from auto imports (#2422)
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
1 parent 7c71c48 commit 3abc04c

10 files changed

+160
-57
lines changed

docs/user-guide/index.md

+42
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
outline: deep
3+
---
4+
15
# User Guide
26

37
## :cd: Installation
@@ -386,3 +390,41 @@ Try searching for existing issues.
386390
If it does not exist, you should open a new issue and share your repository to reproduce the issue.
387391

388392
[vue-eslint-parser]: https://github.com/vuejs/vue-eslint-parser
393+
394+
### Auto Imports Support
395+
396+
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:
397+
398+
::: code-group
399+
400+
```json [Legacy Config]
401+
// .eslintrc
402+
{
403+
"globals": {
404+
"ref": "readonly",
405+
"computed": "readonly",
406+
"watch": "readonly",
407+
"watchEffect": "readonly",
408+
// ...more APIs
409+
}
410+
}
411+
```
412+
413+
```js [Flat Config]
414+
// eslint.config.js
415+
export default [
416+
{
417+
languageOptions: {
418+
globals: {
419+
ref: 'readonly',
420+
computed: 'readonly',
421+
watch: 'readonly',
422+
watchEffect: 'readonly',
423+
// ...more APIs
424+
}
425+
}
426+
}
427+
]
428+
```
429+
430+
:::

lib/rules/no-async-in-computed-properties.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,11 @@ module.exports = {
263263
/** @param {Program} program */
264264
Program(program) {
265265
const tracker = new ReferenceTracker(utils.getScope(context, program))
266-
const traceMap = utils.createCompositionApiTraceMap({
267-
[ReferenceTracker.ESM]: true,
266+
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
268267
computed: {
269268
[ReferenceTracker.CALL]: true
270269
}
271-
})
272-
273-
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
270+
})) {
274271
if (node.type !== 'CallExpression') {
275272
continue
276273
}

lib/rules/no-lifecycle-after-await.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,18 @@ module.exports = {
6363
/** @param {Program} program */
6464
Program(program) {
6565
const tracker = new ReferenceTracker(utils.getScope(context, program))
66-
const traceMap = {
67-
/** @type {TraceMap} */
68-
vue: {
69-
[ReferenceTracker.ESM]: true
70-
}
71-
}
66+
/** @type {TraceMap} */
67+
const traceMap = {}
7268
for (const lifecycleHook of LIFECYCLE_HOOKS) {
73-
traceMap.vue[lifecycleHook] = {
69+
traceMap[lifecycleHook] = {
7470
[ReferenceTracker.CALL]: true
7571
}
7672
}
7773

78-
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
74+
for (const { node } of utils.iterateReferencesTraceMap(
75+
tracker,
76+
traceMap
77+
)) {
7978
lifecycleHookCallNodes.add(node)
8079
}
8180
}

lib/rules/no-side-effects-in-computed-properties.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -183,14 +183,12 @@ module.exports = {
183183
/** @param {Program} program */
184184
Program(program) {
185185
const tracker = new ReferenceTracker(utils.getScope(context, program))
186-
const traceMap = utils.createCompositionApiTraceMap({
187-
[ReferenceTracker.ESM]: true,
186+
187+
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
188188
computed: {
189189
[ReferenceTracker.CALL]: true
190190
}
191-
})
192-
193-
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
191+
})) {
194192
if (node.type !== 'CallExpression') {
195193
continue
196194
}

lib/rules/no-watch-after-await.js

+8-12
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,15 @@ module.exports = {
7878
/** @param {Program} program */
7979
Program(program) {
8080
const tracker = new ReferenceTracker(utils.getScope(context, program))
81-
const traceMap = {
82-
vue: {
83-
[ReferenceTracker.ESM]: true,
84-
watch: {
85-
[ReferenceTracker.CALL]: true
86-
},
87-
watchEffect: {
88-
[ReferenceTracker.CALL]: true
89-
}
90-
}
91-
}
9281

93-
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
82+
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
83+
watch: {
84+
[ReferenceTracker.CALL]: true
85+
},
86+
watchEffect: {
87+
[ReferenceTracker.CALL]: true
88+
}
89+
})) {
9490
watchCallNodes.add(node)
9591
}
9692
}

lib/rules/return-in-computed-property.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,16 @@ module.exports = {
5757
/** @param {Program} program */
5858
Program(program) {
5959
const tracker = new ReferenceTracker(utils.getScope(context, program))
60-
const traceMap = utils.createCompositionApiTraceMap({
61-
[ReferenceTracker.ESM]: true,
60+
const map = {
6261
computed: {
6362
[ReferenceTracker.CALL]: true
6463
}
65-
})
64+
}
6665

67-
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
66+
for (const { node } of utils.iterateReferencesTraceMap(
67+
tracker,
68+
map
69+
)) {
6870
if (node.type !== 'CallExpression') {
6971
continue
7072
}

lib/utils/index.js

+30-3
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ const VUE_BUILTIN_ELEMENT_NAMES = new Set(require('./vue-builtin-elements'))
6262
const path = require('path')
6363
const vueEslintParser = require('vue-eslint-parser')
6464
const { traverseNodes, getFallbackKeys, NS } = vueEslintParser.AST
65-
const { findVariable } = require('@eslint-community/eslint-utils')
65+
const {
66+
findVariable,
67+
ReferenceTracker
68+
} = require('@eslint-community/eslint-utils')
6669
const {
6770
getComponentPropsFromTypeDefine,
6871
getComponentEmitsFromTypeDefine,
@@ -2104,14 +2107,38 @@ module.exports = {
21042107
iterateWatchHandlerValues,
21052108

21062109
/**
2107-
* Wraps composition API trace map in both 'vue' and '@vue/composition-api' imports
2110+
* Wraps composition API trace map in both 'vue' and '@vue/composition-api' imports, or '#imports' from unimport
21082111
* @param {import('@eslint-community/eslint-utils').TYPES.TraceMap} map
21092112
*/
21102113
createCompositionApiTraceMap: (map) => ({
21112114
vue: map,
2112-
'@vue/composition-api': map
2115+
'@vue/composition-api': map,
2116+
'#imports': map
21132117
}),
21142118

2119+
/**
2120+
* Iterates all references in the given trace map.
2121+
* Take the third argument option to detect auto-imported references.
2122+
*
2123+
* @param {import('@eslint-community/eslint-utils').ReferenceTracker} tracker
2124+
* @param {import('@eslint-community/eslint-utils').TYPES.TraceMap} map
2125+
* @returns {ReturnType<import('@eslint-community/eslint-utils').ReferenceTracker['iterateEsmReferences']>}
2126+
*/
2127+
*iterateReferencesTraceMap(tracker, map) {
2128+
const esmTraceMap = this.createCompositionApiTraceMap({
2129+
...map,
2130+
[ReferenceTracker.ESM]: true
2131+
})
2132+
2133+
for (const ref of tracker.iterateEsmReferences(esmTraceMap)) {
2134+
yield ref
2135+
}
2136+
2137+
for (const ref of tracker.iterateGlobalReferences(map)) {
2138+
yield ref
2139+
}
2140+
},
2141+
21152142
/**
21162143
* Checks whether or not the tokens of two given nodes are same.
21172144
* @param {ASTNode} left A node 1 to compare.

lib/utils/property-references.js

+12-16
Original file line numberDiff line numberDiff line change
@@ -119,25 +119,21 @@ function definePropertyReferenceExtractor(
119119
context.getSourceCode().scopeManager.scopes[0]
120120
)
121121
const toRefNodes = new Set()
122-
for (const { node } of tracker.iterateEsmReferences(
123-
utils.createCompositionApiTraceMap({
124-
[eslintUtils.ReferenceTracker.ESM]: true,
125-
toRef: {
126-
[eslintUtils.ReferenceTracker.CALL]: true
127-
}
128-
})
129-
)) {
122+
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
123+
[eslintUtils.ReferenceTracker.ESM]: true,
124+
toRef: {
125+
[eslintUtils.ReferenceTracker.CALL]: true
126+
}
127+
})) {
130128
toRefNodes.add(node)
131129
}
132130
const toRefsNodes = new Set()
133-
for (const { node } of tracker.iterateEsmReferences(
134-
utils.createCompositionApiTraceMap({
135-
[eslintUtils.ReferenceTracker.ESM]: true,
136-
toRefs: {
137-
[eslintUtils.ReferenceTracker.CALL]: true
138-
}
139-
})
140-
)) {
131+
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
132+
[eslintUtils.ReferenceTracker.ESM]: true,
133+
toRefs: {
134+
[eslintUtils.ReferenceTracker.CALL]: true
135+
}
136+
})) {
141137
toRefsNodes.add(node)
142138
}
143139

lib/utils/ref-object-references.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@ const cacheForReactiveVariableReferences = new WeakMap()
8282
*/
8383
function* iterateDefineRefs(globalScope) {
8484
const tracker = new ReferenceTracker(globalScope)
85-
const traceMap = utils.createCompositionApiTraceMap({
86-
[ReferenceTracker.ESM]: true,
85+
for (const { node, path } of utils.iterateReferencesTraceMap(tracker, {
8786
ref: {
8887
[ReferenceTracker.CALL]: true
8988
},
@@ -102,8 +101,7 @@ function* iterateDefineRefs(globalScope) {
102101
toRefs: {
103102
[ReferenceTracker.CALL]: true
104103
}
105-
})
106-
for (const { node, path } of tracker.iterateEsmReferences(traceMap)) {
104+
})) {
107105
const expr = /** @type {CallExpression} */ (node)
108106
yield {
109107
node: expr,

tests/lib/rules/no-ref-as-operand.js

+48
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,54 @@ tester.run('no-ref-as-operand', rule, {
822822
line: 9
823823
}
824824
]
825+
},
826+
// Auto-import
827+
{
828+
code: `
829+
let count = ref(0)
830+
831+
count++ // error
832+
console.log(count + 1) // error
833+
console.log(1 + count) // error
834+
`,
835+
output: `
836+
let count = ref(0)
837+
838+
count.value++ // error
839+
console.log(count.value + 1) // error
840+
console.log(1 + count.value) // error
841+
`,
842+
errors: [
843+
{
844+
message:
845+
'Must use `.value` to read or write the value wrapped by `ref()`.',
846+
line: 4,
847+
column: 7,
848+
endLine: 4,
849+
endColumn: 12
850+
},
851+
{
852+
message:
853+
'Must use `.value` to read or write the value wrapped by `ref()`.',
854+
line: 5,
855+
column: 19,
856+
endLine: 5,
857+
endColumn: 24
858+
},
859+
{
860+
message:
861+
'Must use `.value` to read or write the value wrapped by `ref()`.',
862+
line: 6,
863+
column: 23,
864+
endLine: 6,
865+
endColumn: 28
866+
}
867+
],
868+
languageOptions: {
869+
globals: {
870+
ref: 'readonly'
871+
}
872+
}
825873
}
826874
]
827875
})

0 commit comments

Comments
 (0)