Skip to content

Commit 7c6ac02

Browse files
authored
Add vue/no-expose-after-await rule (#1712)
1 parent f4c12cc commit 7c6ac02

File tree

5 files changed

+407
-0
lines changed

5 files changed

+407
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ For example:
324324
| [vue/no-child-content](./no-child-content.md) | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | :bulb: |
325325
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
326326
| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty | |
327+
| [vue/no-expose-after-await](./no-expose-after-await.md) | disallow asynchronously registered `expose` | |
327328
| [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | require valid keys in model option | |
328329
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | |
329330
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: |

docs/rules/no-expose-after-await.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-expose-after-await
5+
description: disallow asynchronously registered `expose`
6+
---
7+
# vue/no-expose-after-await
8+
9+
> disallow asynchronously registered `expose`
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule reports the `expose()` after `await` expression.
16+
In `setup()` function, `expose()` should be registered synchronously.
17+
18+
<eslint-code-block :rules="{'vue/no-expose-after-await': ['error']}">
19+
20+
```vue
21+
<script>
22+
import { watch } from 'vue'
23+
export default {
24+
async setup(props, { expose }) {
25+
/* ✓ GOOD */
26+
expose({/* ... */})
27+
28+
await doSomething()
29+
30+
/* ✗ BAD */
31+
expose({/* ... */})
32+
}
33+
}
34+
</script>
35+
```
36+
37+
</eslint-code-block>
38+
39+
## :wrench: Options
40+
41+
Nothing.
42+
43+
## :books: Further Reading
44+
45+
- [Vue RFCs - 0042-expose-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0042-expose-api.md)
46+
- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
47+
48+
## :mag: Implementation
49+
50+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-expose-after-await.js)
51+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-expose-after-await.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ module.exports = {
8989
'no-empty-component-block': require('./rules/no-empty-component-block'),
9090
'no-empty-pattern': require('./rules/no-empty-pattern'),
9191
'no-export-in-script-setup': require('./rules/no-export-in-script-setup'),
92+
'no-expose-after-await': require('./rules/no-expose-after-await'),
9293
'no-extra-parens': require('./rules/no-extra-parens'),
9394
'no-invalid-model-keys': require('./rules/no-invalid-model-keys'),
9495
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),

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

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const { findVariable } = require('eslint-utils')
12+
const utils = require('../utils')
13+
14+
// ------------------------------------------------------------------------------
15+
// Helpers
16+
// ------------------------------------------------------------------------------
17+
18+
/**
19+
* Get the callee member node from the given CallExpression
20+
* @param {CallExpression} node CallExpression
21+
*/
22+
function getCalleeMemberNode(node) {
23+
const callee = utils.skipChainExpression(node.callee)
24+
25+
if (callee.type === 'MemberExpression') {
26+
const name = utils.getStaticPropertyName(callee)
27+
if (name) {
28+
return { name, member: callee }
29+
}
30+
}
31+
return null
32+
}
33+
34+
// ------------------------------------------------------------------------------
35+
// Rule Definition
36+
// ------------------------------------------------------------------------------
37+
38+
module.exports = {
39+
meta: {
40+
type: 'problem',
41+
docs: {
42+
description: 'disallow asynchronously registered `expose`',
43+
categories: undefined,
44+
// categories: ['vue3-essential'], TODO Change with the major version
45+
url: 'https://eslint.vuejs.org/rules/no-expose-after-await.html'
46+
},
47+
fixable: null,
48+
schema: [],
49+
messages: {
50+
forbidden: 'The `expose` after `await` expression are forbidden.'
51+
}
52+
},
53+
/** @param {RuleContext} context */
54+
create(context) {
55+
/**
56+
* @typedef {object} SetupScopeData
57+
* @property {boolean} afterAwait
58+
* @property {[number,number]} range
59+
* @property {Set<Identifier>} exposeReferenceIds
60+
* @property {Set<Identifier>} contextReferenceIds
61+
*/
62+
/**
63+
* @typedef {object} ScopeStack
64+
* @property {ScopeStack | null} upper
65+
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
66+
*/
67+
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupScopeData>} */
68+
const setupScopes = new Map()
69+
70+
/** @type {ScopeStack | null} */
71+
let scopeStack = null
72+
73+
return utils.defineVueVisitor(context, {
74+
onSetupFunctionEnter(node) {
75+
const contextParam = node.params[1]
76+
if (!contextParam) {
77+
// no arguments
78+
return
79+
}
80+
if (contextParam.type === 'RestElement') {
81+
// cannot check
82+
return
83+
}
84+
if (contextParam.type === 'ArrayPattern') {
85+
// cannot check
86+
return
87+
}
88+
/** @type {Set<Identifier>} */
89+
const contextReferenceIds = new Set()
90+
/** @type {Set<Identifier>} */
91+
const exposeReferenceIds = new Set()
92+
if (contextParam.type === 'ObjectPattern') {
93+
const exposeProperty = utils.findAssignmentProperty(
94+
contextParam,
95+
'expose'
96+
)
97+
if (!exposeProperty) {
98+
return
99+
}
100+
const exposeParam = exposeProperty.value
101+
// `setup(props, {emit})`
102+
const variable =
103+
exposeParam.type === 'Identifier'
104+
? findVariable(context.getScope(), exposeParam)
105+
: null
106+
if (!variable) {
107+
return
108+
}
109+
for (const reference of variable.references) {
110+
if (!reference.isRead()) {
111+
continue
112+
}
113+
exposeReferenceIds.add(reference.identifier)
114+
}
115+
} else if (contextParam.type === 'Identifier') {
116+
// `setup(props, context)`
117+
const variable = findVariable(context.getScope(), contextParam)
118+
if (!variable) {
119+
return
120+
}
121+
for (const reference of variable.references) {
122+
if (!reference.isRead()) {
123+
continue
124+
}
125+
contextReferenceIds.add(reference.identifier)
126+
}
127+
}
128+
setupScopes.set(node, {
129+
afterAwait: false,
130+
range: node.range,
131+
exposeReferenceIds,
132+
contextReferenceIds
133+
})
134+
},
135+
/**
136+
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
137+
*/
138+
':function'(node) {
139+
scopeStack = {
140+
upper: scopeStack,
141+
scopeNode: node
142+
}
143+
},
144+
':function:exit'() {
145+
scopeStack = scopeStack && scopeStack.upper
146+
},
147+
/** @param {AwaitExpression} node */
148+
AwaitExpression(node) {
149+
if (!scopeStack) {
150+
return
151+
}
152+
const setupScope = setupScopes.get(scopeStack.scopeNode)
153+
if (!setupScope || !utils.inRange(setupScope.range, node)) {
154+
return
155+
}
156+
setupScope.afterAwait = true
157+
},
158+
/** @param {CallExpression} node */
159+
CallExpression(node) {
160+
if (!scopeStack) {
161+
return
162+
}
163+
const setupScope = setupScopes.get(scopeStack.scopeNode)
164+
if (
165+
!setupScope ||
166+
!setupScope.afterAwait ||
167+
!utils.inRange(setupScope.range, node)
168+
) {
169+
return
170+
}
171+
const { contextReferenceIds, exposeReferenceIds } = setupScope
172+
if (
173+
node.callee.type === 'Identifier' &&
174+
exposeReferenceIds.has(node.callee)
175+
) {
176+
// setup(props,{expose}) {expose()}
177+
context.report({
178+
node,
179+
messageId: 'forbidden'
180+
})
181+
} else {
182+
const expose = getCalleeMemberNode(node)
183+
if (
184+
expose &&
185+
expose.name === 'expose' &&
186+
expose.member.object.type === 'Identifier' &&
187+
contextReferenceIds.has(expose.member.object)
188+
) {
189+
// setup(props,context) {context.emit()}
190+
context.report({
191+
node,
192+
messageId: 'forbidden'
193+
})
194+
}
195+
}
196+
},
197+
onSetupFunctionExit(node) {
198+
setupScopes.delete(node)
199+
}
200+
})
201+
}
202+
}

0 commit comments

Comments
 (0)