diff --git a/src/common/error.ts b/src/common/error.ts index 16c8acb..5fc64c3 100644 --- a/src/common/error.ts +++ b/src/common/error.ts @@ -12,6 +12,7 @@ export enum ErrorCode { ASSERT_TYPE_NOT_IDENTICAL, ASSERT_TYPE_IDENTICAL, ASSERT_TYPE_TOO_WIDE, + ASSERT_ERROR, } export const errorMessages: Record = { @@ -28,6 +29,7 @@ export const errorMessages: Record = { [ErrorCode.ASSERT_TYPE_NOT_IDENTICAL]: "Type '{expected}' is not identical to argument type '{argument}'.", [ErrorCode.ASSERT_TYPE_IDENTICAL]: "Type '{expected}' is identical to argument type '{argument}'.", [ErrorCode.ASSERT_TYPE_TOO_WIDE]: "Type '{expected}' is declared too wide for argument type '{argument}'.", + [ErrorCode.ASSERT_ERROR]: 'An error is expected.', }; export function errorMessage(code: ErrorCode, data?: Record): string { diff --git a/src/plugin/assert/tsd/expect-error.ts b/src/plugin/assert/tsd/expect-error.ts new file mode 100644 index 0000000..0068af7 --- /dev/null +++ b/src/plugin/assert/tsd/expect-error.ts @@ -0,0 +1,42 @@ +import { missingArgument } from './util'; +import type ts from 'unleashed-typescript'; +import type { Assertion } from '../../types'; +import type { Compiler } from '../../../typescript/types'; +import { createAssertionDiagnostic } from '../../diagnostics'; +import { ErrorCode, errorMessage } from '../../../common/error'; + +// https://github.dev/SamVerschueren/tsd/blob/e4a398c1b47a4d2f914446b662840e2be5994997/source/lib/compiler.ts#L54-L55 +export function expectError({ node }: Assertion, compiler: Compiler): ts.Diagnostic | undefined { + const argument = node.arguments[0]; + + if (!argument) { + return missingArgument(node, compiler.sourceFile); + } + + let assertDiagnostic: ts.Diagnostic | undefined = undefined; + + if (!compiler.diagnostics.length) { + return createAssertionDiagnostic(errorMessage(ErrorCode.ASSERT_ERROR), compiler.sourceFile, argument.getStart()); + } + + // TODO: Create a method in Compiler for cleanly removing/filter a diagnostic. + compiler.diagnostics = compiler.diagnostics.filter((diagnostic) => { + if (assertDiagnostic || !diagnostic.start) { + return true; + } + + if (diagnostic.start < argument.getStart() || diagnostic.start > argument.getEnd()) { + assertDiagnostic = createAssertionDiagnostic( + errorMessage(ErrorCode.ASSERT_ERROR), + compiler.sourceFile, + diagnostic.start, + ); + + return true; + } + + return false; + }); + + return assertDiagnostic; +} diff --git a/src/plugin/assert/tsd/index.ts b/src/plugin/assert/tsd/index.ts index fe24d76..16b3df1 100644 --- a/src/plugin/assert/tsd/index.ts +++ b/src/plugin/assert/tsd/index.ts @@ -7,15 +7,12 @@ export * from './expect-type'; export * from './expect-not-type'; export * from './expect-assignable'; export * from './expect-not-assignable'; +export * from './expect-error'; // All credits go to tsd! Most of the logic here comme from their code: // https://github.com/SamVerschueren/tsd/blob/main/source/lib/assertions/index.ts // https://github.com/SamVerschueren/tsd/tree/main/source/lib/assertions/handlers -export function expectError(assertion: Assertion, compiler: Compiler): ts.Diagnostic | undefined { - return createAssertionDiagnostic('Not yet implemented.', compiler.sourceFile, assertion.node.getStart()); -} - export function expectDeprecated(assertion: Assertion, compiler: Compiler): ts.Diagnostic | undefined { return createAssertionDiagnostic('Not yet implemented.', compiler.sourceFile, assertion.node.getStart()); } diff --git a/src/plugin/transform.ts b/src/plugin/transform.ts index 44ff422..bdd7b90 100644 --- a/src/plugin/transform.ts +++ b/src/plugin/transform.ts @@ -13,10 +13,6 @@ export function transform({ code, fileName, report, typescript }: TransformSetti const compiler = createCompiler({ config: typescript.config, fileName }); const newCode = new MagicString(code); - if (report.includes('type-error')) { - reportDiagnostics(compiler.diagnostics, newCode, fileName); - } - if (report.includes('type-assertion')) { const assertions = getAssertions(compiler.sourceFile, compiler.typeChecker); const diagnostics = processAssertions(assertions, compiler); @@ -26,6 +22,10 @@ export function transform({ code, fileName, report, typescript }: TransformSetti } } + if (report.includes('type-error')) { + reportDiagnostics(compiler.diagnostics, newCode, fileName); + } + return { code: newCode.toString(), map: newCode.generateMap({ hires: true }), diff --git a/src/typescript/types.ts b/src/typescript/types.ts index 42dddf3..a4fe539 100644 --- a/src/typescript/types.ts +++ b/src/typescript/types.ts @@ -9,8 +9,8 @@ export interface Compiler { program: ts.Program; sourceFile: ts.SourceFile; typeChecker: ts.TypeChecker; + diagnostics: ts.Diagnostic[]; compilerOptions: ts.CompilerOptions; - diagnostics: readonly ts.Diagnostic[]; } export interface TypeScriptConfigOptions { diff --git a/test/tsd.test.ts b/test/tsd.test.ts index b0be797..c0d5d94 100644 --- a/test/tsd.test.ts +++ b/test/tsd.test.ts @@ -29,3 +29,12 @@ test('test-2', () => { test('test-3', () => { tsd.expectNotType('hello'); }); + +test('test-4', () => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + tsd.expectError(42 === 'life'); +}); + +test('test-5', () => { + tsd.expectError(true); +});