From 87f710994656e75b3477c8fa98b7221ad2dbb8f7 Mon Sep 17 00:00:00 2001 From: Dimitri B Date: Thu, 27 May 2021 19:07:26 +0200 Subject: [PATCH] Handle multiple diagnostic errors in a single `expectError` assertion (#103) --- source/lib/compiler.ts | 40 ++++++++++++++----- source/lib/parser.ts | 8 +++- .../expect-error/functions/index.d.ts | 7 ++++ .../fixtures/expect-error/functions/index.js | 6 +-- .../expect-error/functions/index.test-d.ts | 5 ++- 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index fc7b3e5d..0aea524a 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -4,7 +4,7 @@ import { Diagnostic as TSDiagnostic, SourceFile } from '@tsd/typescript'; -import {extractAssertions, parseErrorAssertionToLocation} from './parser'; +import {ExpectedError, extractAssertions, parseErrorAssertionToLocation} from './parser'; import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces'; import {handle} from './assertions'; @@ -30,21 +30,28 @@ const expectErrordiagnosticCodesToIgnore = new Set([ DiagnosticCode.ThisContextOfTypeNotAssignableToMethodOfThisType ]); +type IgnoreDiagnosticResult = 'preserve' | 'ignore' | Location; + /** * Check if the provided diagnostic should be ignored. * * @param diagnostic - The diagnostic to validate. * @param expectedErrors - Map of the expected errors. - * @returns Boolean indicating if the diagnostic should be ignored or not. + * @returns Whether the diagnostic should be `'preserve'`d, `'ignore'`d or, in case that + * the diagnostic is reported from inside of an `expectError` assertion, the `Location` + * of the assertion. */ -const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map): boolean => { +const ignoreDiagnostic = ( + diagnostic: TSDiagnostic, + expectedErrors: Map +): IgnoreDiagnosticResult => { if (ignoredDiagnostics.has(diagnostic.code)) { // Filter out diagnostics which are present in the `ignoredDiagnostics` set - return true; + return 'ignore'; } if (!expectErrordiagnosticCodesToIgnore.has(diagnostic.code)) { - return false; + return 'preserve'; } const diagnosticFileName = (diagnostic.file as SourceFile).fileName; @@ -53,13 +60,11 @@ const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map location.start && start < location.end) { - // Remove the expected error from the Map so it's not being reported as failure - expectedErrors.delete(location); - return true; + return location; } } - return false; + return 'preserve'; }; /** @@ -82,9 +87,20 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { diagnostics.push(...handle(program.getTypeChecker(), assertions)); const expectedErrors = parseErrorAssertionToLocation(assertions); + const expectedErrorsLocationsWithFoundDiagnostics: Location[] = []; for (const diagnostic of tsDiagnostics) { - if (!diagnostic.file || ignoreDiagnostic(diagnostic, expectedErrors)) { + if (!diagnostic.file) { + continue; + } + + const ignoreDiagnosticResult = ignoreDiagnostic(diagnostic, expectedErrors); + + if (ignoreDiagnosticResult !== 'preserve') { + if (ignoreDiagnosticResult !== 'ignore') { + expectedErrorsLocationsWithFoundDiagnostics.push(ignoreDiagnosticResult); + } + continue; } @@ -99,6 +115,10 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { }); } + for (const errorLocationToRemove of expectedErrorsLocationsWithFoundDiagnostics) { + expectedErrors.delete(errorLocationToRemove); + } + for (const [, diagnostic] of expectedErrors) { diagnostics.push({ ...diagnostic, diff --git a/source/lib/parser.ts b/source/lib/parser.ts index cba29f77..24e4819c 100644 --- a/source/lib/parser.ts +++ b/source/lib/parser.ts @@ -42,15 +42,19 @@ export const extractAssertions = (program: Program): Map; + /** * Loop over all the error assertion nodes and convert them to a location map. * * @param assertions - Assertion map. */ -export const parseErrorAssertionToLocation = (assertions: Map>) => { +export const parseErrorAssertionToLocation = ( + assertions: Map> +): Map => { const nodes = assertions.get(Assertion.EXPECT_ERROR); - const expectedErrors = new Map>(); + const expectedErrors = new Map(); if (!nodes) { // Bail out if we don't have any error nodes diff --git a/source/test/fixtures/expect-error/functions/index.d.ts b/source/test/fixtures/expect-error/functions/index.d.ts index 89c68502..acd85899 100644 --- a/source/test/fixtures/expect-error/functions/index.d.ts +++ b/source/test/fixtures/expect-error/functions/index.d.ts @@ -5,3 +5,10 @@ declare const one: { }; export default one; + +export const three: { + (foo: '*'): string; + (foo: 'a' | 'b'): string; + (foo: ReadonlyArray<'a' | 'b'>): string; + (foo: never): string; +}; diff --git a/source/test/fixtures/expect-error/functions/index.js b/source/test/fixtures/expect-error/functions/index.js index f17717f5..d858d09d 100644 --- a/source/test/fixtures/expect-error/functions/index.js +++ b/source/test/fixtures/expect-error/functions/index.js @@ -1,3 +1,3 @@ -module.exports.default = (foo, bar) => { - return foo + bar; -}; +module.exports.default = (foo, bar) => foo + bar; + +exports.three = (foo) => 'a'; diff --git a/source/test/fixtures/expect-error/functions/index.test-d.ts b/source/test/fixtures/expect-error/functions/index.test-d.ts index 6d0ef4b3..3d4da8a3 100644 --- a/source/test/fixtures/expect-error/functions/index.test-d.ts +++ b/source/test/fixtures/expect-error/functions/index.test-d.ts @@ -1,5 +1,8 @@ import {expectError} from '../../../..'; -import one from '.'; +import one, {three} from '.'; expectError(one(true, true)); expectError(one('foo', 'bar')); + +// Produces multiple type checker errors in a single `expectError` assertion +expectError(three(['a', 'bad']));