Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Vue APIs from auto imports #2422

Merged
merged 5 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'
}
}
}
]
})