Skip to content

Commit ee5ea4b

Browse files
authored
Update vue/no-lifecycle-after-await rule to support <script setup> (#1539)
1 parent 774056c commit ee5ea4b

File tree

4 files changed

+146
-27
lines changed

4 files changed

+146
-27
lines changed

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

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,34 @@ module.exports = {
4141
/** @param {RuleContext} context */
4242
create(context) {
4343
/**
44-
* @typedef {object} SetupFunctionData
45-
* @property {Property} setupProperty
44+
* @typedef {object} SetupScopeData
4645
* @property {boolean} afterAwait
46+
* @property {[number,number]} range
4747
*/
4848
/**
4949
* @typedef {object} ScopeStack
5050
* @property {ScopeStack | null} upper
51-
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} functionNode
51+
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
5252
*/
5353
/** @type {Set<ESNode>} */
5454
const lifecycleHookCallNodes = new Set()
55-
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupFunctionData>} */
56-
const setupFunctions = new Map()
55+
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, SetupScopeData>} */
56+
const setupScopes = new Map()
5757

5858
/** @type {ScopeStack | null} */
5959
let scopeStack = null
6060

61-
return Object.assign(
61+
return utils.compositingVisitors(
6262
{
63-
Program() {
63+
/**
64+
* @param {Program} node
65+
*/
66+
Program(node) {
67+
scopeStack = {
68+
upper: scopeStack,
69+
scopeNode: node
70+
}
71+
6472
const tracker = new ReferenceTracker(context.getScope())
6573
const traceMap = {
6674
/** @type {TraceMap} */
@@ -77,37 +85,41 @@ module.exports = {
7785
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
7886
lifecycleHookCallNodes.add(node)
7987
}
80-
}
81-
},
82-
utils.defineVueVisitor(context, {
88+
},
89+
/**
90+
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
91+
*/
8392
':function'(node) {
8493
scopeStack = {
8594
upper: scopeStack,
86-
functionNode: node
95+
scopeNode: node
8796
}
8897
},
89-
onSetupFunctionEnter(node) {
90-
setupFunctions.set(node, {
91-
setupProperty: node.parent,
92-
afterAwait: false
93-
})
98+
':function:exit'() {
99+
scopeStack = scopeStack && scopeStack.upper
94100
},
95-
AwaitExpression() {
101+
/** @param {AwaitExpression} node */
102+
AwaitExpression(node) {
96103
if (!scopeStack) {
97104
return
98105
}
99-
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
100-
if (!setupFunctionData) {
106+
const setupScope = setupScopes.get(scopeStack.scopeNode)
107+
if (!setupScope || !utils.inRange(setupScope.range, node)) {
101108
return
102109
}
103-
setupFunctionData.afterAwait = true
110+
setupScope.afterAwait = true
104111
},
112+
/** @param {CallExpression} node */
105113
CallExpression(node) {
106114
if (!scopeStack) {
107115
return
108116
}
109-
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
110-
if (!setupFunctionData || !setupFunctionData.afterAwait) {
117+
const setupScope = setupScopes.get(scopeStack.scopeNode)
118+
if (
119+
!setupScope ||
120+
!setupScope.afterAwait ||
121+
!utils.inRange(setupScope.range, node)
122+
) {
111123
return
112124
}
113125

@@ -121,11 +133,34 @@ module.exports = {
121133
messageId: 'forbidden'
122134
})
123135
}
136+
}
137+
},
138+
(() => {
139+
const scriptSetup = utils.getScriptSetupElement(context)
140+
if (!scriptSetup) {
141+
return {}
142+
}
143+
return {
144+
/**
145+
* @param {Program} node
146+
*/
147+
Program(node) {
148+
setupScopes.set(node, {
149+
afterAwait: false,
150+
range: scriptSetup.range
151+
})
152+
}
153+
}
154+
})(),
155+
utils.defineVueVisitor(context, {
156+
onSetupFunctionEnter(node) {
157+
setupScopes.set(node, {
158+
afterAwait: false,
159+
range: node.range
160+
})
124161
},
125-
':function:exit'(node) {
126-
scopeStack = scopeStack && scopeStack.upper
127-
128-
setupFunctions.delete(node)
162+
onSetupFunctionExit(node) {
163+
setupScopes.delete(node)
129164
}
130165
})
131166
)

lib/utils/index.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,12 @@ module.exports = {
10031003
vueStack = vueStack.parent
10041004
}
10051005
}
1006-
if (visitor.onSetupFunctionEnter || visitor.onRenderFunctionEnter) {
1006+
if (
1007+
visitor.onSetupFunctionEnter ||
1008+
visitor.onSetupFunctionExit ||
1009+
visitor.onRenderFunctionEnter
1010+
) {
1011+
const setups = new Set()
10071012
/** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property } } node */
10081013
vueVisitor[
10091014
'Property[value.type=/^(Arrow)?FunctionExpression$/] > :function'
@@ -1014,6 +1019,7 @@ module.exports = {
10141019
const name = getStaticPropertyName(prop)
10151020
if (name === 'setup') {
10161021
callVisitor('onSetupFunctionEnter', node)
1022+
setups.add(node)
10171023
} else if (name === 'render') {
10181024
callVisitor('onRenderFunctionEnter', node)
10191025
}
@@ -1023,6 +1029,17 @@ module.exports = {
10231029
node
10241030
)
10251031
}
1032+
if (visitor.onSetupFunctionExit) {
1033+
/** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property } } node */
1034+
vueVisitor[
1035+
'Property[value.type=/^(Arrow)?FunctionExpression$/] > :function:exit'
1036+
] = (node) => {
1037+
if (setups.has(node)) {
1038+
callVisitor('onSetupFunctionExit', node)
1039+
setups.delete(node)
1040+
}
1041+
}
1042+
}
10261043
}
10271044

10281045
return vueVisitor
@@ -1511,6 +1528,14 @@ module.exports = {
15111528
}
15121529
return dp[alen][blen]
15131530
},
1531+
/**
1532+
* Checks whether the target node is within the given range.
1533+
* @param { [number, number] } range
1534+
* @param {ASTNode} target
1535+
*/
1536+
inRange(range, target) {
1537+
return range[0] <= target.range[0] && target.range[1] <= range[1]
1538+
},
15141539
/**
15151540
* Checks whether the given node is Property.
15161541
*/

tests/lib/rules/no-lifecycle-after-await.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,43 @@ tester.run('no-lifecycle-after-await', rule, {
106106
}
107107
</script>
108108
`
109+
},
110+
{
111+
filename: 'test.vue',
112+
code: `
113+
<script setup>
114+
import {onMounted} from 'vue'
115+
onMounted(() => { /* ... */ })
116+
await doSomething()
117+
</script>
118+
`,
119+
parserOptions: { ecmaVersion: 2022 }
120+
},
121+
{
122+
filename: 'test.vue',
123+
code: `
124+
<script setup>
125+
await doSomething()
126+
</script>
127+
<script>
128+
import {onMounted} from 'vue'
129+
onMounted(() => { /* ... */ }) // not error
130+
</script>
131+
`,
132+
parserOptions: { ecmaVersion: 2022 }
133+
},
134+
{
135+
filename: 'test.vue',
136+
code: `
137+
<script setup>
138+
</script>
139+
<script>
140+
import {onMounted} from 'vue'
141+
await doSomething()
142+
onMounted(() => { /* ... */ }) // not error
143+
</script>
144+
`,
145+
parserOptions: { ecmaVersion: 2022 }
109146
}
110147
],
111148
invalid: [
@@ -224,6 +261,24 @@ tester.run('no-lifecycle-after-await', rule, {
224261
messageId: 'forbidden'
225262
}
226263
]
264+
},
265+
{
266+
filename: 'test.vue',
267+
code: `
268+
<script setup>
269+
import {onMounted} from 'vue'
270+
await doSomething()
271+
272+
onMounted(() => { /* ... */ }) // error
273+
</script>
274+
`,
275+
parserOptions: { ecmaVersion: 2022 },
276+
errors: [
277+
{
278+
messageId: 'forbidden',
279+
line: 6
280+
}
281+
]
227282
}
228283
]
229284
})

typings/eslint-plugin-vue/util-types/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export interface VueVisitor extends VueVisitorBase {
1919
node: (FunctionExpression | ArrowFunctionExpression) & { parent: Property },
2020
obj: VueObjectData
2121
): void
22+
onSetupFunctionExit?(
23+
node: (FunctionExpression | ArrowFunctionExpression) & { parent: Property },
24+
obj: VueObjectData
25+
): void
2226
onRenderFunctionEnter?(
2327
node: (FunctionExpression | ArrowFunctionExpression) & { parent: Property },
2428
obj: VueObjectData

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy