|
7 | 7 | */
|
8 | 8 |
|
9 | 9 | import {green, red} from 'chalk';
|
10 |
| -import {relative} from 'path'; |
11 |
| -import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; |
| 10 | +import {RuleFailure, Rules, RuleWalker} from 'tslint'; |
12 | 11 | import * as ts from 'typescript';
|
13 | 12 | import {classNames} from '../material/data/class-names';
|
14 | 13 | import {
|
15 | 14 | isMaterialExportDeclaration,
|
16 | 15 | isMaterialImportDeclaration,
|
17 | 16 | } from '../material/typescript-specifiers';
|
18 |
| -import {getOriginalSymbolFromNode} from '../typescript/identifiers'; |
19 | 17 | import {
|
20 | 18 | isExportSpecifierNode,
|
21 | 19 | isImportSpecifierNode,
|
22 |
| - isNamespaceImportNode |
| 20 | + isNamespaceImportNode, |
23 | 21 | } from '../typescript/imports';
|
24 | 22 |
|
25 | 23 | /**
|
26 | 24 | * Rule that walks through every identifier that is part of Angular Material and replaces the
|
27 | 25 | * outdated name with the new one.
|
28 | 26 | */
|
29 |
| -export class Rule extends Rules.TypedRule { |
30 |
| - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { |
31 |
| - return this.applyWithWalker( |
32 |
| - new SwitchIdentifiersWalker(sourceFile, this.getOptions(), program)); |
| 27 | +export class Rule extends Rules.AbstractRule { |
| 28 | + |
| 29 | + apply(sourceFile: ts.SourceFile): RuleFailure[] { |
| 30 | + return this.applyWithWalker(new SwitchIdentifiersWalker(sourceFile, this.getOptions())); |
33 | 31 | }
|
34 | 32 | }
|
35 | 33 |
|
36 |
| -export class SwitchIdentifiersWalker extends ProgramAwareRuleWalker { |
37 |
| - constructor(sf, opt, prog) { |
38 |
| - super(sf, opt, prog); |
39 |
| - } |
| 34 | +export class SwitchIdentifiersWalker extends RuleWalker { |
40 | 35 |
|
41 |
| - /** List of Angular Material declarations inside of the current source file. */ |
42 |
| - materialDeclarations: ts.Declaration[] = []; |
| 36 | + /** |
| 37 | + * List of identifier names that have been imported from `@angular/material` or `@angular/cdk` |
| 38 | + * in the current source file and therefore can be considered trusted. |
| 39 | + */ |
| 40 | + trustedIdentifiers: Set<string> = new Set(); |
43 | 41 |
|
44 |
| - /** List of Angular Material namespace declarations in the current source file. */ |
45 |
| - materialNamespaceDeclarations: ts.Declaration[] = []; |
| 42 | + /** List of namespaces that have been imported from `@angular/material` or `@angular/cdk`. */ |
| 43 | + trustedNamespaces: Set<string> = new Set(); |
46 | 44 |
|
47 | 45 | /** Method that is called for every identifier inside of the specified project. */
|
48 | 46 | visitIdentifier(identifier: ts.Identifier) {
|
49 |
| - // Store Angular Material namespace identifiers in a list of declarations. |
50 |
| - // Namespace identifiers can be: `import * as md from '@angular/material';` |
51 |
| - this._storeNamespaceImports(identifier); |
52 |
| - |
53 | 47 | // For identifiers that aren't listed in the className data, the whole check can be
|
54 | 48 | // skipped safely.
|
55 | 49 | if (!classNames.some(data => data.replace === identifier.text)) {
|
56 | 50 | return;
|
57 | 51 | }
|
58 | 52 |
|
59 |
| - const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); |
| 53 | + // For namespace imports that are referring to Angular Material or the CDK, we store the |
| 54 | + // namespace name in order to be able to safely find identifiers that don't belong to the |
| 55 | + // developer's application. |
| 56 | + if (isNamespaceImportNode(identifier) && isMaterialImportDeclaration(identifier)) { |
| 57 | + this.trustedNamespaces.add(identifier.text); |
60 | 58 |
|
61 |
| - // If the symbol is not defined or could not be resolved, just skip the following identifier |
62 |
| - // checks. |
63 |
| - if (!symbol || !symbol.name || symbol.name === 'unknown') { |
64 |
| - console.error(`Could not resolve symbol for identifier "${identifier.text}" ` + |
65 |
| - `in file ${this._getRelativeFileName()}`); |
66 |
| - return; |
| 59 | + return this._createFailureWithReplacement(identifier); |
67 | 60 | }
|
68 | 61 |
|
69 |
| - // For export declarations that are referring to Angular Material, the identifier should be |
70 |
| - // switched to the new name. |
| 62 | + // For export declarations that are referring to Angular Material or the CDK, the identifier |
| 63 | + // can be immediately updated to the new name. |
71 | 64 | if (isExportSpecifierNode(identifier) && isMaterialExportDeclaration(identifier)) {
|
72 |
| - return this.createIdentifierFailure(identifier, symbol); |
| 65 | + return this._createFailureWithReplacement(identifier); |
73 | 66 | }
|
74 | 67 |
|
75 |
| - // For import declarations that are referring to Angular Material, the value declarations |
76 |
| - // should be stored so that other identifiers in the file can be compared. |
| 68 | + // For import declarations that are referring to Angular Material or the CDK, the name of |
| 69 | + // the import identifiers. This allows us to identify identifiers that belong to Material and |
| 70 | + // the CDK, and we won't accidentally touch a developer's identifier. |
77 | 71 | if (isImportSpecifierNode(identifier) && isMaterialImportDeclaration(identifier)) {
|
78 |
| - this.materialDeclarations.push(symbol.valueDeclaration); |
| 72 | + this.trustedIdentifiers.add(identifier.text); |
79 | 73 |
|
80 |
| - // For identifiers that are not part of an import or export, the list of Material declarations |
81 |
| - // should be checked to ensure that only identifiers of Angular Material are updated. |
82 |
| - // Identifiers that are imported through an Angular Material namespace will be updated. |
83 |
| - } else if (this.materialDeclarations.indexOf(symbol.valueDeclaration) === -1 && |
84 |
| - !this._isIdentifierFromNamespace(identifier)) { |
85 |
| - return; |
| 74 | + return this._createFailureWithReplacement(identifier); |
86 | 75 | }
|
87 | 76 |
|
88 |
| - return this.createIdentifierFailure(identifier, symbol); |
| 77 | + // In case the identifier is part of a property access expression, we need to verify that the |
| 78 | + // property access originates from a namespace that has been imported from Material or the CDK. |
| 79 | + if (ts.isPropertyAccessExpression(identifier.parent)) { |
| 80 | + const expression = identifier.parent.expression; |
| 81 | + |
| 82 | + if (ts.isIdentifier(expression) && this.trustedNamespaces.has(expression.text)) { |
| 83 | + return this._createFailureWithReplacement(identifier); |
| 84 | + } |
| 85 | + } else if (this.trustedIdentifiers.has(identifier.text)) { |
| 86 | + return this._createFailureWithReplacement(identifier); |
| 87 | + } |
89 | 88 | }
|
90 | 89 |
|
91 | 90 | /** Creates a failure and replacement for the specified identifier. */
|
92 |
| - private createIdentifierFailure(identifier: ts.Identifier, symbol: ts.Symbol) { |
93 |
| - let classData = classNames.find( |
94 |
| - data => data.replace === symbol.name || data.replace === identifier.text); |
| 91 | + private _createFailureWithReplacement(identifier: ts.Identifier) { |
| 92 | + const classData = classNames.find(data => data.replace === identifier.text); |
95 | 93 |
|
96 | 94 | if (!classData) {
|
97 |
| - console.error(`Could not find updated name for identifier "${identifier.getText()}" in ` + |
98 |
| - ` in file ${this._getRelativeFileName()}.`); |
| 95 | + console.error(`Could not find updated name for identifier "${identifier.text}" in ` + |
| 96 | + ` in file ${this.getSourceFile().fileName}.`); |
99 | 97 | return;
|
100 | 98 | }
|
101 | 99 |
|
102 | 100 | const replacement = this.createReplacement(
|
103 |
| - identifier.getStart(), identifier.getWidth(), classData.replaceWith); |
| 101 | + identifier.getStart(), identifier.getWidth(), classData.replaceWith); |
104 | 102 |
|
105 | 103 | this.addFailureAtNode(
|
106 |
| - identifier, |
107 |
| - `Found deprecated identifier "${red(classData.replace)}" which has been renamed to` + |
108 |
| - ` "${green(classData.replaceWith)}"`, |
109 |
| - replacement); |
110 |
| - } |
111 |
| - |
112 |
| - /** Checks namespace imports from Angular Material and stores them in a list. */ |
113 |
| - private _storeNamespaceImports(identifier: ts.Identifier) { |
114 |
| - // In some situations, developers will import Angular Material completely using a namespace |
115 |
| - // import. This is not recommended, but should be still handled in the migration tool. |
116 |
| - if (isNamespaceImportNode(identifier) && isMaterialImportDeclaration(identifier)) { |
117 |
| - const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); |
118 |
| - |
119 |
| - if (symbol) { |
120 |
| - return this.materialNamespaceDeclarations.push(symbol.valueDeclaration); |
121 |
| - } |
122 |
| - } |
123 |
| - } |
124 |
| - |
125 |
| - /** Checks whether the given identifier is part of the Material namespace. */ |
126 |
| - private _isIdentifierFromNamespace(identifier: ts.Identifier) { |
127 |
| - if (identifier.parent && identifier.parent.kind !== ts.SyntaxKind.PropertyAccessExpression) { |
128 |
| - return; |
129 |
| - } |
130 |
| - |
131 |
| - const propertyExpression = identifier.parent as ts.PropertyAccessExpression; |
132 |
| - const expressionSymbol = getOriginalSymbolFromNode(propertyExpression.expression, |
133 |
| - this.getTypeChecker()); |
134 |
| - |
135 |
| - return this.materialNamespaceDeclarations.indexOf(expressionSymbol.valueDeclaration) !== -1; |
136 |
| - } |
137 |
| - |
138 |
| - /** Returns the current source file path relative to the root directory of the project. */ |
139 |
| - private _getRelativeFileName(): string { |
140 |
| - return relative(this.getProgram().getCurrentDirectory(), this.getSourceFile().fileName); |
| 104 | + identifier, |
| 105 | + `Found deprecated identifier "${red(classData.replace)}" which has been renamed to` + |
| 106 | + ` "${green(classData.replaceWith)}"`, |
| 107 | + replacement); |
141 | 108 | }
|
142 | 109 | }
|
0 commit comments