@@ -2,20 +2,24 @@ import * as path from 'path';
22import {
33 flattenDiagnosticMessageText ,
44 createProgram ,
5- SyntaxKind ,
65 Diagnostic as TSDiagnostic ,
76 Program ,
87 SourceFile ,
98 Node ,
10- forEachChild
9+ forEachChild ,
10+ isCallExpression ,
11+ Identifier ,
12+ TypeChecker ,
13+ CallExpression
1114} from '../../libraries/typescript' ;
1215import { Diagnostic , DiagnosticCode , Context , Location } from './interfaces' ;
1316
14- // List of diagnostic codes that should be ignored
17+ // List of diagnostic codes that should be ignored in general
1518const ignoredDiagnostics = new Set < number > ( [
1619 DiagnosticCode . AwaitIsOnlyAllowedInAsyncFunction
1720] ) ;
1821
22+ // List of diagnostic codes which should be ignored inside `expectError` statements
1923const diagnosticCodesToIgnore = new Set < DiagnosticCode > ( [
2024 DiagnosticCode . ArgumentTypeIsNotAssignableToParameterType ,
2125 DiagnosticCode . PropertyDoesNotExistOnType ,
@@ -27,30 +31,23 @@ const diagnosticCodesToIgnore = new Set<DiagnosticCode>([
2731] ) ;
2832
2933/**
30- * Extract all the `expectError` statements and convert it to a range map .
34+ * Extract all assertions .
3135 *
32- * @param program - The TypeScript program.
36+ * @param program - TypeScript program.
3337 */
34- const extractExpectErrorRanges = ( program : Program ) => {
35- const expectedErrors = new Map < Location , Pick < Diagnostic , 'fileName' | 'line' | 'column' > > ( ) ;
38+ const extractAssertions = ( program : Program ) => {
39+ const typeAssertions = new Set < CallExpression > ( ) ;
40+ const errorAssertions = new Set < CallExpression > ( ) ;
3641
3742 function walkNodes ( node : Node ) {
38- if ( node . kind === SyntaxKind . ExpressionStatement && node . getText ( ) . startsWith ( 'expectError' ) ) {
39- const location = {
40- fileName : node . getSourceFile ( ) . fileName ,
41- start : node . getStart ( ) ,
42- end : node . getEnd ( )
43- } ;
44-
45- const pos = node
46- . getSourceFile ( )
47- . getLineAndCharacterOfPosition ( node . getStart ( ) ) ;
48-
49- expectedErrors . set ( location , {
50- fileName : location . fileName ,
51- line : pos . line + 1 ,
52- column : pos . character
53- } ) ;
43+ if ( isCallExpression ( node ) ) {
44+ const text = ( node . expression as Identifier ) . getText ( ) ;
45+
46+ if ( text === 'expectType' ) {
47+ typeAssertions . add ( node ) ;
48+ } else if ( text === 'expectError' ) {
49+ errorAssertions . add ( node ) ;
50+ }
5451 }
5552
5653 forEachChild ( node , walkNodes ) ;
@@ -60,9 +57,88 @@ const extractExpectErrorRanges = (program: Program) => {
6057 walkNodes ( sourceFile ) ;
6158 }
6259
60+ return {
61+ typeAssertions,
62+ errorAssertions
63+ } ;
64+ } ;
65+
66+ /**
67+ * Loop over all the `expectError` nodes and convert them to a range map.
68+ *
69+ * @param nodes - Set of `expectError` nodes.
70+ */
71+ const extractExpectErrorRanges = ( nodes : Set < Node > ) => {
72+ const expectedErrors = new Map < Location , Pick < Diagnostic , 'fileName' | 'line' | 'column' > > ( ) ;
73+
74+ // Iterate over the nodes and add the node range to the map
75+ for ( const node of nodes ) {
76+ const location = {
77+ fileName : node . getSourceFile ( ) . fileName ,
78+ start : node . getStart ( ) ,
79+ end : node . getEnd ( )
80+ } ;
81+
82+ const pos = node
83+ . getSourceFile ( )
84+ . getLineAndCharacterOfPosition ( node . getStart ( ) ) ;
85+
86+ expectedErrors . set ( location , {
87+ fileName : location . fileName ,
88+ line : pos . line + 1 ,
89+ column : pos . character
90+ } ) ;
91+ }
92+
6393 return expectedErrors ;
6494} ;
6595
96+ /**
97+ * Assert the expected type from `expectType` calls with the provided type in the argument.
98+ * Returns a list of custom diagnostics.
99+ *
100+ * @param checker - The TypeScript type checker.
101+ * @param nodes - The `expectType` AST nodes.
102+ * @return List of custom diagnostics.
103+ */
104+ const assertTypes = ( checker : TypeChecker , nodes : Set < CallExpression > ) : Diagnostic [ ] => {
105+ const diagnostics : Diagnostic [ ] = [ ] ;
106+
107+ for ( const node of nodes ) {
108+ if ( ! node . typeArguments ) {
109+ // Skip if the node does not have generics
110+ continue ;
111+ }
112+
113+ // Retrieve the type to be expected. This is the type inside the generic.
114+ const expectedType = checker . getTypeFromTypeNode ( node . typeArguments [ 0 ] ) ;
115+ const argumentType = checker . getTypeAtLocation ( node . arguments [ 0 ] ) ;
116+
117+ if ( ! checker . isAssignableTo ( argumentType , expectedType ) ) {
118+ // The argument type is not assignable to the expected type. TypeScript will catch this for us.
119+ continue ;
120+ }
121+
122+ if ( ! checker . isAssignableTo ( expectedType , argumentType ) ) { // tslint:disable-line:early-exit
123+ /**
124+ * At this point, the expected type is not assignable to the argument type, but the argument type is
125+ * assignable to the expected type. This means our type is too wide.
126+ */
127+ const position = node . getSourceFile ( ) . getLineAndCharacterOfPosition ( node . getStart ( ) ) ;
128+
129+ diagnostics . push ( {
130+ fileName : node . getSourceFile ( ) . fileName ,
131+ message : `Parameter type \`${ checker . typeToString ( expectedType ) } \` is declared too wide for argument type \`${ checker . typeToString ( argumentType ) } \`.` ,
132+ severity : 'error' ,
133+ line : position . line + 1 ,
134+ column : position . character ,
135+ } ) ;
136+ }
137+ }
138+
139+ return diagnostics ;
140+ } ;
141+
66142/**
67143 * Check if the provided diagnostic should be ignored.
68144 *
@@ -112,7 +188,11 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
112188 . getSemanticDiagnostics ( )
113189 . concat ( program . getSyntacticDiagnostics ( ) ) ;
114190
115- const expectedErrors = extractExpectErrorRanges ( program ) ;
191+ const { typeAssertions, errorAssertions} = extractAssertions ( program ) ;
192+
193+ const expectedErrors = extractExpectErrorRanges ( errorAssertions ) ;
194+
195+ result . push ( ...assertTypes ( program . getTypeChecker ( ) , typeAssertions ) ) ;
116196
117197 for ( const diagnostic of diagnostics ) {
118198 if ( ! diagnostic . file || ignoreDiagnostic ( diagnostic , expectedErrors ) ) {
0 commit comments