diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 0ca0382536ce..7182d46e7eb5 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -5,6 +5,8 @@ import { resolve } from 'pathe' import { isPrimitive, notNullish } from './helpers' export { + eachMapping, + type EachMapping, generatedPositionFor, originalPositionFor, TraceMap, diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index b52b255cbc1e..fc3d8c611a83 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -1,6 +1,7 @@ import type { RawSourceMap } from '@ampproject/remapping' import type { File, Task, TaskResultPack, TaskState } from '@vitest/runner' import type { ParsedStack } from '@vitest/utils' +import type { EachMapping } from '@vitest/utils/source-map' import type { ChildProcess } from 'node:child_process' import type { Vitest } from '../node/core' import type { TestProject } from '../node/project' @@ -10,7 +11,7 @@ import type { TscErrorInfo } from './types' import { rm } from 'node:fs/promises' import { performance } from 'node:perf_hooks' import { getTasks } from '@vitest/runner/utils' -import { generatedPositionFor, TraceMap } from '@vitest/utils/source-map' +import { eachMapping, generatedPositionFor, TraceMap } from '@vitest/utils/source-map' import { basename, extname, resolve } from 'pathe' import { x } from 'tinyexec' import { collectTests } from './collect' @@ -161,7 +162,7 @@ export class Typechecker { } errors.forEach(({ error, originalError }) => { const processedPos = traceMap - ? generatedPositionFor(traceMap, { + ? findGeneratedPosition(traceMap, { line: originalError.line, column: originalError.column, source: basename(path), @@ -364,3 +365,40 @@ export class Typechecker { .map(i => [i.id, i.result, { typecheck: true }]) } } + +function findGeneratedPosition(traceMap: TraceMap, { line, column, source }: { line: number; column: number; source: string }) { + const found = generatedPositionFor(traceMap, { + line, + column, + source, + }) + if (found.line !== null) { + return found + } + // find the next source token position when the exact error position doesn't exist in source map. + // this can happen, for example, when the type error is in the comment "// @ts-expect-error" + // and comments are stripped away in the generated code. + const mappings: (EachMapping & { originalLine: number })[] = [] + eachMapping(traceMap, (m) => { + if ( + m.source === source + && m.originalLine !== null + && m.originalColumn !== null + && (line === m.originalLine ? column < m.originalColumn : line < m.originalLine) + ) { + mappings.push(m) + } + }) + const next = mappings + .sort((a, b) => + a.originalLine === b.originalLine ? a.originalColumn - b.originalColumn : a.originalLine - b.originalLine, + ) + .at(0) + if (next) { + return { + line: next.generatedLine, + column: next.generatedColumn, + } + } + return { line: null, column: null } +} diff --git a/test/typescript/failing/expect-error.test-d.ts b/test/typescript/failing/expect-error.test-d.ts index ba2277ca4a7f..021455fd1e0a 100644 --- a/test/typescript/failing/expect-error.test-d.ts +++ b/test/typescript/failing/expect-error.test-d.ts @@ -1,5 +1,6 @@ import { expectTypeOf, test } from 'vitest' +// test('failing test with expect-error', () => { // @ts-expect-error expect nothing expectTypeOf(1).toEqualTypeOf() diff --git a/test/typescript/test/__snapshots__/runner.test.ts.snap b/test/typescript/test/__snapshots__/runner.test.ts.snap index 4175a032a0b8..3dbf1b7e694f 100644 --- a/test/typescript/test/__snapshots__/runner.test.ts.snap +++ b/test/typescript/test/__snapshots__/runner.test.ts.snap @@ -24,12 +24,12 @@ TypeCheckError: This expression is not callable. Type 'ExpectVoid' has n exports[`should fail > typecheck files 3`] = ` " FAIL expect-error.test-d.ts > failing test with expect-error TypeCheckError: Unused '@ts-expect-error' directive. - ❯ expect-error.test-d.ts:4:3 - 2| - 3| test('failing test with expect-error', () => { - 4| // @ts-expect-error expect nothing + ❯ expect-error.test-d.ts:5:3 + 3| // + 4| test('failing test with expect-error', () => { + 5| // @ts-expect-error expect nothing | ^ - 5| expectTypeOf(1).toEqualTypeOf()" + 6| expectTypeOf(1).toEqualTypeOf()" `; exports[`should fail > typecheck files 4`] = ` diff --git a/test/typescript/vitest.config.fails.ts b/test/typescript/vitest.config.fails.ts new file mode 100644 index 000000000000..6e4da311c3b4 --- /dev/null +++ b/test/typescript/vitest.config.fails.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +// pnpm -C test/typescript test -- -c vitest.config.fails.ts +export default defineConfig({ + test: { + dir: './failing', + typecheck: { + enabled: true, + allowJs: true, + include: ['**/*.test-d.*'], + tsconfig: './tsconfig.fails.json', + }, + }, +})