Skip to content

Commit

Permalink
fix(vitest): throw an error if vi.mock is exported (#5034)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Jan 23, 2024
1 parent 7344870 commit 253df1c
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 38 deletions.
12 changes: 2 additions & 10 deletions packages/vite-node/src/source-map-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'
// Only install once if called multiple times
let errorFormatterInstalled = false

// If true, the caches are reset before a stack trace formatting operation
const emptyCacheBetweenOperations = false

// Maps a file path to a string containing the file contents
let fileContentsCache: Record<string, string> = {}
const fileContentsCache: Record<string, string> = {}

// Maps a file path to a source map for that file
let sourceMapCache: Record<string, { url: string | null; map: TraceMap | null }> = {}
const sourceMapCache: Record<string, { url: string | null; map: TraceMap | null }> = {}

// Regex for detecting source maps
const reSourceMap = /^data:application\/json[^,]+base64,/
Expand Down Expand Up @@ -405,11 +402,6 @@ function wrapCallSite(frame: CallSite, state: State) {
// This function is part of the V8 stack trace API, for more info see:
// https://v8.dev/docs/stack-trace-api
function prepareStackTrace(error: Error, stack: CallSite[]) {
if (emptyCacheBetweenOperations) {
fileContentsCache = {}
sourceMapCache = {}
}

const name = error.name || 'Error'
const message = error.message || ''
const errorString = `${name}: ${message}`
Expand Down
58 changes: 30 additions & 28 deletions packages/vitest/src/node/hoistMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,25 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
}
}

function assertNotDefaultExport(node: Positioned<CallExpression>, error: string) {
const defaultExport = findNodeAround(ast, node.start, 'ExportDefaultDeclaration')?.node as Positioned<ExportDefaultDeclaration> | undefined
if (defaultExport?.declaration === node || (defaultExport?.declaration.type === 'AwaitExpression' && defaultExport.declaration.argument === node))
throw createSyntaxError(defaultExport, error)
}

function assertNotNamedExport(node: Positioned<VariableDeclaration>, error: string) {
const nodeExported = findNodeAround(ast, node.start, 'ExportNamedDeclaration')?.node as Positioned<ExportNamedDeclaration> | undefined
if (nodeExported?.declaration === node)
throw createSyntaxError(nodeExported, error)
}

function getVariableDeclaration(node: Positioned<CallExpression>) {
const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned<VariableDeclaration> | undefined
const init = declarationNode?.declarations[0]?.init
if (init && (init === node || (init.type === 'AwaitExpression' && init.argument === node)))
return declarationNode
}

esmWalker(ast, {
onIdentifier(id, info, parentStack) {
const binding = idToImportMap.get(id.name)
Expand Down Expand Up @@ -197,38 +216,21 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
) {
const methodName = node.callee.property.name

if (methodName === 'mock' || methodName === 'unmock')
if (methodName === 'mock' || methodName === 'unmock') {
const method = `${node.callee.object.name}.${methodName}`
assertNotDefaultExport(node, `Cannot export the result of "${method}". Remove export declaration because "${method}" doesn\'t return anything.`)
const declarationNode = getVariableDeclaration(node)
if (declarationNode)
assertNotNamedExport(declarationNode, `Cannot export the result of "${method}". Remove export declaration because "${method}" doesn\'t return anything.`)
hoistedNodes.push(node)
}

if (methodName === 'hoisted') {
// check it's not a default export
const defaultExport = findNodeAround(ast, node.start, 'ExportDefaultDeclaration')?.node as Positioned<ExportDefaultDeclaration> | undefined
if (defaultExport?.declaration === node || (defaultExport?.declaration.type === 'AwaitExpression' && defaultExport.declaration.argument === node))
throw createSyntaxError(defaultExport, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')

const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned<VariableDeclaration> | undefined
const init = declarationNode?.declarations[0]?.init
const isViHoisted = (node: CallExpression) => {
return node.callee.type === 'MemberExpression'
&& isIdentifier(node.callee.object)
&& (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest')
&& isIdentifier(node.callee.property)
&& node.callee.property.name === 'hoisted'
}
assertNotDefaultExport(node, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')

const canMoveDeclaration = (init
&& init.type === 'CallExpression'
&& isViHoisted(init)) /* const v = vi.hoisted() */
|| (init
&& init.type === 'AwaitExpression'
&& init.argument.type === 'CallExpression'
&& isViHoisted(init.argument)) /* const v = await vi.hoisted() */

if (canMoveDeclaration) {
// export const variable = vi.hoisted()
const nodeExported = findNodeAround(ast, declarationNode.start, 'ExportNamedDeclaration')?.node as Positioned<ExportNamedDeclaration> | undefined
if (nodeExported?.declaration === declarationNode)
throw createSyntaxError(nodeExported, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
const declarationNode = getVariableDeclaration(node)
if (declarationNode) {
assertNotNamedExport(declarationNode, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
// hoist "const variable = vi.hoisted(() => {})"
hoistedNodes.push(declarationNode)
}
Expand Down
40 changes: 40 additions & 0 deletions test/core/test/__snapshots__/injector-mock.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,43 @@ exports[`throws an error when nodes are incompatible > correctly throws an error
5| })
6| "
`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as a named export 1`] = `"Cannot export the result of "vi.mock". Remove export declaration because "vi.mock" doesn't return anything."`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as a named export 2`] = `
" 1| import { vi } from 'vitest'
2|
3| export const mocked = vi.mock('./mocked')
| ^
4| "
`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as default export 1`] = `"Cannot export the result of "vi.mock". Remove export declaration because "vi.mock" doesn't return anything."`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as default export 2`] = `
" 1| import { vi } from 'vitest'
2|
3| export default vi.mock('./mocked')
| ^
4| "
`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as a named export 1`] = `"Cannot export the result of "vi.unmock". Remove export declaration because "vi.unmock" doesn't return anything."`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as a named export 2`] = `
" 1| import { vi } from 'vitest'
2|
3| export const mocked = vi.unmock('./mocked')
| ^
4| "
`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as default export 1`] = `"Cannot export the result of "vi.unmock". Remove export declaration because "vi.unmock" doesn't return anything."`;

exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as default export 2`] = `
" 1| import { vi } from 'vitest'
2|
3| export default vi.unmock('./mocked')
| ^
4| "
`;
32 changes: 32 additions & 0 deletions test/core/test/injector-mock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,38 @@ import { vi } from 'vitest'
export default await vi.hoisted(async () => {
return {}
})
`,
],
[
'vi.mock is exported as default export',
`\
import { vi } from 'vitest'
export default vi.mock('./mocked')
`,
],
[
'vi.unmock is exported as default export',
`\
import { vi } from 'vitest'
export default vi.unmock('./mocked')
`,
],
[
'vi.mock is exported as a named export',
`\
import { vi } from 'vitest'
export const mocked = vi.mock('./mocked')
`,
],
[
'vi.unmock is exported as a named export',
`\
import { vi } from 'vitest'
export const mocked = vi.unmock('./mocked')
`,
],
])('correctly throws an error if %s', (_, code) => {
Expand Down

0 comments on commit 253df1c

Please sign in to comment.