diff --git a/src/lib/schematics/update/material/typescript-specifiers.ts b/src/lib/schematics/update/material/typescript-specifiers.ts index c19175bbd62..c9a87fce29c 100644 --- a/src/lib/schematics/update/material/typescript-specifiers.ts +++ b/src/lib/schematics/update/material/typescript-specifiers.ts @@ -15,14 +15,14 @@ export const materialModuleSpecifier = '@angular/material'; /** Name of the Angular CDK module specifier. */ export const cdkModuleSpecifier = '@angular/cdk'; -/** Whether the specified node is part of an Angular Material import declaration. */ +/** Whether the specified node is part of an Angular Material or CDK import declaration. */ export function isMaterialImportDeclaration(node: ts.Node) { return isMaterialDeclaration(getImportDeclaration(node)); } -/** Whether the specified node is part of an Angular Material export declaration. */ +/** Whether the specified node is part of an Angular Material or CDK import declaration. */ export function isMaterialExportDeclaration(node: ts.Node) { - return getExportDeclaration(getImportDeclaration(node)); + return isMaterialDeclaration(getExportDeclaration(node)); } /** Whether the declaration is part of Angular Material. */ diff --git a/src/lib/schematics/update/rules/switchIdentifiersRule.ts b/src/lib/schematics/update/rules/switchIdentifiersRule.ts index cfa4f6098b1..215e920a2e4 100644 --- a/src/lib/schematics/update/rules/switchIdentifiersRule.ts +++ b/src/lib/schematics/update/rules/switchIdentifiersRule.ts @@ -7,136 +7,103 @@ */ import {green, red} from 'chalk'; -import {relative} from 'path'; -import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import {RuleFailure, Rules, RuleWalker} from 'tslint'; import * as ts from 'typescript'; import {classNames} from '../material/data/class-names'; import { isMaterialExportDeclaration, isMaterialImportDeclaration, } from '../material/typescript-specifiers'; -import {getOriginalSymbolFromNode} from '../typescript/identifiers'; import { isExportSpecifierNode, isImportSpecifierNode, - isNamespaceImportNode + isNamespaceImportNode, } from '../typescript/imports'; /** * Rule that walks through every identifier that is part of Angular Material and replaces the * outdated name with the new one. */ -export class Rule extends Rules.TypedRule { - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - return this.applyWithWalker( - new SwitchIdentifiersWalker(sourceFile, this.getOptions(), program)); +export class Rule extends Rules.AbstractRule { + + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker(new SwitchIdentifiersWalker(sourceFile, this.getOptions())); } } -export class SwitchIdentifiersWalker extends ProgramAwareRuleWalker { - constructor(sf, opt, prog) { - super(sf, opt, prog); - } +export class SwitchIdentifiersWalker extends RuleWalker { - /** List of Angular Material declarations inside of the current source file. */ - materialDeclarations: ts.Declaration[] = []; + /** + * List of identifier names that have been imported from `@angular/material` or `@angular/cdk` + * in the current source file and therefore can be considered trusted. + */ + trustedIdentifiers: Set = new Set(); - /** List of Angular Material namespace declarations in the current source file. */ - materialNamespaceDeclarations: ts.Declaration[] = []; + /** List of namespaces that have been imported from `@angular/material` or `@angular/cdk`. */ + trustedNamespaces: Set = new Set(); /** Method that is called for every identifier inside of the specified project. */ visitIdentifier(identifier: ts.Identifier) { - // Store Angular Material namespace identifiers in a list of declarations. - // Namespace identifiers can be: `import * as md from '@angular/material';` - this._storeNamespaceImports(identifier); - // For identifiers that aren't listed in the className data, the whole check can be // skipped safely. if (!classNames.some(data => data.replace === identifier.text)) { return; } - const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); + // For namespace imports that are referring to Angular Material or the CDK, we store the + // namespace name in order to be able to safely find identifiers that don't belong to the + // developer's application. + if (isNamespaceImportNode(identifier) && isMaterialImportDeclaration(identifier)) { + this.trustedNamespaces.add(identifier.text); - // If the symbol is not defined or could not be resolved, just skip the following identifier - // checks. - if (!symbol || !symbol.name || symbol.name === 'unknown') { - console.error(`Could not resolve symbol for identifier "${identifier.text}" ` + - `in file ${this._getRelativeFileName()}`); - return; + return this._createFailureWithReplacement(identifier); } - // For export declarations that are referring to Angular Material, the identifier should be - // switched to the new name. + // For export declarations that are referring to Angular Material or the CDK, the identifier + // can be immediately updated to the new name. if (isExportSpecifierNode(identifier) && isMaterialExportDeclaration(identifier)) { - return this.createIdentifierFailure(identifier, symbol); + return this._createFailureWithReplacement(identifier); } - // For import declarations that are referring to Angular Material, the value declarations - // should be stored so that other identifiers in the file can be compared. + // For import declarations that are referring to Angular Material or the CDK, the name of + // the import identifiers. This allows us to identify identifiers that belong to Material and + // the CDK, and we won't accidentally touch a developer's identifier. if (isImportSpecifierNode(identifier) && isMaterialImportDeclaration(identifier)) { - this.materialDeclarations.push(symbol.valueDeclaration); + this.trustedIdentifiers.add(identifier.text); - // For identifiers that are not part of an import or export, the list of Material declarations - // should be checked to ensure that only identifiers of Angular Material are updated. - // Identifiers that are imported through an Angular Material namespace will be updated. - } else if (this.materialDeclarations.indexOf(symbol.valueDeclaration) === -1 && - !this._isIdentifierFromNamespace(identifier)) { - return; + return this._createFailureWithReplacement(identifier); } - return this.createIdentifierFailure(identifier, symbol); + // In case the identifier is part of a property access expression, we need to verify that the + // property access originates from a namespace that has been imported from Material or the CDK. + if (ts.isPropertyAccessExpression(identifier.parent)) { + const expression = identifier.parent.expression; + + if (ts.isIdentifier(expression) && this.trustedNamespaces.has(expression.text)) { + return this._createFailureWithReplacement(identifier); + } + } else if (this.trustedIdentifiers.has(identifier.text)) { + return this._createFailureWithReplacement(identifier); + } } /** Creates a failure and replacement for the specified identifier. */ - private createIdentifierFailure(identifier: ts.Identifier, symbol: ts.Symbol) { - let classData = classNames.find( - data => data.replace === symbol.name || data.replace === identifier.text); + private _createFailureWithReplacement(identifier: ts.Identifier) { + const classData = classNames.find(data => data.replace === identifier.text); if (!classData) { - console.error(`Could not find updated name for identifier "${identifier.getText()}" in ` + - ` in file ${this._getRelativeFileName()}.`); + console.error(`Could not find updated name for identifier "${identifier.text}" in ` + + ` in file ${this.getSourceFile().fileName}.`); return; } const replacement = this.createReplacement( - identifier.getStart(), identifier.getWidth(), classData.replaceWith); + identifier.getStart(), identifier.getWidth(), classData.replaceWith); this.addFailureAtNode( - identifier, - `Found deprecated identifier "${red(classData.replace)}" which has been renamed to` + - ` "${green(classData.replaceWith)}"`, - replacement); - } - - /** Checks namespace imports from Angular Material and stores them in a list. */ - private _storeNamespaceImports(identifier: ts.Identifier) { - // In some situations, developers will import Angular Material completely using a namespace - // import. This is not recommended, but should be still handled in the migration tool. - if (isNamespaceImportNode(identifier) && isMaterialImportDeclaration(identifier)) { - const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); - - if (symbol) { - return this.materialNamespaceDeclarations.push(symbol.valueDeclaration); - } - } - } - - /** Checks whether the given identifier is part of the Material namespace. */ - private _isIdentifierFromNamespace(identifier: ts.Identifier) { - if (identifier.parent && identifier.parent.kind !== ts.SyntaxKind.PropertyAccessExpression) { - return; - } - - const propertyExpression = identifier.parent as ts.PropertyAccessExpression; - const expressionSymbol = getOriginalSymbolFromNode(propertyExpression.expression, - this.getTypeChecker()); - - return this.materialNamespaceDeclarations.indexOf(expressionSymbol.valueDeclaration) !== -1; - } - - /** Returns the current source file path relative to the root directory of the project. */ - private _getRelativeFileName(): string { - return relative(this.getProgram().getCurrentDirectory(), this.getSourceFile().fileName); + identifier, + `Found deprecated identifier "${red(classData.replace)}" which has been renamed to` + + ` "${green(classData.replaceWith)}"`, + replacement); } } diff --git a/tools/gulp/tasks/material-release.ts b/tools/gulp/tasks/material-release.ts index 331b97988c4..d1483ed0d0f 100644 --- a/tools/gulp/tasks/material-release.ts +++ b/tools/gulp/tasks/material-release.ts @@ -56,7 +56,7 @@ task('schematics:build', tsBuildTask(join(schematicsDir, 'tsconfig.json'))); * Task that will build the material package. Special treatment for this package includes: * - Copying all prebuilt themes into the package * - Bundling theming scss into a single theming file - * - Copying schematics code into the packatge + * - Copying schematics code into the package */ task('material:prepare-release', sequenceTask( ['material:build', 'schematics:build'],