Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update vue/no-expose-after-await rule to support <script setup> #1885

Merged
merged 7 commits into from May 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/.vuepress/components/eslint-code-block.vue
Expand Up @@ -90,7 +90,7 @@ export default {
rules: this.rules,
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: {
jsx: true
Expand Down
22 changes: 19 additions & 3 deletions docs/rules/no-expose-after-await.md
Expand Up @@ -11,14 +11,14 @@ since: v8.1.0

## :book: Rule Details

This rule reports usages of `expose()` after an `await` expression.
In the `setup()` function, `expose()` should be registered synchronously.
This rule reports usages of `expose()` and `defineExpose()` after an `await` expression.
In the `setup()` function, `expose()` should be registered synchronously.
In the `<script setup>`, `defineExpose()` should be registered synchronously.

<eslint-code-block :rules="{'vue/no-expose-after-await': ['error']}">

```vue
<script>
import { watch } from 'vue'
export default {
async setup(props, { expose }) {
/* ✓ GOOD */
Expand All @@ -35,6 +35,22 @@ export default {

</eslint-code-block>

<eslint-code-block :rules="{'vue/no-expose-after-await': ['error']}">

```vue
<script setup>
/* ✓ GOOD */
defineExpose({/* ... */})

await doSomething()

/* ✗ BAD */
defineExpose({/* ... */})
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.
Expand Down
282 changes: 165 additions & 117 deletions lib/rules/no-expose-after-await.js
Expand Up @@ -47,7 +47,7 @@ module.exports = {
fixable: null,
schema: [],
messages: {
forbidden: 'The `expose` after `await` expression are forbidden.'
forbidden: 'The `{{name}}` after `await` expression are forbidden.'
ota-meshi marked this conversation as resolved.
Show resolved Hide resolved
}
},
/** @param {RuleContext} context */
Expand All @@ -56,147 +56,195 @@ module.exports = {
* @typedef {object} SetupScopeData
* @property {boolean} afterAwait
* @property {[number,number]} range
* @property {Set<Identifier>} exposeReferenceIds
* @property {Set<Identifier>} contextReferenceIds
* @property {(node: Identifier, callNode: CallExpression) => boolean} isExposeReferenceId
* @property {(node: Identifier) => boolean} isContextReferenceId
*/
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
*/
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupScopeData>} */
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, SetupScopeData>} */
const setupScopes = new Map()

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

return utils.defineVueVisitor(context, {
onSetupFunctionEnter(node) {
const contextParam = node.params[1]
if (!contextParam) {
// no arguments
return
}
if (contextParam.type === 'RestElement') {
// cannot check
return
}
if (contextParam.type === 'ArrayPattern') {
// cannot check
return
return utils.compositingVisitors(
{
/**
* @param {Program} node
*/
Program(node) {
scopeStack = {
upper: scopeStack,
scopeNode: node
}
}
/** @type {Set<Identifier>} */
const contextReferenceIds = new Set()
/** @type {Set<Identifier>} */
const exposeReferenceIds = new Set()
if (contextParam.type === 'ObjectPattern') {
const exposeProperty = utils.findAssignmentProperty(
contextParam,
'expose'
)
if (!exposeProperty) {
return
},
{
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
':function'(node) {
scopeStack = {
upper: scopeStack,
scopeNode: node
}
const exposeParam = exposeProperty.value
// `setup(props, {emit})`
const variable =
exposeParam.type === 'Identifier'
? findVariable(context.getScope(), exposeParam)
: null
if (!variable) {
},
':function:exit'() {
scopeStack = scopeStack && scopeStack.upper
},
/** @param {AwaitExpression} node */
AwaitExpression(node) {
if (!scopeStack) {
return
}
for (const reference of variable.references) {
if (!reference.isRead()) {
continue
}
exposeReferenceIds.add(reference.identifier)
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (!setupScope || !utils.inRange(setupScope.range, node)) {
return
}
} else if (contextParam.type === 'Identifier') {
// `setup(props, context)`
const variable = findVariable(context.getScope(), contextParam)
if (!variable) {
setupScope.afterAwait = true
},
/** @param {CallExpression} node */
CallExpression(node) {
if (!scopeStack) {
return
}
for (const reference of variable.references) {
if (!reference.isRead()) {
continue
}
contextReferenceIds.add(reference.identifier)
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (
!setupScope ||
!setupScope.afterAwait ||
!utils.inRange(setupScope.range, node)
) {
return
}
}
setupScopes.set(node, {
afterAwait: false,
range: node.range,
exposeReferenceIds,
contextReferenceIds
})
},
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
':function'(node) {
scopeStack = {
upper: scopeStack,
scopeNode: node
}
},
':function:exit'() {
scopeStack = scopeStack && scopeStack.upper
},
/** @param {AwaitExpression} node */
AwaitExpression(node) {
if (!scopeStack) {
return
}
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (!setupScope || !utils.inRange(setupScope.range, node)) {
return
}
setupScope.afterAwait = true
},
/** @param {CallExpression} node */
CallExpression(node) {
if (!scopeStack) {
return
}
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (
!setupScope ||
!setupScope.afterAwait ||
!utils.inRange(setupScope.range, node)
) {
return
}
const { contextReferenceIds, exposeReferenceIds } = setupScope
if (
node.callee.type === 'Identifier' &&
exposeReferenceIds.has(node.callee)
) {
// setup(props,{expose}) {expose()}
context.report({
node,
messageId: 'forbidden'
})
} else {
const expose = getCalleeMemberNode(node)
const { isContextReferenceId, isExposeReferenceId } = setupScope
if (
expose &&
expose.name === 'expose' &&
expose.member.object.type === 'Identifier' &&
contextReferenceIds.has(expose.member.object)
node.callee.type === 'Identifier' &&
isExposeReferenceId(node.callee, node)
) {
// setup(props,context) {context.emit()}
// setup(props,{expose}) {expose()}
context.report({
node,
messageId: 'forbidden'
messageId: 'forbidden',
data: {
name: node.callee.name
}
})
} else {
const expose = getCalleeMemberNode(node)
if (
expose &&
expose.name === 'expose' &&
expose.member.object.type === 'Identifier' &&
isContextReferenceId(expose.member.object)
) {
// setup(props,context) {context.emit()}
context.report({
node,
messageId: 'forbidden',
data: {
name: expose.name
}
})
}
}
}
},
onSetupFunctionExit(node) {
setupScopes.delete(node)
}
})
(() => {
const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup) {
return {}
}
return {
/**
* @param {Program} node
*/
Program(node) {
context
.getScope()
.references.find((ref) => ref.identifier.name === 'defineExpose')
setupScopes.set(node, {
afterAwait: false,
range: scriptSetup.range,
isExposeReferenceId: (_id, callNode) => {
return (
callNode.parent.type === 'ExpressionStatement' &&
callNode.parent.parent === node
)
},
ota-meshi marked this conversation as resolved.
Show resolved Hide resolved
isContextReferenceId: () => false
})
}
}
})(),
utils.defineVueVisitor(context, {
onSetupFunctionEnter(node) {
const contextParam = node.params[1]
if (!contextParam) {
// no arguments
return
}
if (contextParam.type === 'RestElement') {
// cannot check
return
}
if (contextParam.type === 'ArrayPattern') {
// cannot check
return
}
/** @type {Set<Identifier>} */
const contextReferenceIds = new Set()
/** @type {Set<Identifier>} */
const exposeReferenceIds = new Set()
if (contextParam.type === 'ObjectPattern') {
const exposeProperty = utils.findAssignmentProperty(
contextParam,
'expose'
)
if (!exposeProperty) {
return
}
const exposeParam = exposeProperty.value
// `setup(props, {emit})`
const variable =
exposeParam.type === 'Identifier'
? findVariable(context.getScope(), exposeParam)
: null
if (!variable) {
return
}
for (const reference of variable.references) {
if (!reference.isRead()) {
continue
}
exposeReferenceIds.add(reference.identifier)
}
} else if (contextParam.type === 'Identifier') {
// `setup(props, context)`
const variable = findVariable(context.getScope(), contextParam)
if (!variable) {
return
}
for (const reference of variable.references) {
if (!reference.isRead()) {
continue
}
contextReferenceIds.add(reference.identifier)
}
}
setupScopes.set(node, {
afterAwait: false,
range: node.range,
isExposeReferenceId: (id) => exposeReferenceIds.has(id),
isContextReferenceId: (id) => contextReferenceIds.has(id)
})
},
onSetupFunctionExit(node) {
setupScopes.delete(node)
}
})
)
}
}