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 all commits
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
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -119,6 +119,7 @@ module.exports = {
'prefer-spread': 'error',

'dot-notation': 'error',
'arrow-body-style': 'error',

'unicorn/consistent-function-scoping': [
'error',
Expand Down
22 changes: 19 additions & 3 deletions docs/rules/no-expose-after-await.md
Expand Up @@ -13,14 +13,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 @@ -37,6 +37,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
4 changes: 1 addition & 3 deletions lib/rules/attribute-hyphenation.js
Expand Up @@ -107,9 +107,7 @@ module.exports = {
* @param {string} value
*/
function isIgnoredAttribute(value) {
const isIgnored = ignoredAttributes.some((attr) => {
return value.includes(attr)
})
const isIgnored = ignoredAttributes.some((attr) => value.includes(attr))

if (isIgnored) {
return true
Expand Down
5 changes: 2 additions & 3 deletions lib/rules/no-dupe-v-else-if.js
Expand Up @@ -167,11 +167,10 @@ module.exports = {

for (const condition of listToCheck) {
const operands = (condition.operands = condition.operands.filter(
(orOperand) => {
return !currentOrOperands.operands.some((currentOrOperand) =>
(orOperand) =>
!currentOrOperands.operands.some((currentOrOperand) =>
isSubset(currentOrOperand, orOperand)
)
}
))
if (operands.length === 0) {
context.report({
Expand Down
279 changes: 162 additions & 117 deletions lib/rules/no-expose-after-await.js
Expand Up @@ -46,7 +46,7 @@ module.exports = {
fixable: null,
schema: [],
messages: {
forbidden: 'The `expose` after `await` expression are forbidden.'
forbidden: '`{{name}}` is forbidden after an `await` expression.'
}
},
/** @param {RuleContext} context */
Expand All @@ -55,147 +55,192 @@ 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) =>
callNode.parent.type === 'ExpressionStatement' &&
callNode.parent.parent === node,
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)
}
})
)
}
}