|
7 | 7 | */ |
8 | 8 |
|
9 | 9 | import {bold, green} from 'chalk'; |
10 | | -import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; |
| 10 | +import {RuleFailure, Rules, WalkContext} from 'tslint'; |
11 | 11 | import * as ts from 'typescript'; |
12 | 12 | import {constructorChecks} from '../../material/data/constructor-checks'; |
13 | 13 |
|
| 14 | +/** |
| 15 | + * List of diagnostic codes that refer to pre-emit diagnostics which indicate invalid |
| 16 | + * new expression or super call signatures. See the list of diagnostics here: |
| 17 | + * |
| 18 | + * https://github.com/Microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json |
| 19 | + */ |
| 20 | +const signatureErrorDiagnostics = [ |
| 21 | + // Type not assignable error diagnostic. |
| 22 | + 2345, |
| 23 | + // Constructor argument length invalid diagnostics |
| 24 | + 2554, 2555, 2556, 2557, |
| 25 | +]; |
| 26 | + |
14 | 27 | /** |
15 | 28 | * Rule that visits every TypeScript new expression or super call and checks if the parameter |
16 | 29 | * type signature is invalid and needs to be updated manually. |
17 | 30 | */ |
18 | 31 | export class Rule extends Rules.TypedRule { |
19 | 32 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { |
20 | | - return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program)); |
| 33 | + return this.applyWithFunction(sourceFile, visitSourceFile, null, program); |
21 | 34 | } |
22 | 35 | } |
23 | 36 |
|
24 | | -export class Walker extends ProgramAwareRuleWalker { |
| 37 | +/** |
| 38 | + * Function that will be called for each source file of the upgrade project. In order to properly |
| 39 | + * determine invalid constructor signatures, we take advantage of the pre-emit diagnostics from |
| 40 | + * TypeScript. |
| 41 | + * |
| 42 | + * By using the diagnostics we can properly respect type assignability because otherwise we |
| 43 | + * would need to rely on type equality checking which is too strict. |
| 44 | + * See related issue: https://github.com/Microsoft/TypeScript/issues/9879 |
| 45 | + */ |
| 46 | +function visitSourceFile(context: WalkContext<null>, program: ts.Program) { |
| 47 | + const sourceFile = context.sourceFile; |
| 48 | + const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile) |
| 49 | + .filter(diagnostic => signatureErrorDiagnostics.includes(diagnostic.code)) |
| 50 | + .filter(diagnostic => diagnostic.start !== undefined); |
25 | 51 |
|
26 | | - visitNewExpression(node: ts.NewExpression) { |
27 | | - this.checkExpressionSignature(node); |
28 | | - super.visitNewExpression(node); |
29 | | - } |
| 52 | + for (const diagnostic of diagnostics) { |
| 53 | + const node = findConstructorNode(diagnostic, sourceFile); |
30 | 54 |
|
31 | | - visitCallExpression(node: ts.CallExpression) { |
32 | | - if (node.expression.kind === ts.SyntaxKind.SuperKeyword) { |
33 | | - this.checkExpressionSignature(node); |
| 55 | + if (!node) { |
| 56 | + return; |
34 | 57 | } |
35 | 58 |
|
36 | | - return super.visitCallExpression(node); |
37 | | - } |
38 | | - |
39 | | - private getParameterTypesFromSignature(signature: ts.Signature): ts.Type[] { |
40 | | - return signature.getParameters() |
41 | | - .map(param => param.declarations[0] as ts.ParameterDeclaration) |
42 | | - .map(node => node.type) |
43 | | - // TODO(devversion): handle non resolvable constructor types |
44 | | - .map(typeNode => this.getTypeChecker().getTypeFromTypeNode(typeNode!)); |
45 | | - } |
46 | | - |
47 | | - private checkExpressionSignature(node: ts.CallExpression | ts.NewExpression) { |
48 | | - const classType = this.getTypeChecker().getTypeAtLocation(node.expression); |
| 59 | + const classType = program.getTypeChecker().getTypeAtLocation(node.expression); |
49 | 60 | const className = classType.symbol && classType.symbol.name; |
50 | 61 | const isNewExpression = ts.isNewExpression(node); |
51 | 62 |
|
52 | 63 | // TODO(devversion): Consider handling pass-through classes better. |
53 | 64 | // TODO(devversion): e.g. `export class CustomCalendar extends MatCalendar {}` |
54 | | - if (!classType || !constructorChecks.includes(className) || !node.arguments) { |
| 65 | + if (!constructorChecks.includes(className)) { |
55 | 66 | return; |
56 | 67 | } |
57 | 68 |
|
58 | | - const callExpressionSignature = node.arguments |
59 | | - .map(argument => this.getTypeChecker().getTypeAtLocation(argument)); |
60 | 69 | const classSignatures = classType.getConstructSignatures() |
61 | | - .map(signature => this.getParameterTypesFromSignature(signature)); |
62 | | - |
63 | | - // TODO(devversion): we should check if the type is assignable to the signature |
64 | | - // TODO(devversion): blocked on https://github.com/Microsoft/TypeScript/issues/9879 |
65 | | - const doesMatchSignature = classSignatures.some(signature => { |
66 | | - // TODO(devversion): better handling if signature item type is unresolved but assignable |
67 | | - // to everything. |
68 | | - return signature.every((type, index) => callExpressionSignature[index] === type) && |
69 | | - signature.length === callExpressionSignature.length; |
70 | | - }); |
71 | | - |
72 | | - if (!doesMatchSignature) { |
73 | | - const expressionName = isNewExpression ? `new ${className}` : 'super'; |
74 | | - const signatures = classSignatures |
75 | | - .map(signature => signature.map(t => this.getTypeChecker().typeToString(t))) |
76 | | - .map(signature => `${expressionName}(${signature.join(', ')})`) |
77 | | - .join(' or '); |
78 | | - |
79 | | - this.addFailureAtNode(node, `Found "${bold(className)}" constructed with ` + |
80 | | - `an invalid signature. Please manually update the ${bold(expressionName)} expression to ` + |
81 | | - `match the new signature${classSignatures.length > 1 ? 's' : ''}: ${green(signatures)}`); |
82 | | - } |
| 70 | + .map(signature => getParameterTypesFromSignature(signature, program)); |
| 71 | + |
| 72 | + const expressionName = isNewExpression ? `new ${className}` : 'super'; |
| 73 | + const signatures = classSignatures |
| 74 | + .map(signature => signature.map(t => program.getTypeChecker().typeToString(t))) |
| 75 | + .map(signature => `${expressionName}(${signature.join(', ')})`) |
| 76 | + .join(' or '); |
| 77 | + |
| 78 | + context.addFailureAtNode(node, `Found "${bold(className)}" constructed with ` + |
| 79 | + `an invalid signature. Please manually update the ${bold(expressionName)} expression to ` + |
| 80 | + `match the new signature${classSignatures.length > 1 ? 's' : ''}: ${green(signatures)}`); |
83 | 81 | } |
84 | 82 | } |
| 83 | + |
| 84 | +/** Resolves the type for each parameter in the specified signature. */ |
| 85 | +function getParameterTypesFromSignature(signature: ts.Signature, program: ts.Program): ts.Type[] { |
| 86 | + return signature.getParameters() |
| 87 | + .map(param => param.declarations[0] as ts.ParameterDeclaration) |
| 88 | + .map(node => node.type) |
| 89 | + .map(typeNode => program.getTypeChecker().getTypeFromTypeNode(typeNode!)); |
| 90 | +} |
| 91 | + |
| 92 | +/** |
| 93 | + * Walks through each node of a source file in order to find a new-expression node or super-call |
| 94 | + * expression node that is captured by the specified diagnostic. |
| 95 | + */ |
| 96 | +function findConstructorNode(diagnostic: ts.Diagnostic, sourceFile: ts.SourceFile): |
| 97 | + ts.CallExpression | ts.NewExpression | null { |
| 98 | + |
| 99 | + let resolvedNode: ts.Node | null = null; |
| 100 | + |
| 101 | + const _visitNode = (node: ts.Node) => { |
| 102 | + // Check whether the current node contains the diagnostic. If the node contains the diagnostic, |
| 103 | + // walk deeper in order to find all constructor expression nodes. |
| 104 | + if (node.getStart() <= diagnostic.start! && node.getEnd() >= diagnostic.start!) { |
| 105 | + |
| 106 | + if (ts.isNewExpression(node) || |
| 107 | + (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.SuperKeyword)) { |
| 108 | + resolvedNode = node; |
| 109 | + } |
| 110 | + |
| 111 | + ts.forEachChild(node, _visitNode); |
| 112 | + } |
| 113 | + }; |
| 114 | + |
| 115 | + ts.forEachChild(sourceFile, _visitNode); |
| 116 | + |
| 117 | + return resolvedNode; |
| 118 | +} |
0 commit comments