Skip to content

Commit

Permalink
Handle multiple diagnostic errors in a single expectError assertion (
Browse files Browse the repository at this point in the history
  • Loading branch information
BendingBender committed May 27, 2021
1 parent abf7082 commit 87f7109
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 16 deletions.
40 changes: 30 additions & 10 deletions source/lib/compiler.ts
Expand Up @@ -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';

Expand All @@ -30,21 +30,28 @@ const expectErrordiagnosticCodesToIgnore = new Set<DiagnosticCode>([
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<Location, any>): boolean => {
const ignoreDiagnostic = (
diagnostic: TSDiagnostic,
expectedErrors: Map<Location, ExpectedError>
): 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;
Expand All @@ -53,13 +60,11 @@ const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map<Location
const start = diagnostic.start as number;

if (diagnosticFileName === location.fileName && start > 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';
};

/**
Expand All @@ -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;
}

Expand All @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions source/lib/parser.ts
Expand Up @@ -42,15 +42,19 @@ export const extractAssertions = (program: Program): Map<Assertion, Set<CallExpr
return assertions;
};

export type ExpectedError = Pick<Diagnostic, 'fileName' | 'line' | 'column'>;

/**
* Loop over all the error assertion nodes and convert them to a location map.
*
* @param assertions - Assertion map.
*/
export const parseErrorAssertionToLocation = (assertions: Map<Assertion, Set<CallExpression>>) => {
export const parseErrorAssertionToLocation = (
assertions: Map<Assertion, Set<CallExpression>>
): Map<Location, ExpectedError> => {
const nodes = assertions.get(Assertion.EXPECT_ERROR);

const expectedErrors = new Map<Location, Pick<Diagnostic, 'fileName' | 'line' | 'column'>>();
const expectedErrors = new Map<Location, ExpectedError>();

if (!nodes) {
// Bail out if we don't have any error nodes
Expand Down
7 changes: 7 additions & 0 deletions source/test/fixtures/expect-error/functions/index.d.ts
Expand Up @@ -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;
};
6 changes: 3 additions & 3 deletions 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';
5 changes: 4 additions & 1 deletion 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']));

0 comments on commit 87f7109

Please sign in to comment.