Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle multiple diagnostic errors in a single expectError assertion #103

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions source/lib/compiler.ts
Original file line number Diff line number Diff line change
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 @@ -28,21 +28,28 @@ const expectErrordiagnosticCodesToIgnore = new Set<DiagnosticCode>([
DiagnosticCode.PropertyMissingInType1ButRequiredInType2
]);

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 => {
BendingBender marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -51,13 +58,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 @@ -80,9 +85,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 @@ -97,6 +113,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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']));