@@ -6,13 +6,16 @@ import {
66 flattenDiagnosticMessageText ,
77 CompilerOptions ,
88 createProgram ,
9- JsxEmit
9+ JsxEmit ,
10+ SyntaxKind ,
11+ SourceFile ,
12+ Diagnostic as TSDiagnostic
1013} from 'typescript' ;
11- import { Diagnostic , Context } from './interfaces' ;
14+ import { Diagnostic , DiagnosticCode , Context , Position } from './interfaces' ;
1215
1316// List of diagnostic codes that should be ignored
1417const ignoredDiagnostics = new Set < number > ( [
15- 1308 // Support top-level `await`
18+ DiagnosticCode . AwaitIsOnlyAllowedInAsyncFunction
1619] ) ;
1720
1821const loadConfig = ( cwd : string ) : CompilerOptions => {
@@ -36,6 +39,66 @@ const loadConfig = (cwd: string): CompilerOptions => {
3639 } ;
3740} ;
3841
42+ /**
43+ * Extract all the `expectError` statements and convert it to a range map.
44+ *
45+ * @param sourceFile - File to extract the statements from.
46+ */
47+ const extractExpectErrorRanges = ( sourceFile : SourceFile ) => {
48+ const expectedErrors = new Map < Position , Pick < Diagnostic , 'fileName' | 'line' | 'column' > > ( ) ;
49+
50+ for ( const statement of sourceFile . statements ) {
51+ if ( statement . kind !== SyntaxKind . ExpressionStatement || ! statement . getText ( ) . startsWith ( 'expectError' ) ) {
52+ continue ;
53+ }
54+
55+ const position = {
56+ start : statement . getStart ( ) ,
57+ end : statement . getEnd ( )
58+ } ;
59+
60+ const pos = statement . getSourceFile ( ) . getLineAndCharacterOfPosition ( statement . getStart ( ) ) ;
61+
62+ expectedErrors . set ( position , {
63+ fileName : statement . getSourceFile ( ) . fileName ,
64+ line : pos . line + 1 ,
65+ column : pos . character
66+ } ) ;
67+ }
68+
69+ return expectedErrors ;
70+ } ;
71+
72+ /**
73+ * Check if the provided diagnostic should be ignored.
74+ *
75+ * @param diagnostic - The diagnostic to validate.
76+ * @param expectedErrors - Map of the expected errors.
77+ * @return Boolean indicating if the diagnostic should be ignored or not.
78+ */
79+ const ignoreDiagnostic = ( diagnostic : TSDiagnostic , expectedErrors : Map < Position , any > ) : boolean => {
80+ if ( ignoredDiagnostics . has ( diagnostic . code ) ) {
81+ // Filter out diagnostics which are present in the `ignoredDiagnostics` set
82+ return true ;
83+ }
84+
85+ if ( diagnostic . code !== DiagnosticCode . ArgumentTypeIsNotAssignableToParameterType ) {
86+ return false ;
87+ }
88+
89+ for ( const [ range ] of expectedErrors ) {
90+ const start = diagnostic . start as number ;
91+
92+ if ( start > range . start && start < range . end ) {
93+ // Remove the expected error from the Map so it's not being reported as failure
94+ expectedErrors . delete ( range ) ;
95+ return true ;
96+ }
97+ }
98+
99+ return false ;
100+ } ;
101+
39102/**
40103 * Get a list of TypeScript diagnostics within the current context.
41104 *
@@ -55,14 +118,14 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
55118 . getSemanticDiagnostics ( )
56119 . concat ( program . getSyntacticDiagnostics ( ) ) ;
57120
121+ const expectedErrors = extractExpectErrorRanges ( program . getSourceFile ( fileName ) as SourceFile ) ;
122+
58123 for ( const diagnostic of diagnostics ) {
59- if ( ! diagnostic . file || ignoredDiagnostics . has ( diagnostic . code ) ) {
124+ if ( ! diagnostic . file || ignoreDiagnostic ( diagnostic , expectedErrors ) ) {
60125 continue ;
61126 }
62127
63- const position = diagnostic . file . getLineAndCharacterOfPosition (
64- diagnostic . start as number
65- ) ;
128+ const position = diagnostic . file . getLineAndCharacterOfPosition ( diagnostic . start as number ) ;
66129
67130 result . push ( {
68131 fileName : diagnostic . file . fileName ,
@@ -73,5 +136,13 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
73136 } ) ;
74137 }
75138
139+ for ( const [ , diagnostic ] of expectedErrors ) {
140+ result . push ( {
141+ ...diagnostic ,
142+ message : 'Expected an error, but found none.' ,
143+ severity : 'error'
144+ } ) ;
145+ }
146+
76147 return result ;
77148} ;
0 commit comments