Skip to content

Commit 52accc9

Browse files
Update vue/no-expose-after-await rule to support <script setup> (#1885)
* Update `vue/no-expose-after-await` rule to support `<script setup>` * Update lib/rules/no-expose-after-await.js Co-authored-by: Flo Edelmann <[email protected]> * Update lib/rules/no-expose-after-await.js Co-authored-by: Flo Edelmann <[email protected]> * format & fix test * Use arrow-body-style & format * fix messages Co-authored-by: Flo Edelmann <[email protected]>
1 parent a0cf018 commit 52accc9

21 files changed

+429
-394
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ module.exports = {
119119
'prefer-spread': 'error',
120120

121121
'dot-notation': 'error',
122+
'arrow-body-style': 'error',
122123

123124
'unicorn/consistent-function-scoping': [
124125
'error',

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

+19-3
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ since: v8.1.0
1313

1414
## :book: Rule Details
1515

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

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

2122
```vue
2223
<script>
23-
import { watch } from 'vue'
2424
export default {
2525
async setup(props, { expose }) {
2626
/* ✓ GOOD */
@@ -37,6 +37,22 @@ export default {
3737

3838
</eslint-code-block>
3939

40+
<eslint-code-block :rules="{'vue/no-expose-after-await': ['error']}">
41+
42+
```vue
43+
<script setup>
44+
/* ✓ GOOD */
45+
defineExpose({/* ... */})
46+
47+
await doSomething()
48+
49+
/* ✗ BAD */
50+
defineExpose({/* ... */})
51+
</script>
52+
```
53+
54+
</eslint-code-block>
55+
4056
## :wrench: Options
4157

4258
Nothing.

lib/rules/attribute-hyphenation.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,7 @@ module.exports = {
107107
* @param {string} value
108108
*/
109109
function isIgnoredAttribute(value) {
110-
const isIgnored = ignoredAttributes.some((attr) => {
111-
return value.includes(attr)
112-
})
110+
const isIgnored = ignoredAttributes.some((attr) => value.includes(attr))
113111

114112
if (isIgnored) {
115113
return true

lib/rules/no-dupe-v-else-if.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,10 @@ module.exports = {
167167

168168
for (const condition of listToCheck) {
169169
const operands = (condition.operands = condition.operands.filter(
170-
(orOperand) => {
171-
return !currentOrOperands.operands.some((currentOrOperand) =>
170+
(orOperand) =>
171+
!currentOrOperands.operands.some((currentOrOperand) =>
172172
isSubset(currentOrOperand, orOperand)
173173
)
174-
}
175174
))
176175
if (operands.length === 0) {
177176
context.report({

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

+162-117
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module.exports = {
4646
fixable: null,
4747
schema: [],
4848
messages: {
49-
forbidden: 'The `expose` after `await` expression are forbidden.'
49+
forbidden: '`{{name}}` is forbidden after an `await` expression.'
5050
}
5151
},
5252
/** @param {RuleContext} context */
@@ -55,147 +55,192 @@ module.exports = {
5555
* @typedef {object} SetupScopeData
5656
* @property {boolean} afterAwait
5757
* @property {[number,number]} range
58-
* @property {Set<Identifier>} exposeReferenceIds
59-
* @property {Set<Identifier>} contextReferenceIds
58+
* @property {(node: Identifier, callNode: CallExpression) => boolean} isExposeReferenceId
59+
* @property {(node: Identifier) => boolean} isContextReferenceId
6060
*/
6161
/**
6262
* @typedef {object} ScopeStack
6363
* @property {ScopeStack | null} upper
64-
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
64+
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
6565
*/
66-
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupScopeData>} */
66+
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, SetupScopeData>} */
6767
const setupScopes = new Map()
6868

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

72-
return utils.defineVueVisitor(context, {
73-
onSetupFunctionEnter(node) {
74-
const contextParam = node.params[1]
75-
if (!contextParam) {
76-
// no arguments
77-
return
78-
}
79-
if (contextParam.type === 'RestElement') {
80-
// cannot check
81-
return
82-
}
83-
if (contextParam.type === 'ArrayPattern') {
84-
// cannot check
85-
return
72+
return utils.compositingVisitors(
73+
{
74+
/**
75+
* @param {Program} node
76+
*/
77+
Program(node) {
78+
scopeStack = {
79+
upper: scopeStack,
80+
scopeNode: node
81+
}
8682
}
87-
/** @type {Set<Identifier>} */
88-
const contextReferenceIds = new Set()
89-
/** @type {Set<Identifier>} */
90-
const exposeReferenceIds = new Set()
91-
if (contextParam.type === 'ObjectPattern') {
92-
const exposeProperty = utils.findAssignmentProperty(
93-
contextParam,
94-
'expose'
95-
)
96-
if (!exposeProperty) {
97-
return
83+
},
84+
{
85+
/**
86+
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
87+
*/
88+
':function'(node) {
89+
scopeStack = {
90+
upper: scopeStack,
91+
scopeNode: node
9892
}
99-
const exposeParam = exposeProperty.value
100-
// `setup(props, {emit})`
101-
const variable =
102-
exposeParam.type === 'Identifier'
103-
? findVariable(context.getScope(), exposeParam)
104-
: null
105-
if (!variable) {
93+
},
94+
':function:exit'() {
95+
scopeStack = scopeStack && scopeStack.upper
96+
},
97+
/** @param {AwaitExpression} node */
98+
AwaitExpression(node) {
99+
if (!scopeStack) {
106100
return
107101
}
108-
for (const reference of variable.references) {
109-
if (!reference.isRead()) {
110-
continue
111-
}
112-
exposeReferenceIds.add(reference.identifier)
102+
const setupScope = setupScopes.get(scopeStack.scopeNode)
103+
if (!setupScope || !utils.inRange(setupScope.range, node)) {
104+
return
113105
}
114-
} else if (contextParam.type === 'Identifier') {
115-
// `setup(props, context)`
116-
const variable = findVariable(context.getScope(), contextParam)
117-
if (!variable) {
106+
setupScope.afterAwait = true
107+
},
108+
/** @param {CallExpression} node */
109+
CallExpression(node) {
110+
if (!scopeStack) {
118111
return
119112
}
120-
for (const reference of variable.references) {
121-
if (!reference.isRead()) {
122-
continue
123-
}
124-
contextReferenceIds.add(reference.identifier)
113+
const setupScope = setupScopes.get(scopeStack.scopeNode)
114+
if (
115+
!setupScope ||
116+
!setupScope.afterAwait ||
117+
!utils.inRange(setupScope.range, node)
118+
) {
119+
return
125120
}
126-
}
127-
setupScopes.set(node, {
128-
afterAwait: false,
129-
range: node.range,
130-
exposeReferenceIds,
131-
contextReferenceIds
132-
})
133-
},
134-
/**
135-
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
136-
*/
137-
':function'(node) {
138-
scopeStack = {
139-
upper: scopeStack,
140-
scopeNode: node
141-
}
142-
},
143-
':function:exit'() {
144-
scopeStack = scopeStack && scopeStack.upper
145-
},
146-
/** @param {AwaitExpression} node */
147-
AwaitExpression(node) {
148-
if (!scopeStack) {
149-
return
150-
}
151-
const setupScope = setupScopes.get(scopeStack.scopeNode)
152-
if (!setupScope || !utils.inRange(setupScope.range, node)) {
153-
return
154-
}
155-
setupScope.afterAwait = true
156-
},
157-
/** @param {CallExpression} node */
158-
CallExpression(node) {
159-
if (!scopeStack) {
160-
return
161-
}
162-
const setupScope = setupScopes.get(scopeStack.scopeNode)
163-
if (
164-
!setupScope ||
165-
!setupScope.afterAwait ||
166-
!utils.inRange(setupScope.range, node)
167-
) {
168-
return
169-
}
170-
const { contextReferenceIds, exposeReferenceIds } = setupScope
171-
if (
172-
node.callee.type === 'Identifier' &&
173-
exposeReferenceIds.has(node.callee)
174-
) {
175-
// setup(props,{expose}) {expose()}
176-
context.report({
177-
node,
178-
messageId: 'forbidden'
179-
})
180-
} else {
181-
const expose = getCalleeMemberNode(node)
121+
const { isContextReferenceId, isExposeReferenceId } = setupScope
182122
if (
183-
expose &&
184-
expose.name === 'expose' &&
185-
expose.member.object.type === 'Identifier' &&
186-
contextReferenceIds.has(expose.member.object)
123+
node.callee.type === 'Identifier' &&
124+
isExposeReferenceId(node.callee, node)
187125
) {
188-
// setup(props,context) {context.emit()}
126+
// setup(props,{expose}) {expose()}
189127
context.report({
190128
node,
191-
messageId: 'forbidden'
129+
messageId: 'forbidden',
130+
data: {
131+
name: node.callee.name
132+
}
192133
})
134+
} else {
135+
const expose = getCalleeMemberNode(node)
136+
if (
137+
expose &&
138+
expose.name === 'expose' &&
139+
expose.member.object.type === 'Identifier' &&
140+
isContextReferenceId(expose.member.object)
141+
) {
142+
// setup(props,context) {context.emit()}
143+
context.report({
144+
node,
145+
messageId: 'forbidden',
146+
data: {
147+
name: expose.name
148+
}
149+
})
150+
}
193151
}
194152
}
195153
},
196-
onSetupFunctionExit(node) {
197-
setupScopes.delete(node)
198-
}
199-
})
154+
(() => {
155+
const scriptSetup = utils.getScriptSetupElement(context)
156+
if (!scriptSetup) {
157+
return {}
158+
}
159+
return {
160+
/**
161+
* @param {Program} node
162+
*/
163+
Program(node) {
164+
context
165+
.getScope()
166+
.references.find((ref) => ref.identifier.name === 'defineExpose')
167+
setupScopes.set(node, {
168+
afterAwait: false,
169+
range: scriptSetup.range,
170+
isExposeReferenceId: (_id, callNode) =>
171+
callNode.parent.type === 'ExpressionStatement' &&
172+
callNode.parent.parent === node,
173+
isContextReferenceId: () => false
174+
})
175+
}
176+
}
177+
})(),
178+
utils.defineVueVisitor(context, {
179+
onSetupFunctionEnter(node) {
180+
const contextParam = node.params[1]
181+
if (!contextParam) {
182+
// no arguments
183+
return
184+
}
185+
if (contextParam.type === 'RestElement') {
186+
// cannot check
187+
return
188+
}
189+
if (contextParam.type === 'ArrayPattern') {
190+
// cannot check
191+
return
192+
}
193+
/** @type {Set<Identifier>} */
194+
const contextReferenceIds = new Set()
195+
/** @type {Set<Identifier>} */
196+
const exposeReferenceIds = new Set()
197+
if (contextParam.type === 'ObjectPattern') {
198+
const exposeProperty = utils.findAssignmentProperty(
199+
contextParam,
200+
'expose'
201+
)
202+
if (!exposeProperty) {
203+
return
204+
}
205+
const exposeParam = exposeProperty.value
206+
// `setup(props, {emit})`
207+
const variable =
208+
exposeParam.type === 'Identifier'
209+
? findVariable(context.getScope(), exposeParam)
210+
: null
211+
if (!variable) {
212+
return
213+
}
214+
for (const reference of variable.references) {
215+
if (!reference.isRead()) {
216+
continue
217+
}
218+
exposeReferenceIds.add(reference.identifier)
219+
}
220+
} else if (contextParam.type === 'Identifier') {
221+
// `setup(props, context)`
222+
const variable = findVariable(context.getScope(), contextParam)
223+
if (!variable) {
224+
return
225+
}
226+
for (const reference of variable.references) {
227+
if (!reference.isRead()) {
228+
continue
229+
}
230+
contextReferenceIds.add(reference.identifier)
231+
}
232+
}
233+
setupScopes.set(node, {
234+
afterAwait: false,
235+
range: node.range,
236+
isExposeReferenceId: (id) => exposeReferenceIds.has(id),
237+
isContextReferenceId: (id) => contextReferenceIds.has(id)
238+
})
239+
},
240+
onSetupFunctionExit(node) {
241+
setupScopes.delete(node)
242+
}
243+
})
244+
)
200245
}
201246
}

0 commit comments

Comments
 (0)