Skip to content

Commit 87f7109

Browse files
Handle multiple diagnostic errors in a single expectError assertion (#103)
1 parent abf7082 commit 87f7109

File tree

5 files changed

+50
-16
lines changed

5 files changed

+50
-16
lines changed

source/lib/compiler.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
Diagnostic as TSDiagnostic,
55
SourceFile
66
} from '@tsd/typescript';
7-
import {extractAssertions, parseErrorAssertionToLocation} from './parser';
7+
import {ExpectedError, extractAssertions, parseErrorAssertionToLocation} from './parser';
88
import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces';
99
import {handle} from './assertions';
1010

@@ -30,21 +30,28 @@ const expectErrordiagnosticCodesToIgnore = new Set<DiagnosticCode>([
3030
DiagnosticCode.ThisContextOfTypeNotAssignableToMethodOfThisType
3131
]);
3232

33+
type IgnoreDiagnosticResult = 'preserve' | 'ignore' | Location;
34+
3335
/**
3436
* Check if the provided diagnostic should be ignored.
3537
*
3638
* @param diagnostic - The diagnostic to validate.
3739
* @param expectedErrors - Map of the expected errors.
38-
* @returns Boolean indicating if the diagnostic should be ignored or not.
40+
* @returns Whether the diagnostic should be `'preserve'`d, `'ignore'`d or, in case that
41+
* the diagnostic is reported from inside of an `expectError` assertion, the `Location`
42+
* of the assertion.
3943
*/
40-
const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map<Location, any>): boolean => {
44+
const ignoreDiagnostic = (
45+
diagnostic: TSDiagnostic,
46+
expectedErrors: Map<Location, ExpectedError>
47+
): IgnoreDiagnosticResult => {
4148
if (ignoredDiagnostics.has(diagnostic.code)) {
4249
// Filter out diagnostics which are present in the `ignoredDiagnostics` set
43-
return true;
50+
return 'ignore';
4451
}
4552

4653
if (!expectErrordiagnosticCodesToIgnore.has(diagnostic.code)) {
47-
return false;
54+
return 'preserve';
4855
}
4956

5057
const diagnosticFileName = (diagnostic.file as SourceFile).fileName;
@@ -53,13 +60,11 @@ const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map<Location
5360
const start = diagnostic.start as number;
5461

5562
if (diagnosticFileName === location.fileName && start > location.start && start < location.end) {
56-
// Remove the expected error from the Map so it's not being reported as failure
57-
expectedErrors.delete(location);
58-
return true;
63+
return location;
5964
}
6065
}
6166

62-
return false;
67+
return 'preserve';
6368
};
6469

6570
/**
@@ -82,9 +87,20 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
8287
diagnostics.push(...handle(program.getTypeChecker(), assertions));
8388

8489
const expectedErrors = parseErrorAssertionToLocation(assertions);
90+
const expectedErrorsLocationsWithFoundDiagnostics: Location[] = [];
8591

8692
for (const diagnostic of tsDiagnostics) {
87-
if (!diagnostic.file || ignoreDiagnostic(diagnostic, expectedErrors)) {
93+
if (!diagnostic.file) {
94+
continue;
95+
}
96+
97+
const ignoreDiagnosticResult = ignoreDiagnostic(diagnostic, expectedErrors);
98+
99+
if (ignoreDiagnosticResult !== 'preserve') {
100+
if (ignoreDiagnosticResult !== 'ignore') {
101+
expectedErrorsLocationsWithFoundDiagnostics.push(ignoreDiagnosticResult);
102+
}
103+
88104
continue;
89105
}
90106

@@ -99,6 +115,10 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
99115
});
100116
}
101117

118+
for (const errorLocationToRemove of expectedErrorsLocationsWithFoundDiagnostics) {
119+
expectedErrors.delete(errorLocationToRemove);
120+
}
121+
102122
for (const [, diagnostic] of expectedErrors) {
103123
diagnostics.push({
104124
...diagnostic,

source/lib/parser.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,19 @@ export const extractAssertions = (program: Program): Map<Assertion, Set<CallExpr
4242
return assertions;
4343
};
4444

45+
export type ExpectedError = Pick<Diagnostic, 'fileName' | 'line' | 'column'>;
46+
4547
/**
4648
* Loop over all the error assertion nodes and convert them to a location map.
4749
*
4850
* @param assertions - Assertion map.
4951
*/
50-
export const parseErrorAssertionToLocation = (assertions: Map<Assertion, Set<CallExpression>>) => {
52+
export const parseErrorAssertionToLocation = (
53+
assertions: Map<Assertion, Set<CallExpression>>
54+
): Map<Location, ExpectedError> => {
5155
const nodes = assertions.get(Assertion.EXPECT_ERROR);
5256

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

5559
if (!nodes) {
5660
// Bail out if we don't have any error nodes

source/test/fixtures/expect-error/functions/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ declare const one: {
55
};
66

77
export default one;
8+
9+
export const three: {
10+
(foo: '*'): string;
11+
(foo: 'a' | 'b'): string;
12+
(foo: ReadonlyArray<'a' | 'b'>): string;
13+
(foo: never): string;
14+
};
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
module.exports.default = (foo, bar) => {
2-
return foo + bar;
3-
};
1+
module.exports.default = (foo, bar) => foo + bar;
2+
3+
exports.three = (foo) => 'a';
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {expectError} from '../../../..';
2-
import one from '.';
2+
import one, {three} from '.';
33

44
expectError(one(true, true));
55
expectError(one('foo', 'bar'));
6+
7+
// Produces multiple type checker errors in a single `expectError` assertion
8+
expectError(three(['a', 'bad']));

0 commit comments

Comments
 (0)