Skip to content

Commit

Permalink
Update vue/no-lifecycle-after-await rule to support <script setup> (
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jul 3, 2021
1 parent 774056c commit ee5ea4b
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 27 deletions.
87 changes: 61 additions & 26 deletions lib/rules/no-lifecycle-after-await.js
Expand Up @@ -41,26 +41,34 @@ module.exports = {
/** @param {RuleContext} context */
create(context) {
/**
* @typedef {object} SetupFunctionData
* @property {Property} setupProperty
* @typedef {object} SetupScopeData
* @property {boolean} afterAwait
* @property {[number,number]} range
*/
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} functionNode
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
*/
/** @type {Set<ESNode>} */
const lifecycleHookCallNodes = new Set()
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupFunctionData>} */
const setupFunctions = new Map()
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, SetupScopeData>} */
const setupScopes = new Map()

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

return Object.assign(
return utils.compositingVisitors(
{
Program() {
/**
* @param {Program} node
*/
Program(node) {
scopeStack = {
upper: scopeStack,
scopeNode: node
}

const tracker = new ReferenceTracker(context.getScope())
const traceMap = {
/** @type {TraceMap} */
Expand All @@ -77,37 +85,41 @@ module.exports = {
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
lifecycleHookCallNodes.add(node)
}
}
},
utils.defineVueVisitor(context, {
},
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
':function'(node) {
scopeStack = {
upper: scopeStack,
functionNode: node
scopeNode: node
}
},
onSetupFunctionEnter(node) {
setupFunctions.set(node, {
setupProperty: node.parent,
afterAwait: false
})
':function:exit'() {
scopeStack = scopeStack && scopeStack.upper
},
AwaitExpression() {
/** @param {AwaitExpression} node */
AwaitExpression(node) {
if (!scopeStack) {
return
}
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
if (!setupFunctionData) {
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (!setupScope || !utils.inRange(setupScope.range, node)) {
return
}
setupFunctionData.afterAwait = true
setupScope.afterAwait = true
},
/** @param {CallExpression} node */
CallExpression(node) {
if (!scopeStack) {
return
}
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
if (!setupFunctionData || !setupFunctionData.afterAwait) {
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (
!setupScope ||
!setupScope.afterAwait ||
!utils.inRange(setupScope.range, node)
) {
return
}

Expand All @@ -121,11 +133,34 @@ module.exports = {
messageId: 'forbidden'
})
}
}
},
(() => {
const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup) {
return {}
}
return {
/**
* @param {Program} node
*/
Program(node) {
setupScopes.set(node, {
afterAwait: false,
range: scriptSetup.range
})
}
}
})(),
utils.defineVueVisitor(context, {
onSetupFunctionEnter(node) {
setupScopes.set(node, {
afterAwait: false,
range: node.range
})
},
':function:exit'(node) {
scopeStack = scopeStack && scopeStack.upper

setupFunctions.delete(node)
onSetupFunctionExit(node) {
setupScopes.delete(node)
}
})
)
Expand Down
27 changes: 26 additions & 1 deletion lib/utils/index.js
Expand Up @@ -1003,7 +1003,12 @@ module.exports = {
vueStack = vueStack.parent
}
}
if (visitor.onSetupFunctionEnter || visitor.onRenderFunctionEnter) {
if (
visitor.onSetupFunctionEnter ||
visitor.onSetupFunctionExit ||
visitor.onRenderFunctionEnter
) {
const setups = new Set()
/** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property } } node */
vueVisitor[
'Property[value.type=/^(Arrow)?FunctionExpression$/] > :function'
Expand All @@ -1014,6 +1019,7 @@ module.exports = {
const name = getStaticPropertyName(prop)
if (name === 'setup') {
callVisitor('onSetupFunctionEnter', node)
setups.add(node)
} else if (name === 'render') {
callVisitor('onRenderFunctionEnter', node)
}
Expand All @@ -1023,6 +1029,17 @@ module.exports = {
node
)
}
if (visitor.onSetupFunctionExit) {
/** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property } } node */
vueVisitor[
'Property[value.type=/^(Arrow)?FunctionExpression$/] > :function:exit'
] = (node) => {
if (setups.has(node)) {
callVisitor('onSetupFunctionExit', node)
setups.delete(node)
}
}
}
}

return vueVisitor
Expand Down Expand Up @@ -1511,6 +1528,14 @@ module.exports = {
}
return dp[alen][blen]
},
/**
* Checks whether the target node is within the given range.
* @param { [number, number] } range
* @param {ASTNode} target
*/
inRange(range, target) {
return range[0] <= target.range[0] && target.range[1] <= range[1]
},
/**
* Checks whether the given node is Property.
*/
Expand Down
55 changes: 55 additions & 0 deletions tests/lib/rules/no-lifecycle-after-await.js
Expand Up @@ -106,6 +106,43 @@ tester.run('no-lifecycle-after-await', rule, {
}
</script>
`
},
{
filename: 'test.vue',
code: `
<script setup>
import {onMounted} from 'vue'
onMounted(() => { /* ... */ })
await doSomething()
</script>
`,
parserOptions: { ecmaVersion: 2022 }
},
{
filename: 'test.vue',
code: `
<script setup>
await doSomething()
</script>
<script>
import {onMounted} from 'vue'
onMounted(() => { /* ... */ }) // not error
</script>
`,
parserOptions: { ecmaVersion: 2022 }
},
{
filename: 'test.vue',
code: `
<script setup>
</script>
<script>
import {onMounted} from 'vue'
await doSomething()
onMounted(() => { /* ... */ }) // not error
</script>
`,
parserOptions: { ecmaVersion: 2022 }
}
],
invalid: [
Expand Down Expand Up @@ -224,6 +261,24 @@ tester.run('no-lifecycle-after-await', rule, {
messageId: 'forbidden'
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
import {onMounted} from 'vue'
await doSomething()
onMounted(() => { /* ... */ }) // error
</script>
`,
parserOptions: { ecmaVersion: 2022 },
errors: [
{
messageId: 'forbidden',
line: 6
}
]
}
]
})
4 changes: 4 additions & 0 deletions typings/eslint-plugin-vue/util-types/utils.ts
Expand Up @@ -19,6 +19,10 @@ export interface VueVisitor extends VueVisitorBase {
node: (FunctionExpression | ArrowFunctionExpression) & { parent: Property },
obj: VueObjectData
): void
onSetupFunctionExit?(
node: (FunctionExpression | ArrowFunctionExpression) & { parent: Property },
obj: VueObjectData
): void
onRenderFunctionEnter?(
node: (FunctionExpression | ArrowFunctionExpression) & { parent: Property },
obj: VueObjectData
Expand Down

0 comments on commit ee5ea4b

Please sign in to comment.