@@ -2,20 +2,24 @@ import * as path from 'path';
2
2
import {
3
3
flattenDiagnosticMessageText ,
4
4
createProgram ,
5
- SyntaxKind ,
6
5
Diagnostic as TSDiagnostic ,
7
6
Program ,
8
7
SourceFile ,
9
8
Node ,
10
- forEachChild
9
+ forEachChild ,
10
+ isCallExpression ,
11
+ Identifier ,
12
+ TypeChecker ,
13
+ CallExpression
11
14
} from '../../libraries/typescript' ;
12
15
import { Diagnostic , DiagnosticCode , Context , Location } from './interfaces' ;
13
16
14
- // List of diagnostic codes that should be ignored
17
+ // List of diagnostic codes that should be ignored in general
15
18
const ignoredDiagnostics = new Set < number > ( [
16
19
DiagnosticCode . AwaitIsOnlyAllowedInAsyncFunction
17
20
] ) ;
18
21
22
+ // List of diagnostic codes which should be ignored inside `expectError` statements
19
23
const diagnosticCodesToIgnore = new Set < DiagnosticCode > ( [
20
24
DiagnosticCode . ArgumentTypeIsNotAssignableToParameterType ,
21
25
DiagnosticCode . PropertyDoesNotExistOnType ,
@@ -27,30 +31,23 @@ const diagnosticCodesToIgnore = new Set<DiagnosticCode>([
27
31
] ) ;
28
32
29
33
/**
30
- * Extract all the `expectError` statements and convert it to a range map .
34
+ * Extract all assertions .
31
35
*
32
- * @param program - The TypeScript program.
36
+ * @param program - TypeScript program.
33
37
*/
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 > ( ) ;
36
41
37
42
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
+ }
54
51
}
55
52
56
53
forEachChild ( node , walkNodes ) ;
@@ -60,9 +57,88 @@ const extractExpectErrorRanges = (program: Program) => {
60
57
walkNodes ( sourceFile ) ;
61
58
}
62
59
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
+
63
93
return expectedErrors ;
64
94
} ;
65
95
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
+
66
142
/**
67
143
* Check if the provided diagnostic should be ignored.
68
144
*
@@ -112,7 +188,11 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
112
188
. getSemanticDiagnostics ( )
113
189
. concat ( program . getSyntacticDiagnostics ( ) ) ;
114
190
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 ) ) ;
116
196
117
197
for ( const diagnostic of diagnostics ) {
118
198
if ( ! diagnostic . file || ignoreDiagnostic ( diagnostic , expectedErrors ) ) {
0 commit comments