Skip to content

Commit f8bff9e

Browse files
authoredJan 17, 2024
fix(vitest): throw a syntax error if vi.hoisted is directly exported (#4969)
1 parent ba7ae53 commit f8bff9e

File tree

5 files changed

+209
-91
lines changed

5 files changed

+209
-91
lines changed
 

‎packages/vitest/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
"@vitest/snapshot": "workspace:*",
147147
"@vitest/spy": "workspace:*",
148148
"@vitest/utils": "workspace:*",
149-
"acorn-walk": "^8.3.1",
149+
"acorn-walk": "^8.3.2",
150150
"cac": "^6.7.14",
151151
"chai": "^4.3.10",
152152
"debug": "^4.3.4",

‎packages/vitest/src/node/hoistMocks.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import MagicString from 'magic-string'
2-
import type { AwaitExpression, CallExpression, Identifier, ImportDeclaration, VariableDeclaration, Node as _Node } from 'estree'
3-
4-
// TODO: should use findNodeBefore, but it's not typed
2+
import type { AwaitExpression, CallExpression, ExportDefaultDeclaration, ExportNamedDeclaration, Identifier, ImportDeclaration, VariableDeclaration, Node as _Node } from 'estree'
53
import { findNodeAround } from 'acorn-walk'
64
import type { PluginContext } from 'rollup'
75
import { esmWalker } from '@vitest/utils/ast'
@@ -153,6 +151,17 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
153151
const declaredConst = new Set<string>()
154152
const hoistedNodes: Positioned<CallExpression | VariableDeclaration | AwaitExpression>[] = []
155153

154+
function createSyntaxError(node: Positioned<Node>, message: string) {
155+
const _error = new SyntaxError(message)
156+
Error.captureStackTrace(_error, createSyntaxError)
157+
return {
158+
name: 'SyntaxError',
159+
message: _error.message,
160+
stack: _error.stack,
161+
frame: generateCodeFrame(highlightCode(id, code, colors), 4, node.start + 1),
162+
}
163+
}
164+
156165
esmWalker(ast, {
157166
onIdentifier(id, info, parentStack) {
158167
const binding = idToImportMap.get(id.name)
@@ -192,6 +201,11 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
192201
hoistedNodes.push(node)
193202

194203
if (methodName === 'hoisted') {
204+
// check it's not a default export
205+
const defaultExport = findNodeAround(ast, node.start, 'ExportDefaultDeclaration')?.node as Positioned<ExportDefaultDeclaration> | undefined
206+
if (defaultExport?.declaration === node || (defaultExport?.declaration.type === 'AwaitExpression' && defaultExport.declaration.argument === node))
207+
throw createSyntaxError(defaultExport, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
208+
195209
const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned<VariableDeclaration> | undefined
196210
const init = declarationNode?.declarations[0]?.init
197211
const isViHoisted = (node: CallExpression) => {
@@ -211,6 +225,10 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
211225
&& isViHoisted(init.argument)) /* const v = await vi.hoisted() */
212226

213227
if (canMoveDeclaration) {
228+
// export const variable = vi.hoisted()
229+
const nodeExported = findNodeAround(ast, declarationNode.start, 'ExportNamedDeclaration')?.node as Positioned<ExportNamedDeclaration> | undefined
230+
if (nodeExported?.declaration === declarationNode)
231+
throw createSyntaxError(nodeExported, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
214232
// hoist "const variable = vi.hoisted(() => {})"
215233
hoistedNodes.push(declarationNode)
216234
}
@@ -251,15 +269,10 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
251269
function createError(outsideNode: Node, insideNode: Node) {
252270
const outsideCall = getNodeCall(outsideNode)
253271
const insideCall = getNodeCall(insideNode)
254-
const _error = new SyntaxError(`Cannot call ${getNodeName(insideCall)} inside ${getNodeName(outsideCall)}: both methods are hoisted to the top of the file and not actually called inside each other.`)
255-
// throw an object instead of an error so it can be serialized for RPC, TODO: improve error handling in rpc serializer
256-
const error = {
257-
name: 'SyntaxError',
258-
message: _error.message,
259-
stack: _error.stack,
260-
frame: generateCodeFrame(highlightCode(id, code, colors), 4, insideCall.start + 1),
261-
}
262-
throw error
272+
throw createSyntaxError(
273+
insideCall,
274+
`Cannot call ${getNodeName(insideCall)} inside ${getNodeName(outsideCall)}: both methods are hoisted to the top of the file and not actually called inside each other.`,
275+
)
263276
}
264277

265278
// validate hoistedNodes doesn't have nodes inside other nodes

‎pnpm-lock.yaml

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,122 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`throws an error when nodes are incompatible > correctly throws an error 1`] = `"Cannot call vi.hoisted() inside vi.mock(): both methods are hoisted to the top of the file and not actually called inside each other."`;
4-
5-
exports[`throws an error when nodes are incompatible > correctly throws an error 2`] = `
6-
" 3|
7-
4| vi.mock('./mocked', () => {
8-
5| const variable = vi.hoisted(() => 1)
9-
| ^
10-
6| console.log(variable)
11-
7| })"
3+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited assigned vi.hoisted is called inside vi.mock 1`] = `"Cannot call vi.hoisted() inside vi.mock(): both methods are hoisted to the top of the file and not actually called inside each other."`;
4+
5+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited assigned vi.hoisted is called inside vi.mock 2`] = `
6+
" 2|
7+
3| vi.mock('./mocked', async () => {
8+
4| const variable = await vi.hoisted(() => 1)
9+
| ^
10+
5| })
11+
6| "
1212
`;
1313

14-
exports[`throws an error when nodes are incompatible > correctly throws an error 3`] = `"Cannot call vi.hoisted() inside vi.mock(): both methods are hoisted to the top of the file and not actually called inside each other."`;
14+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited vi.hoisted is called inside vi.mock 1`] = `"Cannot call vi.hoisted() inside vi.mock(): both methods are hoisted to the top of the file and not actually called inside each other."`;
1515

16-
exports[`throws an error when nodes are incompatible > correctly throws an error 4`] = `
17-
" 3|
18-
4| vi.mock('./mocked', async () => {
19-
5| await vi.hoisted(() => 1)
16+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited vi.hoisted is called inside vi.mock 2`] = `
17+
" 2|
18+
3| vi.mock('./mocked', async () => {
19+
4| await vi.hoisted(() => 1)
2020
| ^
21-
6| })
22-
7| "
21+
5| })
22+
6| "
2323
`;
2424

25-
exports[`throws an error when nodes are incompatible > correctly throws an error 5`] = `"Cannot call vi.hoisted() inside vi.mock(): both methods are hoisted to the top of the file and not actually called inside each other."`;
25+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited vi.hoisted is exported as default export 1`] = `"Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first."`;
2626

27-
exports[`throws an error when nodes are incompatible > correctly throws an error 6`] = `
28-
" 3|
29-
4| vi.mock('./mocked', async () => {
30-
5| const variable = await vi.hoisted(() => 1)
31-
| ^
32-
6| })
33-
7| "
27+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited vi.hoisted is exported as default export 2`] = `
28+
" 1| import { vi } from 'vitest'
29+
2|
30+
3| export default await vi.hoisted(async () => {
31+
| ^
32+
4| return {}
33+
5| })"
34+
`;
35+
36+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited vi.hoisted is exported as named export 1`] = `"Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first."`;
37+
38+
exports[`throws an error when nodes are incompatible > correctly throws an error if awaited vi.hoisted is exported as named export 2`] = `
39+
" 1| import { vi } from 'vitest'
40+
2|
41+
3| export const values = await vi.hoisted(async () => {
42+
| ^
43+
4| return {}
44+
5| })"
45+
`;
46+
47+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.hoisted is called inside vi.mock 1`] = `"Cannot call vi.hoisted() inside vi.mock(): both methods are hoisted to the top of the file and not actually called inside each other."`;
48+
49+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.hoisted is called inside vi.mock 2`] = `
50+
" 2|
51+
3| vi.mock('./mocked', () => {
52+
4| const variable = vi.hoisted(() => 1)
53+
| ^
54+
5| console.log(variable)
55+
6| })"
56+
`;
57+
58+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.hoisted is exported as a named export 1`] = `"Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first."`;
59+
60+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.hoisted is exported as a named export 2`] = `
61+
" 1| import { vi } from 'vitest'
62+
2|
63+
3| export const values = vi.hoisted(async () => {
64+
| ^
65+
4| return {}
66+
5| })"
67+
`;
68+
69+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.hoisted is exported as default 1`] = `"Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first."`;
70+
71+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.hoisted is exported as default 2`] = `
72+
" 1| import { vi } from 'vitest'
73+
2|
74+
3| export default vi.hoisted(() => {
75+
| ^
76+
4| return {}
77+
5| })"
3478
`;
3579

36-
exports[`throws an error when nodes are incompatible > correctly throws an error 7`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
80+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock inside vi.hoisted 1`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
3781

38-
exports[`throws an error when nodes are incompatible > correctly throws an error 8`] = `
39-
" 3|
40-
4| vi.hoisted(() => {
41-
5| vi.mock('./mocked')
82+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock inside vi.hoisted 2`] = `
83+
" 2|
84+
3| vi.hoisted(() => {
85+
4| vi.mock('./mocked')
4286
| ^
43-
6| })
44-
7| "
87+
5| })
88+
6| "
4589
`;
4690

47-
exports[`throws an error when nodes are incompatible > correctly throws an error 9`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
91+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is called inside assigned awaited vi.hoisted 1`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
4892

49-
exports[`throws an error when nodes are incompatible > correctly throws an error 10`] = `
50-
" 3|
51-
4| const values = vi.hoisted(() => {
52-
5| vi.mock('./mocked')
93+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is called inside assigned awaited vi.hoisted 2`] = `
94+
" 2|
95+
3| const values = await vi.hoisted(async () => {
96+
4| vi.mock('./mocked')
5397
| ^
54-
6| })
55-
7| "
98+
5| })
99+
6| "
56100
`;
57101

58-
exports[`throws an error when nodes are incompatible > correctly throws an error 11`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
102+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is called inside assigned vi.hoisted 1`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
59103

60-
exports[`throws an error when nodes are incompatible > correctly throws an error 12`] = `
61-
" 3|
62-
4| await vi.hoisted(async () => {
63-
5| vi.mock('./mocked')
104+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is called inside assigned vi.hoisted 2`] = `
105+
" 2|
106+
3| const values = vi.hoisted(() => {
107+
4| vi.mock('./mocked')
64108
| ^
65-
6| })
66-
7| "
109+
5| })
110+
6| "
67111
`;
68112

69-
exports[`throws an error when nodes are incompatible > correctly throws an error 13`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
113+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is called inside awaited vi.hoisted 1`] = `"Cannot call vi.mock() inside vi.hoisted(): both methods are hoisted to the top of the file and not actually called inside each other."`;
70114

71-
exports[`throws an error when nodes are incompatible > correctly throws an error 14`] = `
72-
" 3|
73-
4| const values = await vi.hoisted(async () => {
74-
5| vi.mock('./mocked')
115+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is called inside awaited vi.hoisted 2`] = `
116+
" 2|
117+
3| await vi.hoisted(async () => {
118+
4| vi.mock('./mocked')
75119
| ^
76-
6| })
77-
7| "
120+
5| })
121+
6| "
78122
`;

‎test/core/test/injector-mock.test.ts

+82-21
Original file line numberDiff line numberDiff line change
@@ -1214,57 +1214,118 @@ describe('throws an error when nodes are incompatible', () => {
12141214
}
12151215

12161216
it.each([
1217-
`
1218-
import { vi } from 'vitest'
1219-
1220-
vi.mock('./mocked', () => {
1221-
const variable = vi.hoisted(() => 1)
1222-
console.log(variable)
1223-
})
1224-
`,
1225-
`
1217+
[
1218+
'vi.hoisted is called inside vi.mock',
1219+
`\
1220+
import { vi } from 'vitest'
1221+
1222+
vi.mock('./mocked', () => {
1223+
const variable = vi.hoisted(() => 1)
1224+
console.log(variable)
1225+
})
1226+
`,
1227+
],
1228+
[
1229+
'awaited vi.hoisted is called inside vi.mock',
1230+
`\
12261231
import { vi } from 'vitest'
12271232
12281233
vi.mock('./mocked', async () => {
12291234
await vi.hoisted(() => 1)
12301235
})
1231-
`,
1232-
`
1236+
`,
1237+
],
1238+
[
1239+
'awaited assigned vi.hoisted is called inside vi.mock',
1240+
`\
12331241
import { vi } from 'vitest'
12341242
12351243
vi.mock('./mocked', async () => {
12361244
const variable = await vi.hoisted(() => 1)
12371245
})
1238-
`,
1239-
`
1246+
`,
1247+
],
1248+
[
1249+
'vi.mock inside vi.hoisted',
1250+
`\
12401251
import { vi } from 'vitest'
12411252
12421253
vi.hoisted(() => {
12431254
vi.mock('./mocked')
12441255
})
1245-
`,
1246-
`
1256+
`,
1257+
],
1258+
[
1259+
'vi.mock is called inside assigned vi.hoisted',
1260+
`\
12471261
import { vi } from 'vitest'
12481262
12491263
const values = vi.hoisted(() => {
12501264
vi.mock('./mocked')
12511265
})
1252-
`,
1253-
`
1266+
`,
1267+
],
1268+
[
1269+
'vi.mock is called inside awaited vi.hoisted',
1270+
`\
12541271
import { vi } from 'vitest'
12551272
12561273
await vi.hoisted(async () => {
12571274
vi.mock('./mocked')
12581275
})
1259-
`,
1260-
`
1276+
`,
1277+
],
1278+
[
1279+
'vi.mock is called inside assigned awaited vi.hoisted',
1280+
`\
12611281
import { vi } from 'vitest'
12621282
12631283
const values = await vi.hoisted(async () => {
12641284
vi.mock('./mocked')
12651285
})
1266-
`,
1267-
])('correctly throws an error', (code) => {
1286+
`,
1287+
],
1288+
[
1289+
'vi.hoisted is exported as a named export',
1290+
`\
1291+
import { vi } from 'vitest'
1292+
1293+
export const values = vi.hoisted(async () => {
1294+
return {}
1295+
})
1296+
`,
1297+
],
1298+
[
1299+
'vi.hoisted is exported as default',
1300+
`\
1301+
import { vi } from 'vitest'
1302+
1303+
export default vi.hoisted(() => {
1304+
return {}
1305+
})
1306+
`,
1307+
],
1308+
[
1309+
'awaited vi.hoisted is exported as named export',
1310+
`\
1311+
import { vi } from 'vitest'
1312+
1313+
export const values = await vi.hoisted(async () => {
1314+
return {}
1315+
})
1316+
`,
1317+
],
1318+
[
1319+
'awaited vi.hoisted is exported as default export',
1320+
`\
1321+
import { vi } from 'vitest'
1322+
1323+
export default await vi.hoisted(async () => {
1324+
return {}
1325+
})
1326+
`,
1327+
],
1328+
])('correctly throws an error if %s', (_, code) => {
12681329
const error = getErrorWhileHoisting(code)
12691330
expect(error.message).toMatchSnapshot()
12701331
expect(stripAnsi(error.frame)).toMatchSnapshot()

0 commit comments

Comments
 (0)
Please sign in to comment.