Skip to content

Commit

Permalink
feat: support Vue APIs from auto imports (#2422)
Browse files Browse the repository at this point in the history
Co-authored-by: Flo Edelmann <[email protected]>
  • Loading branch information
antfu and FloEdelmann authored Mar 11, 2024
1 parent 7c71c48 commit 3abc04c
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 57 deletions.
42 changes: 42 additions & 0 deletions docs/user-guide/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
outline: deep
---

# User Guide

## :cd: Installation
Expand Down Expand Up @@ -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 [`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

```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
}
}
}
]
```

:::
7 changes: 2 additions & 5 deletions lib/rules/no-async-in-computed-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,11 @@ module.exports = {
/** @param {Program} program */
Program(program) {
const tracker = new ReferenceTracker(utils.getScope(context, program))
const traceMap = utils.createCompositionApiTraceMap({
[ReferenceTracker.ESM]: true,
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
computed: {
[ReferenceTracker.CALL]: true
}
})

for (const { node } of tracker.iterateEsmReferences(traceMap)) {
})) {
if (node.type !== 'CallExpression') {
continue
}
Expand Down
15 changes: 7 additions & 8 deletions lib/rules/no-lifecycle-after-await.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,18 @@ module.exports = {
/** @param {Program} program */
Program(program) {
const tracker = new ReferenceTracker(utils.getScope(context, program))
const traceMap = {
/** @type {TraceMap} */
vue: {
[ReferenceTracker.ESM]: true
}
}
/** @type {TraceMap} */
const traceMap = {}
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
)) {
lifecycleHookCallNodes.add(node)
}
}
Expand Down
8 changes: 3 additions & 5 deletions lib/rules/no-side-effects-in-computed-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,12 @@ module.exports = {
/** @param {Program} program */
Program(program) {
const tracker = new ReferenceTracker(utils.getScope(context, program))
const traceMap = utils.createCompositionApiTraceMap({
[ReferenceTracker.ESM]: true,

for (const { node } of utils.iterateReferencesTraceMap(tracker, {
computed: {
[ReferenceTracker.CALL]: true
}
})

for (const { node } of tracker.iterateEsmReferences(traceMap)) {
})) {
if (node.type !== 'CallExpression') {
continue
}
Expand Down
20 changes: 8 additions & 12 deletions lib/rules/no-watch-after-await.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,15 @@ module.exports = {
/** @param {Program} program */
Program(program) {
const tracker = new ReferenceTracker(utils.getScope(context, program))
const traceMap = {
vue: {
[ReferenceTracker.ESM]: true,
watch: {
[ReferenceTracker.CALL]: true
},
watchEffect: {
[ReferenceTracker.CALL]: true
}
}
}

for (const { node } of tracker.iterateEsmReferences(traceMap)) {
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
watch: {
[ReferenceTracker.CALL]: true
},
watchEffect: {
[ReferenceTracker.CALL]: true
}
})) {
watchCallNodes.add(node)
}
}
Expand Down
10 changes: 6 additions & 4 deletions lib/rules/return-in-computed-property.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,16 @@ module.exports = {
/** @param {Program} program */
Program(program) {
const tracker = new ReferenceTracker(utils.getScope(context, program))
const traceMap = utils.createCompositionApiTraceMap({
[ReferenceTracker.ESM]: true,
const map = {
computed: {
[ReferenceTracker.CALL]: true
}
})
}

for (const { node } of tracker.iterateEsmReferences(traceMap)) {
for (const { node } of utils.iterateReferencesTraceMap(
tracker,
map
)) {
if (node.type !== 'CallExpression') {
continue
}
Expand Down
33 changes: 30 additions & 3 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2104,14 +2107,38 @@ 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
}),

/**
* 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
* @returns {ReturnType<import('@eslint-community/eslint-utils').ReferenceTracker['iterateEsmReferences']>}
*/
*iterateReferencesTraceMap(tracker, map) {
const esmTraceMap = this.createCompositionApiTraceMap({
...map,
[ReferenceTracker.ESM]: true
})

for (const ref of tracker.iterateEsmReferences(esmTraceMap)) {
yield ref
}

for (const ref of tracker.iterateGlobalReferences(map)) {
yield ref
}
},

/**
* Checks whether or not the tokens of two given nodes are same.
* @param {ASTNode} left A node 1 to compare.
Expand Down
28 changes: 12 additions & 16 deletions lib/utils/property-references.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,25 +119,21 @@ function definePropertyReferenceExtractor(
context.getSourceCode().scopeManager.scopes[0]
)
const toRefNodes = new Set()
for (const { node } of tracker.iterateEsmReferences(
utils.createCompositionApiTraceMap({
[eslintUtils.ReferenceTracker.ESM]: true,
toRef: {
[eslintUtils.ReferenceTracker.CALL]: true
}
})
)) {
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 tracker.iterateEsmReferences(
utils.createCompositionApiTraceMap({
[eslintUtils.ReferenceTracker.ESM]: true,
toRefs: {
[eslintUtils.ReferenceTracker.CALL]: true
}
})
)) {
for (const { node } of utils.iterateReferencesTraceMap(tracker, {
[eslintUtils.ReferenceTracker.ESM]: true,
toRefs: {
[eslintUtils.ReferenceTracker.CALL]: true
}
})) {
toRefsNodes.add(node)
}

Expand Down
6 changes: 2 additions & 4 deletions lib/utils/ref-object-references.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ const cacheForReactiveVariableReferences = new WeakMap()
*/
function* iterateDefineRefs(globalScope) {
const tracker = new ReferenceTracker(globalScope)
const traceMap = utils.createCompositionApiTraceMap({
[ReferenceTracker.ESM]: true,
for (const { node, path } of utils.iterateReferencesTraceMap(tracker, {
ref: {
[ReferenceTracker.CALL]: true
},
Expand All @@ -102,8 +101,7 @@ function* iterateDefineRefs(globalScope) {
toRefs: {
[ReferenceTracker.CALL]: true
}
})
for (const { node, path } of tracker.iterateEsmReferences(traceMap)) {
})) {
const expr = /** @type {CallExpression} */ (node)
yield {
node: expr,
Expand Down
48 changes: 48 additions & 0 deletions tests/lib/rules/no-ref-as-operand.js
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,54 @@ 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
`,
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
}
],
languageOptions: {
globals: {
ref: 'readonly'
}
}
}
]
})

0 comments on commit 3abc04c

Please sign in to comment.