diff --git a/src/rules/composable-placement.ts b/src/rules/composable-placement.ts index bf11b9a..c811396 100644 --- a/src/rules/composable-placement.ts +++ b/src/rules/composable-placement.ts @@ -1,5 +1,6 @@ import { Rule } from 'eslint' import * as ESTree from 'estree' +import assert from 'assert' import { AST } from 'vue-eslint-parser' const composableNameRE = /^use[A-Z0-9]/ @@ -86,21 +87,52 @@ export default { ) } + const functionStack: { node: Rule.Node; afterAwait: boolean }[] = [] + return { + ':function'(node: Rule.Node) { + functionStack.push({ + node, + afterAwait: false, + }) + }, + + ':function:exit'(node: Rule.Node) { + const last = functionStack[functionStack.length - 1] + assert(last?.node === node) + functionStack.pop() + }, + + AwaitExpression() { + if (functionStack.length > 0) { + functionStack[functionStack.length - 1]!.afterAwait = true + } + }, + CallExpression(node) { - if (getCalleeName(node.callee)?.match(composableNameRE)) { - const scope = context.sourceCode.getScope(node) - const block = scope.block as Rule.Node + if (!getCalleeName(node.callee)?.match(composableNameRE)) { + return + } + + const { afterAwait } = functionStack[functionStack.length - 1] ?? { + afterAwait: false, + } + if (afterAwait) { + context.report({ + node, + message: 'Composable function must not be placed after await.', + }) + } - const isComposableScope = isComposableFunction(block) - const isSetupScope = isSetupOption(block) - const isScriptSetupRoot = - inScriptSetup(node) && block.type === 'Program' + const scope = context.sourceCode.getScope(node) + const block = scope.block as Rule.Node - if (isComposableScope || isSetupScope || isScriptSetupRoot) { - return - } + const isComposableScope = isComposableFunction(block) + const isSetupScope = isSetupOption(block) + const isScriptSetupRoot = + inScriptSetup(node) && block.type === 'Program' + if (!isComposableScope && !isSetupScope && !isScriptSetupRoot) { context.report({ node, message: diff --git a/test/composable-placement.spec.ts b/test/composable-placement.spec.ts index a7fb29f..d5c31d7 100644 --- a/test/composable-placement.spec.ts +++ b/test/composable-placement.spec.ts @@ -48,6 +48,16 @@ describe('vue-composable/composable-placement', () => { } `, }, + { + code: ` + import { useFoo } from './foo' + + export async function useBar() { + useFoo() + await fetch() + } + `, + }, { code: ` import { defineComponent } from 'vue' @@ -60,6 +70,19 @@ describe('vue-composable/composable-placement', () => { }) `, }, + { + code: ` + import { defineComponent } from 'vue' + import { useFoo } from './foo' + + export default defineComponent({ + async setup() { + useFoo() + await fetch() + } + }) + `, + }, { code: ` import { useFoo } from './foo' @@ -117,6 +140,17 @@ describe('vue-composable/composable-placement', () => { code: ` import { useFoo } from './foo' + export async function useBar() { + await fetch() + useFoo() + } + `, + errors: [{}], + }, + { + code: ` + import { useFoo } from './foo' + export function useBar() { function baz() { useFoo() @@ -138,6 +172,20 @@ describe('vue-composable/composable-placement', () => { `, errors: [{}], }, + { + code: ` + import { defineComponent } from 'vue' + import { useFoo } from './foo' + + export default defineComponent({ + async setup() { + await fetch() + useFoo() + } + }) + `, + errors: [{}], + }, ], }) diff --git a/tsconfig.json b/tsconfig.json index cf6b75d..503e084 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,9 @@ "target": "ES2022", "module": "CommonJS", "moduleResolution": "node", - "strict": true + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true }, "include": ["src/**/*"] }