diff --git a/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_expected_output.ts b/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_expected_output.ts index f162584e9570..9fe7afb87205 100644 --- a/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_expected_output.ts +++ b/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_expected_output.ts @@ -1,4 +1,5 @@ import {Component} from '@angular/core'; +import * as core from '@angular/core'; import {By} from '@angular/platform-browser'; const a = By.css('[cdkPortalOutlet]'); @@ -9,7 +10,7 @@ const b = By.css('[cdkPortalOutlet]'); const c = 'cdkPortalHost'; const d = 'portalHost'; -@Component({ +@core.Component({ template: `
@@ -31,4 +32,4 @@ class E {} 'div[cdkPortalOutlet] {color: blue}' ] }) -class F {} \ No newline at end of file +class F {} diff --git a/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_input.ts b/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_input.ts index 2ba661c35539..0a12f229ec71 100644 --- a/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_input.ts +++ b/src/cdk/schematics/ng-update/test-cases/v6/attribute-selectors_input.ts @@ -1,4 +1,5 @@ import {Component} from '@angular/core'; +import * as core from '@angular/core'; import {By} from '@angular/platform-browser'; const a = By.css('[cdkPortalHost]'); @@ -9,7 +10,7 @@ const b = By.css('[portalHost]'); const c = 'cdkPortalHost'; const d = 'portalHost'; -@Component({ +@core.Component({ template: `
@@ -31,4 +32,4 @@ class E {} 'div[portalHost] {color: blue}' ] }) -class F {} \ No newline at end of file +class F {} diff --git a/src/cdk/schematics/testing/test-case-setup.ts b/src/cdk/schematics/testing/test-case-setup.ts index 6ff1d0c640e9..67d65f9554a1 100644 --- a/src/cdk/schematics/testing/test-case-setup.ts +++ b/src/cdk/schematics/testing/test-case-setup.ts @@ -23,6 +23,9 @@ const TEST_CASE_INPUT_SUFFIX = '_input.ts'; /** Suffix that indicates whether a given file is an expected output of a test case. */ const TEST_CASE_OUTPUT_SUFFIX = '_expected_output.ts'; +/** Name of the folder that can contain test case files which should not run automatically. */ +const MISC_FOLDER_NAME = 'misc'; + /** Reads the UTF8 content of the specified file. Normalizes the path and ensures that */ export function readFileContent(filePath: string): string { return readFileSync(filePath, 'utf8'); @@ -135,7 +138,8 @@ export function findBazelVersionTestCases(basePath: string) { // test case files by using "glob" and store them in our result map. if (!manifestPath) { const runfilesBaseDir = join(runfilesDir!, basePath); - const inputFiles = globSync(`**/*${TEST_CASE_INPUT_SUFFIX}`, {cwd: runfilesBaseDir}); + const inputFiles = globSync(`**/!(${MISC_FOLDER_NAME})/*${TEST_CASE_INPUT_SUFFIX}`, + {cwd: runfilesBaseDir}); inputFiles.forEach(inputFile => { // The target version of an input file will be determined from the first @@ -158,9 +162,13 @@ export function findBazelVersionTestCases(basePath: string) { // In case the mapped runfile starts with the specified base path and ends with "_input.ts", // we store it in our result map because we assume that this is a test case. if (runfilePath.startsWith(basePath) && runfilePath.endsWith(TEST_CASE_INPUT_SUFFIX)) { + const pathSegments = relative(basePath, runfilePath).split(sep); + if (pathSegments.includes(MISC_FOLDER_NAME)) { + return; + } // The target version of an input file will be determined from the first // path segment. (e.g. "v6/my_rule_input.ts" will be for "v6") - const targetVersion = relative(basePath, runfilePath).split(sep)[0]; + const targetVersion = pathSegments[0]; testCasesMap.set(targetVersion, (testCasesMap.get(targetVersion) || []).concat(realPath)); } }); diff --git a/src/cdk/schematics/update-tool/public-api.ts b/src/cdk/schematics/update-tool/public-api.ts index db4d27fcab0f..313b293bca0f 100644 --- a/src/cdk/schematics/update-tool/public-api.ts +++ b/src/cdk/schematics/update-tool/public-api.ts @@ -11,3 +11,5 @@ export * from './target-version'; export * from './version-changes'; export * from './migration-rule'; export * from './component-resource-collector'; +export * from './utils/imports'; +export * from './utils/decorators'; diff --git a/src/cdk/schematics/update-tool/target-version.ts b/src/cdk/schematics/update-tool/target-version.ts index 3566bee4cbaf..d5c7e0a1081f 100644 --- a/src/cdk/schematics/update-tool/target-version.ts +++ b/src/cdk/schematics/update-tool/target-version.ts @@ -21,5 +21,5 @@ export enum TargetVersion { */ export function getAllVersionNames(): string[] { return Object.keys(TargetVersion) - .filter(enumValue => typeof TargetVersion[enumValue] === 'number'); + .filter(enumValue => typeof TargetVersion[enumValue] === 'string'); } diff --git a/src/cdk/schematics/update-tool/utils/decorators.ts b/src/cdk/schematics/update-tool/utils/decorators.ts index 659ea6ecc13f..28e48d797046 100644 --- a/src/cdk/schematics/update-tool/utils/decorators.ts +++ b/src/cdk/schematics/update-tool/utils/decorators.ts @@ -17,7 +17,6 @@ export type CallExpressionDecorator = ts.Decorator&{ export interface NgDecorator { name: string; node: CallExpressionDecorator; - importNode: ts.ImportDeclaration; } /** @@ -27,23 +26,23 @@ export interface NgDecorator { export function getAngularDecorators( typeChecker: ts.TypeChecker, decorators: ReadonlyArray): NgDecorator[] { return decorators.map(node => ({node, importData: getCallDecoratorImport(typeChecker, node)})) - .filter(({importData}) => importData && importData.importModule.startsWith('@angular/')) - .map(({node, importData}) => ({ - node: node as CallExpressionDecorator, - name: importData!.name, - importNode: importData!.node - })); + .filter(({importData}) => importData && importData.moduleName.startsWith('@angular/')) + .map( + ({node, importData}) => + ({node: node as CallExpressionDecorator, name: importData!.symbolName})); } export function getCallDecoratorImport( typeChecker: ts.TypeChecker, decorator: ts.Decorator): Import|null { - // Note that this does not cover the edge case where decorators are called from - // a namespace import: e.g. "@core.Component()". This is not handled by Ngtsc either. - if (!ts.isCallExpression(decorator.expression) || - !ts.isIdentifier(decorator.expression.expression)) { + if (!ts.isCallExpression(decorator.expression)) { return null; } - - const identifier = decorator.expression.expression; - return getImportOfIdentifier(typeChecker, identifier); + const valueExpr = decorator.expression.expression; + let identifier: ts.Identifier|null = null; + if (ts.isIdentifier(valueExpr)) { + identifier = valueExpr; + } else if (ts.isPropertyAccessExpression(valueExpr) && ts.isIdentifier(valueExpr.name)) { + identifier = valueExpr.name; + } + return identifier ? getImportOfIdentifier(identifier, typeChecker) : null; } diff --git a/src/cdk/schematics/update-tool/utils/imports.ts b/src/cdk/schematics/update-tool/utils/imports.ts index 66450acad5fd..fccdb289d25a 100644 --- a/src/cdk/schematics/update-tool/utils/imports.ts +++ b/src/cdk/schematics/update-tool/utils/imports.ts @@ -8,39 +8,117 @@ import * as ts from 'typescript'; -export type Import = { - name: string, - importModule: string, - node: ts.ImportDeclaration -}; - -/** Gets import information about the specified identifier by using the type checker. */ -export function getImportOfIdentifier(typeChecker: ts.TypeChecker, node: ts.Identifier): Import| +/** Interface describing a resolved import. */ +export interface Import { + /** Name of the imported symbol. */ + symbolName: string; + /** Module name from which the symbol has been imported. */ + moduleName: string; +} + + +/** Resolves the import of the specified identifier. */ +export function getImportOfIdentifier(node: ts.Identifier, typeChecker: ts.TypeChecker): Import| null { - const symbol = typeChecker.getSymbolAtLocation(node); + // Free standing identifiers which resolve to an import will be handled + // as direct imports. e.g. "@Component()" where "Component" is an identifier + // referring to an import specifier. + const directImport = getSpecificImportOfIdentifier(node, typeChecker); + if (directImport !== null) { + return directImport; + } else if (ts.isQualifiedName(node.parent) && node.parent.right === node) { + // Determines the import of a qualified name. e.g. "let t: core.Component". In that + // case, the import of the most left identifier will be determined ("core"). + const qualifierRoot = getQualifiedNameRoot(node.parent); + if (qualifierRoot) { + const moduleName = getImportOfNamespacedIdentifier(qualifierRoot, typeChecker); + if (moduleName) { + return {moduleName, symbolName: node.text}; + } + } + } else if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) { + // Determines the import of a property expression. e.g. "@core.Component". In that + // case, the import of the most left identifier will be determined ("core"). + const rootIdentifier = getPropertyAccessRoot(node.parent); + if (rootIdentifier) { + const moduleName = getImportOfNamespacedIdentifier(rootIdentifier, typeChecker); + if (moduleName) { + return {moduleName, symbolName: node.text}; + } + } + } + return null; +} +/** + * Resolves the import of the specified identifier. Expects the identifier to resolve + * to a fine-grained import declaration with import specifiers. + */ +function getSpecificImportOfIdentifier(node: ts.Identifier, typeChecker: ts.TypeChecker): Import| + null { + const symbol = typeChecker.getSymbolAtLocation(node); if (!symbol || !symbol.declarations || !symbol.declarations.length) { return null; } - - const decl = symbol.declarations[0]; - - if (!ts.isImportSpecifier(decl)) { + const declaration = symbol.declarations[0]; + if (!ts.isImportSpecifier(declaration)) { return null; } - - // Since "decl" is an import specifier, we can walk up three times to get a reference + // Since the declaration is an import specifier, we can walk up three times to get a reference // to the import declaration node (NamedImports -> ImportClause -> ImportDeclaration). - const importDecl = decl.parent.parent.parent; - + const importDecl = declaration.parent.parent.parent; if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { return null; } - return { - // Handles aliased imports: e.g. "import {Component as myComp} from ..."; - name: decl.propertyName ? decl.propertyName.text : decl.name.text, - importModule: importDecl.moduleSpecifier.text, - node: importDecl + moduleName: importDecl.moduleSpecifier.text, + symbolName: declaration.propertyName ? declaration.propertyName.text : declaration.name.text }; } + +/** + * Resolves the import of the specified identifier. Expects the identifier to + * resolve to a namespaced import declaration. e.g. "import * as core from ...". + */ +function getImportOfNamespacedIdentifier(node: ts.Identifier, typeChecker: ts.TypeChecker): string| + null { + const symbol = typeChecker.getSymbolAtLocation(node); + if (!symbol || !symbol.declarations || !symbol.declarations.length) { + return null; + } + const declaration = symbol.declarations[0]; + if (!ts.isNamespaceImport(declaration)) { + return null; + } + // Since the declaration is a namespace import, we can walk up three times to get a reference + // to the import declaration node (NamespaceImport -> ImportClause -> ImportDeclaration). + const importDecl = declaration.parent.parent; + if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { + return null; + } + + return importDecl.moduleSpecifier.text; +} + + +/** + * Gets the root identifier of a qualified type chain. For example: "core.GestureConfig" + * will return the "core" identifier. Allowing us to find the import of "core". + */ +function getQualifiedNameRoot(name: ts.QualifiedName): ts.Identifier|null { + while (ts.isQualifiedName(name.left)) { + name = name.left; + } + return ts.isIdentifier(name.left) ? name.left : null; +} + +/** + * Gets the root identifier of a property access chain. For example: "core.GestureConfig" + * will return the "core" identifier. Allowing us to find the import of "core". + */ +function getPropertyAccessRoot(node: ts.PropertyAccessExpression): ts.Identifier|null { + while (ts.isPropertyAccessExpression(node.expression)) { + node = node.expression; + } + return ts.isIdentifier(node.expression) ? node.expression : null; +} diff --git a/src/material/schematics/ng-update/test-cases/v8/material-imports.spec.ts b/src/material/schematics/ng-update/test-cases/v8/misc/material-imports.spec.ts similarity index 96% rename from src/material/schematics/ng-update/test-cases/v8/material-imports.spec.ts rename to src/material/schematics/ng-update/test-cases/v8/misc/material-imports.spec.ts index 2452cba0c8f4..3c1d912d545e 100644 --- a/src/material/schematics/ng-update/test-cases/v8/material-imports.spec.ts +++ b/src/material/schematics/ng-update/test-cases/v8/misc/material-imports.spec.ts @@ -1,5 +1,5 @@ import {createTestCaseSetup, readFileContent} from '@angular/cdk/schematics/testing'; -import {migrationCollection} from '../index.spec'; +import {migrationCollection} from '../../index.spec'; describe('v8 material imports', () => { it('should re-map top-level material imports to the proper entry points', async () => { diff --git a/src/material/schematics/ng-update/test-cases/v8/material-imports_expected_output.ts b/src/material/schematics/ng-update/test-cases/v8/misc/material-imports_expected_output.ts similarity index 100% rename from src/material/schematics/ng-update/test-cases/v8/material-imports_expected_output.ts rename to src/material/schematics/ng-update/test-cases/v8/misc/material-imports_expected_output.ts diff --git a/src/material/schematics/ng-update/test-cases/v8/material-imports_input.ts b/src/material/schematics/ng-update/test-cases/v8/misc/material-imports_input.ts similarity index 100% rename from src/material/schematics/ng-update/test-cases/v8/material-imports_input.ts rename to src/material/schematics/ng-update/test-cases/v8/misc/material-imports_input.ts diff --git a/src/material/schematics/ng-update/test-cases/v9/hammer-migration-v9.spec.ts b/src/material/schematics/ng-update/test-cases/v9/misc/hammer-migration-v9.spec.ts similarity index 99% rename from src/material/schematics/ng-update/test-cases/v9/hammer-migration-v9.spec.ts rename to src/material/schematics/ng-update/test-cases/v9/misc/hammer-migration-v9.spec.ts index c14456a9f0b5..a4b045b854a4 100644 --- a/src/material/schematics/ng-update/test-cases/v9/hammer-migration-v9.spec.ts +++ b/src/material/schematics/ng-update/test-cases/v9/misc/hammer-migration-v9.spec.ts @@ -4,11 +4,11 @@ import {addPackageToPackageJson} from '@angular/cdk/schematics/ng-add/package-co import {createTestCaseSetup} from '@angular/cdk/schematics/testing'; import {readFileSync} from 'fs'; -import {migrationCollection} from '../index.spec'; +import {migrationCollection} from '../../index.spec'; describe('v9 HammerJS removal', () => { const GESTURE_CONFIG_TEMPLATE_PATH = - require.resolve('../../upgrade-rules/hammer-gestures-v9/gesture-config.template'); + require.resolve('../../../upgrade-rules/hammer-gestures-v9/gesture-config.template'); let runner: SchematicTestRunner; let tree: UnitTestTree; diff --git a/src/material/schematics/ng-update/test-cases/v9/material-imports.spec.ts b/src/material/schematics/ng-update/test-cases/v9/misc/material-imports.spec.ts similarity index 94% rename from src/material/schematics/ng-update/test-cases/v9/material-imports.spec.ts rename to src/material/schematics/ng-update/test-cases/v9/misc/material-imports.spec.ts index 4434f4db561c..a6c1833162dc 100644 --- a/src/material/schematics/ng-update/test-cases/v9/material-imports.spec.ts +++ b/src/material/schematics/ng-update/test-cases/v9/misc/material-imports.spec.ts @@ -1,5 +1,5 @@ import {createTestCaseSetup, readFileContent} from '@angular/cdk/schematics/testing'; -import {migrationCollection} from '../index.spec'; +import {migrationCollection} from '../../index.spec'; describe('v9 material imports', () => { it('should re-map top-level material imports to the proper entry points when top-level ' + diff --git a/src/material/schematics/ng-update/test-cases/v9/material-imports_expected_output.ts b/src/material/schematics/ng-update/test-cases/v9/misc/material-imports_expected_output.ts similarity index 100% rename from src/material/schematics/ng-update/test-cases/v9/material-imports_expected_output.ts rename to src/material/schematics/ng-update/test-cases/v9/misc/material-imports_expected_output.ts diff --git a/src/material/schematics/ng-update/test-cases/v9/material-imports_input.ts b/src/material/schematics/ng-update/test-cases/v9/misc/material-imports_input.ts similarity index 100% rename from src/material/schematics/ng-update/test-cases/v9/material-imports_input.ts rename to src/material/schematics/ng-update/test-cases/v9/misc/material-imports_input.ts diff --git a/src/material/schematics/ng-update/upgrade-rules/hammer-gestures-v9/hammer-gestures-rule.ts b/src/material/schematics/ng-update/upgrade-rules/hammer-gestures-v9/hammer-gestures-rule.ts index 0163dc56ac38..dedea3f9f708 100644 --- a/src/material/schematics/ng-update/upgrade-rules/hammer-gestures-v9/hammer-gestures-rule.ts +++ b/src/material/schematics/ng-update/upgrade-rules/hammer-gestures-v9/hammer-gestures-rule.ts @@ -19,7 +19,9 @@ import { MigrationRule, PostMigrationAction, ResolvedResource, - TargetVersion + TargetVersion, + Import, + getImportOfIdentifier, } from '@angular/cdk/schematics'; import { addSymbolToNgModuleMetadata, @@ -38,7 +40,6 @@ import {getProjectFromProgram} from './cli-workspace'; import {findHammerScriptImportElements} from './find-hammer-script-tags'; import {findMainModuleExpression} from './find-main-module'; import {isHammerJsUsedInTemplate} from './hammer-template-check'; -import {getImportOfIdentifier, Import} from './identifier-imports'; import {ImportManager} from './import-manager'; import {removeElementFromArrayExpression} from './remove-array-element'; import {removeElementFromHtml} from './remove-element-from-html'; diff --git a/src/material/schematics/ng-update/upgrade-rules/hammer-gestures-v9/identifier-imports.ts b/src/material/schematics/ng-update/upgrade-rules/hammer-gestures-v9/identifier-imports.ts deleted file mode 100644 index e49b3d6a902b..000000000000 --- a/src/material/schematics/ng-update/upgrade-rules/hammer-gestures-v9/identifier-imports.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import * as ts from 'typescript'; - -/** Interface describing a resolved import. */ -export interface Import { - /** Name of the imported symbol. */ - symbolName: string; - /** Module name from which the symbol has been imported. */ - moduleName: string; -} - - -/** Resolves the import of the specified identifier. */ -export function getImportOfIdentifier(node: ts.Identifier, typeChecker: ts.TypeChecker): Import| - null { - const directImport = getSpecificImportOfIdentifier(node, typeChecker); - if (directImport !== null) { - return directImport; - } else if (ts.isQualifiedName(node.parent) && node.parent.right === node) { - const qualifierRoot = getQualifiedNameRoot(node.parent); - if (qualifierRoot) { - const moduleName = getImportOfNamespacedIdentifier(qualifierRoot, typeChecker); - if (moduleName) { - return {moduleName, symbolName: node.text}; - } - } - } else if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) { - const rootIdentifier = getPropertyAccessRoot(node.parent); - if (rootIdentifier) { - const moduleName = getImportOfNamespacedIdentifier(rootIdentifier, typeChecker); - if (moduleName) { - return {moduleName, symbolName: node.text}; - } - } - } - return null; -} - -/** - * Resolves the import of the specified identifier. Expects the identifier to resolve - * to a fine-grained import declaration with import specifiers. - */ -function getSpecificImportOfIdentifier(node: ts.Identifier, typeChecker: ts.TypeChecker): Import| - null { - const symbol = typeChecker.getSymbolAtLocation(node); - if (!symbol || !symbol.declarations || !symbol.declarations.length) { - return null; - } - const declaration = symbol.declarations[0]; - if (!ts.isImportSpecifier(declaration)) { - return null; - } - const importDecl = declaration.parent.parent.parent; - if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { - return null; - } - return { - moduleName: importDecl.moduleSpecifier.text, - symbolName: declaration.propertyName ? declaration.propertyName.text : declaration.name.text - }; -} - -/** - * Resolves the import of the specified identifier. Expects the identifier to - * resolve to a namespaced import declaration. e.g. "import * as core from ...". - */ -function getImportOfNamespacedIdentifier(node: ts.Identifier, typeChecker: ts.TypeChecker): string| - null { - const symbol = typeChecker.getSymbolAtLocation(node); - if (!symbol || !symbol.declarations || !symbol.declarations.length) { - return null; - } - const declaration = symbol.declarations[0]; - if (!ts.isNamespaceImport(declaration)) { - return null; - } - const importDecl = declaration.parent.parent; - if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { - return null; - } - - return importDecl.moduleSpecifier.text; -} - - -/** - * Gets the root identifier of a qualified type chain. For example: "core.GestureConfig" - * will return the "matCore" identifier. Allowing us to find the import of "core". - */ -function getQualifiedNameRoot(name: ts.QualifiedName): ts.Identifier|null { - while (ts.isQualifiedName(name.left)) { - name = name.left; - } - return ts.isIdentifier(name.left) ? name.left : null; -} - -/** - * Gets the root identifier of a property access chain. For example: "core.GestureConfig" - * will return the "matCore" identifier. Allowing us to find the import of "core". - */ -function getPropertyAccessRoot(node: ts.PropertyAccessExpression): ts.Identifier|null { - while (ts.isPropertyAccessExpression(node.expression)) { - node = node.expression; - } - return ts.isIdentifier(node.expression) ? node.expression : null; -}