feat(core): add automated migration to replace async with waitForAsync (#39212)
Adds a migration that finds all imports and calls to the deprecated `async` function from `@angular/core/testing` and replaces them with `waitForAsync`. These changes also move a bit of code out of the `Renderer2` migration so that it can be reused. PR Close #39212
This commit is contained in:
parent
437e563507
commit
5ce71e0fbc
|
@ -23,5 +23,6 @@ pkg_npm(
|
||||||
"//packages/core/schematics/migrations/template-var-assignment",
|
"//packages/core/schematics/migrations/template-var-assignment",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
||||||
|
"//packages/core/schematics/migrations/wait-for-async",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -64,6 +64,11 @@
|
||||||
"version": "11.0.0-beta",
|
"version": "11.0.0-beta",
|
||||||
"description": "ViewEncapsulation.Native has been removed as of Angular version 11. This migration replaces any usages with ViewEncapsulation.ShadowDom.",
|
"description": "ViewEncapsulation.Native has been removed as of Angular version 11. This migration replaces any usages with ViewEncapsulation.ShadowDom.",
|
||||||
"factory": "./migrations/native-view-encapsulation/index"
|
"factory": "./migrations/native-view-encapsulation/index"
|
||||||
|
},
|
||||||
|
"migration-v11-wait-for-async": {
|
||||||
|
"version": "11.0.0-beta",
|
||||||
|
"description": "`async` to `waitForAsync` migration. The `async` testing function has been renamed to `waitForAsync` to avoid confusion with the native `async` keyword.",
|
||||||
|
"factory": "./migrations/wait-for-async/index"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ ts_library(
|
||||||
"//packages/core/schematics/migrations/template-var-assignment",
|
"//packages/core/schematics/migrations/template-var-assignment",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3",
|
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3",
|
||||||
|
"//packages/core/schematics/migrations/wait-for-async",
|
||||||
"//packages/core/schematics/utils",
|
"//packages/core/schematics/utils",
|
||||||
"//packages/core/schematics/utils/tslint",
|
"//packages/core/schematics/utils/tslint",
|
||||||
"@npm//tslint",
|
"@npm//tslint",
|
||||||
|
|
|
@ -8,11 +8,12 @@
|
||||||
|
|
||||||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import {getImportSpecifier} from '../../utils/typescript/imports';
|
|
||||||
|
|
||||||
|
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
||||||
|
import {closestNode} from '../../utils/typescript/nodes';
|
||||||
import {getHelper, HelperFunction} from '../renderer-to-renderer2/helpers';
|
import {getHelper, HelperFunction} from '../renderer-to-renderer2/helpers';
|
||||||
import {migrateExpression, replaceImport} from '../renderer-to-renderer2/migration';
|
import {migrateExpression} from '../renderer-to-renderer2/migration';
|
||||||
import {findRendererReferences, getNamedImports} from '../renderer-to-renderer2/util';
|
import {findRendererReferences} from '../renderer-to-renderer2/util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works:
|
* TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works:
|
||||||
|
@ -24,8 +25,9 @@ export class Rule extends Rules.TypedRule {
|
||||||
const printer = ts.createPrinter();
|
const printer = ts.createPrinter();
|
||||||
const failures: RuleFailure[] = [];
|
const failures: RuleFailure[] = [];
|
||||||
const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer');
|
const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer');
|
||||||
const rendererImport =
|
const rendererImport = rendererImportSpecifier ?
|
||||||
rendererImportSpecifier ? getNamedImports(rendererImportSpecifier) : null;
|
closestNode<ts.NamedImports>(rendererImportSpecifier, ts.SyntaxKind.NamedImports) :
|
||||||
|
null;
|
||||||
|
|
||||||
// If there are no imports for the `Renderer`, we can exit early.
|
// If there are no imports for the `Renderer`, we can exit early.
|
||||||
if (!rendererImportSpecifier || !rendererImport) {
|
if (!rendererImportSpecifier || !rendererImport) {
|
||||||
|
@ -36,8 +38,7 @@ export class Rule extends Rules.TypedRule {
|
||||||
findRendererReferences(sourceFile, typeChecker, rendererImportSpecifier);
|
findRendererReferences(sourceFile, typeChecker, rendererImportSpecifier);
|
||||||
const helpersToAdd = new Set<HelperFunction>();
|
const helpersToAdd = new Set<HelperFunction>();
|
||||||
|
|
||||||
failures.push(
|
failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer));
|
||||||
this._getNamedImportsFailure(rendererImport, rendererImportSpecifier, sourceFile, printer));
|
|
||||||
typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile)));
|
typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile)));
|
||||||
forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));
|
forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));
|
||||||
|
|
||||||
|
@ -65,10 +66,9 @@ export class Rule extends Rules.TypedRule {
|
||||||
|
|
||||||
/** Gets a failure for an import of the Renderer. */
|
/** Gets a failure for an import of the Renderer. */
|
||||||
private _getNamedImportsFailure(
|
private _getNamedImportsFailure(
|
||||||
node: ts.NamedImports, importSpecifier: ts.ImportSpecifier, sourceFile: ts.SourceFile,
|
node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure {
|
||||||
printer: ts.Printer): RuleFailure {
|
|
||||||
const replacementText = printer.printNode(
|
const replacementText = printer.printNode(
|
||||||
ts.EmitHint.Unspecified, replaceImport(node, importSpecifier, 'Renderer2'), sourceFile);
|
ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile);
|
||||||
|
|
||||||
return new RuleFailure(
|
return new RuleFailure(
|
||||||
sourceFile, node.getStart(), node.getEnd(),
|
sourceFile, node.getStart(), node.getEnd(),
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* @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 {Replacement, RuleFailure, Rules} from 'tslint';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {findAsyncReferences} from '../../migrations/wait-for-async/util';
|
||||||
|
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
||||||
|
import {closestNode} from '../../utils/typescript/nodes';
|
||||||
|
|
||||||
|
/** Name of the deprecated function that we're removing. */
|
||||||
|
const deprecatedFunction = 'async';
|
||||||
|
|
||||||
|
/** Name of the function that will replace the deprecated one. */
|
||||||
|
const newFunction = 'waitForAsync';
|
||||||
|
|
||||||
|
/** TSLint rule that migrates from `async` to `waitForAsync`. */
|
||||||
|
export class Rule extends Rules.TypedRule {
|
||||||
|
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||||
|
const failures: RuleFailure[] = [];
|
||||||
|
const asyncImportSpecifier =
|
||||||
|
getImportSpecifier(sourceFile, '@angular/core/testing', deprecatedFunction);
|
||||||
|
const asyncImport = asyncImportSpecifier ?
|
||||||
|
closestNode<ts.NamedImports>(asyncImportSpecifier, ts.SyntaxKind.NamedImports) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
// If there are no imports of `async`, we can exit early.
|
||||||
|
if (asyncImportSpecifier && asyncImport) {
|
||||||
|
const typeChecker = program.getTypeChecker();
|
||||||
|
const printer = ts.createPrinter();
|
||||||
|
failures.push(this._getNamedImportsFailure(asyncImport, sourceFile, printer));
|
||||||
|
findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier)
|
||||||
|
.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets a failure for an import of the `async` function. */
|
||||||
|
private _getNamedImportsFailure(
|
||||||
|
node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure {
|
||||||
|
const replacementText = printer.printNode(
|
||||||
|
ts.EmitHint.Unspecified, replaceImport(node, deprecatedFunction, newFunction), sourceFile);
|
||||||
|
|
||||||
|
return new RuleFailure(
|
||||||
|
sourceFile, node.getStart(), node.getEnd(),
|
||||||
|
`Imports of the deprecated ${deprecatedFunction} function are not allowed. Use ${
|
||||||
|
newFunction} instead.`,
|
||||||
|
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets a failure for an identifier node. */
|
||||||
|
private _getIdentifierNodeFailure(node: ts.Identifier, sourceFile: ts.SourceFile): RuleFailure {
|
||||||
|
return new RuleFailure(
|
||||||
|
sourceFile, node.getStart(), node.getEnd(),
|
||||||
|
`References to the deprecated ${deprecatedFunction} function are not allowed. Use ${
|
||||||
|
newFunction} instead.`,
|
||||||
|
this.ruleName, new Replacement(node.getStart(), node.getWidth(), newFunction));
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,11 +12,12 @@ import * as ts from 'typescript';
|
||||||
|
|
||||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||||
import {createMigrationProgram} from '../../utils/typescript/compiler_host';
|
import {createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||||
import {getImportSpecifier} from '../../utils/typescript/imports';
|
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
||||||
|
import {closestNode} from '../../utils/typescript/nodes';
|
||||||
|
|
||||||
import {getHelper, HelperFunction} from './helpers';
|
import {getHelper, HelperFunction} from './helpers';
|
||||||
import {migrateExpression, replaceImport} from './migration';
|
import {migrateExpression} from './migration';
|
||||||
import {findRendererReferences, getNamedImports} from './util';
|
import {findRendererReferences} from './util';
|
||||||
|
|
||||||
const MODULE_AUGMENTATION_FILENAME = 'ɵɵRENDERER_MIGRATION_CORE_AUGMENTATION.d.ts';
|
const MODULE_AUGMENTATION_FILENAME = 'ɵɵRENDERER_MIGRATION_CORE_AUGMENTATION.d.ts';
|
||||||
|
|
||||||
|
@ -63,8 +64,9 @@ function runRendererToRenderer2Migration(tree: Tree, tsconfigPath: string, baseP
|
||||||
|
|
||||||
sourceFiles.forEach(sourceFile => {
|
sourceFiles.forEach(sourceFile => {
|
||||||
const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer');
|
const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer');
|
||||||
const rendererImport =
|
const rendererImport = rendererImportSpecifier ?
|
||||||
rendererImportSpecifier ? getNamedImports(rendererImportSpecifier) : null;
|
closestNode<ts.NamedImports>(rendererImportSpecifier, ts.SyntaxKind.NamedImports) :
|
||||||
|
null;
|
||||||
|
|
||||||
// If there are no imports for the `Renderer`, we can exit early.
|
// If there are no imports for the `Renderer`, we can exit early.
|
||||||
if (!rendererImportSpecifier || !rendererImport) {
|
if (!rendererImportSpecifier || !rendererImport) {
|
||||||
|
@ -81,8 +83,8 @@ function runRendererToRenderer2Migration(tree: Tree, tsconfigPath: string, baseP
|
||||||
update.insertRight(
|
update.insertRight(
|
||||||
rendererImport.getStart(),
|
rendererImport.getStart(),
|
||||||
printer.printNode(
|
printer.printNode(
|
||||||
ts.EmitHint.Unspecified,
|
ts.EmitHint.Unspecified, replaceImport(rendererImport, 'Renderer', 'Renderer2'),
|
||||||
replaceImport(rendererImport, rendererImportSpecifier, 'Renderer2'), sourceFile));
|
sourceFile));
|
||||||
|
|
||||||
// Change the method parameter and property types to `Renderer2`.
|
// Change the method parameter and property types to `Renderer2`.
|
||||||
typedNodes.forEach(node => {
|
typedNodes.forEach(node => {
|
||||||
|
|
|
@ -13,27 +13,6 @@ import {HelperFunction} from './helpers';
|
||||||
/** A call expression that is based on a property access. */
|
/** A call expression that is based on a property access. */
|
||||||
type PropertyAccessCallExpression = ts.CallExpression&{expression: ts.PropertyAccessExpression};
|
type PropertyAccessCallExpression = ts.CallExpression&{expression: ts.PropertyAccessExpression};
|
||||||
|
|
||||||
/** Replaces an import inside an import statement with a different one. */
|
|
||||||
export function replaceImport(
|
|
||||||
node: ts.NamedImports, existingImport: ts.ImportSpecifier, newImportName: string) {
|
|
||||||
const isAlreadyImported = node.elements.find(element => {
|
|
||||||
const {name, propertyName} = element;
|
|
||||||
return propertyName ? propertyName.text === newImportName : name.text === newImportName;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isAlreadyImported) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ts.updateNamedImports(node, [
|
|
||||||
...node.elements.filter(current => current !== existingImport),
|
|
||||||
// Create a new import while trying to preserve the alias of the old one.
|
|
||||||
ts.createImportSpecifier(
|
|
||||||
existingImport.propertyName ? ts.createIdentifier(newImportName) : undefined,
|
|
||||||
existingImport.propertyName ? existingImport.name : ts.createIdentifier(newImportName))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrates a function call expression from `Renderer` to `Renderer2`.
|
* Migrates a function call expression from `Renderer` to `Renderer2`.
|
||||||
* Returns null if the expression should be dropped.
|
* Returns null if the expression should be dropped.
|
||||||
|
|
|
@ -54,20 +54,6 @@ export function findRendererReferences(
|
||||||
return {typedNodes, methodCalls, forwardRefs};
|
return {typedNodes, methodCalls, forwardRefs};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets the closest `NamedImports` to an `ImportSpecifier`. */
|
|
||||||
export function getNamedImports(specifier: ts.ImportSpecifier): ts.NamedImports|null {
|
|
||||||
let current: ts.Node = specifier;
|
|
||||||
|
|
||||||
while (current && !ts.isSourceFile(current)) {
|
|
||||||
if (ts.isNamedImports(current)) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
current = current.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Finds the identifier referring to the `Renderer` inside a `forwardRef` call expression. */
|
/** Finds the identifier referring to the `Renderer` inside a `forwardRef` call expression. */
|
||||||
function findRendererIdentifierInForwardRef(
|
function findRendererIdentifierInForwardRef(
|
||||||
typeChecker: ts.TypeChecker, node: ts.CallExpression,
|
typeChecker: ts.TypeChecker, node: ts.CallExpression,
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "wait-for-async",
|
||||||
|
srcs = glob(["**/*.ts"]),
|
||||||
|
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||||
|
visibility = [
|
||||||
|
"//packages/core/schematics:__pkg__",
|
||||||
|
"//packages/core/schematics/migrations/google3:__pkg__",
|
||||||
|
"//packages/core/schematics/test:__pkg__",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//packages/core/schematics/utils",
|
||||||
|
"@npm//@angular-devkit/schematics",
|
||||||
|
"@npm//@types/node",
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,21 @@
|
||||||
|
## async -> waitForAsync migration
|
||||||
|
|
||||||
|
Automatically migrates from `async` to `waitForAsync` by changing function calls and renaming imports.
|
||||||
|
|
||||||
|
#### Before
|
||||||
|
```ts
|
||||||
|
import { async } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
// async testing logic
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After
|
||||||
|
```ts
|
||||||
|
import { waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', waitForAsync(() => {
|
||||||
|
// async testing logic
|
||||||
|
}));
|
||||||
|
```
|
|
@ -0,0 +1,92 @@
|
||||||
|
/**
|
||||||
|
* @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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||||
|
import {relative} from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||||
|
import {createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||||
|
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
||||||
|
import {closestNode} from '../../utils/typescript/nodes';
|
||||||
|
|
||||||
|
import {findAsyncReferences} from './util';
|
||||||
|
|
||||||
|
const MODULE_AUGMENTATION_FILENAME = 'ɵɵASYNC_MIGRATION_CORE_AUGMENTATION.d.ts';
|
||||||
|
|
||||||
|
/** Migration that switches from `async` to `waitForAsync`. */
|
||||||
|
export default function(): Rule {
|
||||||
|
return (tree: Tree) => {
|
||||||
|
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
|
||||||
|
const basePath = process.cwd();
|
||||||
|
const allPaths = [...buildPaths, ...testPaths];
|
||||||
|
|
||||||
|
if (!allPaths.length) {
|
||||||
|
throw new SchematicsException(
|
||||||
|
'Could not find any tsconfig file. Cannot migrate async usages to waitForAsync.');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tsconfigPath of allPaths) {
|
||||||
|
runWaitForAsyncMigration(tree, tsconfigPath, basePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function runWaitForAsyncMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||||
|
const {program} = createMigrationProgram(tree, tsconfigPath, basePath, fileName => {
|
||||||
|
// In case the module augmentation file has been requested, we return a source file that
|
||||||
|
// augments "@angular/core/testing" to include a named export called "async". This ensures that
|
||||||
|
// we can rely on the type checker for this migration after `async` has been removed.
|
||||||
|
if (fileName === MODULE_AUGMENTATION_FILENAME) {
|
||||||
|
return `
|
||||||
|
import '@angular/core/testing';
|
||||||
|
declare module "@angular/core/testing" {
|
||||||
|
function async(fn: Function): any;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [MODULE_AUGMENTATION_FILENAME]);
|
||||||
|
const typeChecker = program.getTypeChecker();
|
||||||
|
const printer = ts.createPrinter();
|
||||||
|
const sourceFiles = program.getSourceFiles().filter(
|
||||||
|
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
|
||||||
|
const deprecatedFunction = 'async';
|
||||||
|
const newFunction = 'waitForAsync';
|
||||||
|
|
||||||
|
sourceFiles.forEach(sourceFile => {
|
||||||
|
const asyncImportSpecifier =
|
||||||
|
getImportSpecifier(sourceFile, '@angular/core/testing', deprecatedFunction);
|
||||||
|
const asyncImport = asyncImportSpecifier ?
|
||||||
|
closestNode<ts.NamedImports>(asyncImportSpecifier, ts.SyntaxKind.NamedImports) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
// If there are no imports for `async`, we can exit early.
|
||||||
|
if (!asyncImportSpecifier || !asyncImport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||||
|
|
||||||
|
// Change the `async` import to `waitForAsync`.
|
||||||
|
update.remove(asyncImport.getStart(), asyncImport.getWidth());
|
||||||
|
update.insertRight(
|
||||||
|
asyncImport.getStart(),
|
||||||
|
printer.printNode(
|
||||||
|
ts.EmitHint.Unspecified, replaceImport(asyncImport, deprecatedFunction, newFunction),
|
||||||
|
sourceFile));
|
||||||
|
|
||||||
|
// Change `async` calls to `waitForAsync`.
|
||||||
|
findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier).forEach(node => {
|
||||||
|
update.remove(node.getStart(), node.getWidth());
|
||||||
|
update.insertRight(node.getStart(), newFunction);
|
||||||
|
});
|
||||||
|
|
||||||
|
tree.commitUpdate(update);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
|
||||||
|
import {isReferenceToImport} from '../../utils/typescript/symbol';
|
||||||
|
|
||||||
|
/** Finds calls to the `async` function. */
|
||||||
|
export function findAsyncReferences(
|
||||||
|
sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker,
|
||||||
|
asyncImportSpecifier: ts.ImportSpecifier) {
|
||||||
|
const results = new Set<ts.Identifier>();
|
||||||
|
|
||||||
|
ts.forEachChild(sourceFile, function visitNode(node: ts.Node) {
|
||||||
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
|
||||||
|
node.expression.text === 'async' &&
|
||||||
|
isReferenceToImport(typeChecker, node.expression, asyncImportSpecifier)) {
|
||||||
|
results.add(node.expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.forEachChild(node, visitNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ ts_library(
|
||||||
"//packages/core/schematics/migrations/template-var-assignment",
|
"//packages/core/schematics/migrations/template-var-assignment",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
||||||
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
||||||
|
"//packages/core/schematics/migrations/wait-for-async",
|
||||||
"//packages/core/schematics/utils",
|
"//packages/core/schematics/utils",
|
||||||
"@npm//@angular-devkit/core",
|
"@npm//@angular-devkit/core",
|
||||||
"@npm//@angular-devkit/schematics",
|
"@npm//@angular-devkit/schematics",
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
/**
|
||||||
|
* @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 {readFileSync, writeFileSync} from 'fs';
|
||||||
|
import {dirname, join} from 'path';
|
||||||
|
import * as shx from 'shelljs';
|
||||||
|
import {Configuration, Linter} from 'tslint';
|
||||||
|
|
||||||
|
describe('Google3 waitForAsync TSLint rule', () => {
|
||||||
|
const rulesDirectory = dirname(require.resolve('../../migrations/google3/waitForAsyncRule'));
|
||||||
|
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test');
|
||||||
|
shx.mkdir('-p', tmpDir);
|
||||||
|
|
||||||
|
// We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
|
||||||
|
writeFile('testing.d.ts', `
|
||||||
|
export declare function async(fn: Function): any;
|
||||||
|
`);
|
||||||
|
|
||||||
|
writeFile('tsconfig.json', JSON.stringify({
|
||||||
|
compilerOptions: {
|
||||||
|
module: 'es2015',
|
||||||
|
baseUrl: './',
|
||||||
|
paths: {
|
||||||
|
'@angular/core/testing': ['testing.d.ts'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => shx.rm('-r', tmpDir));
|
||||||
|
|
||||||
|
function runTSLint(fix: boolean) {
|
||||||
|
const program = Linter.createProgram(join(tmpDir, 'tsconfig.json'));
|
||||||
|
const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program);
|
||||||
|
const config = Configuration.parseConfigFile({rules: {'wait-for-async': true}});
|
||||||
|
|
||||||
|
program.getRootFileNames().forEach(fileName => {
|
||||||
|
linter.lint(fileName, program.getSourceFile(fileName)!.getFullText(), config);
|
||||||
|
});
|
||||||
|
|
||||||
|
return linter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(fileName: string, content: string) {
|
||||||
|
writeFileSync(join(tmpDir, fileName), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFile(fileName: string) {
|
||||||
|
return readFileSync(join(tmpDir, fileName), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should flag async imports and usages', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should also work', async(() => {
|
||||||
|
expect(inject('bar')).toBe('bar');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
const linter = runTSLint(false);
|
||||||
|
const failures = linter.getResult().failures.map(failure => failure.getFailure());
|
||||||
|
expect(failures.length).toBe(3);
|
||||||
|
expect(failures[0]).toMatch(/Imports of the deprecated async function are not allowed/);
|
||||||
|
expect(failures[1]).toMatch(/References to the deprecated async function are not allowed/);
|
||||||
|
expect(failures[2]).toMatch(/References to the deprecated async function are not allowed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change async imports to waitForAsync', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
expect(getFile('/index.ts'))
|
||||||
|
.toContain(`import { inject, waitForAsync } from '@angular/core/testing';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change aliased async imports to waitForAsync', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async as renamedAsync, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', renamedAsync(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
expect(getFile('/index.ts'))
|
||||||
|
.toContain(`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change async imports if they are not from @angular/core/testing', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { inject } from '@angular/core/testing';
|
||||||
|
import { async } from './my-test-library';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
const content = getFile('/index.ts');
|
||||||
|
expect(content).toContain(`import { inject } from '@angular/core/testing';`);
|
||||||
|
expect(content).toContain(`import { async } from './my-test-library';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change imports if waitForAsync was already imported', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async, inject, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should also work', waitForAsync(() => {
|
||||||
|
expect(inject('bar')).toBe('bar');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
expect(getFile('/index.ts'))
|
||||||
|
.toContain(`import { async, inject, waitForAsync } from '@angular/core/testing';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change calls from `async` to `waitForAsync`', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should also work', async(() => {
|
||||||
|
expect(inject('bar')).toBe('bar');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
|
||||||
|
const content = getFile('/index.ts');
|
||||||
|
expect(content).toContain(`import { inject, waitForAsync } from '@angular/core/testing';`);
|
||||||
|
expect(content).toContain(`it('should work', waitForAsync(() => {`);
|
||||||
|
expect(content).toContain(`it('should also work', waitForAsync(() => {`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change aliased calls', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async as renamedAsync, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', renamedAsync(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
runTSLint(true);
|
||||||
|
|
||||||
|
const content = getFile('/index.ts');
|
||||||
|
expect(content).toContain(
|
||||||
|
`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`);
|
||||||
|
expect(content).toContain(`it('should work', renamedAsync(() => {`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,161 @@
|
||||||
|
/**
|
||||||
|
* @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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
|
||||||
|
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
|
||||||
|
import {HostTree} from '@angular-devkit/schematics';
|
||||||
|
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
|
||||||
|
import * as shx from 'shelljs';
|
||||||
|
|
||||||
|
describe('waitForAsync migration', () => {
|
||||||
|
let runner: SchematicTestRunner;
|
||||||
|
let host: TempScopedNodeJsSyncHost;
|
||||||
|
let tree: UnitTestTree;
|
||||||
|
let tmpDirPath: string;
|
||||||
|
let previousWorkingDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
|
||||||
|
host = new TempScopedNodeJsSyncHost();
|
||||||
|
tree = new UnitTestTree(new HostTree(host));
|
||||||
|
|
||||||
|
writeFile('/tsconfig.json', JSON.stringify({
|
||||||
|
compilerOptions: {
|
||||||
|
lib: ['es2015'],
|
||||||
|
strictNullChecks: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
writeFile('/angular.json', JSON.stringify({
|
||||||
|
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
|
||||||
|
}));
|
||||||
|
// We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
|
||||||
|
writeFile('/node_modules/@angular/core/testing/index.d.ts', `
|
||||||
|
export declare function async(fn: Function): any;
|
||||||
|
`);
|
||||||
|
|
||||||
|
previousWorkingDir = shx.pwd();
|
||||||
|
tmpDirPath = getSystemPath(host.root);
|
||||||
|
|
||||||
|
// Switch into the temporary directory path. This allows us to run
|
||||||
|
// the schematic against our custom unit test tree.
|
||||||
|
shx.cd(tmpDirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
shx.cd(previousWorkingDir);
|
||||||
|
shx.rm('-r', tmpDirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change async imports to waitForAsync', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`import { inject, waitForAsync } from '@angular/core/testing';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change aliased async imports to waitForAsync', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async as renamedAsync, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', renamedAsync(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change async imports if they are not from @angular/core/testing', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { inject } from '@angular/core/testing';
|
||||||
|
import { async } from './my-test-library';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
expect(content).toContain(`import { inject } from '@angular/core/testing';`);
|
||||||
|
expect(content).toContain(`import { async } from './my-test-library';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change imports if waitForAsync was already imported', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async, inject, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should also work', waitForAsync(() => {
|
||||||
|
expect(inject('bar')).toBe('bar');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`import { async, inject, waitForAsync } from '@angular/core/testing';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change calls from `async` to `waitForAsync`', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', async(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should also work', async(() => {
|
||||||
|
expect(inject('bar')).toBe('bar');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
expect(content).toContain(`import { inject, waitForAsync } from '@angular/core/testing';`);
|
||||||
|
expect(content).toContain(`it('should work', waitForAsync(() => {`);
|
||||||
|
expect(content).toContain(`it('should also work', waitForAsync(() => {`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change aliased calls', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import { async as renamedAsync, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
it('should work', renamedAsync(() => {
|
||||||
|
expect(inject('foo')).toBe('foo');
|
||||||
|
}));
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
expect(content).toContain(
|
||||||
|
`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`);
|
||||||
|
expect(content).toContain(`it('should work', renamedAsync(() => {`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeFile(filePath: string, contents: string) {
|
||||||
|
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMigration() {
|
||||||
|
return runner.runSchematicAsync('migration-v11-wait-for-async', {}, tree).toPromise();
|
||||||
|
}
|
||||||
|
});
|
|
@ -4,7 +4,7 @@
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": ["es2015"],
|
"lib": ["es2015"],
|
||||||
"types": [],
|
"types": ["jasmine"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@angular/core": ["../"],
|
"@angular/core": ["../"],
|
||||||
|
|
|
@ -68,11 +68,7 @@ export function getImportSpecifier(
|
||||||
node.moduleSpecifier.text === moduleName) {
|
node.moduleSpecifier.text === moduleName) {
|
||||||
const namedBindings = node.importClause && node.importClause.namedBindings;
|
const namedBindings = node.importClause && node.importClause.namedBindings;
|
||||||
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
||||||
const match = namedBindings.elements.find(element => {
|
const match = findImportSpecifier(namedBindings.elements, specifierName);
|
||||||
const {name, propertyName} = element;
|
|
||||||
return propertyName ? propertyName.text === specifierName : name.text === specifierName;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
@ -82,3 +78,42 @@ export function getImportSpecifier(
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces an import inside a named imports node with a different one.
|
||||||
|
* @param node Node that contains the imports.
|
||||||
|
* @param existingImport Import that should be replaced.
|
||||||
|
* @param newImportName Import that should be inserted.
|
||||||
|
*/
|
||||||
|
export function replaceImport(
|
||||||
|
node: ts.NamedImports, existingImport: string, newImportName: string) {
|
||||||
|
const isAlreadyImported = findImportSpecifier(node.elements, newImportName);
|
||||||
|
if (isAlreadyImported) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingImportNode = findImportSpecifier(node.elements, existingImport);
|
||||||
|
if (!existingImportNode) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.updateNamedImports(node, [
|
||||||
|
...node.elements.filter(current => current !== existingImportNode),
|
||||||
|
// Create a new import while trying to preserve the alias of the old one.
|
||||||
|
ts.createImportSpecifier(
|
||||||
|
existingImportNode.propertyName ? ts.createIdentifier(newImportName) : undefined,
|
||||||
|
existingImportNode.propertyName ? existingImportNode.name :
|
||||||
|
ts.createIdentifier(newImportName))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Finds an import specifier with a particular name. */
|
||||||
|
function findImportSpecifier(
|
||||||
|
nodes: ts.NodeArray<ts.ImportSpecifier>, specifierName: string): ts.ImportSpecifier|undefined {
|
||||||
|
return nodes.find(element => {
|
||||||
|
const {name, propertyName} = element;
|
||||||
|
return propertyName ? propertyName.text === specifierName : name.text === specifierName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -12,3 +12,17 @@ import * as ts from 'typescript';
|
||||||
export function hasModifier(node: ts.Node, modifierKind: ts.SyntaxKind) {
|
export function hasModifier(node: ts.Node, modifierKind: ts.SyntaxKind) {
|
||||||
return !!node.modifiers && node.modifiers.some(m => m.kind === modifierKind);
|
return !!node.modifiers && node.modifiers.some(m => m.kind === modifierKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Find the closest parent node of a particular kind. */
|
||||||
|
export function closestNode<T extends ts.Node>(node: ts.Node, kind: ts.SyntaxKind): T|null {
|
||||||
|
let current: ts.Node = node;
|
||||||
|
|
||||||
|
while (current && !ts.isSourceFile(current)) {
|
||||||
|
if (current.kind === kind) {
|
||||||
|
return current as T;
|
||||||
|
}
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue