Skip to content

Commit f4de98e

Browse files
authored
New: Add vue/no-watch-after-await rule (#1068)
* New: Add `vue/no-watch-after-await` rule * Add check for watchEffect. * Update vue/no-watch-after-await
1 parent 35efedc commit f4de98e

File tree

6 files changed

+390
-0
lines changed

6 files changed

+390
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
6161
| [vue/no-unused-components](./no-unused-components.md) | disallow registering components that are not used inside templates | |
6262
| [vue/no-unused-vars](./no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes | |
6363
| [vue/no-use-v-if-with-v-for](./no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for | |
64+
| [vue/no-watch-after-await](./no-watch-after-await.md) | disallow asynchronously registered `watch` | |
6465
| [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements | |
6566
| [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: |
6667
| [vue/require-render-return](./require-render-return.md) | enforce render function to always return value | |

docs/rules/no-watch-after-await.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-watch-after-await
5+
description: disallow asynchronously registered `watch`
6+
---
7+
# vue/no-watch-after-await
8+
> disallow asynchronously registered `watch`
9+
10+
- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
11+
12+
## :book: Rule Details
13+
14+
This rule reports the `watch()` after `await` expression.
15+
In `setup()` function, `watch()` should be registered synchronously.
16+
17+
<eslint-code-block :rules="{'vue/no-watch-after-await': ['error']}">
18+
19+
```vue
20+
<script>
21+
import { watch } from 'vue'
22+
export default {
23+
async setup() {
24+
/* ✓ GOOD */
25+
watch(watchSource, () => { /* ... */ })
26+
27+
await doSomething()
28+
29+
/* ✗ BAD */
30+
watch(watchSource, () => { /* ... */ })
31+
}
32+
}
33+
</script>
34+
```
35+
36+
</eslint-code-block>
37+
38+
This rule is not reported when using the stop handle.
39+
40+
<eslint-code-block :rules="{'vue/no-watch-after-await': ['error']}">
41+
42+
```vue
43+
<script>
44+
import { watch } from 'vue'
45+
export default {
46+
async setup() {
47+
await doSomething()
48+
49+
/* ✓ GOOD */
50+
const stopHandle = watch(watchSource, () => { /* ... */ })
51+
52+
// later
53+
stopHandle()
54+
}
55+
}
56+
</script>
57+
```
58+
59+
</eslint-code-block>
60+
61+
## :wrench: Options
62+
63+
Nothing.
64+
65+
## :books: Further reading
66+
67+
- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
68+
- [Vue Composition API - API Reference - Stopping the Watcher](https://composition-api.vuejs.org/api.html#stopping-the-watcher)
69+
70+
## :mag: Implementation
71+
72+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-watch-after-await.js)
73+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-watch-after-await.js)

lib/configs/vue3-essential.js

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
'vue/no-unused-components': 'error',
3030
'vue/no-unused-vars': 'error',
3131
'vue/no-use-v-if-with-v-for': 'error',
32+
'vue/no-watch-after-await': 'error',
3233
'vue/require-component-is': 'error',
3334
'vue/require-prop-type-constructor': 'error',
3435
'vue/require-render-return': 'error',

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ module.exports = {
7575
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
7676
'no-v-html': require('./rules/no-v-html'),
7777
'no-v-model-argument': require('./rules/no-v-model-argument'),
78+
'no-watch-after-await': require('./rules/no-watch-after-await'),
7879
'object-curly-spacing': require('./rules/object-curly-spacing'),
7980
'order-in-components': require('./rules/order-in-components'),
8081
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),

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

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
const { ReferenceTracker } = require('eslint-utils')
7+
const utils = require('../utils')
8+
9+
function isMaybeUsedStopHandle (node) {
10+
const parent = node.parent
11+
if (parent) {
12+
if (parent.type === 'VariableDeclarator') {
13+
// var foo = watch()
14+
return true
15+
}
16+
if (parent.type === 'AssignmentExpression') {
17+
// foo = watch()
18+
return true
19+
}
20+
if (parent.type === 'CallExpression') {
21+
// foo(watch())
22+
return true
23+
}
24+
if (parent.type === 'Property') {
25+
// {foo: watch()}
26+
return true
27+
}
28+
if (parent.type === 'ArrayExpression') {
29+
// [watch()]
30+
return true
31+
}
32+
}
33+
return false
34+
}
35+
36+
module.exports = {
37+
meta: {
38+
type: 'suggestion',
39+
docs: {
40+
description: 'disallow asynchronously registered `watch`',
41+
categories: ['vue3-essential'],
42+
url: 'https://eslint.vuejs.org/rules/no-watch-after-await.html'
43+
},
44+
fixable: null,
45+
schema: [],
46+
messages: {
47+
forbidden: 'The `watch` after `await` expression are forbidden.'
48+
}
49+
},
50+
create (context) {
51+
const watchCallNodes = new Set()
52+
const setupFunctions = new Map()
53+
const forbiddenNodes = new Map()
54+
55+
function addForbiddenNode (property, node) {
56+
let list = forbiddenNodes.get(property)
57+
if (!list) {
58+
list = []
59+
forbiddenNodes.set(property, list)
60+
}
61+
list.push(node)
62+
}
63+
64+
let scopeStack = { upper: null, functionNode: null }
65+
66+
return Object.assign(
67+
{
68+
'Program' () {
69+
const tracker = new ReferenceTracker(context.getScope())
70+
const traceMap = {
71+
vue: {
72+
[ReferenceTracker.ESM]: true,
73+
watch: {
74+
[ReferenceTracker.CALL]: true
75+
},
76+
watchEffect: {
77+
[ReferenceTracker.CALL]: true
78+
}
79+
}
80+
}
81+
82+
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
83+
watchCallNodes.add(node)
84+
}
85+
},
86+
'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
87+
if (utils.getStaticPropertyName(node) !== 'setup') {
88+
return
89+
}
90+
91+
setupFunctions.set(node.value, {
92+
setupProperty: node,
93+
afterAwait: false
94+
})
95+
},
96+
':function' (node) {
97+
scopeStack = { upper: scopeStack, functionNode: node }
98+
},
99+
'AwaitExpression' () {
100+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
101+
if (!setupFunctionData) {
102+
return
103+
}
104+
setupFunctionData.afterAwait = true
105+
},
106+
'CallExpression' (node) {
107+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
108+
if (!setupFunctionData || !setupFunctionData.afterAwait) {
109+
return
110+
}
111+
112+
if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) {
113+
addForbiddenNode(setupFunctionData.setupProperty, node)
114+
}
115+
},
116+
':function:exit' (node) {
117+
scopeStack = scopeStack.upper
118+
119+
setupFunctions.delete(node)
120+
}
121+
},
122+
utils.executeOnVue(context, obj => {
123+
const reportsList = obj.properties
124+
.map(item => forbiddenNodes.get(item))
125+
.filter(reports => !!reports)
126+
for (const reports of reportsList) {
127+
for (const node of reports) {
128+
context.report({
129+
node,
130+
messageId: 'forbidden'
131+
})
132+
}
133+
}
134+
})
135+
)
136+
}
137+
}

0 commit comments

Comments
 (0)