|
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