Skip to content

Commit 3cc5ac0

Browse files
authored
New: Add vue/no-lifecycle-after-await rule (#1067)
1 parent a634e3c commit 3cc5ac0

File tree

5 files changed

+340
-0
lines changed

5 files changed

+340
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ For example:
167167
| [vue/no-deprecated-v-bind-sync](./no-deprecated-v-bind-sync.md) | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: |
168168
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
169169
| [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | |
170+
| [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks | |
170171
| [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | |
171172
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
172173
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |

Diff for: docs/rules/no-lifecycle-after-await.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-lifecycle-after-await
5+
description: disallow asynchronously registered lifecycle hooks
6+
---
7+
# vue/no-lifecycle-after-await
8+
> disallow asynchronously registered lifecycle hooks
9+
10+
## :book: Rule Details
11+
12+
This rule reports the lifecycle hooks after `await` expression.
13+
In `setup()` function, `onXXX` lifecycle hooks should be registered synchronously.
14+
15+
<eslint-code-block :rules="{'vue/no-lifecycle-after-await': ['error']}">
16+
17+
```vue
18+
<script>
19+
import { onMounted } from 'vue'
20+
export default {
21+
async setup() {
22+
/* ✓ GOOD */
23+
onMounted(() => { /* ... */ })
24+
25+
await doSomething()
26+
27+
/* ✗ BAD */
28+
onMounted(() => { /* ... */ })
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-lifecycle-after-await.js)
47+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lifecycle-after-await.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ module.exports = {
4949
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
5050
'no-empty-pattern': require('./rules/no-empty-pattern'),
5151
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
52+
'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
5253
'no-multi-spaces': require('./rules/no-multi-spaces'),
5354
'no-multiple-template-root': require('./rules/no-multiple-template-root'),
5455
'no-parsing-error': require('./rules/no-parsing-error'),

Diff for: lib/rules/no-lifecycle-after-await.js

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

Diff for: tests/lib/rules/no-lifecycle-after-await.js

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* @author Yosuke Ota
3+
*/
4+
'use strict'
5+
6+
const RuleTester = require('eslint').RuleTester
7+
const rule = require('../../../lib/rules/no-lifecycle-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-lifecycle-after-await', rule, {
15+
valid: [
16+
{
17+
filename: 'test.vue',
18+
code: `
19+
<script>
20+
import {onMounted} from 'vue'
21+
export default {
22+
async setup() {
23+
onMounted(() => { /* ... */ }) // ok
24+
25+
await doSomething()
26+
}
27+
}
28+
</script>
29+
`
30+
}, {
31+
filename: 'test.vue',
32+
code: `
33+
<script>
34+
import {onMounted} from 'vue'
35+
export default {
36+
async setup() {
37+
onMounted(() => { /* ... */ })
38+
}
39+
}
40+
</script>
41+
`
42+
}, {
43+
filename: 'test.vue',
44+
code: `
45+
<script>
46+
import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onUnmounted, onUpdated, onActivated, onDeactivated} from 'vue'
47+
export default {
48+
async setup() {
49+
onBeforeMount(() => { /* ... */ })
50+
onBeforeUnmount(() => { /* ... */ })
51+
onBeforeUpdate(() => { /* ... */ })
52+
onErrorCaptured(() => { /* ... */ })
53+
onMounted(() => { /* ... */ })
54+
onRenderTracked(() => { /* ... */ })
55+
onRenderTriggered(() => { /* ... */ })
56+
onUnmounted(() => { /* ... */ })
57+
onUpdated(() => { /* ... */ })
58+
onActivated(() => { /* ... */ })
59+
onDeactivated(() => { /* ... */ })
60+
61+
await doSomething()
62+
}
63+
}
64+
</script>
65+
`
66+
},
67+
{
68+
filename: 'test.vue',
69+
code: `
70+
<script>
71+
import {onMounted} from 'vue'
72+
export default {
73+
async _setup() {
74+
await doSomething()
75+
76+
onMounted(() => { /* ... */ }) // error
77+
}
78+
}
79+
</script>
80+
`
81+
}
82+
],
83+
invalid: [
84+
{
85+
filename: 'test.vue',
86+
code: `
87+
<script>
88+
import {onMounted} from 'vue'
89+
export default {
90+
async setup() {
91+
await doSomething()
92+
93+
onMounted(() => { /* ... */ }) // error
94+
}
95+
}
96+
</script>
97+
`,
98+
errors: [
99+
{
100+
message: 'The lifecycle hooks after `await` expression are forbidden.',
101+
line: 8,
102+
column: 11,
103+
endLine: 8,
104+
endColumn: 41
105+
}
106+
]
107+
},
108+
{
109+
filename: 'test.vue',
110+
code: `
111+
<script>
112+
import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onUnmounted, onUpdated, onActivated, onDeactivated} from 'vue'
113+
export default {
114+
async setup() {
115+
await doSomething()
116+
117+
onBeforeMount(() => { /* ... */ })
118+
onBeforeUnmount(() => { /* ... */ })
119+
onBeforeUpdate(() => { /* ... */ })
120+
onErrorCaptured(() => { /* ... */ })
121+
onMounted(() => { /* ... */ })
122+
onRenderTracked(() => { /* ... */ })
123+
onRenderTriggered(() => { /* ... */ })
124+
onUnmounted(() => { /* ... */ })
125+
onUpdated(() => { /* ... */ })
126+
onActivated(() => { /* ... */ })
127+
onDeactivated(() => { /* ... */ })
128+
}
129+
}
130+
</script>
131+
`,
132+
errors: [
133+
{
134+
messageId: 'forbidden',
135+
line: 8
136+
},
137+
{
138+
messageId: 'forbidden',
139+
line: 9
140+
},
141+
{
142+
messageId: 'forbidden',
143+
line: 10
144+
},
145+
{
146+
messageId: 'forbidden',
147+
line: 11
148+
},
149+
{
150+
messageId: 'forbidden',
151+
line: 12
152+
},
153+
{
154+
messageId: 'forbidden',
155+
line: 13
156+
},
157+
{
158+
messageId: 'forbidden',
159+
line: 14
160+
},
161+
{
162+
messageId: 'forbidden',
163+
line: 15
164+
},
165+
{
166+
messageId: 'forbidden',
167+
line: 16
168+
},
169+
{
170+
messageId: 'forbidden',
171+
line: 17
172+
},
173+
{
174+
messageId: 'forbidden',
175+
line: 18
176+
}
177+
]
178+
}
179+
]
180+
})

0 commit comments

Comments
 (0)