Skip to content

Commit ac342f7

Browse files
committed
New: Add vue/no-watch-after-await rule
1 parent ab3bf36 commit ac342f7

File tree

6 files changed

+275
-2
lines changed

6 files changed

+275
-2
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ For example:
164164
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
165165
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
166166
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
167+
| [vue/no-watch-after-await](./no-watch-after-await.md) | disallow asynchronously registered `watch` | |
167168
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
168169
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
169170
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
## :book: Rule Details
11+
12+
This rule reports the `watch()` after `await` expression.
13+
In `setup()` function, `watch()` should be registered synchronously.
14+
15+
<eslint-code-block :rules="{'vue/no-watch-after-await': ['error']}">
16+
17+
```vue
18+
<script>
19+
import { watch } from 'vue'
20+
export default {
21+
async setup() {
22+
/* ✓ GOOD */
23+
watch(() => { /* ... */ })
24+
25+
await doSomething()
26+
27+
/* ✗ BAD */
28+
watch(() => { /* ... */ })
29+
}
30+
}
31+
</script>
32+
```
33+
34+
</eslint-code-block>
35+
36+
## :wrench: Options
37+
38+
Nothing.
39+
40+
## :books: Further reading
41+
42+
- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
43+
44+
## :mag: Implementation
45+
46+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-watch-after-await.js)
47+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-watch-after-await.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ module.exports = {
6363
'no-unused-vars': require('./rules/no-unused-vars'),
6464
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
6565
'no-v-html': require('./rules/no-v-html'),
66+
'no-watch-after-await': require('./rules/no-watch-after-await'),
6667
'object-curly-spacing': require('./rules/object-curly-spacing'),
6768
'order-in-components': require('./rules/order-in-components'),
6869
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
module.exports = {
10+
meta: {
11+
type: 'suggestion',
12+
docs: {
13+
description: 'disallow asynchronously registered `watch`',
14+
category: undefined,
15+
url: 'https://eslint.vuejs.org/rules/no-watch-after-await.html'
16+
},
17+
fixable: null,
18+
schema: [],
19+
messages: {
20+
forbidden: 'The `watch` after `await` expression are forbidden.'
21+
}
22+
},
23+
create (context) {
24+
const watchCallNodes = new Set()
25+
const setupFunctions = new Map()
26+
const forbiddenNodes = new Map()
27+
28+
function addForbiddenNode (property, node) {
29+
let list = forbiddenNodes.get(property)
30+
if (!list) {
31+
list = []
32+
forbiddenNodes.set(property, list)
33+
}
34+
list.push(node)
35+
}
36+
37+
let scopeStack = { upper: null, functionNode: null }
38+
39+
return Object.assign(
40+
{
41+
'Program' () {
42+
const tracker = new ReferenceTracker(context.getScope())
43+
const traceMap = {
44+
vue: {
45+
[ReferenceTracker.ESM]: true,
46+
watch: {
47+
[ReferenceTracker.CALL]: true
48+
}
49+
}
50+
}
51+
52+
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
53+
watchCallNodes.add(node)
54+
}
55+
},
56+
'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
57+
if (utils.getStaticPropertyName(node) !== 'setup') {
58+
return
59+
}
60+
61+
setupFunctions.set(node.value, {
62+
setupProperty: node,
63+
afterAwait: false
64+
})
65+
},
66+
':function' (node) {
67+
scopeStack = { upper: scopeStack, functionNode: node }
68+
},
69+
'AwaitExpression' () {
70+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
71+
if (!setupFunctionData) {
72+
return
73+
}
74+
setupFunctionData.afterAwait = true
75+
},
76+
'CallExpression' (node) {
77+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
78+
if (!setupFunctionData || !setupFunctionData.afterAwait) {
79+
return
80+
}
81+
82+
if (watchCallNodes.has(node)) {
83+
addForbiddenNode(setupFunctionData.setupProperty, node)
84+
}
85+
},
86+
':function:exit' (node) {
87+
scopeStack = scopeStack.upper
88+
89+
setupFunctions.delete(node)
90+
}
91+
},
92+
utils.executeOnVue(context, obj => {
93+
const reportsList = obj.properties
94+
.map(item => forbiddenNodes.get(item))
95+
.filter(reports => !!reports)
96+
for (const reports of reportsList) {
97+
for (const node of reports) {
98+
context.report({
99+
node,
100+
messageId: 'forbidden'
101+
})
102+
}
103+
}
104+
})
105+
)
106+
}
107+
}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"test:base": "mocha \"tests/lib/**/*.js\" --reporter dot",
99
"test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000",
1010
"debug": "mocha --inspect-brk \"tests/lib/**/*.js\" --reporter dot --timeout 60000",
11+
"cover:report": "nyc report --reporter=html",
1112
"lint": "eslint . --rulesdir eslint-internal-rules",
1213
"pretest": "npm run lint",
1314
"preversion": "npm test && npm run update && git add .",
@@ -47,9 +48,10 @@
4748
"eslint": "^5.0.0 || ^6.0.0"
4849
},
4950
"dependencies": {
51+
"eslint-utils": "^2.0.0",
5052
"natural-compare": "^1.4.0",
51-
"vue-eslint-parser": "^7.0.0",
52-
"semver": "^5.6.0"
53+
"semver": "^5.6.0",
54+
"vue-eslint-parser": "^7.0.0"
5355
},
5456
"devDependencies": {
5557
"@types/node": "^4.2.16",
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @author Yosuke Ota
3+
*/
4+
'use strict'
5+
6+
const RuleTester = require('eslint').RuleTester
7+
const rule = require('../../../lib/rules/no-watch-after-await')
8+
9+
const tester = new RuleTester({
10+
parser: require.resolve('vue-eslint-parser'),
11+
parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
12+
})
13+
14+
tester.run('no-watch-after-await', rule, {
15+
valid: [
16+
{
17+
filename: 'test.vue',
18+
code: `
19+
<script>
20+
import {watch} from 'vue'
21+
export default {
22+
async setup() {
23+
watch(() => { /* ... */ }) // ok
24+
25+
await doSomething()
26+
}
27+
}
28+
</script>
29+
`
30+
},
31+
{
32+
filename: 'test.vue',
33+
code: `
34+
<script>
35+
import {watch} from 'vue'
36+
export default {
37+
async setup() {
38+
watch(() => { /* ... */ })
39+
}
40+
}
41+
</script>
42+
`
43+
},
44+
{
45+
filename: 'test.vue',
46+
code: `
47+
<script>
48+
import {onMounted} from 'vue'
49+
export default {
50+
async _setup() {
51+
await doSomething()
52+
53+
onMounted(() => { /* ... */ }) // error
54+
}
55+
}
56+
</script>
57+
`
58+
}
59+
],
60+
invalid: [
61+
{
62+
filename: 'test.vue',
63+
code: `
64+
<script>
65+
import {watch} from 'vue'
66+
export default {
67+
async setup() {
68+
await doSomething()
69+
70+
watch(() => { /* ... */ }) // error
71+
}
72+
}
73+
</script>
74+
`,
75+
errors: [
76+
{
77+
message: 'The `watch` after `await` expression are forbidden.',
78+
line: 8,
79+
column: 11,
80+
endLine: 8,
81+
endColumn: 37
82+
}
83+
]
84+
},
85+
{
86+
filename: 'test.vue',
87+
code: `
88+
<script>
89+
import {watch} from 'vue'
90+
export default {
91+
async setup() {
92+
await doSomething()
93+
94+
watch(() => { /* ... */ })
95+
96+
await doSomething()
97+
98+
watch(() => { /* ... */ })
99+
}
100+
}
101+
</script>
102+
`,
103+
errors: [
104+
{
105+
messageId: 'forbidden',
106+
line: 8
107+
},
108+
{
109+
messageId: 'forbidden',
110+
line: 12
111+
}
112+
]
113+
}
114+
]
115+
})

0 commit comments

Comments
 (0)