mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(migrations): remove no longer accessible migrations (#43947)
With this change we remove no longer needed migrations. These migrations are no longer accessible following a change angular/angular-cli#21986 in the Angular CLI. The CLI now shows an error message when users try to update `@angular/` and `@nguniversal/` packages across multiple major versions. Example to update from version 11 to version 13 the recommand approach is to run the following commands ``` ng update @angular/core@12 ng update @angular/core@13 (which is the same as ng update @angular/core) ``` NB: It is recommand that when update from one major to another, users verify that their applications is working as expected. PR Close #43947
This commit is contained in:
parent
e2f9dcff68
commit
57cbb4f2e8
166 changed files with 23 additions and 21626 deletions
|
|
@ -13,27 +13,7 @@ pkg_npm(
|
|||
],
|
||||
visibility = ["//packages/core:__pkg__"],
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/abstract-control-parent",
|
||||
"//packages/core/schematics/migrations/activated-route-snapshot-fragment",
|
||||
"//packages/core/schematics/migrations/can-activate-with-redirect-to",
|
||||
"//packages/core/schematics/migrations/deep-shadow-piercing-selector",
|
||||
"//packages/core/schematics/migrations/dynamic-queries",
|
||||
"//packages/core/schematics/migrations/initial-navigation",
|
||||
"//packages/core/schematics/migrations/missing-injectable",
|
||||
"//packages/core/schematics/migrations/module-with-providers",
|
||||
"//packages/core/schematics/migrations/move-document",
|
||||
"//packages/core/schematics/migrations/native-view-encapsulation",
|
||||
"//packages/core/schematics/migrations/navigation-extras-omissions",
|
||||
"//packages/core/schematics/migrations/relative-link-resolution",
|
||||
"//packages/core/schematics/migrations/renderer-to-renderer2",
|
||||
"//packages/core/schematics/migrations/router-link-empty-expression",
|
||||
"//packages/core/schematics/migrations/router-preserve-query-params",
|
||||
"//packages/core/schematics/migrations/static-queries",
|
||||
"//packages/core/schematics/migrations/template-var-assignment",
|
||||
"//packages/core/schematics/migrations/testbed-teardown",
|
||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
||||
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
||||
"//packages/core/schematics/migrations/wait-for-async",
|
||||
"//packages/core/schematics/migrations/xhr-factory",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,105 +1,5 @@
|
|||
{
|
||||
"schematics": {
|
||||
"migration-v8-move-document": {
|
||||
"version": "8.0.0-beta",
|
||||
"description": "@DOCUMENT migration. Migrates DOCUMENT Injection token from platform-browser imports to common import.",
|
||||
"factory": "./migrations/move-document/index"
|
||||
},
|
||||
"migration-v8-static-queries": {
|
||||
"version": "8.0.0-beta",
|
||||
"description": "ViewChild and ContentChild to explicit query timing migration. In Angular version 8, developers need to explicitly specify the timing of ViewChild and ContentChild queries. Read more about this here: https://v8.angular.io/guide/static-query-migration",
|
||||
"factory": "./migrations/static-queries/index"
|
||||
},
|
||||
"migration-v8-template-local-variables": {
|
||||
"version": "8.0.0-beta",
|
||||
"description": "Template Variable Assignment migration. Warns developers if values are assigned to template variables.",
|
||||
"factory": "./migrations/template-var-assignment/index"
|
||||
},
|
||||
"migration-v9-renderer-to-renderer2": {
|
||||
"version": "9.0.0-beta",
|
||||
"description": "Renderer to Renderer2 migration. As of Angular 9, the Renderer class is no longer available. Renderer2 should be used instead. Read more about this here: https://v9.angular.io/guide/migration-renderer",
|
||||
"factory": "./migrations/renderer-to-renderer2/index"
|
||||
},
|
||||
"migration-v9-undecorated-classes-with-di": {
|
||||
"version": "9.0.0-beta",
|
||||
"description": "Undecorated classes with DI migration. As of Angular 9, it is no longer supported to use Angular DI on a class that does not have an Angular decorator. Read more about this here: https://v9.angular.io/guide/migration-undecorated-classes",
|
||||
"factory": "./migrations/undecorated-classes-with-di/index"
|
||||
},
|
||||
"migration-v9-dynamic-queries": {
|
||||
"version": "9-beta",
|
||||
"description": "Static flag migration. Removes the `static` flag from dynamic queries. As of Angular 9, the \"static\" flag defaults to false and is no longer required for your view and content queries. Read more about this here: https://v9.angular.io/guide/migration-dynamic-flag",
|
||||
"factory": "./migrations/dynamic-queries/index"
|
||||
},
|
||||
"migration-v10-missing-injectable": {
|
||||
"version": "10.0.0-beta",
|
||||
"description": "Missing @Injectable and incomplete provider definition migration. As of Angular 9, enforcement of @Injectable decorators for DI is a bit stricter and incomplete provider definitions behave differently. Read more about this here: https://v9.angular.io/guide/migration-injectable",
|
||||
"factory": "./migrations/missing-injectable/index"
|
||||
},
|
||||
"migration-v10-module-with-providers": {
|
||||
"version": "10.0.0-beta",
|
||||
"description": "ModuleWithProviders migration. As of Angular 10, the ModuleWithProviders type requires a generic. This migration adds the generic where it is missing. Read more about this here: https://v10.angular.io/guide/migration-module-with-providers",
|
||||
"factory": "./migrations/module-with-providers/index"
|
||||
},
|
||||
"migration-v10-undecorated-classes-with-decorated-fields": {
|
||||
"version": "10.0.0-beta",
|
||||
"description": "Undecorated classes with Angular features migration. In version 10, classes that use Angular features and do not have an Angular decorator are no longer supported. Read more about this here: https://v10.angular.io/guide/migration-undecorated-classes",
|
||||
"factory": "./migrations/undecorated-classes-with-decorated-fields/index"
|
||||
},
|
||||
"migration-v11-navigation-extras-omissions": {
|
||||
"version": "11.0.0-beta",
|
||||
"description": "NavigationExtras omissions migration. In version 11, some unsupported properties were omitted from the `extras` parameter of the `Router.navigateByUrl` and `Router.createUrlTree` methods.",
|
||||
"factory": "./migrations/navigation-extras-omissions/index"
|
||||
},
|
||||
"migration-v11-router-relative-link-resolution-default": {
|
||||
"version": "11.0.0-beta",
|
||||
"description": "The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'.\nThis migration updates `RouterModule` configurations that use the default value to \nnow specifically use 'legacy' to prevent breakages when updating.",
|
||||
"factory": "./migrations/relative-link-resolution/index"
|
||||
},
|
||||
"migration-v11-abstract-control-parent": {
|
||||
"version": "11.0.0-beta",
|
||||
"description": "In Angular version 11, the type of `AbstractControl.parent` can be `null` to reflect the runtime value more accurately. This migration automatically adds non-null assertions to existing accesses of the `parent` property on types like `FormControl`, `FormArray` and `FormGroup`.",
|
||||
"factory": "./migrations/abstract-control-parent/index"
|
||||
},
|
||||
"migration-v11-native-view-encapsulation": {
|
||||
"version": "11.0.0-beta",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"migration-v11-router-preserve-query-params": {
|
||||
"version": "11.0.0-beta",
|
||||
"description": "NavigationExtras.preserveQueryParams has been removed as of Angular version 11. This migration replaces any usages with the appropriate assignment of the queryParamsHandling key.",
|
||||
"factory": "./migrations/router-preserve-query-params/index"
|
||||
},
|
||||
"migration-v11-router-initial-navigation-options": {
|
||||
"version": "11.0.0-beta",
|
||||
"description": "Updates the `initialNavigation` property for `RouterModule.forRoot`.",
|
||||
"factory": "./migrations/initial-navigation/index"
|
||||
},
|
||||
"migration-v11.1-can-activate-with-redirect-to": {
|
||||
"version": "11.1.0-beta",
|
||||
"description": "Removes `canActivate` from a `Route` config when `redirectTo` is also present",
|
||||
"factory": "./migrations/can-activate-with-redirect-to/index"
|
||||
},
|
||||
"migration-v12-activated-route-snapshot-fragment": {
|
||||
"version": "12.0.0-beta",
|
||||
"description": "In Angular version 12, the type of ActivatedRouteSnapshot.fragment is nullable. This migration automatically adds non-null assertions to it.",
|
||||
"factory": "./migrations/activated-route-snapshot-fragment/index"
|
||||
},
|
||||
"migration-v12-xhr-factory": {
|
||||
"version": "12.0.0-next.6",
|
||||
"description": "`XhrFactory` has been moved from `@angular/common/http` to `@angular/common`.",
|
||||
"factory": "./migrations/xhr-factory/index"
|
||||
},
|
||||
"migration-v12-deep-shadow-piercing-selector": {
|
||||
"version": "12.0.2",
|
||||
"description": "Automatically migrates shadow-piercing selector from `/deep/` to the recommended alternative `::ng-deep`.",
|
||||
"factory": "./migrations/deep-shadow-piercing-selector/index"
|
||||
},
|
||||
"migration-v13-router-link-empty-expression": {
|
||||
"version": "13.0.0-beta",
|
||||
"description": "Migrates `[routerLink]=\"\"` in templates to `[routerLink]=\"[]\"` because these links are likely intended to route to the current page with updated fragment/query params.",
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "abstract-control-parent",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
## `AbstractControl.parent` migration
|
||||
|
||||
As of Angular v11, the type of `AbstractControl.parent` can be null. This migration automatically
|
||||
identifies usages and adds non-null assertions.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
@Component()
|
||||
export class MyComponent {
|
||||
private _control = new FormControl();
|
||||
|
||||
getParentValue() {
|
||||
return this._control.parent.value; // <- Compilation error in v11.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
@Component()
|
||||
export class MyComponent {
|
||||
private _control = new FormControl();
|
||||
|
||||
getParentValue() {
|
||||
return this._control.parent!.value; // <- Non-null assertion added during the migration.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,57 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {findParentAccesses} from './util';
|
||||
|
||||
|
||||
/** Migration that marks accesses of `AbstractControl.parent` as non-null. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate AbstractControl.parent accesses.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runNativeAbstractControlParentMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runNativeAbstractControlParentMigration(
|
||||
tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
// We sort the nodes based on their position in the file and we offset the positions by one
|
||||
// for each non-null assertion that we've added. We have to do it this way, rather than
|
||||
// creating and printing a new AST node like in other migrations, because property access
|
||||
// expressions can be nested (e.g. `control.parent.parent.value`), but the node positions
|
||||
// aren't being updated as we're inserting new code. If we were to go through the AST,
|
||||
// we'd have to update the `SourceFile` and start over after each operation.
|
||||
findParentAccesses(typeChecker, sourceFile)
|
||||
.sort((a, b) => a.getStart() - b.getStart())
|
||||
.forEach((node, index) => {
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
update.insertRight(node.getStart() + node.getWidth() + index, '!');
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,57 +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 {normalize} from 'path';
|
||||
import ts from 'typescript';
|
||||
import {isNullCheck, isSafeAccess} from '../../utils/typescript/nodes';
|
||||
import {hasOneOfTypes, isNullableType} from '../../utils/typescript/symbol';
|
||||
|
||||
/** Names of symbols from `@angular/forms` whose `parent` accesses have to be migrated. */
|
||||
const abstractControlSymbols = ['AbstractControl', 'FormArray', 'FormControl', 'FormGroup'];
|
||||
|
||||
/**
|
||||
* Finds the `PropertyAccessExpression`-s that are accessing the `parent` property in
|
||||
* such a way that may result in a compilation error after the v11 type changes.
|
||||
*/
|
||||
export function findParentAccesses(
|
||||
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): ts.PropertyAccessExpression[] {
|
||||
const results: ts.PropertyAccessExpression[] = [];
|
||||
|
||||
sourceFile.forEachChild(function walk(node: ts.Node) {
|
||||
if (ts.isPropertyAccessExpression(node) && node.name.text === 'parent' && !isNullCheck(node) &&
|
||||
!isSafeAccess(node) && results.indexOf(node) === -1 &&
|
||||
isAbstractControlReference(typeChecker, node) && isNullableType(typeChecker, node)) {
|
||||
results.unshift(node);
|
||||
}
|
||||
|
||||
node.forEachChild(walk);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Checks whether a property access is on an `AbstractControl` coming from `@angular/forms`. */
|
||||
function isAbstractControlReference(
|
||||
typeChecker: ts.TypeChecker, node: ts.PropertyAccessExpression): boolean {
|
||||
let current: ts.Expression = node;
|
||||
const formsPattern = /node_modules\/?.*\/@angular\/forms/;
|
||||
// Walks up the property access chain and tries to find a symbol tied to a `SourceFile`.
|
||||
// If such a node is found, we check whether the type is one of the `AbstractControl` symbols
|
||||
// and whether it comes from the `@angular/forms` directory in the `node_modules`.
|
||||
while (ts.isPropertyAccessExpression(current)) {
|
||||
const symbol = typeChecker.getTypeAtLocation(current.expression)?.getSymbol();
|
||||
if (symbol) {
|
||||
const sourceFile = symbol.valueDeclaration?.getSourceFile();
|
||||
return sourceFile != null &&
|
||||
formsPattern.test(normalize(sourceFile.fileName).replace(/\\/g, '/')) &&
|
||||
hasOneOfTypes(typeChecker, current.expression, abstractControlSymbols);
|
||||
}
|
||||
current = current.expression;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "activated-route-snapshot-fragment",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
## `ActivatedRouteSnapshot.fragment` migration
|
||||
|
||||
The value if `ActivatedRouteSnapshot.fragment` is becoming nullable. This migration adds non-null
|
||||
assertions to it.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||
|
||||
@Component({})
|
||||
export class YourComponent {
|
||||
private _activatedRouteSnapshot: ActivatedRouteSnapshot;
|
||||
|
||||
getFragmentValue() {
|
||||
return this._activatedRouteSnapshot.fragment.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({})
|
||||
export class YourComponent {
|
||||
private _activatedRouteSnapshot: ActivatedRouteSnapshot;
|
||||
|
||||
getFragmentValue() {
|
||||
return this._activatedRouteSnapshot.fragment!.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,62 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {findFragmentAccesses, migrateActivatedRouteSnapshotFragment} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* Migration that marks accesses of `ActivatedRouteSnapshot.fragment` as non-null.
|
||||
*/
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate ' +
|
||||
'`ActivatedRouteSnapshot.fragment` accesses.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runActivatedRouteSnapshotFragmentMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runActivatedRouteSnapshotFragmentMigration(
|
||||
tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
const printer = ts.createPrinter();
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const nodesToMigrate = findFragmentAccesses(typeChecker, sourceFile);
|
||||
|
||||
if (nodesToMigrate.size > 0) {
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
nodesToMigrate.forEach(node => {
|
||||
update.remove(node.getStart(), node.getWidth());
|
||||
update.insertRight(
|
||||
node.getStart(),
|
||||
printer.printNode(
|
||||
ts.EmitHint.Unspecified, migrateActivatedRouteSnapshotFragment(node), sourceFile));
|
||||
});
|
||||
tree.commitUpdate(update);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,39 +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 ts from 'typescript';
|
||||
import {isNullCheck, isSafeAccess} from '../../utils/typescript/nodes';
|
||||
import {hasOneOfTypes, isNullableType} from '../../utils/typescript/symbol';
|
||||
|
||||
/**
|
||||
* Finds all the accesses of `ActivatedRouteSnapshot.fragment`
|
||||
* that need to be migrated within a particular file.
|
||||
*/
|
||||
export function findFragmentAccesses(
|
||||
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): Set<ts.PropertyAccessExpression> {
|
||||
const results = new Set<ts.PropertyAccessExpression>();
|
||||
|
||||
sourceFile.forEachChild(function walk(node: ts.Node) {
|
||||
if (ts.isPropertyAccessExpression(node) && node.name.text === 'fragment' &&
|
||||
!results.has(node) && !isNullCheck(node) && !isSafeAccess(node) &&
|
||||
hasOneOfTypes(typeChecker, node.expression, ['ActivatedRouteSnapshot']) &&
|
||||
isNullableType(typeChecker, node)) {
|
||||
results.add(node);
|
||||
}
|
||||
|
||||
node.forEachChild(walk);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Migrates an `ActivatedRouteSnapshot.fragment` access. */
|
||||
export function migrateActivatedRouteSnapshotFragment(node: ts.PropertyAccessExpression): ts.Node {
|
||||
// Turns `foo.fragment` into `foo.fragment!`.
|
||||
return ts.createNonNullExpression(node);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "can-activate-with-redirect-to",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
## Router migration to remove canActivate property from Routes that also have redirectTo
|
||||
|
||||
The activation stage of the router happens after redirects so any `canActivate` guards
|
||||
will not be executed. This invalid configuration is now an error. This migration
|
||||
removes `canActivate` from the `Route` to fix pre-existing invalid configurations.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', redirectTo: 'other', canActivate: [MyGuard]}
|
||||
];
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', redirectTo: 'other'}
|
||||
];
|
||||
```
|
||||
|
|
@ -1,56 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {findLiteralsToMigrate, migrateLiteral} from './util';
|
||||
|
||||
|
||||
/** Migration that removes `canActivate` property from routes that also have `redirectTo`. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate ' +
|
||||
'Router.navigateByUrl and Router.createUrlTree calls.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runCanActivateWithRedirectToMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runCanActivateWithRedirectToMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const printer = ts.createPrinter();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const literalsToMigrate = findLiteralsToMigrate(sourceFile);
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
|
||||
for (const literal of Array.from(literalsToMigrate)) {
|
||||
const migratedNode = migrateLiteral(literal);
|
||||
update.remove(literal.getStart(), literal.getWidth());
|
||||
update.insertRight(
|
||||
literal.getStart(), printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile));
|
||||
}
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,62 +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 ts from 'typescript';
|
||||
|
||||
const CAN_ACTIVATE = 'canActivate';
|
||||
const REDIRECT_TO = 'redirectTo';
|
||||
|
||||
export function migrateLiteral(node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression {
|
||||
const propertiesToKeep: ts.ObjectLiteralElementLike[] = [];
|
||||
node.properties.forEach(property => {
|
||||
// Only look for regular and shorthand property assignments since resolving things
|
||||
// like spread operators becomes too complicated for this migration.
|
||||
if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
||||
(ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) ||
|
||||
ts.isIdentifier(property.name))) {
|
||||
if (property.name.text !== CAN_ACTIVATE) {
|
||||
propertiesToKeep.push(property);
|
||||
}
|
||||
} else {
|
||||
propertiesToKeep.push(property);
|
||||
}
|
||||
});
|
||||
|
||||
return ts.createObjectLiteral(propertiesToKeep);
|
||||
}
|
||||
|
||||
|
||||
export function findLiteralsToMigrate(sourceFile: ts.SourceFile) {
|
||||
const results = new Set<ts.ObjectLiteralExpression>();
|
||||
|
||||
sourceFile.forEachChild(function visitNode(node: ts.Node) {
|
||||
if (!ts.isObjectLiteralExpression(node)) {
|
||||
node.forEachChild(visitNode);
|
||||
return;
|
||||
}
|
||||
if (hasProperty(node, REDIRECT_TO) && hasProperty(node, CAN_ACTIVATE)) {
|
||||
results.add(node);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function hasProperty(node: ts.ObjectLiteralExpression, propertyName: string): boolean {
|
||||
for (const property of node.properties) {
|
||||
// Only look for regular and shorthand property assignments since resolving things
|
||||
// like spread operators becomes too complicated for this migration.
|
||||
if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
||||
(ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) ||
|
||||
ts.isIdentifier(property.name)) &&
|
||||
property.name.text === propertyName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "deep-shadow-piercing-selector",
|
||||
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 = [
|
||||
"@npm//@angular-devkit/core",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
## shadow-piercing selector `/deep/` to `::ng-deep`
|
||||
|
||||
Automatically migrates shadow-piercing selector from `/deep/` to `::ng-deep`.
|
||||
|
||||
#### Before
|
||||
```css
|
||||
:host /deep/ * {
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```css
|
||||
:host ::ng-deep * {
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
|
@ -1,41 +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 {extname, join} from '@angular-devkit/core';
|
||||
import {DirEntry, Rule} from '@angular-devkit/schematics';
|
||||
|
||||
const VALID_EXTENSIONS = ['.scss', '.sass', '.css', '.styl', '.less', '.ts'];
|
||||
|
||||
function* visitFiles(directory: DirEntry): IterableIterator<string> {
|
||||
for (const path of directory.subfiles) {
|
||||
const extension = extname(path);
|
||||
if (VALID_EXTENSIONS.includes(extension)) {
|
||||
yield join(directory.path, path);
|
||||
}
|
||||
}
|
||||
|
||||
for (const path of directory.subdirs) {
|
||||
if (path === 'node_modules' || path.startsWith('.') || path === 'dist') {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield* visitFiles(directory.dir(path));
|
||||
}
|
||||
}
|
||||
|
||||
export default function(): Rule {
|
||||
return (tree) => {
|
||||
// Visit all files in an Angular workspace monorepo.
|
||||
for (const file of visitFiles(tree.root)) {
|
||||
const content = tree.read(file)?.toString();
|
||||
if (content?.includes('/deep/ ')) {
|
||||
tree.overwrite(file, content.replace(/\/deep\/ /g, '::ng-deep '));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "dynamic-queries",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
## Dynamic queries migration
|
||||
|
||||
Automatically migrates dynamic queries to remove their `static` flag. This flag will no
|
||||
longer be necessary in version 9 for dynamic queries, as `false` is the default value.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class MyDirective {
|
||||
@ViewChild('child', { static: false }) child: any;
|
||||
@ViewChild('secondChild', { read: ElementRef, static: false }) secondChild: ElementRef;
|
||||
@ContentChild('thirdChild', { static: false }) thirdChild: any;
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class MyDirective {
|
||||
@ViewChild('child') child: any;
|
||||
@ViewChild('secondChild', { read: ElementRef }) secondChild: ElementRef;
|
||||
@ContentChild('thirdChild') thirdChild: any;
|
||||
}
|
||||
```
|
||||
|
|
@ -1,69 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
|
||||
import {identifyDynamicQueryNodes, removeOptionsParameter, removeStaticFlag} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* Runs the dynamic queries migration for all TypeScript projects in the current CLI workspace.
|
||||
*/
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate dynamic queries.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runDynamicQueryMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runDynamicQueryMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
const printer = ts.createPrinter();
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const result = identifyDynamicQueryNodes(typeChecker, sourceFile);
|
||||
|
||||
if (result.removeProperty.length || result.removeParameter.length) {
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
|
||||
result.removeProperty.forEach(node => {
|
||||
update.remove(node.getStart(), node.getWidth());
|
||||
update.insertRight(
|
||||
node.getStart(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, removeStaticFlag(node), sourceFile));
|
||||
});
|
||||
|
||||
result.removeParameter.forEach(node => {
|
||||
update.remove(node.getStart(), node.getWidth());
|
||||
update.insertRight(
|
||||
node.getStart(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, removeOptionsParameter(node), sourceFile));
|
||||
});
|
||||
|
||||
tree.commitUpdate(update);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,77 +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 ts from 'typescript';
|
||||
import {getAngularDecorators} from '../../utils/ng_decorators';
|
||||
|
||||
/**
|
||||
* Identifies the nodes that should be migrated by the dynamic
|
||||
* queries schematic. Splits the nodes into the following categories:
|
||||
* - `removeProperty` - queries from which we should only remove the `static` property of the
|
||||
* `options` parameter (e.g. `@ViewChild('child', {static: false, read: ElementRef})`).
|
||||
* - `removeParameter` - queries from which we should drop the entire `options` parameter.
|
||||
* (e.g. `@ViewChild('child', {static: false})`).
|
||||
*/
|
||||
export function identifyDynamicQueryNodes(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) {
|
||||
const removeProperty: ts.ObjectLiteralExpression[] = [];
|
||||
const removeParameter: ts.CallExpression[] = [];
|
||||
|
||||
sourceFile.forEachChild(function walk(node: ts.Node) {
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
node.members.forEach(member => {
|
||||
const angularDecorators =
|
||||
member.decorators && getAngularDecorators(typeChecker, member.decorators);
|
||||
|
||||
if (angularDecorators) {
|
||||
angularDecorators
|
||||
// Filter out the queries that can have the `static` flag.
|
||||
.filter(decorator => {
|
||||
return decorator.name === 'ViewChild' || decorator.name === 'ContentChild';
|
||||
})
|
||||
// Filter out the queries where the `static` flag is explicitly set to `false`.
|
||||
.filter(decorator => {
|
||||
const options = decorator.node.expression.arguments[1];
|
||||
return options && ts.isObjectLiteralExpression(options) &&
|
||||
options.properties.some(
|
||||
property => ts.isPropertyAssignment(property) &&
|
||||
property.initializer.kind === ts.SyntaxKind.FalseKeyword);
|
||||
})
|
||||
.forEach(decorator => {
|
||||
const options =
|
||||
decorator.node.expression.arguments[1] as ts.ObjectLiteralExpression;
|
||||
|
||||
// At this point we know that at least one property is the `static` flag. If this is
|
||||
// the only property we can drop the entire object literal, otherwise we have to
|
||||
// drop only the property.
|
||||
if (options.properties.length === 1) {
|
||||
removeParameter.push(decorator.node.expression);
|
||||
} else {
|
||||
removeProperty.push(options);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
node.forEachChild(walk);
|
||||
});
|
||||
|
||||
return {removeProperty, removeParameter};
|
||||
}
|
||||
|
||||
/** Removes the `options` parameter from the call expression of a query decorator. */
|
||||
export function removeOptionsParameter(node: ts.CallExpression): ts.CallExpression {
|
||||
return ts.updateCall(node, node.expression, node.typeArguments, [node.arguments[0]]);
|
||||
}
|
||||
|
||||
/** Removes the `static` property from an object literal expression. */
|
||||
export function removeStaticFlag(node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression {
|
||||
return ts.updateObjectLiteral(
|
||||
node,
|
||||
node.properties.filter(property => property.name && property.name.getText() !== 'static'));
|
||||
}
|
||||
|
|
@ -6,28 +6,7 @@ ts_library(
|
|||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = ["//packages/core/schematics/test/google3:__pkg__"],
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/annotations",
|
||||
"//packages/compiler-cli/src/ngtsc/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/core/schematics/migrations/activated-route-snapshot-fragment",
|
||||
"//packages/core/schematics/migrations/can-activate-with-redirect-to",
|
||||
"//packages/core/schematics/migrations/dynamic-queries",
|
||||
"//packages/core/schematics/migrations/initial-navigation",
|
||||
"//packages/core/schematics/migrations/initial-navigation/google3",
|
||||
"//packages/core/schematics/migrations/missing-injectable",
|
||||
"//packages/core/schematics/migrations/missing-injectable/google3",
|
||||
"//packages/core/schematics/migrations/navigation-extras-omissions",
|
||||
"//packages/core/schematics/migrations/relative-link-resolution",
|
||||
"//packages/core/schematics/migrations/relative-link-resolution/google3",
|
||||
"//packages/core/schematics/migrations/renderer-to-renderer2",
|
||||
"//packages/core/schematics/migrations/static-queries",
|
||||
"//packages/core/schematics/migrations/template-var-assignment",
|
||||
"//packages/core/schematics/migrations/testbed-teardown",
|
||||
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
|
||||
"//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/tslint",
|
||||
"@npm//tslint",
|
||||
|
|
|
|||
|
|
@ -1,40 +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 {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {findFragmentAccesses, migrateActivatedRouteSnapshotFragment} from '../activated-route-snapshot-fragment/util';
|
||||
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
if (sourceFile.isDeclarationFile || program.isSourceFileFromExternalLibrary(sourceFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const failures: RuleFailure[] = [];
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const nodesToMigrate = findFragmentAccesses(typeChecker, sourceFile);
|
||||
|
||||
if (nodesToMigrate.size > 0) {
|
||||
const printer = ts.createPrinter();
|
||||
nodesToMigrate.forEach(node => {
|
||||
const sourceFile = node.getSourceFile();
|
||||
const migratedNode = migrateActivatedRouteSnapshotFragment(node);
|
||||
const replacement = new Replacement(
|
||||
node.getStart(), node.getWidth(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile));
|
||||
failures.push(new RuleFailure(
|
||||
sourceFile, node.getStart(), node.getEnd(),
|
||||
'`ActivatedRouteSnapshot.fragment` is nullable.', this.ruleName, replacement));
|
||||
});
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +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 {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
import {findLiteralsToMigrate, migrateLiteral} from '../can-activate-with-redirect-to/util';
|
||||
|
||||
|
||||
/** TSLint rule that removes canActivate from Route configs that also have redirectTo. */
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const failures: RuleFailure[] = [];
|
||||
const printer = ts.createPrinter();
|
||||
const literalsToMigrate = findLiteralsToMigrate(sourceFile);
|
||||
|
||||
for (const literal of Array.from(literalsToMigrate)) {
|
||||
const migratedNode = migrateLiteral(literal);
|
||||
failures.push(new RuleFailure(
|
||||
sourceFile, literal.getStart(), literal.getEnd(),
|
||||
'canActivate cannot be used with redirectTo.', this.ruleName,
|
||||
new Replacement(
|
||||
literal.getStart(), literal.getWidth(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile))));
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +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 {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {identifyDynamicQueryNodes, removeOptionsParameter, removeStaticFlag} from '../dynamic-queries/util';
|
||||
|
||||
const RULE_NAME = 'dynamic-queries';
|
||||
const FAILURE_MESSAGE =
|
||||
'The static flag defaults to false, so setting it false manually is unnecessary.';
|
||||
|
||||
/**
|
||||
* TSLint rule that removes the `static` flag from dynamic queries.
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const printer = ts.createPrinter();
|
||||
const failures: RuleFailure[] = [];
|
||||
const result = identifyDynamicQueryNodes(program.getTypeChecker(), sourceFile);
|
||||
|
||||
result.removeProperty.forEach(node => {
|
||||
failures.push(new RuleFailure(
|
||||
sourceFile, node.getStart(), node.getEnd(), FAILURE_MESSAGE, RULE_NAME,
|
||||
new Replacement(
|
||||
node.getStart(), node.getWidth(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, removeStaticFlag(node), sourceFile))));
|
||||
});
|
||||
|
||||
result.removeParameter.forEach(node => {
|
||||
failures.push(new RuleFailure(
|
||||
sourceFile, node.getStart(), node.getEnd(), FAILURE_MESSAGE, RULE_NAME,
|
||||
new Replacement(
|
||||
node.getStart(), node.getWidth(),
|
||||
printer.printNode(
|
||||
ts.EmitHint.Unspecified, removeOptionsParameter(node), sourceFile))));
|
||||
});
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +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 compiler from '@angular/compiler';
|
||||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
|
||||
import {NgQueryResolveVisitor} from '../static-queries/angular/ng_query_visitor';
|
||||
import {QueryTiming} from '../static-queries/angular/query-definition';
|
||||
import {QueryUsageStrategy} from '../static-queries/strategies/usage_strategy/usage_strategy';
|
||||
import {getTransformedQueryCallExpr} from '../static-queries/transform';
|
||||
|
||||
const FAILURE_MESSAGE = 'Query does not explicitly specify its timing. Read more here: ' +
|
||||
'https://github.com/angular/angular/pull/28810';
|
||||
|
||||
/**
|
||||
* Rule that reports if an Angular "ViewChild" or "ContentChild" query is not explicitly
|
||||
* specifying its timing. The rule also provides TSLint automatic replacements that can
|
||||
* be applied in order to automatically migrate to the explicit query timing API.
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const queryVisitor = new NgQueryResolveVisitor(program.getTypeChecker());
|
||||
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
|
||||
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f)!);
|
||||
const printer = ts.createPrinter();
|
||||
const failures: RuleFailure[] = [];
|
||||
|
||||
// Analyze source files by detecting queries, class relations and component templates.
|
||||
rootSourceFiles.forEach(sourceFile => {
|
||||
queryVisitor.visitNode(sourceFile);
|
||||
templateVisitor.visitNode(sourceFile);
|
||||
});
|
||||
|
||||
const {resolvedQueries, classMetadata} = queryVisitor;
|
||||
|
||||
// Add all resolved templates to the class metadata so that we can also
|
||||
// check component templates for static query usage.
|
||||
templateVisitor.resolvedTemplates.forEach(template => {
|
||||
if (classMetadata.has(template.container)) {
|
||||
classMetadata.get(template.container)!.template = template;
|
||||
}
|
||||
});
|
||||
|
||||
const queries = resolvedQueries.get(sourceFile);
|
||||
const usageStrategy = new QueryUsageStrategy(classMetadata, typeChecker, compiler);
|
||||
|
||||
// No queries detected for the given source file.
|
||||
if (!queries) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Compute the query usage for all resolved queries and update the
|
||||
// query definitions to explicitly declare the query timing (static or dynamic)
|
||||
queries.forEach(q => {
|
||||
const queryExpr = q.decorator.node.expression;
|
||||
const {timing, message} = usageStrategy.detectTiming(q);
|
||||
const result = getTransformedQueryCallExpr(q, timing, !!message);
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newText = printer.printNode(ts.EmitHint.Unspecified, result.node, sourceFile);
|
||||
|
||||
// Replace the existing query decorator call expression with the
|
||||
// updated call expression node.
|
||||
const fix = new Replacement(queryExpr.getStart(), queryExpr.getWidth(), newText);
|
||||
const failureMessage = `${FAILURE_MESSAGE}. Based on analysis of the query it can be ` +
|
||||
`marked as "{static: ${(timing === QueryTiming.STATIC).toString()}}".`;
|
||||
|
||||
failures.push(new RuleFailure(
|
||||
sourceFile, queryExpr.getStart(), queryExpr.getEnd(), failureMessage, this.ruleName,
|
||||
fix));
|
||||
});
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +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 {RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {InitialNavigationCollector} from '../initial-navigation/collector';
|
||||
import {TslintUpdateRecorder} from '../initial-navigation/google3/tslint_update_recorder';
|
||||
import {InitialNavigationTransform} from '../initial-navigation/transform';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* TSLint rule that updates RouterModule `forRoot` options to be in line with v10 updates.
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const ruleName = this.ruleName;
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const sourceFiles = program.getSourceFiles().filter(
|
||||
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
|
||||
const initialNavigationCollector = new InitialNavigationCollector(typeChecker);
|
||||
const failures: RuleFailure[] = [];
|
||||
|
||||
// Analyze source files by detecting all ExtraOptions#InitialNavigation assignments
|
||||
sourceFiles.forEach(sourceFile => initialNavigationCollector.visitNode(sourceFile));
|
||||
|
||||
const {assignments} = initialNavigationCollector;
|
||||
const transformer = new InitialNavigationTransform(getUpdateRecorder);
|
||||
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();
|
||||
|
||||
transformer.migrateInitialNavigationAssignments(Array.from(assignments));
|
||||
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
failures.push(...updateRecorders.get(sourceFile)!.failures);
|
||||
}
|
||||
|
||||
return failures;
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const recorder = new TslintUpdateRecorder(ruleName, sourceFile);
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +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 {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
import {findLiteralsToMigrate, migrateLiteral} from '../../migrations/navigation-extras-omissions/util';
|
||||
|
||||
|
||||
/** TSLint rule that migrates `navigateByUrl` and `createUrlTree` calls to an updated signature. */
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const failures: RuleFailure[] = [];
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const printer = ts.createPrinter();
|
||||
const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker);
|
||||
|
||||
literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => {
|
||||
const migratedNode = migrateLiteral(methodName, instance);
|
||||
|
||||
if (migratedNode !== instance) {
|
||||
failures.push(new RuleFailure(
|
||||
sourceFile, instance.getStart(), instance.getEnd(),
|
||||
'Object used in navigateByUrl or createUrlTree call contains unsupported properties.',
|
||||
this.ruleName,
|
||||
new Replacement(
|
||||
instance.getStart(), instance.getWidth(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile))));
|
||||
}
|
||||
}));
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +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 {forwardRefResolver} from '@angular/compiler-cli/src/ngtsc/annotations';
|
||||
import {Reference} from '@angular/compiler-cli/src/ngtsc/imports';
|
||||
import {DynamicValue, PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
|
||||
import {StaticInterpreter} from '@angular/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter';
|
||||
import {reflectObjectLiteral, TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection';
|
||||
import {RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {NgDefinitionCollector} from '../missing-injectable/definition_collector';
|
||||
import {TslintUpdateRecorder} from '../missing-injectable/google3/tslint_update_recorder';
|
||||
import {MissingInjectableTransform} from '../missing-injectable/transform';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* TSLint rule that flags classes which are declared as providers in "NgModule",
|
||||
* "Directive" or "Component" definitions while not being decorated with any
|
||||
* Angular decorator (e.g. "@Injectable").
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const ruleName = this.ruleName;
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const sourceFiles = program.getSourceFiles().filter(
|
||||
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
|
||||
const definitionCollector = new NgDefinitionCollector(typeChecker);
|
||||
const failures: RuleFailure[] = [];
|
||||
|
||||
// Analyze source files by detecting all "NgModule", "Directive" or
|
||||
// "Component" definitions.
|
||||
sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile));
|
||||
|
||||
const {resolvedModules, resolvedDirectives} = definitionCollector;
|
||||
const transformer = new MissingInjectableTransform(typeChecker, getUpdateRecorder, {
|
||||
Reference,
|
||||
DynamicValue,
|
||||
PartialEvaluator,
|
||||
StaticInterpreter,
|
||||
TypeScriptReflectionHost,
|
||||
forwardRefResolver,
|
||||
reflectObjectLiteral,
|
||||
});
|
||||
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();
|
||||
|
||||
[...transformer.migrateModules(resolvedModules),
|
||||
...transformer.migrateDirectives(resolvedDirectives),
|
||||
].forEach(({message, node}) => {
|
||||
// Only report failures for the current source file that is visited.
|
||||
if (node.getSourceFile() === sourceFile) {
|
||||
failures.push(new RuleFailure(node.getSourceFile(), node.getStart(), 0, message, ruleName));
|
||||
}
|
||||
});
|
||||
|
||||
// Record the changes collected in the import manager and NgModule manager.
|
||||
transformer.recordChanges();
|
||||
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
failures.push(...updateRecorders.get(sourceFile)!.failures);
|
||||
}
|
||||
|
||||
return failures;
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const recorder = new TslintUpdateRecorder(ruleName, sourceFile);
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +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 compiler from '@angular/compiler';
|
||||
import {RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
|
||||
import {createHtmlSourceFile} from '../../utils/tslint/tslint_html_source_file';
|
||||
import {analyzeResolvedTemplate} from '../template-var-assignment/analyze_template';
|
||||
|
||||
const FAILURE_MESSAGE = 'Found assignment to template variable. This does not work with Ivy and ' +
|
||||
'needs to be updated.';
|
||||
|
||||
/**
|
||||
* Rule that reports if an Angular template contains property assignments to template variables.
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
|
||||
const failures: RuleFailure[] = [];
|
||||
|
||||
// Analyze the current source files by detecting all referenced HTML templates.
|
||||
templateVisitor.visitNode(sourceFile);
|
||||
|
||||
const {resolvedTemplates} = templateVisitor;
|
||||
|
||||
// Analyze each resolved template and print a warning for property writes to
|
||||
// template variables.
|
||||
resolvedTemplates.forEach(template => {
|
||||
const filePath = template.filePath;
|
||||
const nodes = analyzeResolvedTemplate(template, compiler);
|
||||
const templateFile =
|
||||
template.inline ? sourceFile : createHtmlSourceFile(filePath, template.content);
|
||||
|
||||
if (!nodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.forEach(n => {
|
||||
failures.push(new RuleFailure(
|
||||
templateFile, template.start + n.start, template.start + n.end, FAILURE_MESSAGE,
|
||||
this.ruleName));
|
||||
});
|
||||
});
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +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 {RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {RelativeLinkResolutionCollector} from '../relative-link-resolution/collector';
|
||||
import {TslintUpdateRecorder} from '../relative-link-resolution/google3/tslint_update_recorder';
|
||||
import {RelativeLinkResolutionTransform} from '../relative-link-resolution/transform';
|
||||
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const ruleName = this.ruleName;
|
||||
const sourceFiles = program.getSourceFiles().filter(
|
||||
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
|
||||
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();
|
||||
const relativeLinkResolutionCollector = new RelativeLinkResolutionCollector(typeChecker);
|
||||
|
||||
// Analyze source files by detecting all modules.
|
||||
sourceFiles.forEach(sourceFile => relativeLinkResolutionCollector.visitNode(sourceFile));
|
||||
|
||||
const {forRootCalls, extraOptionsLiterals} = relativeLinkResolutionCollector;
|
||||
const transformer = new RelativeLinkResolutionTransform(getUpdateRecorder);
|
||||
transformer.migrateRouterModuleForRootCalls(forRootCalls);
|
||||
transformer.migrateObjectLiterals(extraOptionsLiterals);
|
||||
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!.failures;
|
||||
}
|
||||
return [];
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const recorder = new TslintUpdateRecorder(ruleName, sourceFile);
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,144 +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 {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
||||
import {closestNode} from '../../utils/typescript/nodes';
|
||||
import {getHelper, HelperFunction} from '../renderer-to-renderer2/helpers';
|
||||
import {migrateExpression} from '../renderer-to-renderer2/migration';
|
||||
import {findRendererReferences} from '../renderer-to-renderer2/util';
|
||||
|
||||
/**
|
||||
* TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works:
|
||||
* https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const printer = ts.createPrinter();
|
||||
const failures: RuleFailure[] = [];
|
||||
const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer');
|
||||
const rendererImport = rendererImportSpecifier ?
|
||||
closestNode<ts.NamedImports>(rendererImportSpecifier, ts.SyntaxKind.NamedImports) :
|
||||
null;
|
||||
|
||||
// If there are no imports for the `Renderer`, we can exit early.
|
||||
if (!rendererImportSpecifier || !rendererImport) {
|
||||
return failures;
|
||||
}
|
||||
|
||||
const {typedNodes, methodCalls, forwardRefs} =
|
||||
findRendererReferences(sourceFile, typeChecker, rendererImportSpecifier);
|
||||
const helpersToAdd = new Set<HelperFunction>();
|
||||
|
||||
failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer));
|
||||
typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile)));
|
||||
forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));
|
||||
|
||||
methodCalls.forEach(call => {
|
||||
const {failure, requiredHelpers} =
|
||||
this._getMethodCallFailure(call, sourceFile, typeChecker, printer);
|
||||
|
||||
failures.push(failure);
|
||||
|
||||
if (requiredHelpers) {
|
||||
requiredHelpers.forEach(helperName => helpersToAdd.add(helperName));
|
||||
}
|
||||
});
|
||||
|
||||
// Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them.
|
||||
// The safest way to do so is to declare helper functions similar to the ones emitted by TS
|
||||
// which encapsulate the extra "glue" logic. We should only emit these functions once per
|
||||
// file and only if they're needed.
|
||||
if (helpersToAdd.size) {
|
||||
failures.push(this._getHelpersFailure(helpersToAdd, sourceFile, printer));
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
/** Gets a failure for an import of the Renderer. */
|
||||
private _getNamedImportsFailure(
|
||||
node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure {
|
||||
const replacementText = printer.printNode(
|
||||
ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile);
|
||||
|
||||
return new RuleFailure(
|
||||
sourceFile, node.getStart(), node.getEnd(),
|
||||
'Imports of deprecated Renderer are not allowed. Please use Renderer2 instead.',
|
||||
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText));
|
||||
}
|
||||
|
||||
/** Gets a failure for a typed node (e.g. function parameter or property). */
|
||||
private _getTypedNodeFailure(
|
||||
node: ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression,
|
||||
sourceFile: ts.SourceFile): RuleFailure {
|
||||
const type = node.type!;
|
||||
|
||||
return new RuleFailure(
|
||||
sourceFile, type.getStart(), type.getEnd(),
|
||||
'References to deprecated Renderer are not allowed. Please use Renderer2 instead.',
|
||||
this.ruleName, new Replacement(type.getStart(), type.getWidth(), 'Renderer2'));
|
||||
}
|
||||
|
||||
/** 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 deprecated Renderer are not allowed. Please use Renderer2 instead.',
|
||||
this.ruleName, new Replacement(node.getStart(), node.getWidth(), 'Renderer2'));
|
||||
}
|
||||
|
||||
/** Gets a failure for a Renderer method call. */
|
||||
private _getMethodCallFailure(
|
||||
call: ts.CallExpression, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker,
|
||||
printer: ts.Printer): {failure: RuleFailure, requiredHelpers?: HelperFunction[]} {
|
||||
const {node, requiredHelpers} = migrateExpression(call, typeChecker);
|
||||
let fix: Replacement|undefined;
|
||||
|
||||
if (node) {
|
||||
// If we migrated the node to a new expression, replace only the call expression.
|
||||
fix = new Replacement(
|
||||
call.getStart(), call.getWidth(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, node, sourceFile));
|
||||
} else if (call.parent && ts.isExpressionStatement(call.parent)) {
|
||||
// Otherwise if the call is inside an expression statement, drop the entire statement.
|
||||
// This takes care of any trailing semicolons. We only need to drop nodes for cases like
|
||||
// `setBindingDebugInfo` which have been noop for a while so they can be removed safely.
|
||||
fix = new Replacement(call.parent.getStart(), call.parent.getWidth(), '');
|
||||
}
|
||||
|
||||
return {
|
||||
failure: new RuleFailure(
|
||||
sourceFile, call.getStart(), call.getEnd(), 'Calls to Renderer methods are not allowed',
|
||||
this.ruleName, fix),
|
||||
requiredHelpers
|
||||
};
|
||||
}
|
||||
|
||||
/** Gets a failure that inserts the required helper functions at the bottom of the file. */
|
||||
private _getHelpersFailure(
|
||||
helpersToAdd: Set<HelperFunction>, sourceFile: ts.SourceFile,
|
||||
printer: ts.Printer): RuleFailure {
|
||||
const helpers: Replacement[] = [];
|
||||
const endOfFile = sourceFile.endOfFileToken;
|
||||
|
||||
helpersToAdd.forEach(helperName => {
|
||||
helpers.push(new Replacement(
|
||||
endOfFile.getStart(), endOfFile.getWidth(), getHelper(helperName, sourceFile, printer)));
|
||||
});
|
||||
|
||||
// Add a failure at the end of the file which we can use as an anchor to insert the helpers.
|
||||
return new RuleFailure(
|
||||
sourceFile, endOfFile.getStart(), endOfFile.getStart() + 1,
|
||||
'File should contain Renderer helper functions. Run tslint with --fix to generate them.',
|
||||
this.ruleName, helpers);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +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 {forwardRefResolver} from '@angular/compiler-cli/src/ngtsc/annotations';
|
||||
import {Reference} from '@angular/compiler-cli/src/ngtsc/imports';
|
||||
import {DynamicValue, PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
|
||||
import {StaticInterpreter} from '@angular/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter';
|
||||
import {reflectObjectLiteral, TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection';
|
||||
import {RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
import {TslintUpdateRecorder} from '../undecorated-classes-with-decorated-fields/google3/tslint_update_recorder';
|
||||
import {UndecoratedClassesWithDecoratedFieldsTransform} from '../undecorated-classes-with-decorated-fields/transform';
|
||||
|
||||
/**
|
||||
* TSLint rule that adds an Angular decorator to classes that have Angular field decorators.
|
||||
* https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const ruleName = this.ruleName;
|
||||
const sourceFiles = program.getSourceFiles().filter(
|
||||
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
|
||||
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();
|
||||
const transform =
|
||||
new UndecoratedClassesWithDecoratedFieldsTransform(typeChecker, getUpdateRecorder, {
|
||||
Reference,
|
||||
DynamicValue,
|
||||
PartialEvaluator,
|
||||
StaticInterpreter,
|
||||
TypeScriptReflectionHost,
|
||||
forwardRefResolver,
|
||||
reflectObjectLiteral,
|
||||
});
|
||||
|
||||
// Migrate all source files in the project.
|
||||
transform.migrate(sourceFiles);
|
||||
|
||||
// Record the changes collected in the import manager.
|
||||
transform.recordChanges();
|
||||
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!.failures;
|
||||
}
|
||||
return [];
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): TslintUpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const recorder = new TslintUpdateRecorder(ruleName, sourceFile);
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,11 @@
|
|||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {findAsyncReferences} from '../../migrations/wait-for-async/util';
|
||||
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
||||
import {closestNode} from '../../utils/typescript/nodes';
|
||||
import {isReferenceToImport} from '../../utils/typescript/symbol';
|
||||
|
||||
// This rule is also used inside of Google by Typescript linting.
|
||||
|
||||
/** Name of the deprecated function that we're removing. */
|
||||
const deprecatedFunction = 'async';
|
||||
|
|
@ -34,8 +36,8 @@ export class Rule extends Rules.TypedRule {
|
|||
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)));
|
||||
this.findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier)
|
||||
.forEach((node) => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));
|
||||
}
|
||||
|
||||
return failures;
|
||||
|
|
@ -62,4 +64,22 @@ export class Rule extends Rules.TypedRule {
|
|||
newFunction} instead.`,
|
||||
this.ruleName, new Replacement(node.getStart(), node.getWidth(), newFunction));
|
||||
}
|
||||
|
||||
/** Finds calls to the `async` function. */
|
||||
private 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 === deprecatedFunction &&
|
||||
isReferenceToImport(typeChecker, node.expression, asyncImportSpecifier)) {
|
||||
results.add(node.expression);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visitNode);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "initial-navigation",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/migrations/google3:__pkg__",
|
||||
"//packages/core/schematics/migrations/initial-navigation/google3:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
## initialNavigation migration
|
||||
|
||||
Automatically migrates the `initialNavigation` property of the `RouterModule` to the newly
|
||||
available options: `enabledNonBlocking` (default), `enabledBlocking`, and `disabled`.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(ROUTES, {initialNavigation: 'legacy_disabled'}),
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(ROUTES, {initialNavigation: 'disabled'}),
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
```
|
||||
|
||||
### Disclaimer
|
||||
|
||||
The migration only covers the most common patterns where developers set the `ExtraOptions#InitialNavigation`
|
||||
option to an outdated value. Therefore, if a user declares the option using a number of other methods,
|
||||
e.g. shorthand notation, variable declaration, or some other crafty method, they will have to migrate
|
||||
those options by hand. Otherwise, the compiler will error if the types are sufficiently enforced.
|
||||
|
||||
The basic migration strategy is as follows:
|
||||
* `legacy_disabled` || `false` => `disabled`
|
||||
* `legacy_enabled` || `true` => `enabledNonBlocking` (new default)
|
||||
|
|
@ -1,117 +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 ts from 'typescript';
|
||||
import {isExtraOptions, isRouterModuleForRoot} from './util';
|
||||
|
||||
|
||||
/** The property name for the options that need to be migrated */
|
||||
const INITIAL_NAVIGATION = 'initialNavigation';
|
||||
|
||||
/**
|
||||
* Visitor that walks through specified TypeScript nodes and collects all
|
||||
* found ExtraOptions#InitialNavigation assignments.
|
||||
*/
|
||||
export class InitialNavigationCollector {
|
||||
public assignments: Set<ts.PropertyAssignment> = new Set();
|
||||
|
||||
constructor(private readonly typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
let extraOptionsLiteral: ts.ObjectLiteralExpression|null = null;
|
||||
if (isRouterModuleForRoot(this.typeChecker, node) && node.arguments.length > 0) {
|
||||
if (node.arguments.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ts.isObjectLiteralExpression(node.arguments[1])) {
|
||||
extraOptionsLiteral = node.arguments[1] as ts.ObjectLiteralExpression;
|
||||
} else if (ts.isIdentifier(node.arguments[1])) {
|
||||
extraOptionsLiteral =
|
||||
this.getLiteralNeedingMigrationFromIdentifier(node.arguments[1] as ts.Identifier);
|
||||
}
|
||||
} else if (ts.isVariableDeclaration(node)) {
|
||||
extraOptionsLiteral = this.getLiteralNeedingMigration(node);
|
||||
}
|
||||
|
||||
if (extraOptionsLiteral !== null) {
|
||||
this.visitExtraOptionsLiteral(extraOptionsLiteral);
|
||||
} else {
|
||||
// no match found, continue iteration
|
||||
ts.forEachChild(node, n => this.visitNode(n));
|
||||
}
|
||||
}
|
||||
|
||||
visitExtraOptionsLiteral(extraOptionsLiteral: ts.ObjectLiteralExpression) {
|
||||
for (const prop of extraOptionsLiteral.properties) {
|
||||
if (ts.isPropertyAssignment(prop) &&
|
||||
(ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name))) {
|
||||
if (prop.name.text === INITIAL_NAVIGATION && isValidInitialNavigationValue(prop)) {
|
||||
this.assignments.add(prop);
|
||||
}
|
||||
} else if (ts.isSpreadAssignment(prop) && ts.isIdentifier(prop.expression)) {
|
||||
const literalFromSpreadAssignment =
|
||||
this.getLiteralNeedingMigrationFromIdentifier(prop.expression);
|
||||
if (literalFromSpreadAssignment !== null) {
|
||||
this.visitExtraOptionsLiteral(literalFromSpreadAssignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getLiteralNeedingMigrationFromIdentifier(id: ts.Identifier): ts.ObjectLiteralExpression
|
||||
|null {
|
||||
const symbolForIdentifier = this.typeChecker.getSymbolAtLocation(id);
|
||||
if (symbolForIdentifier === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (symbolForIdentifier.declarations === undefined ||
|
||||
symbolForIdentifier.declarations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const declarationNode = symbolForIdentifier.declarations[0];
|
||||
if (!ts.isVariableDeclaration(declarationNode) || declarationNode.initializer === undefined ||
|
||||
!ts.isObjectLiteralExpression(declarationNode.initializer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return declarationNode.initializer;
|
||||
}
|
||||
|
||||
private getLiteralNeedingMigration(node: ts.VariableDeclaration): ts.ObjectLiteralExpression
|
||||
|null {
|
||||
if (node.initializer === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// declaration could be `x: ExtraOptions = {}` or `x = {} as ExtraOptions`
|
||||
if (ts.isAsExpression(node.initializer) &&
|
||||
ts.isObjectLiteralExpression(node.initializer.expression) &&
|
||||
isExtraOptions(this.typeChecker, node.initializer.type)) {
|
||||
return node.initializer.expression;
|
||||
} else if (
|
||||
node.type !== undefined && ts.isObjectLiteralExpression(node.initializer) &&
|
||||
isExtraOptions(this.typeChecker, node.type)) {
|
||||
return node.initializer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the value assigned to an `initialNavigation` assignment
|
||||
* conforms to the expected types for ExtraOptions#InitialNavigation
|
||||
* @param node the property assignment to check
|
||||
*/
|
||||
function isValidInitialNavigationValue(node: ts.PropertyAssignment): boolean {
|
||||
return ts.isStringLiteralLike(node.initializer) ||
|
||||
node.initializer.kind === ts.SyntaxKind.FalseKeyword ||
|
||||
node.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "google3",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = ["//packages/core/schematics/migrations/google3:__pkg__"],
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/initial-navigation",
|
||||
"@npm//tslint",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,26 +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 {Replacement, RuleFailure} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {UpdateRecorder} from '../update_recorder';
|
||||
|
||||
export class TslintUpdateRecorder implements UpdateRecorder {
|
||||
failures: RuleFailure[] = [];
|
||||
|
||||
constructor(private ruleName: string, private sourceFile: ts.SourceFile) {}
|
||||
|
||||
updateNode(node: ts.Node, newText: string): void {
|
||||
this.failures.push(new RuleFailure(
|
||||
this.sourceFile, node.getStart(), node.getEnd(), `Node needs to be updated to: ${newText}`,
|
||||
this.ruleName, Replacement.replaceFromTo(node.getStart(), node.getEnd(), newText)));
|
||||
}
|
||||
|
||||
commitUpdate() {}
|
||||
}
|
||||
|
|
@ -1,73 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {InitialNavigationCollector} from './collector';
|
||||
import {InitialNavigationTransform} from './transform';
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
/** Entry point for the v10 "initialNavigation RouterModule options" schematic. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
|
||||
if (!buildPaths.length && !testPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot update the "initialNavigation" option for RouterModule');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
|
||||
runInitialNavigationMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runInitialNavigationMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const initialNavigationCollector = new InitialNavigationCollector(typeChecker);
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
// Analyze source files by detecting all modules.
|
||||
sourceFiles.forEach(sourceFile => initialNavigationCollector.visitNode(sourceFile));
|
||||
|
||||
const {assignments} = initialNavigationCollector;
|
||||
const transformer = new InitialNavigationTransform(getUpdateRecorder);
|
||||
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
|
||||
transformer.migrateInitialNavigationAssignments(Array.from(assignments));
|
||||
|
||||
// Walk through each update recorder and commit the update. We need to commit the
|
||||
// updates in batches per source file as there can be only one recorder per source
|
||||
// file in order to avoid shift character offsets.
|
||||
updateRecorders.forEach(recorder => recorder.commitUpdate());
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
const recorder: UpdateRecorder = {
|
||||
updateNode(node: ts.Node, newText: string) {
|
||||
treeRecorder.remove(node.getStart(), node.getWidth());
|
||||
treeRecorder.insertRight(node.getStart(), newText);
|
||||
},
|
||||
commitUpdate() {
|
||||
tree.commitUpdate(treeRecorder);
|
||||
}
|
||||
};
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +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 ts from 'typescript';
|
||||
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
|
||||
export class InitialNavigationTransform {
|
||||
private printer = ts.createPrinter();
|
||||
|
||||
constructor(private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {}
|
||||
|
||||
/** Migrate the ExtraOptions#InitialNavigation property assignments. */
|
||||
migrateInitialNavigationAssignments(literals: ts.PropertyAssignment[]) {
|
||||
literals.forEach(l => this.migrateAssignment(l));
|
||||
}
|
||||
|
||||
/** Migrate an ExtraOptions#InitialNavigation expression to use the new options format. */
|
||||
migrateAssignment(assignment: ts.PropertyAssignment) {
|
||||
const newInitializer = getUpdatedInitialNavigationValue(assignment.initializer);
|
||||
if (newInitializer) {
|
||||
const newAssignment =
|
||||
ts.updatePropertyAssignment(assignment, assignment.name, newInitializer);
|
||||
this._updateNode(assignment, newAssignment);
|
||||
}
|
||||
}
|
||||
|
||||
private _updateNode(node: ts.Node, newNode: ts.Node) {
|
||||
const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile());
|
||||
const recorder = this.getUpdateRecorder(node.getSourceFile());
|
||||
recorder.updateNode(node, newText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the deprecated initialNavigation options to their v10 equivalents
|
||||
* (or as close as we can get).
|
||||
* @param initializer the old initializer to update
|
||||
*/
|
||||
function getUpdatedInitialNavigationValue(initializer: ts.Expression): ts.Expression|null {
|
||||
const oldText: string|boolean = ts.isStringLiteralLike(initializer) ?
|
||||
initializer.text :
|
||||
initializer.kind === ts.SyntaxKind.TrueKeyword;
|
||||
let newText: string|undefined;
|
||||
switch (oldText) {
|
||||
case false:
|
||||
case 'legacy_disabled':
|
||||
newText = 'disabled';
|
||||
break;
|
||||
case true:
|
||||
case 'legacy_enabled':
|
||||
newText = 'enabledNonBlocking';
|
||||
break;
|
||||
}
|
||||
|
||||
return !!newText ? ts.createIdentifier(`'${newText}'`) : null;
|
||||
}
|
||||
|
|
@ -1,19 +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 ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Update recorder interface that is used to transform source files in a non-colliding
|
||||
* way. Also this indirection makes it possible to re-use logic for both TSLint rules
|
||||
* and CLI devkit schematic updates.
|
||||
*/
|
||||
export interface UpdateRecorder {
|
||||
updateNode(node: ts.Node, newText: string): void;
|
||||
commitUpdate(): void;
|
||||
}
|
||||
|
|
@ -1,33 +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 ts from 'typescript';
|
||||
import {getImportOfIdentifier} from '../../utils/typescript/imports';
|
||||
|
||||
/** Determine whether a node is a ModuleWithProviders type reference node without a generic type */
|
||||
export function isRouterModuleForRoot(
|
||||
typeChecker: ts.TypeChecker, node: ts.Node): node is ts.CallExpression {
|
||||
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
||||
!ts.isIdentifier(node.expression.expression) || node.expression.name.text !== 'forRoot') {
|
||||
return false;
|
||||
}
|
||||
const imp = getImportOfIdentifier(typeChecker, node.expression.expression);
|
||||
return !!imp && imp.name === 'RouterModule' && imp.importModule === '@angular/router' &&
|
||||
!node.typeArguments;
|
||||
}
|
||||
|
||||
export function isExtraOptions(
|
||||
typeChecker: ts.TypeChecker, node: ts.Node): node is ts.TypeReferenceNode {
|
||||
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const imp = getImportOfIdentifier(typeChecker, node.typeName);
|
||||
return imp !== null && imp.name === 'ExtraOptions' && imp.importModule === '@angular/router' &&
|
||||
!node.typeArguments;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "missing-injectable",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/migrations/google3:__pkg__",
|
||||
"//packages/core/schematics/migrations/missing-injectable/google3:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/compiler-cli/private",
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,104 +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 ts from 'typescript';
|
||||
|
||||
import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators';
|
||||
import {getPropertyNameText} from '../../utils/typescript/property_name';
|
||||
|
||||
export interface ResolvedNgModule {
|
||||
name: string;
|
||||
node: ts.ClassDeclaration;
|
||||
decorator: NgDecorator;
|
||||
providersExpr: ts.Expression|null;
|
||||
}
|
||||
|
||||
export interface ResolvedDirective {
|
||||
name: string;
|
||||
node: ts.ClassDeclaration;
|
||||
decorator: NgDecorator;
|
||||
providersExpr: ts.Expression|null;
|
||||
viewProvidersExpr: ts.Expression|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that walks through specified TypeScript nodes and collects all
|
||||
* found NgModule, Directive or Component definitions.
|
||||
*/
|
||||
export class NgDefinitionCollector {
|
||||
resolvedModules: ResolvedNgModule[] = [];
|
||||
resolvedDirectives: ResolvedDirective[] = [];
|
||||
|
||||
constructor(public typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
this.visitClassDeclaration(node);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, n => this.visitNode(n));
|
||||
}
|
||||
|
||||
private visitClassDeclaration(node: ts.ClassDeclaration) {
|
||||
if (!node.decorators || !node.decorators.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators);
|
||||
const directiveDecorator =
|
||||
ngDecorators.find(({name}) => name === 'Component' || name == 'Directive');
|
||||
const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule');
|
||||
|
||||
if (ngModuleDecorator) {
|
||||
this._visitNgModuleClass(node, ngModuleDecorator);
|
||||
} else if (directiveDecorator) {
|
||||
this._visitDirectiveClass(node, directiveDecorator);
|
||||
}
|
||||
}
|
||||
|
||||
private _visitDirectiveClass(node: ts.ClassDeclaration, decorator: NgDecorator) {
|
||||
const decoratorCall = decorator.node.expression;
|
||||
const metadata = decoratorCall.arguments[0];
|
||||
|
||||
if (!metadata || !ts.isObjectLiteralExpression(metadata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providersNode = metadata.properties.filter(ts.isPropertyAssignment)
|
||||
.find(p => getPropertyNameText(p.name) === 'providers');
|
||||
|
||||
const viewProvidersNode = metadata.properties.filter(ts.isPropertyAssignment)
|
||||
.find(p => getPropertyNameText(p.name) === 'viewProviders');
|
||||
|
||||
this.resolvedDirectives.push({
|
||||
name: node.name ? node.name.text : 'default',
|
||||
node,
|
||||
decorator,
|
||||
providersExpr: providersNode !== undefined ? providersNode.initializer : null,
|
||||
viewProvidersExpr: viewProvidersNode !== undefined ? viewProvidersNode.initializer : null,
|
||||
});
|
||||
}
|
||||
|
||||
private _visitNgModuleClass(node: ts.ClassDeclaration, decorator: NgDecorator) {
|
||||
const decoratorCall = decorator.node.expression;
|
||||
const metadata = decoratorCall.arguments[0];
|
||||
|
||||
if (!metadata || !ts.isObjectLiteralExpression(metadata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providersNode = metadata.properties.filter(ts.isPropertyAssignment)
|
||||
.find(p => getPropertyNameText(p.name) === 'providers');
|
||||
this.resolvedModules.push({
|
||||
name: node.name ? node.name.text : 'default',
|
||||
node,
|
||||
decorator,
|
||||
providersExpr: providersNode !== undefined ? providersNode.initializer : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "google3",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = ["//packages/core/schematics/migrations/google3:__pkg__"],
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/missing-injectable",
|
||||
"@npm//tslint",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,67 +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 {Replacement, RuleFailure} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {UpdateRecorder} from '../update_recorder';
|
||||
|
||||
export class TslintUpdateRecorder implements UpdateRecorder {
|
||||
failures: RuleFailure[] = [];
|
||||
|
||||
constructor(private ruleName: string, private sourceFile: ts.SourceFile) {}
|
||||
|
||||
addClassDecorator(node: ts.ClassDeclaration, decoratorText: string, className: string) {
|
||||
// Adding a decorator should be the last replacement. Replacements/rule failures
|
||||
// are handled in reverse and in case a decorator and import are inserted at
|
||||
// the start of the file, the class decorator should come after the import.
|
||||
this.failures.unshift(new RuleFailure(
|
||||
this.sourceFile, node.getStart(), 0,
|
||||
`Class needs to be decorated with ` +
|
||||
`"${decoratorText}" because it has been provided by "${className}".`,
|
||||
this.ruleName, Replacement.appendText(node.getStart(), `${decoratorText}\n`)));
|
||||
}
|
||||
|
||||
addNewImport(start: number, importText: string) {
|
||||
this.failures.push(new RuleFailure(
|
||||
this.sourceFile, start, 0, `Source file needs to have import: "${importText}"`,
|
||||
this.ruleName, Replacement.appendText(start, importText)));
|
||||
}
|
||||
|
||||
updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string): void {
|
||||
const fix = [
|
||||
Replacement.deleteText(namedBindings.getStart(), namedBindings.getWidth()),
|
||||
Replacement.appendText(namedBindings.getStart(), newNamedBindings),
|
||||
];
|
||||
this.failures.push(new RuleFailure(
|
||||
this.sourceFile, namedBindings.getStart(), namedBindings.getEnd(),
|
||||
`Import needs to be updated to import symbols: "${newNamedBindings}"`, this.ruleName, fix));
|
||||
}
|
||||
|
||||
replaceDecorator(decorator: ts.Node, newText: string, className: string): void {
|
||||
const fix = [
|
||||
Replacement.deleteText(decorator.getStart(), decorator.getWidth()),
|
||||
Replacement.appendText(decorator.getStart(), newText),
|
||||
];
|
||||
this.failures.push(new RuleFailure(
|
||||
this.sourceFile, decorator.getStart(), decorator.getEnd(),
|
||||
`Decorator needs to be replaced with "${newText}" because it has been provided ` +
|
||||
`by "${className}"`,
|
||||
this.ruleName, fix));
|
||||
}
|
||||
|
||||
|
||||
updateObjectLiteral(node: ts.ObjectLiteralExpression, newText: string): void {
|
||||
this.failures.push(new RuleFailure(
|
||||
this.sourceFile, node.getStart(), node.getEnd(),
|
||||
`Object literal needs to be updated to: ${newText}`, this.ruleName,
|
||||
Replacement.replaceFromTo(node.getStart(), node.getEnd(), newText)));
|
||||
}
|
||||
|
||||
commitUpdate() {}
|
||||
}
|
||||
|
|
@ -1,134 +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 {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm';
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {NgDefinitionCollector} from './definition_collector';
|
||||
import {MissingInjectableTransform} from './transform';
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
/** Entry point for the V9 "missing @Injectable" schematic. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree, ctx: SchematicContext) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const failures: string[] = [];
|
||||
|
||||
if (!buildPaths.length && !testPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot add the "@Injectable" decorator to providers ' +
|
||||
'which don\'t have that decorator set.');
|
||||
}
|
||||
|
||||
let compilerCliMigrationsModule;
|
||||
try {
|
||||
// Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import
|
||||
// workaround. Once TypeScript provides support for keeping the dynamic import this workaround
|
||||
// can be changed to a direct dynamic import.
|
||||
compilerCliMigrationsModule = await loadCompilerCliMigrationsModule();
|
||||
} catch (e) {
|
||||
throw new SchematicsException(
|
||||
`Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`);
|
||||
}
|
||||
|
||||
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
|
||||
failures.push(...runMissingInjectableMigration(
|
||||
tree, tsconfigPath, basePath, compilerCliMigrationsModule));
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
ctx.logger.info('Could not migrate all providers automatically. Please');
|
||||
ctx.logger.info('manually migrate the following instances:');
|
||||
failures.forEach(message => ctx.logger.warn(`⮑ ${message}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runMissingInjectableMigration(
|
||||
tree: Tree, tsconfigPath: string, basePath: string,
|
||||
compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations')):
|
||||
string[] {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const failures: string[] = [];
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const definitionCollector = new NgDefinitionCollector(typeChecker);
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
// Analyze source files by detecting all modules, directives and components.
|
||||
sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile));
|
||||
|
||||
const {resolvedModules, resolvedDirectives} = definitionCollector;
|
||||
const transformer =
|
||||
new MissingInjectableTransform(typeChecker, getUpdateRecorder, compilerCliMigrationsModule);
|
||||
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
|
||||
|
||||
[...transformer.migrateModules(resolvedModules),
|
||||
...transformer.migrateDirectives(resolvedDirectives),
|
||||
].forEach(({message, node}) => {
|
||||
const nodeSourceFile = node.getSourceFile();
|
||||
const relativeFilePath = relative(basePath, nodeSourceFile.fileName);
|
||||
const {line, character} =
|
||||
ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart());
|
||||
failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`);
|
||||
});
|
||||
|
||||
// Record the changes collected in the import manager and transformer.
|
||||
transformer.recordChanges();
|
||||
|
||||
// Walk through each update recorder and commit the update. We need to commit the
|
||||
// updates in batches per source file as there can be only one recorder per source
|
||||
// file in order to avoid shift character offsets.
|
||||
updateRecorders.forEach(recorder => recorder.commitUpdate());
|
||||
|
||||
return failures;
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
const recorder: UpdateRecorder = {
|
||||
addClassDecorator(node: ts.ClassDeclaration, text: string) {
|
||||
// New imports should be inserted at the left while decorators should be inserted
|
||||
// at the right in order to ensure that imports are inserted before the decorator
|
||||
// if the start position of import and decorator is the source file start.
|
||||
treeRecorder.insertRight(node.getStart(), `${text}\n`);
|
||||
},
|
||||
replaceDecorator(decorator: ts.Decorator, newText: string) {
|
||||
treeRecorder.remove(decorator.getStart(), decorator.getWidth());
|
||||
treeRecorder.insertRight(decorator.getStart(), newText);
|
||||
},
|
||||
addNewImport(start: number, importText: string) {
|
||||
// New imports should be inserted at the left while decorators should be inserted
|
||||
// at the right in order to ensure that imports are inserted before the decorator
|
||||
// if the start position of import and decorator is the source file start.
|
||||
treeRecorder.insertLeft(start, importText);
|
||||
},
|
||||
updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string) {
|
||||
treeRecorder.remove(namedBindings.getStart(), namedBindings.getWidth());
|
||||
treeRecorder.insertRight(namedBindings.getStart(), newNamedBindings);
|
||||
},
|
||||
updateObjectLiteral(node: ts.ObjectLiteralExpression, newText: string) {
|
||||
treeRecorder.remove(node.getStart(), node.getWidth());
|
||||
treeRecorder.insertRight(node.getStart(), newText);
|
||||
},
|
||||
commitUpdate() {
|
||||
tree.commitUpdate(treeRecorder);
|
||||
}
|
||||
};
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +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 ts from 'typescript';
|
||||
|
||||
import type {ResolvedValue, TypeScriptReflectionHost} from '@angular/compiler-cli/private/migrations';
|
||||
|
||||
export interface ProviderLiteral {
|
||||
node: ts.ObjectLiteralExpression;
|
||||
resolvedValue: ResolvedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function to create an evaluator for providers. This is required to be a
|
||||
* factory function because the underlying class extends a class that is only available
|
||||
* from within a dynamically imported module (`@angular/compiler-cli/private/migrations`)
|
||||
* and is therefore not available at module evaluation time.
|
||||
*/
|
||||
export function createProvidersEvaluator(
|
||||
compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations'),
|
||||
host: TypeScriptReflectionHost, checker: ts.TypeChecker): {
|
||||
evaluate:
|
||||
(expr: ts.Expression) => {
|
||||
resolvedValue: ResolvedValue, literals: ProviderLiteral[]
|
||||
}
|
||||
} {
|
||||
/**
|
||||
* Providers evaluator that extends the ngtsc static interpreter. This is necessary because
|
||||
* the static interpreter by default only exposes the resolved value, but we are also interested
|
||||
* in the TypeScript nodes that declare providers. It would be possible to manually traverse the
|
||||
* AST to collect these nodes, but that would mean that we need to re-implement the static
|
||||
* interpreter in order to handle all possible scenarios. (e.g. spread operator, function calls,
|
||||
* callee scope). This can be avoided by simply extending the static interpreter and intercepting
|
||||
* the "visitObjectLiteralExpression" method.
|
||||
*/
|
||||
class ProvidersEvaluator extends compilerCliMigrationsModule.StaticInterpreter {
|
||||
private _providerLiterals: ProviderLiteral[] = [];
|
||||
|
||||
override visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: any) {
|
||||
const resolvedValue =
|
||||
super.visitObjectLiteralExpression(node, {...context, insideProviderDef: true});
|
||||
// do not collect nested object literals. e.g. a provider could use a
|
||||
// spread assignment (which resolves to another object literal). In that
|
||||
// case the referenced object literal is not a provider object literal.
|
||||
if (!context.insideProviderDef) {
|
||||
this._providerLiterals.push({node, resolvedValue});
|
||||
}
|
||||
return resolvedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given expression and returns its statically resolved value
|
||||
* and a list of object literals which define Angular providers.
|
||||
*/
|
||||
evaluate(expr: ts.Expression) {
|
||||
this._providerLiterals = [];
|
||||
const resolvedValue = this.visit(expr, {
|
||||
originatingFile: expr.getSourceFile(),
|
||||
absoluteModuleName: null,
|
||||
resolutionContext: expr.getSourceFile().fileName,
|
||||
scope: new Map(),
|
||||
foreignFunctionResolver: compilerCliMigrationsModule.forwardRefResolver
|
||||
});
|
||||
return {resolvedValue, literals: this._providerLiterals};
|
||||
}
|
||||
}
|
||||
|
||||
return new ProvidersEvaluator(host, checker, /* dependencyTracker */ null);
|
||||
}
|
||||
|
|
@ -1,239 +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 type {ResolvedValue} from '@angular/compiler-cli/private/migrations';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {ImportManager} from '../../utils/import_manager';
|
||||
import {getAngularDecorators} from '../../utils/ng_decorators';
|
||||
|
||||
import {ResolvedDirective, ResolvedNgModule} from './definition_collector';
|
||||
import {createProvidersEvaluator, ProviderLiteral} from './providers_evaluator';
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
/**
|
||||
* Name of decorators which imply that a given class does not need to be migrated.
|
||||
* - `@Injectable`, `@Directive`, `@Component` and `@Pipe` instruct the compiler
|
||||
* to generate a factory definition.
|
||||
* - `@NgModule` instructs the compiler to generate a provider definition that holds
|
||||
* the factory function.
|
||||
*/
|
||||
const NO_MIGRATE_DECORATORS = ['Injectable', 'Directive', 'Component', 'Pipe', 'NgModule'];
|
||||
|
||||
export interface AnalysisFailure {
|
||||
node: ts.Node;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class MissingInjectableTransform {
|
||||
private printer = ts.createPrinter();
|
||||
private importManager = new ImportManager(this.getUpdateRecorder, this.printer);
|
||||
private providersEvaluator;
|
||||
|
||||
/** Set of provider class declarations which were already checked or migrated. */
|
||||
private visitedProviderClasses = new Set<ts.ClassDeclaration>();
|
||||
|
||||
/** Set of provider object literals which were already checked or migrated. */
|
||||
private visitedProviderLiterals = new Set<ts.ObjectLiteralExpression>();
|
||||
|
||||
constructor(
|
||||
private typeChecker: ts.TypeChecker,
|
||||
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder,
|
||||
private compilerCliMigrationsModule:
|
||||
typeof import('@angular/compiler-cli/private/migrations')) {
|
||||
this.providersEvaluator = createProvidersEvaluator(
|
||||
compilerCliMigrationsModule,
|
||||
new compilerCliMigrationsModule.TypeScriptReflectionHost(typeChecker), typeChecker);
|
||||
}
|
||||
|
||||
recordChanges() {
|
||||
this.importManager.recordChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates all specified NgModule's by walking through referenced providers
|
||||
* and decorating them with "@Injectable" if needed.
|
||||
*/
|
||||
migrateModules(modules: ResolvedNgModule[]): AnalysisFailure[] {
|
||||
return modules.reduce(
|
||||
(failures, node) => failures.concat(this.migrateModule(node)), [] as AnalysisFailure[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates all specified directives by walking through referenced providers
|
||||
* and decorating them with "@Injectable" if needed.
|
||||
*/
|
||||
migrateDirectives(directives: ResolvedDirective[]): AnalysisFailure[] {
|
||||
return directives.reduce(
|
||||
(failures, node) => failures.concat(this.migrateDirective(node)), [] as AnalysisFailure[]);
|
||||
}
|
||||
|
||||
/** Migrates a given NgModule by walking through the referenced providers. */
|
||||
migrateModule(module: ResolvedNgModule): AnalysisFailure[] {
|
||||
if (module.providersExpr === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {resolvedValue, literals} = this.providersEvaluator.evaluate(module.providersExpr);
|
||||
this._migrateLiteralProviders(literals);
|
||||
|
||||
if (!Array.isArray(resolvedValue)) {
|
||||
return [
|
||||
{node: module.providersExpr, message: 'Providers of module are not statically analyzable.'}
|
||||
];
|
||||
}
|
||||
|
||||
return this._visitProviderResolvedValue(resolvedValue, module);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Migrates a given directive by walking through defined providers. This method
|
||||
* also handles components with "viewProviders" defined.
|
||||
*/
|
||||
migrateDirective(directive: ResolvedDirective): AnalysisFailure[] {
|
||||
const failures: AnalysisFailure[] = [];
|
||||
|
||||
// Migrate "providers" on directives and components if defined.
|
||||
if (directive.providersExpr) {
|
||||
const {resolvedValue, literals} = this.providersEvaluator.evaluate(directive.providersExpr);
|
||||
this._migrateLiteralProviders(literals);
|
||||
if (!Array.isArray(resolvedValue)) {
|
||||
return [
|
||||
{node: directive.providersExpr, message: `Providers are not statically analyzable.`}
|
||||
];
|
||||
}
|
||||
failures.push(...this._visitProviderResolvedValue(resolvedValue, directive));
|
||||
}
|
||||
|
||||
// Migrate "viewProviders" on components if defined.
|
||||
if (directive.viewProvidersExpr) {
|
||||
const {resolvedValue, literals} =
|
||||
this.providersEvaluator.evaluate(directive.viewProvidersExpr);
|
||||
this._migrateLiteralProviders(literals);
|
||||
if (!Array.isArray(resolvedValue)) {
|
||||
return [
|
||||
{node: directive.viewProvidersExpr, message: `Providers are not statically analyzable.`}
|
||||
];
|
||||
}
|
||||
failures.push(...this._visitProviderResolvedValue(resolvedValue, directive));
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a given provider class if it is not decorated with
|
||||
* any Angular decorator.
|
||||
*/
|
||||
migrateProviderClass(node: ts.ClassDeclaration, context: ResolvedNgModule|ResolvedDirective) {
|
||||
if (this.visitedProviderClasses.has(node)) {
|
||||
return;
|
||||
}
|
||||
this.visitedProviderClasses.add(node);
|
||||
|
||||
const sourceFile = node.getSourceFile();
|
||||
|
||||
// We cannot migrate provider classes outside of source files. This is because the
|
||||
// migration for third-party library files should happen in "ngcc", and in general
|
||||
// would also involve metadata parsing.
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ngDecorators =
|
||||
node.decorators ? getAngularDecorators(this.typeChecker, node.decorators) : null;
|
||||
|
||||
if (ngDecorators !== null &&
|
||||
ngDecorators.some(d => NO_MIGRATE_DECORATORS.indexOf(d.name) !== -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateRecorder = this.getUpdateRecorder(sourceFile);
|
||||
const importExpr =
|
||||
this.importManager.addImportToSourceFile(sourceFile, 'Injectable', '@angular/core');
|
||||
const newDecoratorExpr = ts.createDecorator(ts.createCall(importExpr, undefined, undefined));
|
||||
const newDecoratorText =
|
||||
this.printer.printNode(ts.EmitHint.Unspecified, newDecoratorExpr, sourceFile);
|
||||
|
||||
|
||||
// In case the class is already decorated with "@Inject(..)", we replace the "@Inject"
|
||||
// decorator with "@Injectable()" since using "@Inject(..)" on a class is a noop and
|
||||
// most likely was meant to be "@Injectable()".
|
||||
const existingInjectDecorator =
|
||||
ngDecorators !== null ? ngDecorators.find(d => d.name === 'Inject') : null;
|
||||
if (existingInjectDecorator) {
|
||||
updateRecorder.replaceDecorator(existingInjectDecorator.node, newDecoratorText, context.name);
|
||||
} else {
|
||||
updateRecorder.addClassDecorator(node, newDecoratorText, context.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates object literal providers which do not use "useValue", "useClass",
|
||||
* "useExisting" or "useFactory". These providers behave differently in Ivy. e.g.
|
||||
*
|
||||
* ```ts
|
||||
* {provide: X} -> {provide: X, useValue: undefined} // this is how it behaves in VE
|
||||
* {provide: X} -> {provide: X, useClass: X} // this is how it behaves in Ivy
|
||||
* ```
|
||||
*
|
||||
* To ensure forward compatibility, we migrate these empty object literal providers
|
||||
* to explicitly use `useValue: undefined`.
|
||||
*/
|
||||
private _migrateLiteralProviders(literals: ProviderLiteral[]) {
|
||||
for (let {node, resolvedValue} of literals) {
|
||||
if (this.visitedProviderLiterals.has(node)) {
|
||||
continue;
|
||||
}
|
||||
this.visitedProviderLiterals.add(node);
|
||||
|
||||
if (!resolvedValue || !(resolvedValue instanceof Map) || !resolvedValue.has('provide') ||
|
||||
resolvedValue.has('useClass') || resolvedValue.has('useValue') ||
|
||||
resolvedValue.has('useExisting') || resolvedValue.has('useFactory')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceFile = node.getSourceFile();
|
||||
const newObjectLiteral = ts.updateObjectLiteral(
|
||||
node,
|
||||
node.properties.concat(
|
||||
ts.createPropertyAssignment('useValue', ts.createIdentifier('undefined'))));
|
||||
|
||||
this.getUpdateRecorder(sourceFile)
|
||||
.updateObjectLiteral(
|
||||
node, this.printer.printNode(ts.EmitHint.Unspecified, newObjectLiteral, sourceFile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits the given resolved value of a provider. Providers can be nested in
|
||||
* arrays and we need to recursively walk through the providers to be able to
|
||||
* migrate all referenced provider classes. e.g. "providers: [[A, [B]]]".
|
||||
*/
|
||||
private _visitProviderResolvedValue(value: ResolvedValue, module: ResolvedNgModule):
|
||||
AnalysisFailure[] {
|
||||
if (value instanceof this.compilerCliMigrationsModule.Reference &&
|
||||
ts.isClassDeclaration(value.node)) {
|
||||
this.migrateProviderClass(value.node, module);
|
||||
} else if (value instanceof Map) {
|
||||
// If a "ClassProvider" has the "deps" property set, then we do not need to
|
||||
// decorate the class. This is because the class is instantiated through the
|
||||
// specified "deps" and the class does not need a factory definition.
|
||||
if (value.has('provide') && value.has('useClass') && value.get('deps') == null) {
|
||||
return this._visitProviderResolvedValue(value.get('useClass')!, module);
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
return value.reduce(
|
||||
(res, v) => res.concat(this._visitProviderResolvedValue(v, module)),
|
||||
[] as AnalysisFailure[]);
|
||||
} else if (value instanceof this.compilerCliMigrationsModule.DynamicValue) {
|
||||
return [{node: value.node, message: `Provider is not statically analyzable.`}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +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 ts from 'typescript';
|
||||
import {ImportManagerUpdateRecorder} from '../../utils/import_manager';
|
||||
|
||||
/**
|
||||
* Update recorder interface that is used to transform source files in a non-colliding
|
||||
* way. Also this indirection makes it possible to re-use logic for both TSLint rules
|
||||
* and CLI devkit schematic updates.
|
||||
*/
|
||||
export interface UpdateRecorder extends ImportManagerUpdateRecorder {
|
||||
addClassDecorator(node: ts.ClassDeclaration, text: string, className: string): void;
|
||||
replaceDecorator(node: ts.Decorator, newText: string, className: string): void;
|
||||
updateObjectLiteral(node: ts.ObjectLiteralExpression, newText: string): void;
|
||||
commitUpdate(): void;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "module-with-providers",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/compiler-cli/private",
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
## ModuleWithProviders migration
|
||||
|
||||
`ModuleWithProviders` type will not default to the `any` type for its generic in a future version of Angular.
|
||||
This migration adds a generic to any `ModuleWithProvider` types found.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { NgModule, ModuleWithProviders } from '@angular/core';
|
||||
|
||||
@NgModule({})
|
||||
export class MyModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
ngModule: MyModule
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { NgModule, ModuleWithProviders } from '@angular/core';
|
||||
|
||||
@NgModule({})
|
||||
export class MyModule {
|
||||
static forRoot(): ModuleWithProviders<MyModule> {
|
||||
ngModule: MyModule
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,80 +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 ts from 'typescript';
|
||||
|
||||
import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators';
|
||||
|
||||
import {isModuleWithProvidersNotGeneric} from './util';
|
||||
|
||||
export interface ResolvedNgModule {
|
||||
name: string;
|
||||
node: ts.ClassDeclaration;
|
||||
decorator: NgDecorator;
|
||||
/**
|
||||
* List of found static method declarations on the module which do not
|
||||
* declare an explicit return type.
|
||||
*/
|
||||
staticMethodsWithoutType: ts.MethodDeclaration[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that walks through specified TypeScript nodes and collects all
|
||||
* found NgModule static methods without types and all ModuleWithProviders
|
||||
* usages without generic types attached.
|
||||
*/
|
||||
export class Collector {
|
||||
resolvedModules: ResolvedNgModule[] = [];
|
||||
resolvedNonGenerics: ts.TypeReferenceNode[] = [];
|
||||
|
||||
constructor(public typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
this.visitClassDeclaration(node);
|
||||
} else if (isModuleWithProvidersNotGeneric(this.typeChecker, node)) {
|
||||
this.resolvedNonGenerics.push(node);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, n => this.visitNode(n));
|
||||
}
|
||||
|
||||
private visitClassDeclaration(node: ts.ClassDeclaration) {
|
||||
if (!node.decorators || !node.decorators.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators);
|
||||
const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule');
|
||||
|
||||
if (ngModuleDecorator) {
|
||||
this._visitNgModuleClass(node, ngModuleDecorator);
|
||||
}
|
||||
}
|
||||
|
||||
private _visitNgModuleClass(node: ts.ClassDeclaration, decorator: NgDecorator) {
|
||||
const decoratorCall = decorator.node.expression;
|
||||
const metadata = decoratorCall.arguments[0];
|
||||
|
||||
if (!metadata || !ts.isObjectLiteralExpression(metadata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resolvedModules.push({
|
||||
name: node.name ? node.name.text : 'default',
|
||||
node,
|
||||
decorator,
|
||||
staticMethodsWithoutType: node.members.filter(isStaticMethodNoType),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isStaticMethodNoType(node: ts.ClassElement): node is ts.MethodDeclaration {
|
||||
return ts.isMethodDeclaration(node) && !!node.modifiers &&
|
||||
node.modifiers.findIndex(m => m.kind === ts.SyntaxKind.StaticKeyword) > -1 && !node.type;
|
||||
}
|
||||
|
|
@ -1,106 +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 {Rule, SchematicContext, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm';
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
|
||||
import {Collector} from './collector';
|
||||
import {AnalysisFailure, ModuleWithProvidersTransform} from './transform';
|
||||
|
||||
|
||||
/**
|
||||
* Runs the ModuleWithProviders migration for all TypeScript projects in the current CLI workspace.
|
||||
*/
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree, ctx: SchematicContext) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
const failures: string[] = [];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate ModuleWithProviders.');
|
||||
}
|
||||
|
||||
let compilerCliMigrationsModule;
|
||||
try {
|
||||
// Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import
|
||||
// workaround. Once TypeScript provides support for keeping the dynamic import this workaround
|
||||
// can be changed to a direct dynamic import.
|
||||
compilerCliMigrationsModule = await loadCompilerCliMigrationsModule();
|
||||
} catch (e) {
|
||||
throw new SchematicsException(
|
||||
`Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`);
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
failures.push(...runModuleWithProvidersMigration(
|
||||
tree, tsconfigPath, basePath, compilerCliMigrationsModule));
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
ctx.logger.info('Could not migrate all instances of ModuleWithProviders');
|
||||
ctx.logger.info('Please manually fix the following failures:');
|
||||
failures.forEach(message => ctx.logger.warn(`⮑ ${message}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runModuleWithProvidersMigration(
|
||||
tree: Tree, tsconfigPath: string, basePath: string,
|
||||
compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations')) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const failures: string[] = [];
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const collector = new Collector(typeChecker);
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
// Analyze source files by detecting all modules.
|
||||
sourceFiles.forEach(sourceFile => collector.visitNode(sourceFile));
|
||||
|
||||
const {resolvedModules, resolvedNonGenerics} = collector;
|
||||
const transformer =
|
||||
new ModuleWithProvidersTransform(typeChecker, getUpdateRecorder, compilerCliMigrationsModule);
|
||||
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
|
||||
|
||||
[...resolvedModules.reduce(
|
||||
(failures, m) => failures.concat(transformer.migrateModule(m)), [] as AnalysisFailure[]),
|
||||
...resolvedNonGenerics.reduce(
|
||||
(failures, t) => failures.concat(transformer.migrateType(t)), [] as AnalysisFailure[])]
|
||||
.forEach(({message, node}) => {
|
||||
const nodeSourceFile = node.getSourceFile();
|
||||
const relativeFilePath = relative(basePath, nodeSourceFile.fileName);
|
||||
const {line, character} =
|
||||
ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart());
|
||||
failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`);
|
||||
});
|
||||
|
||||
// Walk through each update recorder and commit the update. We need to commit the
|
||||
// updates in batches per source file as there can be only one recorder per source
|
||||
// file in order to avoid shift character offsets.
|
||||
updateRecorders.forEach(recorder => tree.commitUpdate(recorder));
|
||||
|
||||
return failures;
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const recorder = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +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 {UpdateRecorder} from '@angular-devkit/schematics';
|
||||
import type {ResolvedValue, ResolvedValueMap} from '@angular/compiler-cli/private/migrations';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {ResolvedNgModule} from './collector';
|
||||
import {createModuleWithProvidersType} from './util';
|
||||
|
||||
export interface AnalysisFailure {
|
||||
node: ts.Node;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const TODO_COMMENT = 'TODO: The following node requires a generic type for `ModuleWithProviders`';
|
||||
|
||||
export class ModuleWithProvidersTransform {
|
||||
private printer = ts.createPrinter();
|
||||
private partialEvaluator = new this.compilerCliMigrationsModule.PartialEvaluator(
|
||||
new this.compilerCliMigrationsModule.TypeScriptReflectionHost(this.typeChecker),
|
||||
this.typeChecker,
|
||||
/* dependencyTracker */ null);
|
||||
|
||||
constructor(
|
||||
private typeChecker: ts.TypeChecker,
|
||||
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder,
|
||||
private compilerCliMigrationsModule:
|
||||
typeof import('@angular/compiler-cli/private/migrations')) {}
|
||||
|
||||
/** Migrates a given NgModule by walking through the referenced providers and static methods. */
|
||||
migrateModule(module: ResolvedNgModule): AnalysisFailure[] {
|
||||
return module.staticMethodsWithoutType.map(this._migrateStaticNgModuleMethod.bind(this))
|
||||
.filter(v => v) as AnalysisFailure[];
|
||||
}
|
||||
|
||||
/** Migrates a ModuleWithProviders type definition that has no explicit generic type */
|
||||
migrateType(type: ts.TypeReferenceNode): AnalysisFailure[] {
|
||||
const parent = type.parent;
|
||||
let moduleText: string|undefined;
|
||||
if ((ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent)) && parent.body) {
|
||||
const returnStatement = parent.body.statements.find(ts.isReturnStatement);
|
||||
|
||||
// No return type found, exit
|
||||
if (!returnStatement || !returnStatement.expression) {
|
||||
return [{node: parent, message: `Return type is not statically analyzable.`}];
|
||||
}
|
||||
|
||||
moduleText = this._getNgModuleTypeOfExpression(returnStatement.expression);
|
||||
} else if (ts.isPropertyDeclaration(parent) || ts.isVariableDeclaration(parent)) {
|
||||
if (!parent.initializer) {
|
||||
addTodoToNode(type, TODO_COMMENT);
|
||||
this._updateNode(type, type);
|
||||
return [{node: parent, message: `Unable to determine type for declaration.`}];
|
||||
}
|
||||
|
||||
moduleText = this._getNgModuleTypeOfExpression(parent.initializer);
|
||||
}
|
||||
|
||||
if (moduleText) {
|
||||
this._addGenericToTypeReference(type, moduleText);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{node: parent, message: `Type is not statically analyzable.`}];
|
||||
}
|
||||
|
||||
/** Add a given generic to a type reference node */
|
||||
private _addGenericToTypeReference(node: ts.TypeReferenceNode, typeName: string) {
|
||||
const newGenericExpr = createModuleWithProvidersType(typeName, node);
|
||||
this._updateNode(node, newGenericExpr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a given static method if its ModuleWithProviders does not provide
|
||||
* a generic type.
|
||||
*/
|
||||
private _updateStaticMethodType(method: ts.MethodDeclaration, typeName: string) {
|
||||
const newGenericExpr =
|
||||
createModuleWithProvidersType(typeName, method.type as ts.TypeReferenceNode);
|
||||
const newMethodDecl = ts.updateMethod(
|
||||
method, method.decorators, method.modifiers, method.asteriskToken, method.name,
|
||||
method.questionToken, method.typeParameters, method.parameters, newGenericExpr,
|
||||
method.body);
|
||||
|
||||
this._updateNode(method, newMethodDecl);
|
||||
}
|
||||
|
||||
/** Whether the resolved value map represents a ModuleWithProviders object */
|
||||
isModuleWithProvidersType(value: ResolvedValueMap): boolean {
|
||||
const ngModule = value.get('ngModule') !== undefined;
|
||||
const providers = value.get('providers') !== undefined;
|
||||
|
||||
return ngModule && (value.size === 1 || (providers && value.size === 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the generic type of a suspected ModuleWithProviders return type and add it
|
||||
* explicitly
|
||||
*/
|
||||
private _migrateStaticNgModuleMethod(node: ts.MethodDeclaration): AnalysisFailure|null {
|
||||
const returnStatement = node.body &&
|
||||
node.body.statements.find(n => ts.isReturnStatement(n)) as ts.ReturnStatement | undefined;
|
||||
|
||||
// No return type found, exit
|
||||
if (!returnStatement || !returnStatement.expression) {
|
||||
return {node: node, message: `Return type is not statically analyzable.`};
|
||||
}
|
||||
|
||||
const moduleText = this._getNgModuleTypeOfExpression(returnStatement.expression);
|
||||
|
||||
if (moduleText) {
|
||||
this._updateStaticMethodType(node, moduleText);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {node: node, message: `Method type is not statically analyzable.`};
|
||||
}
|
||||
|
||||
/** Evaluate and return the ngModule type from an expression */
|
||||
private _getNgModuleTypeOfExpression(expr: ts.Expression): string|undefined {
|
||||
const evaluatedExpr = this.partialEvaluator.evaluate(expr);
|
||||
return this._getTypeOfResolvedValue(evaluatedExpr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits a given object literal expression to determine the ngModule type. If the expression
|
||||
* cannot be resolved, add a TODO to alert the user.
|
||||
*/
|
||||
private _getTypeOfResolvedValue(value: ResolvedValue): string|undefined {
|
||||
if (value instanceof Map && this.isModuleWithProvidersType(value)) {
|
||||
const mapValue = value.get('ngModule')!;
|
||||
if (mapValue instanceof this.compilerCliMigrationsModule.Reference &&
|
||||
ts.isClassDeclaration(mapValue.node) && mapValue.node.name) {
|
||||
return mapValue.node.name.text;
|
||||
} else if (mapValue instanceof this.compilerCliMigrationsModule.DynamicValue) {
|
||||
addTodoToNode(mapValue.node, TODO_COMMENT);
|
||||
this._updateNode(mapValue.node, mapValue.node);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _updateNode(node: ts.Node, newNode: ts.Node) {
|
||||
const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile());
|
||||
const recorder = this.getUpdateRecorder(node.getSourceFile());
|
||||
|
||||
recorder.remove(node.getStart(), node.getWidth());
|
||||
recorder.insertRight(node.getStart(), newText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a to-do to the given TypeScript node which alerts developers to fix
|
||||
* potential issues identified by the migration.
|
||||
*/
|
||||
function addTodoToNode(node: ts.Node, text: string) {
|
||||
ts.setSyntheticLeadingComments(node, [{
|
||||
pos: -1,
|
||||
end: -1,
|
||||
hasTrailingNewLine: false,
|
||||
kind: ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
text: ` ${text} `
|
||||
}]);
|
||||
}
|
||||
|
|
@ -1,31 +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 ts from 'typescript';
|
||||
import {getImportOfIdentifier} from '../../utils/typescript/imports';
|
||||
|
||||
/** Add a generic type to a type reference. */
|
||||
export function createModuleWithProvidersType(
|
||||
type: string, node?: ts.TypeReferenceNode): ts.TypeReferenceNode {
|
||||
const typeNode = node || ts.createTypeReferenceNode('ModuleWithProviders', []);
|
||||
const typeReferenceNode = ts.createTypeReferenceNode(ts.createIdentifier(type), []);
|
||||
return ts.updateTypeReferenceNode(
|
||||
typeNode, typeNode.typeName, ts.createNodeArray([typeReferenceNode]));
|
||||
}
|
||||
|
||||
/** Determine whether a node is a ModuleWithProviders type reference node without a generic type */
|
||||
export function isModuleWithProvidersNotGeneric(
|
||||
typeChecker: ts.TypeChecker, node: ts.Node): node is ts.TypeReferenceNode {
|
||||
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const imp = getImportOfIdentifier(typeChecker, node.typeName);
|
||||
return !!imp && imp.name === 'ModuleWithProviders' && imp.importModule === '@angular/core' &&
|
||||
!node.typeArguments;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "move-document",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/migrations/move-document/google3:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,72 +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 ts from 'typescript';
|
||||
|
||||
export const COMMON_IMPORT = '@angular/common';
|
||||
export const PLATFORM_BROWSER_IMPORT = '@angular/platform-browser';
|
||||
export const DOCUMENT_TOKEN_NAME = 'DOCUMENT';
|
||||
|
||||
/** This contains the metadata necessary to move items from one import to another */
|
||||
export interface ResolvedDocumentImport {
|
||||
platformBrowserImport: ts.NamedImports|null;
|
||||
commonImport: ts.NamedImports|null;
|
||||
documentElement: ts.ImportSpecifier|null;
|
||||
}
|
||||
|
||||
/** Visitor that can be used to find a set of imports in a TypeScript file. */
|
||||
export class DocumentImportVisitor {
|
||||
importsMap: Map<ts.SourceFile, ResolvedDocumentImport> = new Map();
|
||||
|
||||
constructor(public typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
if (ts.isNamedImports(node)) {
|
||||
this.visitNamedImport(node);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, node => this.visitNode(node));
|
||||
}
|
||||
|
||||
private visitNamedImport(node: ts.NamedImports) {
|
||||
if (!node.elements || !node.elements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importDeclaration = node.parent.parent;
|
||||
// If this is not a StringLiteral it will be a grammar error
|
||||
const moduleSpecifier = importDeclaration.moduleSpecifier as ts.StringLiteral;
|
||||
const sourceFile = node.getSourceFile();
|
||||
let imports = this.importsMap.get(sourceFile);
|
||||
if (!imports) {
|
||||
imports = {
|
||||
platformBrowserImport: null,
|
||||
commonImport: null,
|
||||
documentElement: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (moduleSpecifier.text === PLATFORM_BROWSER_IMPORT) {
|
||||
const documentElement = this.getDocumentElement(node);
|
||||
if (documentElement) {
|
||||
imports.platformBrowserImport = node;
|
||||
imports.documentElement = documentElement;
|
||||
}
|
||||
} else if (moduleSpecifier.text === COMMON_IMPORT) {
|
||||
imports.commonImport = node;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this.importsMap.set(sourceFile, imports);
|
||||
}
|
||||
|
||||
private getDocumentElement(node: ts.NamedImports): ts.ImportSpecifier|undefined {
|
||||
const elements = node.elements;
|
||||
return elements.find(el => (el.propertyName || el.name).escapedText === DOCUMENT_TOKEN_NAME);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
|
||||
import {COMMON_IMPORT, DOCUMENT_TOKEN_NAME, DocumentImportVisitor, ResolvedDocumentImport} from './document_import_visitor';
|
||||
import {addToImport, createImport, removeFromImport} from './move-import';
|
||||
|
||||
|
||||
/** Entry point for the V8 move-document migration. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
|
||||
if (!buildPaths.length && !testPaths.length) {
|
||||
throw new SchematicsException(`Could not find any tsconfig file. Cannot migrate DOCUMENT
|
||||
to new import source.`);
|
||||
}
|
||||
|
||||
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
|
||||
runMoveDocumentMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the DOCUMENT InjectionToken import migration for the given TypeScript project. The
|
||||
* schematic analyzes the imports within the project and moves the deprecated symbol to the
|
||||
* new import source.
|
||||
*/
|
||||
function runMoveDocumentMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const visitor = new DocumentImportVisitor(typeChecker);
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
// Analyze source files by finding imports.
|
||||
sourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile));
|
||||
|
||||
const {importsMap} = visitor;
|
||||
|
||||
// Walk through all source files that contain resolved queries and update
|
||||
// the source files if needed. Note that we need to update multiple queries
|
||||
// within a source file within the same recorder in order to not throw off
|
||||
// the TypeScript node offsets.
|
||||
importsMap.forEach((resolvedImport: ResolvedDocumentImport, sourceFile: ts.SourceFile) => {
|
||||
const {platformBrowserImport, commonImport, documentElement} = resolvedImport;
|
||||
if (!documentElement || !platformBrowserImport) {
|
||||
return;
|
||||
}
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
|
||||
const platformBrowserDeclaration = platformBrowserImport.parent.parent;
|
||||
const newPlatformBrowserText =
|
||||
removeFromImport(platformBrowserImport, sourceFile, DOCUMENT_TOKEN_NAME);
|
||||
const newCommonText = commonImport ?
|
||||
addToImport(commonImport, sourceFile, documentElement.name, documentElement.propertyName) :
|
||||
createImport(COMMON_IMPORT, sourceFile, documentElement.name, documentElement.propertyName);
|
||||
|
||||
// Replace the existing query decorator call expression with the updated
|
||||
// call expression node.
|
||||
update.remove(platformBrowserDeclaration.getStart(), platformBrowserDeclaration.getWidth());
|
||||
update.insertRight(platformBrowserDeclaration.getStart(), newPlatformBrowserText);
|
||||
|
||||
if (commonImport) {
|
||||
const commonDeclaration = commonImport.parent.parent;
|
||||
update.remove(commonDeclaration.getStart(), commonDeclaration.getWidth());
|
||||
update.insertRight(commonDeclaration.getStart(), newCommonText);
|
||||
} else {
|
||||
update.insertRight(platformBrowserDeclaration.getStart(), newCommonText);
|
||||
}
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,64 +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 ts from 'typescript';
|
||||
|
||||
export function removeFromImport(
|
||||
importNode: ts.NamedImports, sourceFile: ts.SourceFile, importName: string): string {
|
||||
const printer = ts.createPrinter();
|
||||
const elements = importNode.elements.filter(
|
||||
el => String((el.propertyName || el.name).escapedText) !== importName);
|
||||
|
||||
if (!elements.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const oldDeclaration = importNode.parent.parent;
|
||||
const newImport = ts.createNamedImports(elements);
|
||||
const importClause = ts.createImportClause(undefined, newImport);
|
||||
const newDeclaration = ts.createImportDeclaration(
|
||||
undefined, undefined, importClause, oldDeclaration.moduleSpecifier);
|
||||
|
||||
return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile);
|
||||
}
|
||||
|
||||
export function addToImport(
|
||||
importNode: ts.NamedImports, sourceFile: ts.SourceFile, name: ts.Identifier,
|
||||
propertyName?: ts.Identifier): string {
|
||||
const printer = ts.createPrinter();
|
||||
const propertyNameIdentifier =
|
||||
propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined;
|
||||
const nameIdentifier = ts.createIdentifier(String(name.escapedText));
|
||||
const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier);
|
||||
const elements = [...importNode.elements];
|
||||
|
||||
elements.push(newSpecfier);
|
||||
|
||||
const oldDeclaration = importNode.parent.parent;
|
||||
const newImport = ts.createNamedImports(elements);
|
||||
const importClause = ts.createImportClause(undefined, newImport);
|
||||
const newDeclaration = ts.createImportDeclaration(
|
||||
undefined, undefined, importClause, oldDeclaration.moduleSpecifier);
|
||||
|
||||
return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile);
|
||||
}
|
||||
|
||||
export function createImport(
|
||||
importSource: string, sourceFile: ts.SourceFile, name: ts.Identifier,
|
||||
propertyName?: ts.Identifier) {
|
||||
const printer = ts.createPrinter();
|
||||
const propertyNameIdentifier =
|
||||
propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined;
|
||||
const nameIdentifier = ts.createIdentifier(String(name.escapedText));
|
||||
const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier);
|
||||
const newNamedImports = ts.createNamedImports([newSpecfier]);
|
||||
const importClause = ts.createImportClause(undefined, newNamedImports);
|
||||
const moduleSpecifier = ts.createStringLiteral(importSource);
|
||||
const newImport = ts.createImportDeclaration(undefined, undefined, importClause, moduleSpecifier);
|
||||
|
||||
return printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "native-view-encapsulation",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
## `ViewEncapsulation.Native` migration
|
||||
|
||||
Automatically migrates usages of `ViewEncapsulation.Native` to `ViewEncapsulation.ShadowDom`.
|
||||
For most practical purposes the `Native` mode is compatible with the `ShadowDom` mode.
|
||||
|
||||
The migration covers any reference to the `Native` value that can be traced to `@angular/core`.
|
||||
Some examples:
|
||||
* Inside the `encapsulation` property of `Component` decorators.
|
||||
* In property assignments for the `COMPILER_OPTIONS` provider.
|
||||
* In variables.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '...',
|
||||
encapsulation: ViewEncapsulation.Native
|
||||
})
|
||||
export class App {
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '...',
|
||||
encapsulation: ViewEncapsulation.ShadowDom
|
||||
})
|
||||
export class App {
|
||||
}
|
||||
```
|
||||
|
|
@ -1,52 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {findNativeEncapsulationNodes} from './util';
|
||||
|
||||
|
||||
/** Migration that switches from `ViewEncapsulation.Native` to `ViewEncapsulation.ShadowDom`. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate away from Native view encapsulation.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runNativeViewEncapsulationMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runNativeViewEncapsulationMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
const identifiers = findNativeEncapsulationNodes(typeChecker, sourceFile);
|
||||
|
||||
identifiers.forEach(node => {
|
||||
update.remove(node.getStart(), node.getWidth());
|
||||
update.insertRight(node.getStart(), 'ShadowDom');
|
||||
});
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,38 +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 ts from 'typescript';
|
||||
|
||||
import {getImportOfIdentifier} from '../../utils/typescript/imports';
|
||||
|
||||
/** Finds all the Identifier nodes in a file that refer to `Native` view encapsulation. */
|
||||
export function findNativeEncapsulationNodes(
|
||||
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): Set<ts.Identifier> {
|
||||
const results = new Set<ts.Identifier>();
|
||||
|
||||
sourceFile.forEachChild(function walkNode(node: ts.Node) {
|
||||
// Note that we look directly for nodes in the form of `<something>.Native`, rather than going
|
||||
// for `Component` class decorators, because it's much simpler and it allows us to handle cases
|
||||
// where `ViewEncapsulation.Native` might be used in a different context (e.g. a variable).
|
||||
// Using the encapsulation outside of a decorator is an edge case, but we do have public APIs
|
||||
// where it can be passed in (see the `defaultViewEncapsulation` property on the
|
||||
// `COMPILER_OPTIONS` provider).
|
||||
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) &&
|
||||
node.name.text === 'Native' && ts.isIdentifier(node.expression)) {
|
||||
const expressionImport = getImportOfIdentifier(typeChecker, node.expression);
|
||||
if (expressionImport && expressionImport.name === 'ViewEncapsulation' &&
|
||||
expressionImport.importModule === '@angular/core') {
|
||||
results.add(node.name);
|
||||
}
|
||||
} else {
|
||||
node.forEachChild(walkNode);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "navigation-extras-omissions",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
## Router.navigateByUrl and Router.createUrlTree extras migration
|
||||
|
||||
Previously the `extras` parameter of `Router.navigateByUrl` and `Router.createUrlTree` accepted the
|
||||
full `NavigationExtras` object, even though only a subset of properties was supported. This
|
||||
migration removes the unsupported properties from the relevant method call sites.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({})
|
||||
export class MyComponent {
|
||||
constructor(private _router: Router) {}
|
||||
|
||||
goHome() {
|
||||
this._router.navigateByUrl('/', {skipLocationChange: false, fragment: 'foo'});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({})
|
||||
export class MyComponent {
|
||||
constructor(private _router: Router) {}
|
||||
|
||||
goHome() {
|
||||
this._router.navigateByUrl('/', { /* Removed unsupported properties by Angular migration: fragment. */ skipLocationChange: false });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,61 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {findLiteralsToMigrate, migrateLiteral} from './util';
|
||||
|
||||
|
||||
/** Migration that switches `Router.navigateByUrl` and `Router.createUrlTree` to a new signature. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate ' +
|
||||
'Router.navigateByUrl and Router.createUrlTree calls.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runNavigationExtrasOmissionsMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runNavigationExtrasOmissionsMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const printer = ts.createPrinter();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker);
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
|
||||
literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => {
|
||||
const migratedNode = migrateLiteral(methodName, instance);
|
||||
|
||||
if (migratedNode !== instance) {
|
||||
update.remove(instance.getStart(), instance.getWidth());
|
||||
update.insertRight(
|
||||
instance.getStart(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile));
|
||||
}
|
||||
}));
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,125 +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 ts from 'typescript';
|
||||
|
||||
import {getImportSpecifier} from '../../utils/typescript/imports';
|
||||
import {isReferenceToImport} from '../../utils/typescript/symbol';
|
||||
|
||||
/**
|
||||
* Configures the methods that the migration should be looking for
|
||||
* and the properties from `NavigationExtras` that should be preserved.
|
||||
*/
|
||||
const methodConfig = new Map<string, Set<string>>([
|
||||
['navigateByUrl', new Set<string>(['skipLocationChange', 'replaceUrl', 'state'])],
|
||||
[
|
||||
'createUrlTree', new Set<string>([
|
||||
'relativeTo', 'queryParams', 'fragment', 'preserveQueryParams', 'queryParamsHandling',
|
||||
'preserveFragment'
|
||||
])
|
||||
]
|
||||
]);
|
||||
|
||||
export function migrateLiteral(
|
||||
methodName: string, node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression {
|
||||
const allowedProperties = methodConfig.get(methodName);
|
||||
|
||||
if (!allowedProperties) {
|
||||
throw Error(`Attempting to migrate unconfigured method called ${methodName}.`);
|
||||
}
|
||||
|
||||
const propertiesToKeep: ts.ObjectLiteralElementLike[] = [];
|
||||
const removedPropertyNames: string[] = [];
|
||||
|
||||
node.properties.forEach(property => {
|
||||
// Only look for regular and shorthand property assignments since resolving things
|
||||
// like spread operators becomes too complicated for this migration.
|
||||
if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
||||
(ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) ||
|
||||
ts.isIdentifier(property.name))) {
|
||||
if (allowedProperties.has(property.name.text)) {
|
||||
propertiesToKeep.push(property);
|
||||
} else {
|
||||
removedPropertyNames.push(property.name.text);
|
||||
}
|
||||
} else {
|
||||
propertiesToKeep.push(property);
|
||||
}
|
||||
});
|
||||
|
||||
// Don't modify the node if there's nothing to remove.
|
||||
if (removedPropertyNames.length === 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Note that the trailing/leading spaces are necessary so the comment looks good.
|
||||
const removalComment =
|
||||
` Removed unsupported properties by Angular migration: ${removedPropertyNames.join(', ')}. `;
|
||||
|
||||
if (propertiesToKeep.length > 0) {
|
||||
propertiesToKeep[0] = addUniqueLeadingComment(propertiesToKeep[0], removalComment);
|
||||
return ts.createObjectLiteral(propertiesToKeep);
|
||||
} else {
|
||||
return addUniqueLeadingComment(ts.createObjectLiteral(propertiesToKeep), removalComment);
|
||||
}
|
||||
}
|
||||
|
||||
export function findLiteralsToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) {
|
||||
const results = new Map<string, Set<ts.ObjectLiteralExpression>>(
|
||||
Array.from(methodConfig.keys(), key => [key, new Set()]));
|
||||
const routerImport = getImportSpecifier(sourceFile, '@angular/router', 'Router');
|
||||
const seenLiterals = new Map<ts.ObjectLiteralExpression, string>();
|
||||
|
||||
if (routerImport) {
|
||||
sourceFile.forEachChild(function visitNode(node: ts.Node) {
|
||||
// Look for calls that look like `foo.<method to migrate>` with more than one parameter.
|
||||
if (ts.isCallExpression(node) && node.arguments.length > 1 &&
|
||||
ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name) &&
|
||||
methodConfig.has(node.expression.name.text)) {
|
||||
// Check whether the type of the object on which the
|
||||
// function is called refers to the Router import.
|
||||
if (isReferenceToImport(typeChecker, node.expression.expression, routerImport)) {
|
||||
const methodName = node.expression.name.text;
|
||||
const parameterDeclaration =
|
||||
typeChecker.getTypeAtLocation(node.arguments[1]).getSymbol()?.valueDeclaration;
|
||||
|
||||
// Find the source of the object literal.
|
||||
if (parameterDeclaration && ts.isObjectLiteralExpression(parameterDeclaration)) {
|
||||
if (!seenLiterals.has(parameterDeclaration)) {
|
||||
results.get(methodName)!.add(parameterDeclaration);
|
||||
seenLiterals.set(parameterDeclaration, methodName);
|
||||
// If the same literal has been passed into multiple different methods, we can't
|
||||
// migrate it, because the supported properties are different. When we detect such
|
||||
// a case, we drop it from the results so that it gets ignored. If it's used multiple
|
||||
// times for the same method, it can still be migrated.
|
||||
} else if (seenLiterals.get(parameterDeclaration) !== methodName) {
|
||||
results.forEach(literals => literals.delete(parameterDeclaration));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node.forEachChild(visitNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Adds a leading comment to a node, if the node doesn't have such a comment already. */
|
||||
function addUniqueLeadingComment<T extends ts.Node>(node: T, comment: string): T {
|
||||
const existingComments = ts.getSyntheticLeadingComments(node);
|
||||
|
||||
// This logic is primarily to ensure that we don't add the same comment multiple
|
||||
// times when tslint runs over the same file again with outdated information.
|
||||
if (!existingComments || existingComments.every(c => c.text !== comment)) {
|
||||
return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, comment);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "relative-link-resolution",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/migrations/google3:__pkg__",
|
||||
"//packages/core/schematics/migrations/relative-link-resolution/google3:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/compiler-cli",
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
## relativeLinkResolution migration
|
||||
|
||||
The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'.
|
||||
This migration updates `RouterModule` configurations that use the default value to
|
||||
now specifically use 'legacy' to prevent breakages when updating.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(ROUTES),
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(ROUTES, {relativeLinkResolution: 'legacy'}),
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
```
|
||||
|
|
@ -1,88 +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 ts from 'typescript';
|
||||
|
||||
import {isExtraOptions, isRouterModuleForRoot} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* Visitor that walks through specified TypeScript nodes and collects all
|
||||
* found ExtraOptions#RelativeLinkResolution assignments.
|
||||
*/
|
||||
export class RelativeLinkResolutionCollector {
|
||||
readonly forRootCalls: ts.CallExpression[] = [];
|
||||
readonly extraOptionsLiterals: ts.ObjectLiteralExpression[] = [];
|
||||
|
||||
constructor(private readonly typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
let forRootCall: ts.CallExpression|null = null;
|
||||
let literal: ts.ObjectLiteralExpression|null = null;
|
||||
if (isRouterModuleForRoot(this.typeChecker, node) && node.arguments.length > 0) {
|
||||
if (node.arguments.length === 1) {
|
||||
forRootCall = node;
|
||||
} else if (ts.isObjectLiteralExpression(node.arguments[1])) {
|
||||
literal = node.arguments[1] as ts.ObjectLiteralExpression;
|
||||
} else if (ts.isIdentifier(node.arguments[1])) {
|
||||
literal = this.getLiteralNeedingMigrationFromIdentifier(node.arguments[1] as ts.Identifier);
|
||||
}
|
||||
} else if (ts.isVariableDeclaration(node)) {
|
||||
literal = this.getLiteralNeedingMigration(node);
|
||||
}
|
||||
|
||||
if (literal !== null) {
|
||||
this.extraOptionsLiterals.push(literal);
|
||||
} else if (forRootCall !== null) {
|
||||
this.forRootCalls.push(forRootCall);
|
||||
} else {
|
||||
// no match found, continue iteration
|
||||
ts.forEachChild(node, n => this.visitNode(n));
|
||||
}
|
||||
}
|
||||
|
||||
private getLiteralNeedingMigrationFromIdentifier(id: ts.Identifier): ts.ObjectLiteralExpression
|
||||
|null {
|
||||
const symbolForIdentifier = this.typeChecker.getSymbolAtLocation(id);
|
||||
if (symbolForIdentifier === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (symbolForIdentifier.declarations === undefined ||
|
||||
symbolForIdentifier.declarations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const declarationNode = symbolForIdentifier.declarations[0];
|
||||
if (!ts.isVariableDeclaration(declarationNode) || declarationNode.initializer === undefined ||
|
||||
!ts.isObjectLiteralExpression(declarationNode.initializer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return declarationNode.initializer;
|
||||
}
|
||||
|
||||
private getLiteralNeedingMigration(node: ts.VariableDeclaration): ts.ObjectLiteralExpression
|
||||
|null {
|
||||
if (node.initializer === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// declaration could be `x: ExtraOptions = {}` or `x = {} as ExtraOptions`
|
||||
if (ts.isAsExpression(node.initializer) &&
|
||||
ts.isObjectLiteralExpression(node.initializer.expression) &&
|
||||
isExtraOptions(this.typeChecker, node.initializer.type)) {
|
||||
return node.initializer.expression;
|
||||
} else if (
|
||||
node.type !== undefined && ts.isObjectLiteralExpression(node.initializer) &&
|
||||
isExtraOptions(this.typeChecker, node.type)) {
|
||||
return node.initializer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "google3",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = ["//packages/core/schematics/migrations/google3:__pkg__"],
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/relative-link-resolution",
|
||||
"@npm//tslint",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,28 +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 {Replacement, RuleFailure} from 'tslint';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {UpdateRecorder} from '../update_recorder';
|
||||
|
||||
export class TslintUpdateRecorder implements UpdateRecorder {
|
||||
failures: RuleFailure[] = [];
|
||||
|
||||
constructor(private ruleName: string, private sourceFile: ts.SourceFile) {}
|
||||
|
||||
updateNode(node: ts.Node, newText: string) {
|
||||
this.failures.unshift(new RuleFailure(
|
||||
this.sourceFile, node.getStart(), 0,
|
||||
'The relativeLinkResolution default is changing from `legacy` to `corrected`. To keep behavior consistent' +
|
||||
' when the change is merged, specify `legacy` rather than using the default.',
|
||||
this.ruleName, Replacement.replaceFromTo(node.getStart(), node.getEnd(), `${newText}`)));
|
||||
}
|
||||
|
||||
commitUpdate() {}
|
||||
}
|
||||
|
|
@ -1,74 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {RelativeLinkResolutionCollector} from './collector';
|
||||
import {RelativeLinkResolutionTransform} from './transform';
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
/** Entry point for the v11 "relativeLinkResolution RouterModule options" schematic. */
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
|
||||
if (!buildPaths.length && !testPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot update the "relativeLinkResolution" option for RouterModule');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
|
||||
runRelativeLinkResolutionMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runRelativeLinkResolutionMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const relativeLinkResolutionCollector = new RelativeLinkResolutionCollector(typeChecker);
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
// Analyze source files by detecting all modules.
|
||||
sourceFiles.forEach(sourceFile => relativeLinkResolutionCollector.visitNode(sourceFile));
|
||||
|
||||
const {forRootCalls, extraOptionsLiterals} = relativeLinkResolutionCollector;
|
||||
const transformer = new RelativeLinkResolutionTransform(getUpdateRecorder);
|
||||
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
|
||||
transformer.migrateRouterModuleForRootCalls(forRootCalls);
|
||||
transformer.migrateObjectLiterals(extraOptionsLiterals);
|
||||
|
||||
// Walk through each update recorder and commit the update. We need to commit the
|
||||
// updates in batches per source file as there can be only one recorder per source
|
||||
// file in order to avoid shift character offsets.
|
||||
updateRecorders.forEach(recorder => recorder.commitUpdate());
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile)!;
|
||||
}
|
||||
const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
const recorder: UpdateRecorder = {
|
||||
updateNode(node: ts.Node, newText: string) {
|
||||
treeRecorder.remove(node.getStart(), node.getWidth());
|
||||
treeRecorder.insertRight(node.getStart(), newText);
|
||||
},
|
||||
commitUpdate() {
|
||||
tree.commitUpdate(treeRecorder);
|
||||
}
|
||||
};
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +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 ts from 'typescript';
|
||||
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
|
||||
const RELATIVE_LINK_RESOLUTION = 'relativeLinkResolution';
|
||||
|
||||
export class RelativeLinkResolutionTransform {
|
||||
private printer = ts.createPrinter();
|
||||
|
||||
constructor(private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {}
|
||||
|
||||
/** Migrate the ExtraOptions#RelativeLinkResolution property assignments. */
|
||||
migrateRouterModuleForRootCalls(calls: ts.CallExpression[]) {
|
||||
calls.forEach(c => {
|
||||
this._updateCallExpressionWithoutExtraOptions(c);
|
||||
});
|
||||
}
|
||||
|
||||
migrateObjectLiterals(vars: ts.ObjectLiteralExpression[]) {
|
||||
vars.forEach(v => this._maybeUpdateLiteral(v));
|
||||
}
|
||||
|
||||
private _updateCallExpressionWithoutExtraOptions(callExpression: ts.CallExpression) {
|
||||
const args = callExpression.arguments;
|
||||
const emptyLiteral = ts.createObjectLiteral();
|
||||
const newNode = ts.updateCall(
|
||||
callExpression, callExpression.expression, callExpression.typeArguments,
|
||||
[args[0], this._getMigratedLiteralExpression(emptyLiteral)]);
|
||||
this._updateNode(callExpression, newNode);
|
||||
}
|
||||
|
||||
private _getMigratedLiteralExpression(literal: ts.ObjectLiteralExpression) {
|
||||
if (literal.properties.some(
|
||||
prop => ts.isPropertyAssignment(prop) &&
|
||||
prop.name.getText() === RELATIVE_LINK_RESOLUTION)) {
|
||||
// literal already defines a value for relativeLinkResolution. Skip it
|
||||
return literal;
|
||||
}
|
||||
const legacyExpression =
|
||||
ts.createPropertyAssignment(RELATIVE_LINK_RESOLUTION, ts.createIdentifier(`'legacy'`));
|
||||
return ts.updateObjectLiteral(literal, [...literal.properties, legacyExpression]);
|
||||
}
|
||||
|
||||
private _maybeUpdateLiteral(literal: ts.ObjectLiteralExpression) {
|
||||
const updatedLiteral = this._getMigratedLiteralExpression(literal);
|
||||
if (updatedLiteral !== literal) {
|
||||
this._updateNode(literal, updatedLiteral);
|
||||
}
|
||||
}
|
||||
|
||||
private _updateNode(node: ts.Node, newNode: ts.Node) {
|
||||
const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile());
|
||||
const recorder = this.getUpdateRecorder(node.getSourceFile());
|
||||
recorder.updateNode(node, newText);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +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 ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Update recorder interface that is used to transform source files in a non-colliding
|
||||
* way. Also this indirection makes it possible to re-use logic for both TSLint rules
|
||||
* and CLI devkit schematic updates.
|
||||
*/
|
||||
export interface UpdateRecorder {
|
||||
updateNode(node: ts.Node, newText: string): void;
|
||||
commitUpdate(): void;
|
||||
}
|
||||
|
|
@ -1,33 +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 ts from 'typescript';
|
||||
import {getImportOfIdentifier} from '../../utils/typescript/imports';
|
||||
|
||||
/** Determine whether a node is a ModuleWithProviders type reference node without a generic type */
|
||||
export function isRouterModuleForRoot(
|
||||
typeChecker: ts.TypeChecker, node: ts.Node): node is ts.CallExpression {
|
||||
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
||||
!ts.isIdentifier(node.expression.expression) || node.expression.name.text !== 'forRoot') {
|
||||
return false;
|
||||
}
|
||||
const imp = getImportOfIdentifier(typeChecker, node.expression.expression);
|
||||
return !!imp && imp.name === 'RouterModule' && imp.importModule === '@angular/router' &&
|
||||
!node.typeArguments;
|
||||
}
|
||||
|
||||
export function isExtraOptions(
|
||||
typeChecker: ts.TypeChecker, node: ts.Node): node is ts.TypeReferenceNode {
|
||||
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const imp = getImportOfIdentifier(typeChecker, node.typeName);
|
||||
return imp !== null && imp.name === 'ExtraOptions' && imp.importModule === '@angular/router' &&
|
||||
!node.typeArguments;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "renderer-to-renderer2",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
## Renderer -> Renderer2 migration
|
||||
|
||||
Automatically migrates from `Renderer` to `Renderer2` by changing method calls, renaming imports
|
||||
and renaming types. Tries to either map method calls directly from one renderer to the other, or
|
||||
if that's not possible, inserts custom helper functions at the bottom of the file.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Renderer, ElementRef } from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
export class MyComponent {
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
changeColor() {
|
||||
this._renderer.setElementStyle(this._element.nativeElement, 'color', 'purple');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Renderer2, ElementRef } from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
export class MyComponent {
|
||||
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
|
||||
|
||||
changeColor() {
|
||||
this._renderer.setStyle(this._element.nativeElement, 'color', 'purple');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,403 +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 ts from 'typescript';
|
||||
|
||||
/** Names of the helper functions that are supported for this migration. */
|
||||
export const enum HelperFunction {
|
||||
any = 'AnyDuringRendererMigration',
|
||||
createElement = '__ngRendererCreateElementHelper',
|
||||
createText = '__ngRendererCreateTextHelper',
|
||||
createTemplateAnchor = '__ngRendererCreateTemplateAnchorHelper',
|
||||
projectNodes = '__ngRendererProjectNodesHelper',
|
||||
animate = '__ngRendererAnimateHelper',
|
||||
destroyView = '__ngRendererDestroyViewHelper',
|
||||
detachView = '__ngRendererDetachViewHelper',
|
||||
attachViewAfter = '__ngRendererAttachViewAfterHelper',
|
||||
splitNamespace = '__ngRendererSplitNamespaceHelper',
|
||||
setElementAttribute = '__ngRendererSetElementAttributeHelper'
|
||||
}
|
||||
|
||||
/** Gets the string representation of a helper function. */
|
||||
export function getHelper(
|
||||
name: HelperFunction, sourceFile: ts.SourceFile, printer: ts.Printer): string {
|
||||
const helperDeclaration = getHelperDeclaration(name);
|
||||
return '\n' + printer.printNode(ts.EmitHint.Unspecified, helperDeclaration, sourceFile) + '\n';
|
||||
}
|
||||
|
||||
/** Creates a function declaration for the specified helper name. */
|
||||
function getHelperDeclaration(name: HelperFunction): ts.Node {
|
||||
switch (name) {
|
||||
case HelperFunction.any:
|
||||
return createAnyTypeHelper();
|
||||
case HelperFunction.createElement:
|
||||
return getCreateElementHelper();
|
||||
case HelperFunction.createText:
|
||||
return getCreateTextHelper();
|
||||
case HelperFunction.createTemplateAnchor:
|
||||
return getCreateTemplateAnchorHelper();
|
||||
case HelperFunction.projectNodes:
|
||||
return getProjectNodesHelper();
|
||||
case HelperFunction.animate:
|
||||
return getAnimateHelper();
|
||||
case HelperFunction.destroyView:
|
||||
return getDestroyViewHelper();
|
||||
case HelperFunction.detachView:
|
||||
return getDetachViewHelper();
|
||||
case HelperFunction.attachViewAfter:
|
||||
return getAttachViewAfterHelper();
|
||||
case HelperFunction.setElementAttribute:
|
||||
return getSetElementAttributeHelper();
|
||||
case HelperFunction.splitNamespace:
|
||||
return getSplitNamespaceHelper();
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a helper for a custom `any` type during the migration. */
|
||||
function createAnyTypeHelper(): ts.TypeAliasDeclaration {
|
||||
// type AnyDuringRendererMigration = any;
|
||||
return ts.createTypeAliasDeclaration(
|
||||
[], [], HelperFunction.any, [], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
}
|
||||
|
||||
/** Creates a function parameter that is typed as `any`. */
|
||||
function getAnyTypedParameter(
|
||||
parameterName: string|ts.Identifier, isRequired = true): ts.ParameterDeclaration {
|
||||
// Declare the parameter as `any` so we don't have to add extra logic to ensure that the
|
||||
// generated code will pass type checking. Use our custom `any` type so people have an incentive
|
||||
// to clean it up afterwards and to avoid potentially introducing lint warnings in G3.
|
||||
const type = ts.createTypeReferenceNode(HelperFunction.any, []);
|
||||
return ts.createParameter(
|
||||
[], [], undefined, parameterName,
|
||||
isRequired ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken), type);
|
||||
}
|
||||
|
||||
/** Creates a helper for `createElement`. */
|
||||
function getCreateElementHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const parent = ts.createIdentifier('parent');
|
||||
const namespaceAndName = ts.createIdentifier('namespaceAndName');
|
||||
const name = ts.createIdentifier('name');
|
||||
const namespace = ts.createIdentifier('namespace');
|
||||
|
||||
// [namespace, name] = splitNamespace(namespaceAndName);
|
||||
const namespaceAndNameVariable = ts.createVariableDeclaration(
|
||||
ts.createArrayBindingPattern(
|
||||
[namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))),
|
||||
undefined,
|
||||
ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName]));
|
||||
|
||||
// `renderer.createElement(name, namespace)`.
|
||||
const creationCall =
|
||||
ts.createCall(ts.createPropertyAccess(renderer, 'createElement'), [], [name, namespace]);
|
||||
|
||||
return getCreationHelper(
|
||||
HelperFunction.createElement, creationCall, renderer, parent, [namespaceAndName],
|
||||
[ts.createVariableStatement(
|
||||
undefined,
|
||||
ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const))]);
|
||||
}
|
||||
|
||||
/** Creates a helper for `createText`. */
|
||||
function getCreateTextHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const parent = ts.createIdentifier('parent');
|
||||
const value = ts.createIdentifier('value');
|
||||
|
||||
// `renderer.createText(value)`.
|
||||
const creationCall = ts.createCall(ts.createPropertyAccess(renderer, 'createText'), [], [value]);
|
||||
|
||||
return getCreationHelper(HelperFunction.createText, creationCall, renderer, parent, [value]);
|
||||
}
|
||||
|
||||
/** Creates a helper for `createTemplateAnchor`. */
|
||||
function getCreateTemplateAnchorHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const parent = ts.createIdentifier('parent');
|
||||
|
||||
// `renderer.createComment('')`.
|
||||
const creationCall = ts.createCall(
|
||||
ts.createPropertyAccess(renderer, 'createComment'), [], [ts.createStringLiteral('')]);
|
||||
|
||||
return getCreationHelper(HelperFunction.createTemplateAnchor, creationCall, renderer, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function declaration for a creation helper. This is reused between `createElement`,
|
||||
* `createText` and `createTemplateAnchor` which follow a very similar pattern.
|
||||
* @param functionName Function that the helper should have.
|
||||
* @param creationCall Expression that is used to create a node inside the function.
|
||||
* @param rendererParameter Parameter for the `renderer`.
|
||||
* @param parentParameter Parameter for the `parent` inside the function.
|
||||
* @param extraParameters Extra parameters to be added to the end.
|
||||
* @param precedingVariables Extra variables to be added before the one that creates the `node`.
|
||||
*/
|
||||
function getCreationHelper(
|
||||
functionName: HelperFunction, creationCall: ts.CallExpression, renderer: ts.Identifier,
|
||||
parent: ts.Identifier, extraParameters: ts.Identifier[] = [],
|
||||
precedingVariables: ts.VariableStatement[] = []): ts.FunctionDeclaration {
|
||||
const node = ts.createIdentifier('node');
|
||||
|
||||
// `const node = {{creationCall}}`.
|
||||
const nodeVariableStatement = ts.createVariableStatement(
|
||||
undefined,
|
||||
ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(node, undefined, creationCall)], ts.NodeFlags.Const));
|
||||
|
||||
// `if (parent) { renderer.appendChild(parent, node) }`.
|
||||
const guardedAppendChildCall = ts.createIf(
|
||||
parent,
|
||||
ts.createBlock(
|
||||
[ts.createExpressionStatement(
|
||||
ts.createCall(ts.createPropertyAccess(renderer, 'appendChild'), [], [parent, node]))],
|
||||
true));
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, functionName, [],
|
||||
[renderer, parent, ...extraParameters].map(name => getAnyTypedParameter(name)), undefined,
|
||||
ts.createBlock(
|
||||
[
|
||||
...precedingVariables, nodeVariableStatement, guardedAppendChildCall,
|
||||
ts.createReturn(node)
|
||||
],
|
||||
true));
|
||||
}
|
||||
|
||||
/** Creates a helper for `projectNodes`. */
|
||||
function getProjectNodesHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const parent = ts.createIdentifier('parent');
|
||||
const nodes = ts.createIdentifier('nodes');
|
||||
const incrementor = ts.createIdentifier('i');
|
||||
|
||||
// for (let i = 0; i < nodes.length; i++) {
|
||||
// renderer.appendChild(parent, nodes[i]);
|
||||
// }
|
||||
const loopInitializer = ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))],
|
||||
ts.NodeFlags.Let);
|
||||
const loopCondition = ts.createBinary(
|
||||
incrementor, ts.SyntaxKind.LessThanToken,
|
||||
ts.createPropertyAccess(nodes, ts.createIdentifier('length')));
|
||||
const appendStatement = ts.createExpressionStatement(ts.createCall(
|
||||
ts.createPropertyAccess(renderer, 'appendChild'), [],
|
||||
[parent, ts.createElementAccess(nodes, incrementor)]));
|
||||
const loop = ts.createFor(
|
||||
loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken),
|
||||
ts.createBlock([appendStatement]));
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, HelperFunction.projectNodes, [],
|
||||
[renderer, parent, nodes].map(name => getAnyTypedParameter(name)), undefined,
|
||||
ts.createBlock([loop], true));
|
||||
}
|
||||
|
||||
/** Creates a helper for `animate`. */
|
||||
function getAnimateHelper(): ts.FunctionDeclaration {
|
||||
// throw new Error('...');
|
||||
const throwStatement = ts.createThrow(ts.createNew(
|
||||
ts.createIdentifier('Error'), [],
|
||||
[ts.createStringLiteral('Renderer.animate is no longer supported!')]));
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, HelperFunction.animate, [], [], undefined,
|
||||
ts.createBlock([throwStatement], true));
|
||||
}
|
||||
|
||||
/** Creates a helper for `destroyView`. */
|
||||
function getDestroyViewHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const allNodes = ts.createIdentifier('allNodes');
|
||||
const incrementor = ts.createIdentifier('i');
|
||||
|
||||
// for (let i = 0; i < allNodes.length; i++) {
|
||||
// renderer.destroyNode(allNodes[i]);
|
||||
// }
|
||||
const loopInitializer = ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))],
|
||||
ts.NodeFlags.Let);
|
||||
const loopCondition = ts.createBinary(
|
||||
incrementor, ts.SyntaxKind.LessThanToken,
|
||||
ts.createPropertyAccess(allNodes, ts.createIdentifier('length')));
|
||||
const destroyStatement = ts.createExpressionStatement(ts.createCall(
|
||||
ts.createPropertyAccess(renderer, 'destroyNode'), [],
|
||||
[ts.createElementAccess(allNodes, incrementor)]));
|
||||
const loop = ts.createFor(
|
||||
loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken),
|
||||
ts.createBlock([destroyStatement]));
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, HelperFunction.destroyView, [],
|
||||
[renderer, allNodes].map(name => getAnyTypedParameter(name)), undefined,
|
||||
ts.createBlock([loop], true));
|
||||
}
|
||||
|
||||
/** Creates a helper for `detachView`. */
|
||||
function getDetachViewHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const rootNodes = ts.createIdentifier('rootNodes');
|
||||
const incrementor = ts.createIdentifier('i');
|
||||
const node = ts.createIdentifier('node');
|
||||
|
||||
// for (let i = 0; i < rootNodes.length; i++) {
|
||||
// const node = rootNodes[i];
|
||||
// renderer.removeChild(renderer.parentNode(node), node);
|
||||
// }
|
||||
const loopInitializer = ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))],
|
||||
ts.NodeFlags.Let);
|
||||
const loopCondition = ts.createBinary(
|
||||
incrementor, ts.SyntaxKind.LessThanToken,
|
||||
ts.createPropertyAccess(rootNodes, ts.createIdentifier('length')));
|
||||
|
||||
// const node = rootNodes[i];
|
||||
const nodeVariableStatement = ts.createVariableStatement(
|
||||
undefined,
|
||||
ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(
|
||||
node, undefined, ts.createElementAccess(rootNodes, incrementor))],
|
||||
ts.NodeFlags.Const));
|
||||
// renderer.removeChild(renderer.parentNode(node), node);
|
||||
const removeCall = ts.createCall(
|
||||
ts.createPropertyAccess(renderer, 'removeChild'), [],
|
||||
[ts.createCall(ts.createPropertyAccess(renderer, 'parentNode'), [], [node]), node]);
|
||||
|
||||
const loop = ts.createFor(
|
||||
loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken),
|
||||
ts.createBlock([nodeVariableStatement, ts.createExpressionStatement(removeCall)]));
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, HelperFunction.detachView, [],
|
||||
[renderer, rootNodes].map(name => getAnyTypedParameter(name)), undefined,
|
||||
ts.createBlock([loop], true));
|
||||
}
|
||||
|
||||
/** Creates a helper for `attachViewAfter` */
|
||||
function getAttachViewAfterHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const node = ts.createIdentifier('node');
|
||||
const rootNodes = ts.createIdentifier('rootNodes');
|
||||
const parent = ts.createIdentifier('parent');
|
||||
const nextSibling = ts.createIdentifier('nextSibling');
|
||||
const incrementor = ts.createIdentifier('i');
|
||||
const createConstWithMethodCallInitializer = (constName: ts.Identifier, methodToCall: string) => {
|
||||
return ts.createVariableStatement(
|
||||
undefined,
|
||||
ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(
|
||||
constName, undefined,
|
||||
ts.createCall(ts.createPropertyAccess(renderer, methodToCall), [], [node]))],
|
||||
ts.NodeFlags.Const));
|
||||
};
|
||||
|
||||
// const parent = renderer.parentNode(node);
|
||||
const parentVariableStatement = createConstWithMethodCallInitializer(parent, 'parentNode');
|
||||
|
||||
// const nextSibling = renderer.nextSibling(node);
|
||||
const nextSiblingVariableStatement =
|
||||
createConstWithMethodCallInitializer(nextSibling, 'nextSibling');
|
||||
|
||||
// for (let i = 0; i < rootNodes.length; i++) {
|
||||
// renderer.insertBefore(parentElement, rootNodes[i], nextSibling);
|
||||
// }
|
||||
const loopInitializer = ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))],
|
||||
ts.NodeFlags.Let);
|
||||
const loopCondition = ts.createBinary(
|
||||
incrementor, ts.SyntaxKind.LessThanToken,
|
||||
ts.createPropertyAccess(rootNodes, ts.createIdentifier('length')));
|
||||
const insertBeforeCall = ts.createCall(
|
||||
ts.createPropertyAccess(renderer, 'insertBefore'), [],
|
||||
[parent, ts.createElementAccess(rootNodes, incrementor), nextSibling]);
|
||||
const loop = ts.createFor(
|
||||
loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken),
|
||||
ts.createBlock([ts.createExpressionStatement(insertBeforeCall)]));
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, HelperFunction.attachViewAfter, [],
|
||||
[renderer, node, rootNodes].map(name => getAnyTypedParameter(name)), undefined,
|
||||
ts.createBlock([parentVariableStatement, nextSiblingVariableStatement, loop], true));
|
||||
}
|
||||
|
||||
/** Creates a helper for `setElementAttribute` */
|
||||
function getSetElementAttributeHelper(): ts.FunctionDeclaration {
|
||||
const renderer = ts.createIdentifier('renderer');
|
||||
const element = ts.createIdentifier('element');
|
||||
const namespaceAndName = ts.createIdentifier('namespaceAndName');
|
||||
const value = ts.createIdentifier('value');
|
||||
const name = ts.createIdentifier('name');
|
||||
const namespace = ts.createIdentifier('namespace');
|
||||
|
||||
// [namespace, name] = splitNamespace(namespaceAndName);
|
||||
const namespaceAndNameVariable = ts.createVariableDeclaration(
|
||||
ts.createArrayBindingPattern(
|
||||
[namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))),
|
||||
undefined,
|
||||
ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName]));
|
||||
|
||||
// renderer.setAttribute(element, name, value, namespace);
|
||||
const setCall = ts.createCall(
|
||||
ts.createPropertyAccess(renderer, 'setAttribute'), [], [element, name, value, namespace]);
|
||||
|
||||
// renderer.removeAttribute(element, name, namespace);
|
||||
const removeCall = ts.createCall(
|
||||
ts.createPropertyAccess(renderer, 'removeAttribute'), [], [element, name, namespace]);
|
||||
|
||||
// if (value != null) { setCall() } else { removeCall }
|
||||
const ifStatement = ts.createIf(
|
||||
ts.createBinary(value, ts.SyntaxKind.ExclamationEqualsToken, ts.createNull()),
|
||||
ts.createBlock([ts.createExpressionStatement(setCall)], true),
|
||||
ts.createBlock([ts.createExpressionStatement(removeCall)], true));
|
||||
|
||||
const functionBody = ts.createBlock(
|
||||
[
|
||||
ts.createVariableStatement(
|
||||
undefined,
|
||||
ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const)),
|
||||
ifStatement
|
||||
],
|
||||
true);
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, HelperFunction.setElementAttribute, [],
|
||||
[
|
||||
getAnyTypedParameter(renderer), getAnyTypedParameter(element),
|
||||
getAnyTypedParameter(namespaceAndName), getAnyTypedParameter(value, false)
|
||||
],
|
||||
undefined, functionBody);
|
||||
}
|
||||
|
||||
/** Creates a helper for splitting a name that might contain a namespace. */
|
||||
function getSplitNamespaceHelper(): ts.FunctionDeclaration {
|
||||
const name = ts.createIdentifier('name');
|
||||
const match = ts.createIdentifier('match');
|
||||
const regex = ts.createRegularExpressionLiteral('/^:([^:]+):(.+)$/');
|
||||
const matchCall = ts.createCall(ts.createPropertyAccess(name, 'match'), [], [regex]);
|
||||
|
||||
// const match = name.split(regex);
|
||||
const matchVariable = ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration(match, undefined, matchCall)], ts.NodeFlags.Const);
|
||||
|
||||
// return [match[1], match[2]];
|
||||
const matchReturn = ts.createReturn(
|
||||
ts.createArrayLiteral([ts.createElementAccess(match, 1), ts.createElementAccess(match, 2)]));
|
||||
|
||||
// if (name[0] === ':') { const match = ...; return ...; }
|
||||
const ifStatement = ts.createIf(
|
||||
ts.createBinary(
|
||||
ts.createElementAccess(name, 0), ts.SyntaxKind.EqualsEqualsEqualsToken,
|
||||
ts.createStringLiteral(':')),
|
||||
ts.createBlock([ts.createVariableStatement([], matchVariable), matchReturn], true));
|
||||
|
||||
// return ['', name];
|
||||
const elseReturn = ts.createReturn(ts.createArrayLiteral([ts.createStringLiteral(''), name]));
|
||||
|
||||
return ts.createFunctionDeclaration(
|
||||
[], [], undefined, HelperFunction.splitNamespace, [], [getAnyTypedParameter(name)], undefined,
|
||||
ts.createBlock([ifStatement, elseReturn], true));
|
||||
}
|
||||
|
|
@ -1,140 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {basename, join, relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
||||
import {closestNode} from '../../utils/typescript/nodes';
|
||||
|
||||
import {getHelper, HelperFunction} from './helpers';
|
||||
import {migrateExpression} from './migration';
|
||||
import {findRendererReferences} from './util';
|
||||
|
||||
const MODULE_AUGMENTATION_FILENAME = 'ɵɵRENDERER_MIGRATION_CORE_AUGMENTATION.d.ts';
|
||||
|
||||
/**
|
||||
* Migration that switches from `Renderer` to `Renderer2`. More information on how it works:
|
||||
* https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw
|
||||
*/
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate Renderer usages to Renderer2.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runRendererToRenderer2Migration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runRendererToRenderer2Migration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
// Technically we can get away with using `MODULE_AUGMENTATION_FILENAME` as the path, but as of
|
||||
// TS 4.2, the module resolution caching seems to be more aggressive which causes the file to be
|
||||
// retained between test runs. We can avoid it by using the full path.
|
||||
const augmentedFilePath = join(basePath, MODULE_AUGMENTATION_FILENAME);
|
||||
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" to include a named export called "Renderer". This ensures that
|
||||
// we can rely on the type checker for this migration in v9 where "Renderer" has been removed.
|
||||
if (basename(fileName) === MODULE_AUGMENTATION_FILENAME) {
|
||||
return `
|
||||
import '@angular/core';
|
||||
declare module "@angular/core" {
|
||||
class Renderer {}
|
||||
}
|
||||
`;
|
||||
}
|
||||
return undefined;
|
||||
}, [augmentedFilePath]);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const printer = ts.createPrinter();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer');
|
||||
const rendererImport = rendererImportSpecifier ?
|
||||
closestNode<ts.NamedImports>(rendererImportSpecifier, ts.SyntaxKind.NamedImports) :
|
||||
null;
|
||||
|
||||
// If there are no imports for the `Renderer`, we can exit early.
|
||||
if (!rendererImportSpecifier || !rendererImport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {typedNodes, methodCalls, forwardRefs} =
|
||||
findRendererReferences(sourceFile, typeChecker, rendererImportSpecifier);
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
const helpersToAdd = new Set<HelperFunction>();
|
||||
|
||||
// Change the `Renderer` import to `Renderer2`.
|
||||
update.remove(rendererImport.getStart(), rendererImport.getWidth());
|
||||
update.insertRight(
|
||||
rendererImport.getStart(),
|
||||
printer.printNode(
|
||||
ts.EmitHint.Unspecified, replaceImport(rendererImport, 'Renderer', 'Renderer2'),
|
||||
sourceFile));
|
||||
|
||||
// Change the method parameter and property types to `Renderer2`.
|
||||
typedNodes.forEach(node => {
|
||||
const type = node.type;
|
||||
|
||||
if (type) {
|
||||
update.remove(type.getStart(), type.getWidth());
|
||||
update.insertRight(type.getStart(), 'Renderer2');
|
||||
}
|
||||
});
|
||||
|
||||
// Change all identifiers inside `forwardRef` referring to the `Renderer`.
|
||||
forwardRefs.forEach(identifier => {
|
||||
update.remove(identifier.getStart(), identifier.getWidth());
|
||||
update.insertRight(identifier.getStart(), 'Renderer2');
|
||||
});
|
||||
|
||||
// Migrate all of the method calls.
|
||||
methodCalls.forEach(call => {
|
||||
const {node, requiredHelpers} = migrateExpression(call, typeChecker);
|
||||
|
||||
if (node) {
|
||||
// If we migrated the node to a new expression, replace only the call expression.
|
||||
update.remove(call.getStart(), call.getWidth());
|
||||
update.insertRight(
|
||||
call.getStart(), printer.printNode(ts.EmitHint.Unspecified, node, sourceFile));
|
||||
} else if (call.parent && ts.isExpressionStatement(call.parent)) {
|
||||
// Otherwise if the call is inside an expression statement, drop the entire statement.
|
||||
// This takes care of any trailing semicolons. We only need to drop nodes for cases like
|
||||
// `setBindingDebugInfo` which have been noop for a while so they can be removed safely.
|
||||
update.remove(call.parent.getStart(), call.parent.getWidth());
|
||||
}
|
||||
|
||||
if (requiredHelpers) {
|
||||
requiredHelpers.forEach(helperName => helpersToAdd.add(helperName));
|
||||
}
|
||||
});
|
||||
|
||||
// Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them.
|
||||
// The safest way to do so is to declare helper functions similar to the ones emitted by TS
|
||||
// which encapsulate the extra "glue" logic. We should only emit these functions once per file.
|
||||
helpersToAdd.forEach(helperName => {
|
||||
update.insertLeft(
|
||||
sourceFile.endOfFileToken.getStart(), getHelper(helperName, sourceFile, printer));
|
||||
});
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,244 +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 ts from 'typescript';
|
||||
|
||||
import {HelperFunction} from './helpers';
|
||||
|
||||
/** A call expression that is based on a property access. */
|
||||
type PropertyAccessCallExpression = ts.CallExpression&{expression: ts.PropertyAccessExpression};
|
||||
|
||||
/**
|
||||
* Migrates a function call expression from `Renderer` to `Renderer2`.
|
||||
* Returns null if the expression should be dropped.
|
||||
*/
|
||||
export function migrateExpression(node: ts.CallExpression, typeChecker: ts.TypeChecker):
|
||||
{node: ts.Node|null, requiredHelpers?: HelperFunction[]} {
|
||||
if (isPropertyAccessCallExpression(node)) {
|
||||
switch (node.expression.name.getText()) {
|
||||
case 'setElementProperty':
|
||||
return {node: renameMethodCall(node, 'setProperty')};
|
||||
case 'setText':
|
||||
return {node: renameMethodCall(node, 'setValue')};
|
||||
case 'listenGlobal':
|
||||
return {node: renameMethodCall(node, 'listen')};
|
||||
case 'selectRootElement':
|
||||
return {node: migrateSelectRootElement(node)};
|
||||
case 'setElementClass':
|
||||
return {node: migrateSetElementClass(node)};
|
||||
case 'setElementStyle':
|
||||
return {node: migrateSetElementStyle(node, typeChecker)};
|
||||
case 'invokeElementMethod':
|
||||
return {node: migrateInvokeElementMethod(node)};
|
||||
case 'setBindingDebugInfo':
|
||||
return {node: null};
|
||||
case 'createViewRoot':
|
||||
return {node: migrateCreateViewRoot(node)};
|
||||
case 'setElementAttribute':
|
||||
return {
|
||||
node: switchToHelperCall(node, HelperFunction.setElementAttribute, node.arguments),
|
||||
requiredHelpers: [
|
||||
HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.setElementAttribute
|
||||
]
|
||||
};
|
||||
case 'createElement':
|
||||
return {
|
||||
node: switchToHelperCall(node, HelperFunction.createElement, node.arguments.slice(0, 2)),
|
||||
requiredHelpers:
|
||||
[HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.createElement]
|
||||
};
|
||||
case 'createText':
|
||||
return {
|
||||
node: switchToHelperCall(node, HelperFunction.createText, node.arguments.slice(0, 2)),
|
||||
requiredHelpers: [HelperFunction.any, HelperFunction.createText]
|
||||
};
|
||||
case 'createTemplateAnchor':
|
||||
return {
|
||||
node: switchToHelperCall(
|
||||
node, HelperFunction.createTemplateAnchor, node.arguments.slice(0, 1)),
|
||||
requiredHelpers: [HelperFunction.any, HelperFunction.createTemplateAnchor]
|
||||
};
|
||||
case 'projectNodes':
|
||||
return {
|
||||
node: switchToHelperCall(node, HelperFunction.projectNodes, node.arguments),
|
||||
requiredHelpers: [HelperFunction.any, HelperFunction.projectNodes]
|
||||
};
|
||||
case 'animate':
|
||||
return {
|
||||
node: migrateAnimateCall(),
|
||||
requiredHelpers: [HelperFunction.any, HelperFunction.animate]
|
||||
};
|
||||
case 'destroyView':
|
||||
return {
|
||||
node: switchToHelperCall(node, HelperFunction.destroyView, [node.arguments[1]]),
|
||||
requiredHelpers: [HelperFunction.any, HelperFunction.destroyView]
|
||||
};
|
||||
case 'detachView':
|
||||
return {
|
||||
node: switchToHelperCall(node, HelperFunction.detachView, [node.arguments[0]]),
|
||||
requiredHelpers: [HelperFunction.any, HelperFunction.detachView]
|
||||
};
|
||||
case 'attachViewAfter':
|
||||
return {
|
||||
node: switchToHelperCall(node, HelperFunction.attachViewAfter, node.arguments),
|
||||
requiredHelpers: [HelperFunction.any, HelperFunction.attachViewAfter]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {node};
|
||||
}
|
||||
|
||||
/** Checks whether a node is a PropertyAccessExpression. */
|
||||
function isPropertyAccessCallExpression(node: ts.Node): node is PropertyAccessCallExpression {
|
||||
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression);
|
||||
}
|
||||
|
||||
/** Renames a method call while keeping all of the parameters in place. */
|
||||
function renameMethodCall(node: PropertyAccessCallExpression, newName: string): ts.CallExpression {
|
||||
const newExpression = ts.updatePropertyAccess(
|
||||
node.expression, node.expression.expression, ts.createIdentifier(newName));
|
||||
|
||||
return ts.updateCall(node, newExpression, node.typeArguments, node.arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a `selectRootElement` call by removing the last argument which is no longer supported.
|
||||
*/
|
||||
function migrateSelectRootElement(node: ts.CallExpression): ts.Node {
|
||||
// The only thing we need to do is to drop the last argument
|
||||
// (`debugInfo`), if the consumer was passing it in.
|
||||
if (node.arguments.length > 1) {
|
||||
return ts.updateCall(node, node.expression, node.typeArguments, [node.arguments[0]]);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a call to `setElementClass` either to a call to `addClass` or `removeClass`, or
|
||||
* to an expression like `isAdd ? addClass(el, className) : removeClass(el, className)`.
|
||||
*/
|
||||
function migrateSetElementClass(node: PropertyAccessCallExpression): ts.Node {
|
||||
// Clone so we don't mutate by accident. Note that we assume that
|
||||
// the user's code is providing all three required arguments.
|
||||
const outputMethodArgs = node.arguments.slice();
|
||||
const isAddArgument = outputMethodArgs.pop()!;
|
||||
const createRendererCall = (isAdd: boolean) => {
|
||||
const innerExpression = node.expression.expression;
|
||||
const topExpression =
|
||||
ts.createPropertyAccess(innerExpression, isAdd ? 'addClass' : 'removeClass');
|
||||
return ts.createCall(topExpression, [], node.arguments.slice(0, 2));
|
||||
};
|
||||
|
||||
// If the call has the `isAdd` argument as a literal boolean, we can map it directly to
|
||||
// `addClass` or `removeClass`. Note that we can't use the type checker here, because it
|
||||
// won't tell us whether the value resolves to true or false.
|
||||
if (isAddArgument.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
isAddArgument.kind === ts.SyntaxKind.FalseKeyword) {
|
||||
return createRendererCall(isAddArgument.kind === ts.SyntaxKind.TrueKeyword);
|
||||
}
|
||||
|
||||
// Otherwise create a ternary on the variable.
|
||||
return ts.createConditional(isAddArgument, createRendererCall(true), createRendererCall(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a call to `setElementStyle` call either to a call to
|
||||
* `setStyle` or `removeStyle`. or to an expression like
|
||||
* `value == null ? removeStyle(el, key) : setStyle(el, key, value)`.
|
||||
*/
|
||||
function migrateSetElementStyle(
|
||||
node: PropertyAccessCallExpression, typeChecker: ts.TypeChecker): ts.Node {
|
||||
const args = node.arguments;
|
||||
const addMethodName = 'setStyle';
|
||||
const removeMethodName = 'removeStyle';
|
||||
const lastArgType = args[2] ?
|
||||
typeChecker.typeToString(
|
||||
typeChecker.getTypeAtLocation(args[2]), node, ts.TypeFormatFlags.AddUndefined) :
|
||||
null;
|
||||
|
||||
// Note that for a literal null, TS considers it a `NullKeyword`,
|
||||
// whereas a literal `undefined` is just an Identifier.
|
||||
if (args.length === 2 || lastArgType === 'null' || lastArgType === 'undefined') {
|
||||
// If we've got a call with two arguments, or one with three arguments where the last one is
|
||||
// `undefined` or `null`, we can safely switch to a `removeStyle` call.
|
||||
const innerExpression = node.expression.expression;
|
||||
const topExpression = ts.createPropertyAccess(innerExpression, removeMethodName);
|
||||
return ts.createCall(topExpression, [], args.slice(0, 2));
|
||||
} else if (args.length === 3) {
|
||||
// We need the checks for string literals, because the type of something
|
||||
// like `"blue"` is the literal `blue`, not `string`.
|
||||
if (lastArgType === 'string' || lastArgType === 'number' || ts.isStringLiteral(args[2]) ||
|
||||
ts.isNoSubstitutionTemplateLiteral(args[2]) || ts.isNumericLiteral(args[2])) {
|
||||
// If we've got three arguments and the last one is a string literal or a number, we
|
||||
// can safely rename to `setStyle`.
|
||||
return renameMethodCall(node, addMethodName);
|
||||
} else {
|
||||
// Otherwise migrate to a ternary that looks like:
|
||||
// `value == null ? removeStyle(el, key) : setStyle(el, key, value)`
|
||||
const condition = ts.createBinary(args[2], ts.SyntaxKind.EqualsEqualsToken, ts.createNull());
|
||||
const whenNullCall = renameMethodCall(
|
||||
ts.createCall(node.expression, [], args.slice(0, 2)) as PropertyAccessCallExpression,
|
||||
removeMethodName);
|
||||
return ts.createConditional(condition, whenNullCall, renameMethodCall(node, addMethodName));
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a call to `invokeElementMethod(target, method, [arg1, arg2])` either to
|
||||
* `target.method(arg1, arg2)` or `(target as any)[method].apply(target, [arg1, arg2])`.
|
||||
*/
|
||||
function migrateInvokeElementMethod(node: ts.CallExpression): ts.Node {
|
||||
const [target, name, args] = node.arguments;
|
||||
const isNameStatic = ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name);
|
||||
const isArgsStatic = !args || ts.isArrayLiteralExpression(args);
|
||||
|
||||
if (isNameStatic && isArgsStatic) {
|
||||
// If the name is a static string and the arguments are an array literal,
|
||||
// we can safely convert the node into a call expression.
|
||||
const expression = ts.createPropertyAccess(
|
||||
target, (name as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text);
|
||||
const callArguments = args ? (args as ts.ArrayLiteralExpression).elements : [];
|
||||
return ts.createCall(expression, [], callArguments);
|
||||
} else {
|
||||
// Otherwise create an expression in the form of `(target as any)[name].apply(target, args)`.
|
||||
const asExpression = ts.createParen(
|
||||
ts.createAsExpression(target, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
|
||||
const elementAccess = ts.createElementAccess(asExpression, name);
|
||||
const applyExpression = ts.createPropertyAccess(elementAccess, 'apply');
|
||||
return ts.createCall(applyExpression, [], args ? [target, args] : [target]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Migrates a call to `createViewRoot` to whatever node was passed in as the first argument. */
|
||||
function migrateCreateViewRoot(node: ts.CallExpression): ts.Node {
|
||||
return node.arguments[0];
|
||||
}
|
||||
|
||||
/** Migrates a call to `migrate` a direct call to the helper. */
|
||||
function migrateAnimateCall() {
|
||||
return ts.createCall(ts.createIdentifier(HelperFunction.animate), [], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches out a call to the `Renderer` to a call to one of our helper functions.
|
||||
* Most of the helpers accept an instance of `Renderer2` as the first argument and all
|
||||
* subsequent arguments differ.
|
||||
* @param node Node of the original method call.
|
||||
* @param helper Name of the helper with which to replace the original call.
|
||||
* @param args Arguments that should be passed into the helper after the renderer argument.
|
||||
*/
|
||||
function switchToHelperCall(
|
||||
node: PropertyAccessCallExpression, helper: HelperFunction,
|
||||
args: ts.Expression[]|ts.NodeArray<ts.Expression>): ts.Node {
|
||||
return ts.createCall(ts.createIdentifier(helper), [], [node.expression.expression, ...args]);
|
||||
}
|
||||
|
|
@ -1,80 +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 ts from 'typescript';
|
||||
|
||||
import {getImportSpecifier} from '../../utils/typescript/imports';
|
||||
import {isReferenceToImport} from '../../utils/typescript/symbol';
|
||||
|
||||
/**
|
||||
* Finds typed nodes (e.g. function parameters or class properties) that are referencing the old
|
||||
* `Renderer`, as well as calls to the `Renderer` methods.
|
||||
*/
|
||||
export function findRendererReferences(
|
||||
sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker,
|
||||
rendererImportSpecifier: ts.ImportSpecifier) {
|
||||
const typedNodes = new Set<ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression>();
|
||||
const methodCalls = new Set<ts.CallExpression>();
|
||||
const forwardRefs = new Set<ts.Identifier>();
|
||||
const forwardRefSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'forwardRef');
|
||||
|
||||
ts.forEachChild(sourceFile, function visitNode(node: ts.Node) {
|
||||
if ((ts.isParameter(node) || ts.isPropertyDeclaration(node)) &&
|
||||
isReferenceToImport(typeChecker, node.name, rendererImportSpecifier)) {
|
||||
typedNodes.add(node);
|
||||
} else if (
|
||||
ts.isAsExpression(node) &&
|
||||
isReferenceToImport(typeChecker, node.type, rendererImportSpecifier)) {
|
||||
typedNodes.add(node);
|
||||
} else if (ts.isCallExpression(node)) {
|
||||
if (ts.isPropertyAccessExpression(node.expression) &&
|
||||
isReferenceToImport(typeChecker, node.expression.expression, rendererImportSpecifier)) {
|
||||
methodCalls.add(node);
|
||||
} else if (
|
||||
// If we're dealing with a forwardRef that's returning a Renderer.
|
||||
forwardRefSpecifier && ts.isIdentifier(node.expression) &&
|
||||
isReferenceToImport(typeChecker, node.expression, forwardRefSpecifier) &&
|
||||
node.arguments.length) {
|
||||
const rendererIdentifier =
|
||||
findRendererIdentifierInForwardRef(typeChecker, node, rendererImportSpecifier);
|
||||
if (rendererIdentifier) {
|
||||
forwardRefs.add(rendererIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visitNode);
|
||||
});
|
||||
|
||||
return {typedNodes, methodCalls, forwardRefs};
|
||||
}
|
||||
|
||||
/** Finds the identifier referring to the `Renderer` inside a `forwardRef` call expression. */
|
||||
function findRendererIdentifierInForwardRef(
|
||||
typeChecker: ts.TypeChecker, node: ts.CallExpression,
|
||||
rendererImport: ts.ImportSpecifier|null): ts.Identifier|null {
|
||||
const firstArg = node.arguments[0];
|
||||
|
||||
if (ts.isArrowFunction(firstArg) && rendererImport) {
|
||||
// Check if the function is `forwardRef(() => Renderer)`.
|
||||
if (ts.isIdentifier(firstArg.body) &&
|
||||
isReferenceToImport(typeChecker, firstArg.body, rendererImport)) {
|
||||
return firstArg.body;
|
||||
} else if (ts.isBlock(firstArg.body) && ts.isReturnStatement(firstArg.body.statements[0])) {
|
||||
// Otherwise check if the expression is `forwardRef(() => { return Renderer })`.
|
||||
const returnStatement = firstArg.body.statements[0] as ts.ReturnStatement;
|
||||
|
||||
if (returnStatement.expression && ts.isIdentifier(returnStatement.expression) &&
|
||||
isReferenceToImport(typeChecker, returnStatement.expression, rendererImport)) {
|
||||
return returnStatement.expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "router-preserve-query-params",
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
## Router's NavigationExtras.preserveQueryParams migration
|
||||
|
||||
Previously the `NatigationExtras` property of `preserveQueryParams` defined what should be done with
|
||||
query parameters on navigation. This migration updates the usages of `preserveQueryParams` to
|
||||
instead use the `queryParamsHandling` property.
|
||||
|
||||
#### Before
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({})
|
||||
export class MyComponent {
|
||||
constructor(private _router: Router) {}
|
||||
|
||||
goHome() {
|
||||
this._router.navigate('/', {preserveQueryParams: true, skipLocationChange: 'foo'});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({})
|
||||
export class MyComponent {
|
||||
constructor(private _router: Router) {}
|
||||
|
||||
goHome() {
|
||||
this._router.navigate('/', { queryParamsHandling: 'preserve', skipLocationChange: 'foo' });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,64 +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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
import {findLiteralsToMigrate, migrateLiteral} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* Migration that switches `NavigationExtras.preserveQueryParams` to set the coresponding value via
|
||||
* `NavigationExtras`'s `queryParamsHandling` attribute.
|
||||
*/
|
||||
export default function(): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate ' +
|
||||
'NavigationExtras.preserveQueryParams usages.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runPreserveQueryParamsMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runPreserveQueryParamsMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const printer = ts.createPrinter();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const literalsToMigrate = findLiteralsToMigrate(sourceFile, typeChecker);
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
|
||||
literalsToMigrate.forEach((instances, methodName) => instances.forEach(instance => {
|
||||
const migratedNode = migrateLiteral(methodName, instance);
|
||||
|
||||
if (migratedNode !== instance) {
|
||||
update.remove(instance.getStart(), instance.getWidth());
|
||||
update.insertRight(
|
||||
instance.getStart(),
|
||||
printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile));
|
||||
}
|
||||
}));
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,105 +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 ts from 'typescript';
|
||||
|
||||
import {getImportSpecifier} from '../../utils/typescript/imports';
|
||||
import {isReferenceToImport} from '../../utils/typescript/symbol';
|
||||
|
||||
/**
|
||||
* Configures the methods that the migration should be looking for
|
||||
* and the properties from `NavigationExtras` that should be preserved.
|
||||
*/
|
||||
const methodConfig = new Set<string>(['navigate', 'createUrlTree']);
|
||||
|
||||
const preserveQueryParamsKey = 'preserveQueryParams';
|
||||
|
||||
export function migrateLiteral(
|
||||
methodName: string, node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression {
|
||||
const isMigratableMethod = methodConfig.has(methodName);
|
||||
|
||||
if (!isMigratableMethod) {
|
||||
throw Error(`Attempting to migrate unconfigured method called ${methodName}.`);
|
||||
}
|
||||
|
||||
|
||||
const propertiesToKeep: ts.ObjectLiteralElementLike[] = [];
|
||||
let propertyToMigrate: ts.PropertyAssignment|ts.ShorthandPropertyAssignment|undefined = undefined;
|
||||
|
||||
for (const property of node.properties) {
|
||||
// Only look for regular and shorthand property assignments since resolving things
|
||||
// like spread operators becomes too complicated for this migration.
|
||||
if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
||||
(ts.isStringLiteralLike(property.name) || ts.isNumericLiteral(property.name) ||
|
||||
ts.isIdentifier(property.name)) &&
|
||||
(property.name.text === preserveQueryParamsKey)) {
|
||||
propertyToMigrate = property;
|
||||
continue;
|
||||
}
|
||||
propertiesToKeep.push(property);
|
||||
}
|
||||
|
||||
// Don't modify the node if there's nothing to migrate.
|
||||
if (propertyToMigrate === undefined) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if ((ts.isShorthandPropertyAssignment(propertyToMigrate) &&
|
||||
propertyToMigrate.objectAssignmentInitializer?.kind === ts.SyntaxKind.TrueKeyword) ||
|
||||
(ts.isPropertyAssignment(propertyToMigrate) &&
|
||||
propertyToMigrate.initializer.kind === ts.SyntaxKind.TrueKeyword)) {
|
||||
return ts.updateObjectLiteral(
|
||||
node,
|
||||
propertiesToKeep.concat(
|
||||
ts.createPropertyAssignment('queryParamsHandling', ts.createIdentifier(`'preserve'`))));
|
||||
}
|
||||
|
||||
return ts.updateObjectLiteral(node, propertiesToKeep);
|
||||
}
|
||||
|
||||
export function findLiteralsToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) {
|
||||
const results = new Map<string, Set<ts.ObjectLiteralExpression>>(
|
||||
Array.from(methodConfig.keys(), key => [key, new Set()]));
|
||||
const routerImport = getImportSpecifier(sourceFile, '@angular/router', 'Router');
|
||||
const seenLiterals = new Map<ts.ObjectLiteralExpression, string>();
|
||||
|
||||
if (routerImport) {
|
||||
sourceFile.forEachChild(function visitNode(node: ts.Node) {
|
||||
// Look for calls that look like `foo.<method to migrate>` with more than one parameter.
|
||||
if (ts.isCallExpression(node) && node.arguments.length > 1 &&
|
||||
ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name) &&
|
||||
methodConfig.has(node.expression.name.text)) {
|
||||
// Check whether the type of the object on which the
|
||||
// function is called refers to the Router import.
|
||||
if (isReferenceToImport(typeChecker, node.expression.expression, routerImport)) {
|
||||
const methodName = node.expression.name.text;
|
||||
const parameterDeclaration =
|
||||
typeChecker.getTypeAtLocation(node.arguments[1]).getSymbol()?.valueDeclaration;
|
||||
|
||||
// Find the source of the object literal.
|
||||
if (parameterDeclaration && ts.isObjectLiteralExpression(parameterDeclaration)) {
|
||||
if (!seenLiterals.has(parameterDeclaration)) {
|
||||
results.get(methodName)!.add(parameterDeclaration);
|
||||
seenLiterals.set(parameterDeclaration, methodName);
|
||||
// If the same literal has been passed into multiple different methods, we can't
|
||||
// migrate it, because the supported properties are different. When we detect such
|
||||
// a case, we drop it from the results so that it gets ignored. If it's used multiple
|
||||
// times for the same method, it can still be migrated.
|
||||
} else if (seenLiterals.get(parameterDeclaration) !== methodName) {
|
||||
results.forEach(literals => literals.delete(parameterDeclaration));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node.forEachChild(visitNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "static-queries",
|
||||
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/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/core",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//rxjs",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,88 +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 ts from 'typescript';
|
||||
import {getAngularDecorators} from '../../../utils/ng_decorators';
|
||||
import {getPropertyNameText, hasPropertyNameText} from '../../../utils/typescript/property_name';
|
||||
|
||||
/** Analyzes the given class and resolves the name of all inputs which are declared. */
|
||||
export function getInputNamesOfClass(
|
||||
node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): string[] {
|
||||
const resolvedInputSetters: string[] = [];
|
||||
|
||||
// Determines the names of all inputs defined in the current class declaration by
|
||||
// checking whether a given property/getter/setter has the "@Input" decorator applied.
|
||||
node.members.forEach(m => {
|
||||
if (!m.decorators || !m.decorators.length ||
|
||||
!ts.isPropertyDeclaration(m) && !ts.isSetAccessor(m) && !ts.isGetAccessor(m)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputDecorator =
|
||||
getAngularDecorators(typeChecker, m.decorators!).find(d => d.name === 'Input');
|
||||
|
||||
if (inputDecorator && hasPropertyNameText(m.name)) {
|
||||
resolvedInputSetters.push(m.name.text);
|
||||
}
|
||||
});
|
||||
|
||||
// Besides looking for immediate setters in the current class declaration, developers
|
||||
// can also define inputs in the directive metadata using the "inputs" property. We
|
||||
// also need to determine these inputs which are declared in the directive metadata.
|
||||
const metadataInputs = getInputNamesFromMetadata(node, typeChecker);
|
||||
|
||||
if (metadataInputs) {
|
||||
resolvedInputSetters.push(...metadataInputs);
|
||||
}
|
||||
|
||||
return resolvedInputSetters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the names of all inputs declared in the directive/component metadata
|
||||
* of the given class.
|
||||
*/
|
||||
function getInputNamesFromMetadata(
|
||||
node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): string[]|null {
|
||||
if (!node.decorators || !node.decorators.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decorator = getAngularDecorators(typeChecker, node.decorators)
|
||||
.find(d => d.name === 'Directive' || d.name === 'Component');
|
||||
|
||||
// In case no directive/component decorator could be found for this class, just
|
||||
// return null as there is no metadata where an input could be declared.
|
||||
if (!decorator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decoratorCall = decorator.node.expression;
|
||||
|
||||
// In case the decorator does define any metadata, there is no metadata
|
||||
// where inputs could be declared. This is an edge case because there
|
||||
// always needs to be an object literal, but in case there isn't we just
|
||||
// want to skip the invalid decorator and return null.
|
||||
if (decoratorCall.arguments.length !== 1 ||
|
||||
!ts.isObjectLiteralExpression(decoratorCall.arguments[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = decoratorCall.arguments[0] as ts.ObjectLiteralExpression;
|
||||
const inputs = metadata.properties.filter(ts.isPropertyAssignment)
|
||||
.find(p => getPropertyNameText(p.name) === 'inputs');
|
||||
|
||||
// In case there is no "inputs" property in the directive metadata,
|
||||
// just return "null" as no inputs can be declared for this class.
|
||||
if (!inputs || !ts.isArrayLiteralExpression(inputs.initializer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return inputs.initializer.elements.filter(ts.isStringLiteralLike)
|
||||
.map(element => element.text.split(':')[0].trim());
|
||||
}
|
||||
|
|
@ -1,158 +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 ts from 'typescript';
|
||||
|
||||
import {ResolvedTemplate} from '../../../utils/ng_component_template';
|
||||
import {getAngularDecorators} from '../../../utils/ng_decorators';
|
||||
import {findParentClassDeclaration, getBaseTypeIdentifiers} from '../../../utils/typescript/class_declaration';
|
||||
import {getPropertyNameText} from '../../../utils/typescript/property_name';
|
||||
|
||||
import {getInputNamesOfClass} from './directive_inputs';
|
||||
import {NgQueryDefinition, QueryType} from './query-definition';
|
||||
|
||||
|
||||
/** Resolved metadata of a given class. */
|
||||
export interface ClassMetadata {
|
||||
/** List of class declarations that derive from the given class. */
|
||||
derivedClasses: ts.ClassDeclaration[];
|
||||
/** Super class of the given class. */
|
||||
superClass: ts.ClassDeclaration|null;
|
||||
/** List of property names that declare an Angular input within the given class. */
|
||||
ngInputNames: string[];
|
||||
/** Component template that belongs to that class if present. */
|
||||
template?: ResolvedTemplate;
|
||||
}
|
||||
|
||||
/** Type that describes a map which can be used to get a class declaration's metadata. */
|
||||
export type ClassMetadataMap = Map<ts.ClassDeclaration, ClassMetadata>;
|
||||
|
||||
/**
|
||||
* Visitor that can be used to determine Angular queries within given TypeScript nodes.
|
||||
* Besides resolving queries, the visitor also records class relations and searches for
|
||||
* Angular input setters which can be used to analyze the timing usage of a given query.
|
||||
*/
|
||||
export class NgQueryResolveVisitor {
|
||||
/** Resolved Angular query definitions. */
|
||||
resolvedQueries = new Map<ts.SourceFile, NgQueryDefinition[]>();
|
||||
|
||||
/** Maps a class declaration to its class metadata. */
|
||||
classMetadata: ClassMetadataMap = new Map();
|
||||
|
||||
constructor(public typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.PropertyDeclaration:
|
||||
this.visitPropertyDeclaration(node as ts.PropertyDeclaration);
|
||||
break;
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
this.visitClassDeclaration(node as ts.ClassDeclaration);
|
||||
break;
|
||||
case ts.SyntaxKind.GetAccessor:
|
||||
case ts.SyntaxKind.SetAccessor:
|
||||
this.visitAccessorDeclaration(node as ts.AccessorDeclaration);
|
||||
break;
|
||||
}
|
||||
|
||||
ts.forEachChild(node, n => this.visitNode(n));
|
||||
}
|
||||
|
||||
private visitPropertyDeclaration(node: ts.PropertyDeclaration) {
|
||||
this._recordQueryDeclaration(node, node, getPropertyNameText(node.name));
|
||||
}
|
||||
|
||||
private visitAccessorDeclaration(node: ts.AccessorDeclaration) {
|
||||
this._recordQueryDeclaration(node, null, getPropertyNameText(node.name));
|
||||
}
|
||||
|
||||
private visitClassDeclaration(node: ts.ClassDeclaration) {
|
||||
this._recordClassInputSetters(node);
|
||||
this._recordClassInheritances(node);
|
||||
}
|
||||
|
||||
private _recordQueryDeclaration(
|
||||
node: ts.Node, property: ts.PropertyDeclaration|null, queryName: string|null) {
|
||||
if (!node.decorators || !node.decorators.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators);
|
||||
const queryDecorator =
|
||||
ngDecorators.find(({name}) => name === 'ViewChild' || name === 'ContentChild');
|
||||
|
||||
// Ensure that the current property declaration is defining a query.
|
||||
if (!queryDecorator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryContainer = findParentClassDeclaration(node);
|
||||
|
||||
// If the query is not located within a class declaration, skip this node.
|
||||
if (!queryContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceFile = node.getSourceFile();
|
||||
const newQueries = this.resolvedQueries.get(sourceFile) || [];
|
||||
|
||||
this.resolvedQueries.set(sourceFile, newQueries.concat({
|
||||
name: queryName,
|
||||
type: queryDecorator.name === 'ViewChild' ? QueryType.ViewChild : QueryType.ContentChild,
|
||||
node,
|
||||
property,
|
||||
decorator: queryDecorator,
|
||||
container: queryContainer,
|
||||
}));
|
||||
}
|
||||
|
||||
private _recordClassInputSetters(node: ts.ClassDeclaration) {
|
||||
const resolvedInputNames = getInputNamesOfClass(node, this.typeChecker);
|
||||
|
||||
if (resolvedInputNames) {
|
||||
const classMetadata = this._getClassMetadata(node);
|
||||
|
||||
classMetadata.ngInputNames = resolvedInputNames;
|
||||
this.classMetadata.set(node, classMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
private _recordClassInheritances(node: ts.ClassDeclaration) {
|
||||
const baseTypes = getBaseTypeIdentifiers(node);
|
||||
|
||||
if (!baseTypes || baseTypes.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const superClass = baseTypes[0];
|
||||
const baseClassMetadata = this._getClassMetadata(node);
|
||||
|
||||
// We need to resolve the value declaration through the resolved type as the base
|
||||
// class could be declared in different source files and the local symbol won't
|
||||
// contain a value declaration as the value is not declared locally.
|
||||
const symbol = this.typeChecker.getTypeAtLocation(superClass).getSymbol();
|
||||
|
||||
if (symbol && symbol.valueDeclaration && ts.isClassDeclaration(symbol.valueDeclaration)) {
|
||||
const extendedClass = symbol.valueDeclaration;
|
||||
const classMetadataExtended = this._getClassMetadata(extendedClass);
|
||||
|
||||
// Record all classes that derive from the given class. This makes it easy to
|
||||
// determine all classes that could potentially use inherited queries statically.
|
||||
classMetadataExtended.derivedClasses.push(node);
|
||||
this.classMetadata.set(extendedClass, classMetadataExtended);
|
||||
|
||||
// Record the super class of the current class.
|
||||
baseClassMetadata.superClass = extendedClass;
|
||||
this.classMetadata.set(node, baseClassMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
private _getClassMetadata(node: ts.ClassDeclaration): ClassMetadata {
|
||||
return this.classMetadata.get(node) || {derivedClasses: [], superClass: null, ngInputNames: []};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +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 ts from 'typescript';
|
||||
import {NgDecorator} from '../../../utils/ng_decorators';
|
||||
|
||||
/** Timing of a given query. Either static or dynamic. */
|
||||
export enum QueryTiming {
|
||||
STATIC,
|
||||
DYNAMIC,
|
||||
}
|
||||
|
||||
/** Type of a given query. */
|
||||
export enum QueryType {
|
||||
ViewChild,
|
||||
ContentChild
|
||||
}
|
||||
|
||||
export interface NgQueryDefinition {
|
||||
/** Name of the query. Set to "null" in case the query name is not statically analyzable. */
|
||||
name: string|null;
|
||||
/** Type of the query definition. */
|
||||
type: QueryType;
|
||||
/** Node that declares this query. */
|
||||
node: ts.Node;
|
||||
/**
|
||||
* Property declaration that refers to the query value. For accessors there
|
||||
* is no property that is guaranteed to access the query value.
|
||||
*/
|
||||
property: ts.PropertyDeclaration|null;
|
||||
/** Decorator that declares this as a query. */
|
||||
decorator: NgDecorator;
|
||||
/** Class declaration that holds this query. */
|
||||
container: ts.ClassDeclaration;
|
||||
}
|
||||
|
|
@ -1,27 +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 ts from 'typescript';
|
||||
import {ClassMetadataMap} from './ng_query_visitor';
|
||||
|
||||
/**
|
||||
* Gets all chained super-class TypeScript declarations for the given class
|
||||
* by using the specified class metadata map.
|
||||
*/
|
||||
export function getSuperClassDeclarations(
|
||||
classDecl: ts.ClassDeclaration, classMetadataMap: ClassMetadataMap) {
|
||||
const declarations: ts.ClassDeclaration[] = [];
|
||||
|
||||
let current = classMetadataMap.get(classDecl);
|
||||
while (current && current.superClass) {
|
||||
declarations.push(current.superClass);
|
||||
current = classMetadataMap.get(current.superClass);
|
||||
}
|
||||
|
||||
return declarations;
|
||||
}
|
||||
|
|
@ -1,274 +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 {logging} from '@angular-devkit/core';
|
||||
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {relative} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {loadEsmModule} from '../../utils/load_esm';
|
||||
import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||
|
||||
import {NgQueryResolveVisitor} from './angular/ng_query_visitor';
|
||||
import {QueryTemplateStrategy} from './strategies/template_strategy/template_strategy';
|
||||
import {QueryTestStrategy} from './strategies/test_strategy/test_strategy';
|
||||
import {TimingStrategy} from './strategies/timing-strategy';
|
||||
import {QueryUsageStrategy} from './strategies/usage_strategy/usage_strategy';
|
||||
import {getTransformedQueryCallExpr} from './transform';
|
||||
|
||||
enum SELECTED_STRATEGY {
|
||||
TEMPLATE,
|
||||
USAGE,
|
||||
TESTS,
|
||||
}
|
||||
|
||||
interface AnalyzedProject {
|
||||
program: ts.Program;
|
||||
host: ts.CompilerHost;
|
||||
queryVisitor: NgQueryResolveVisitor;
|
||||
sourceFiles: ts.SourceFile[];
|
||||
basePath: string;
|
||||
typeChecker: ts.TypeChecker;
|
||||
tsconfigPath: string;
|
||||
}
|
||||
|
||||
/** Entry point for the V8 static-query migration. */
|
||||
export default function(): Rule {
|
||||
return runMigration;
|
||||
}
|
||||
|
||||
/** Runs the V8 migration static-query migration for all determined TypeScript projects. */
|
||||
async function runMigration(tree: Tree, context: SchematicContext) {
|
||||
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const logger = context.logger;
|
||||
|
||||
if (!buildPaths.length && !testPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate queries ' +
|
||||
'to add static flag.');
|
||||
}
|
||||
|
||||
const analyzedFiles = new Set<string>();
|
||||
const buildProjects = new Set<AnalyzedProject>();
|
||||
const failures = [];
|
||||
const strategy = process.env['NG_STATIC_QUERY_USAGE_STRATEGY'] === 'true' ?
|
||||
SELECTED_STRATEGY.USAGE :
|
||||
SELECTED_STRATEGY.TEMPLATE;
|
||||
|
||||
for (const tsconfigPath of buildPaths) {
|
||||
const project = analyzeProject(tree, tsconfigPath, basePath, analyzedFiles, logger);
|
||||
if (project) {
|
||||
buildProjects.add(project);
|
||||
}
|
||||
}
|
||||
|
||||
let compilerModule;
|
||||
try {
|
||||
// Load ESM `@angular/compiler` using the TypeScript dynamic import workaround.
|
||||
// Once TypeScript provides support for keeping the dynamic import this workaround can be
|
||||
// changed to a direct dynamic import.
|
||||
compilerModule = await loadEsmModule<typeof import('@angular/compiler')>('@angular/compiler');
|
||||
} catch (e) {
|
||||
throw new SchematicsException(
|
||||
`Unable to load the '@angular/compiler' package. Details: ${e.message}`);
|
||||
}
|
||||
|
||||
let compilerCliModule;
|
||||
try {
|
||||
// Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround.
|
||||
// Once TypeScript provides support for keeping the dynamic import this workaround can be
|
||||
// changed to a direct dynamic import.
|
||||
compilerCliModule =
|
||||
await loadEsmModule<typeof import('@angular/compiler-cli')>('@angular/compiler-cli');
|
||||
} catch (e) {
|
||||
throw new SchematicsException(
|
||||
`Unable to load the '@angular/compiler' package. Details: ${e.message}`);
|
||||
}
|
||||
|
||||
if (buildProjects.size) {
|
||||
for (let project of Array.from(buildProjects.values())) {
|
||||
failures.push(...await runStaticQueryMigration(
|
||||
tree, project, strategy, logger, compilerModule, compilerCliModule));
|
||||
}
|
||||
}
|
||||
|
||||
// For the "test" tsconfig projects we always want to use the test strategy as
|
||||
// we can't detect the proper timing within spec files.
|
||||
for (const tsconfigPath of testPaths) {
|
||||
const project = await analyzeProject(tree, tsconfigPath, basePath, analyzedFiles, logger);
|
||||
if (project) {
|
||||
failures.push(...await runStaticQueryMigration(
|
||||
tree, project, SELECTED_STRATEGY.TESTS, logger, compilerModule, compilerCliModule));
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
logger.info('');
|
||||
logger.info('Some queries could not be migrated automatically. Please go');
|
||||
logger.info('through these manually and apply the appropriate timing.');
|
||||
logger.info('For more info on how to choose a flag, please see: ');
|
||||
logger.info('https://v8.angular.io/guide/static-query-migration');
|
||||
failures.forEach(failure => logger.warn(`⮑ ${failure}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the given TypeScript project by looking for queries that need to be
|
||||
* migrated. In case there are no queries that can be migrated, null is returned.
|
||||
*/
|
||||
function analyzeProject(
|
||||
tree: Tree, tsconfigPath: string, basePath: string, analyzedFiles: Set<string>,
|
||||
logger: logging.LoggerApi): AnalyzedProject|null {
|
||||
const {program, host} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||
const syntacticDiagnostics = program.getSyntacticDiagnostics();
|
||||
|
||||
// Syntactic TypeScript errors can throw off the query analysis and therefore we want
|
||||
// to notify the developer that we couldn't analyze parts of the project. Developers
|
||||
// can just re-run the migration after fixing these failures.
|
||||
if (syntacticDiagnostics.length) {
|
||||
logger.warn(
|
||||
`\nTypeScript project "${tsconfigPath}" has syntactical errors which could cause ` +
|
||||
`an incomplete migration. Please fix the following failures and rerun the migration:`);
|
||||
logger.error(ts.formatDiagnostics(syntacticDiagnostics, host));
|
||||
logger.info(
|
||||
'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"\n');
|
||||
}
|
||||
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const sourceFiles =
|
||||
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
|
||||
const queryVisitor = new NgQueryResolveVisitor(typeChecker);
|
||||
|
||||
// Analyze all project source-files and collect all queries that
|
||||
// need to be migrated.
|
||||
sourceFiles.forEach(sourceFile => {
|
||||
const relativePath = relative(basePath, sourceFile.fileName);
|
||||
|
||||
// Only look for queries within the current source files if the
|
||||
// file has not been analyzed before.
|
||||
if (!analyzedFiles.has(relativePath)) {
|
||||
analyzedFiles.add(relativePath);
|
||||
queryVisitor.visitNode(sourceFile);
|
||||
}
|
||||
});
|
||||
|
||||
if (queryVisitor.resolvedQueries.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {program, host, tsconfigPath, typeChecker, basePath, queryVisitor, sourceFiles};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the static query migration for the given project. The schematic analyzes all
|
||||
* queries within the project and sets up the query timing based on the current usage
|
||||
* of the query property. e.g. a view query that is not used in any lifecycle hook does
|
||||
* not need to be static and can be set up with "static: false".
|
||||
*/
|
||||
async function runStaticQueryMigration(
|
||||
tree: Tree, project: AnalyzedProject, selectedStrategy: SELECTED_STRATEGY,
|
||||
logger: logging.LoggerApi, compilerModule: typeof import('@angular/compiler'),
|
||||
compilerCliModule: typeof import('@angular/compiler-cli')): Promise<string[]> {
|
||||
const {sourceFiles, typeChecker, host, queryVisitor, tsconfigPath, basePath} = project;
|
||||
const printer = ts.createPrinter();
|
||||
const failureMessages: string[] = [];
|
||||
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
|
||||
|
||||
// If the "usage" strategy is selected, we also need to add the query visitor
|
||||
// to the analysis visitors so that query usage in templates can be also checked.
|
||||
if (selectedStrategy === SELECTED_STRATEGY.USAGE) {
|
||||
sourceFiles.forEach(s => templateVisitor.visitNode(s));
|
||||
}
|
||||
|
||||
const {resolvedQueries, classMetadata} = queryVisitor;
|
||||
const {resolvedTemplates} = templateVisitor;
|
||||
|
||||
if (selectedStrategy === SELECTED_STRATEGY.USAGE) {
|
||||
// Add all resolved templates to the class metadata if the usage strategy is used. This
|
||||
// is necessary in order to be able to check component templates for static query usage.
|
||||
resolvedTemplates.forEach(template => {
|
||||
if (classMetadata.has(template.container)) {
|
||||
classMetadata.get(template.container)!.template = template;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let strategy: TimingStrategy;
|
||||
if (selectedStrategy === SELECTED_STRATEGY.USAGE) {
|
||||
strategy = new QueryUsageStrategy(classMetadata, typeChecker, compilerModule);
|
||||
} else if (selectedStrategy === SELECTED_STRATEGY.TESTS) {
|
||||
strategy = new QueryTestStrategy();
|
||||
} else {
|
||||
strategy = new QueryTemplateStrategy(
|
||||
tsconfigPath, classMetadata, host, compilerModule, compilerCliModule);
|
||||
}
|
||||
|
||||
try {
|
||||
strategy.setup();
|
||||
} catch (e) {
|
||||
if (selectedStrategy === SELECTED_STRATEGY.TEMPLATE) {
|
||||
logger.warn(
|
||||
`\nThe template migration strategy uses the Angular compiler ` +
|
||||
`internally and therefore projects that no longer build successfully after ` +
|
||||
`the update cannot use the template migration strategy. Please ensure ` +
|
||||
`there are no AOT compilation errors.\n`);
|
||||
}
|
||||
// In case the strategy could not be set up properly, we just exit the
|
||||
// migration. We don't want to throw an exception as this could mean
|
||||
// that other migrations are interrupted.
|
||||
logger.warn(
|
||||
`Could not setup migration strategy for "${project.tsconfigPath}". The ` +
|
||||
`following error has been reported:\n`);
|
||||
logger.error(`${e.toString()}\n`);
|
||||
logger.info(
|
||||
'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"\n');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Walk through all source files that contain resolved queries and update
|
||||
// the source files if needed. Note that we need to update multiple queries
|
||||
// within a source file within the same recorder in order to not throw off
|
||||
// the TypeScript node offsets.
|
||||
resolvedQueries.forEach((queries, sourceFile) => {
|
||||
const relativePath = relative(basePath, sourceFile.fileName);
|
||||
const update = tree.beginUpdate(relativePath);
|
||||
|
||||
// Compute the query timing for all resolved queries and update the
|
||||
// query definitions to explicitly set the determined query timing.
|
||||
queries.forEach(q => {
|
||||
const queryExpr = q.decorator.node.expression;
|
||||
const {timing, message} = strategy.detectTiming(q);
|
||||
const result = getTransformedQueryCallExpr(q, timing, !!message);
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newText = printer.printNode(ts.EmitHint.Unspecified, result.node, sourceFile);
|
||||
|
||||
// Replace the existing query decorator call expression with the updated
|
||||
// call expression node.
|
||||
update.remove(queryExpr.getStart(), queryExpr.getWidth());
|
||||
update.insertRight(queryExpr.getStart(), newText);
|
||||
|
||||
if (result.failureMessage || message) {
|
||||
const {line, character} =
|
||||
ts.getLineAndCharacterOfPosition(sourceFile, q.decorator.node.getStart());
|
||||
failureMessages.push(
|
||||
`${relativePath}@${line + 1}:${character + 1}: ${result.failureMessage || message}`);
|
||||
}
|
||||
});
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
|
||||
return failureMessages;
|
||||
}
|
||||
|
|
@ -1,238 +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 type {AotCompiler, CompileDirectiveMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompileStylesheetMetadata, NgAnalyzedModules, QueryMatch, StaticSymbol, TemplateAst} from '@angular/compiler';
|
||||
import {resolve} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {ClassMetadataMap} from '../../angular/ng_query_visitor';
|
||||
import {NgQueryDefinition, QueryTiming, QueryType} from '../../angular/query-definition';
|
||||
import {TimingResult, TimingStrategy} from '../timing-strategy';
|
||||
|
||||
const QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE = 'Timing could not be determined. This happens ' +
|
||||
'if the query is not declared in any component.';
|
||||
|
||||
export class QueryTemplateStrategy implements TimingStrategy {
|
||||
private compiler: AotCompiler|null = null;
|
||||
private metadataResolver: CompileMetadataResolver|null = null;
|
||||
private analyzedQueries = new Map<string, QueryTiming>();
|
||||
|
||||
constructor(
|
||||
private projectPath: string, private classMetadata: ClassMetadataMap,
|
||||
private host: ts.CompilerHost, private compilerModule: typeof import('@angular/compiler'),
|
||||
private compilerCliModule: typeof import('@angular/compiler-cli')) {}
|
||||
|
||||
/**
|
||||
* Sets up the template strategy by creating the AngularCompilerProgram. Returns false if
|
||||
* the AOT compiler program could not be created due to failure diagnostics.
|
||||
*/
|
||||
setup() {
|
||||
const {rootNames, options} = this.compilerCliModule.readConfiguration(this.projectPath);
|
||||
|
||||
// https://github.com/angular/angular/commit/ec4381dd401f03bded652665b047b6b90f2b425f made Ivy
|
||||
// the default. This breaks the assumption that "createProgram" from compiler-cli returns the
|
||||
// NGC program. In order to ensure that the migration runs properly, we set "enableIvy" to
|
||||
// false.
|
||||
options.enableIvy = false;
|
||||
|
||||
const aotProgram = this.compilerCliModule.createProgram({rootNames, options, host: this.host});
|
||||
|
||||
// The "AngularCompilerProgram" does not expose the "AotCompiler" instance, nor does it
|
||||
// expose the logic that is necessary to analyze the determined modules. We work around
|
||||
// this by just accessing the necessary private properties using the bracket notation.
|
||||
this.compiler = (aotProgram as any)['compiler'];
|
||||
this.metadataResolver = this.compiler!['_metadataResolver'];
|
||||
|
||||
// Modify the "DirectiveNormalizer" to not normalize any referenced external stylesheets.
|
||||
// This is necessary because in CLI projects preprocessor files are commonly referenced
|
||||
// and we don't want to parse them in order to extract relative style references. This
|
||||
// breaks the analysis of the project because we instantiate a standalone AOT compiler
|
||||
// program which does not contain the custom logic by the Angular CLI Webpack compiler plugin.
|
||||
const directiveNormalizer = this.metadataResolver!['_directiveNormalizer'];
|
||||
directiveNormalizer['_normalizeStylesheet'] = (metadata: CompileStylesheetMetadata) => {
|
||||
return new this.compilerModule.CompileStylesheetMetadata(
|
||||
{styles: metadata.styles, styleUrls: [], moduleUrl: metadata.moduleUrl!});
|
||||
};
|
||||
|
||||
// Retrieves the analyzed modules of the current program. This data can be
|
||||
// used to determine the timing for registered queries.
|
||||
const analyzedModules = (aotProgram as any)['analyzedModules'] as NgAnalyzedModules;
|
||||
|
||||
const ngStructuralDiagnostics = aotProgram.getNgStructuralDiagnostics();
|
||||
if (ngStructuralDiagnostics.length) {
|
||||
throw new Error(this.compilerCliModule.formatDiagnostics(ngStructuralDiagnostics, this.host));
|
||||
}
|
||||
|
||||
analyzedModules.files.forEach(file => {
|
||||
file.directives.forEach(directive => this._analyzeDirective(directive, analyzedModules));
|
||||
});
|
||||
}
|
||||
|
||||
/** Analyzes a given directive by determining the timing of all matched view queries. */
|
||||
private _analyzeDirective(symbol: StaticSymbol, analyzedModules: NgAnalyzedModules) {
|
||||
const metadata = this.metadataResolver!.getDirectiveMetadata(symbol);
|
||||
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(symbol);
|
||||
|
||||
if (!metadata.isComponent || !ngModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedTemplate = this._parseTemplate(metadata, ngModule);
|
||||
const queryTimingMap = this.findStaticQueryIds(parsedTemplate);
|
||||
const {staticQueryIds} = staticViewQueryIds(queryTimingMap);
|
||||
|
||||
metadata.viewQueries.forEach((query, index) => {
|
||||
// Query ids are computed by adding "one" to the index. This is done within
|
||||
// the "view_compiler.ts" in order to support using a bloom filter for queries.
|
||||
const queryId = index + 1;
|
||||
const queryKey =
|
||||
this._getViewQueryUniqueKey(symbol.filePath, symbol.name, query.propertyName);
|
||||
this.analyzedQueries.set(
|
||||
queryKey, staticQueryIds.has(queryId) ? QueryTiming.STATIC : QueryTiming.DYNAMIC);
|
||||
});
|
||||
}
|
||||
|
||||
/** Detects the timing of the query definition. */
|
||||
detectTiming(query: NgQueryDefinition): TimingResult {
|
||||
if (query.type === QueryType.ContentChild) {
|
||||
return {timing: null, message: 'Content queries cannot be migrated automatically.'};
|
||||
} else if (!query.name) {
|
||||
// In case the query property name is not statically analyzable, we mark this
|
||||
// query as unresolved. NGC currently skips these view queries as well.
|
||||
return {timing: null, message: 'Query is not statically analyzable.'};
|
||||
}
|
||||
|
||||
const propertyName = query.name;
|
||||
const classMetadata = this.classMetadata.get(query.container);
|
||||
|
||||
// In case there is no class metadata or there are no derived classes that
|
||||
// could access the current query, we just look for the query analysis of
|
||||
// the class that declares the query. e.g. only the template of the class
|
||||
// that declares the view query affects the query timing.
|
||||
if (!classMetadata || !classMetadata.derivedClasses.length) {
|
||||
const timing = this._getQueryTimingFromClass(query.container, propertyName);
|
||||
|
||||
if (timing === null) {
|
||||
return {timing: null, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE};
|
||||
}
|
||||
|
||||
return {timing};
|
||||
}
|
||||
|
||||
let resolvedTiming: QueryTiming|null = null;
|
||||
let timingMismatch = false;
|
||||
|
||||
// In case there are multiple components that use the same query (e.g. through inheritance),
|
||||
// we need to check if all components use the query with the same timing. If that is not
|
||||
// the case, the query timing is ambiguous and the developer needs to fix the query manually.
|
||||
[query.container, ...classMetadata.derivedClasses].forEach(classDecl => {
|
||||
const classTiming = this._getQueryTimingFromClass(classDecl, propertyName);
|
||||
|
||||
if (classTiming === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In case there is no resolved timing yet, save the new timing. Timings from other
|
||||
// components that use the query with a different timing, cause the timing to be
|
||||
// mismatched. In that case we can't detect a working timing for all components.
|
||||
if (resolvedTiming === null) {
|
||||
resolvedTiming = classTiming;
|
||||
} else if (resolvedTiming !== classTiming) {
|
||||
timingMismatch = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (resolvedTiming === null) {
|
||||
return {timing: QueryTiming.DYNAMIC, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE};
|
||||
} else if (timingMismatch) {
|
||||
return {timing: null, message: 'Multiple components use the query with different timings.'};
|
||||
}
|
||||
return {timing: resolvedTiming};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timing that has been resolved for a given query when it's used within the
|
||||
* specified class declaration. e.g. queries from an inherited class can be used.
|
||||
*/
|
||||
private _getQueryTimingFromClass(classDecl: ts.ClassDeclaration, queryName: string): QueryTiming
|
||||
|null {
|
||||
if (!classDecl.name) {
|
||||
return null;
|
||||
}
|
||||
const filePath = classDecl.getSourceFile().fileName;
|
||||
const queryKey = this._getViewQueryUniqueKey(filePath, classDecl.name.text, queryName);
|
||||
|
||||
if (this.analyzedQueries.has(queryKey)) {
|
||||
return this.analyzedQueries.get(queryKey)!;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _parseTemplate(component: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata):
|
||||
TemplateAst[] {
|
||||
return this
|
||||
.compiler
|
||||
.template;
|
||||
}
|
||||
|
||||
private _getViewQueryUniqueKey(filePath: string, className: string, propName: string) {
|
||||
return `${resolve(filePath)}#${className}-${propName}`;
|
||||
}
|
||||
|
||||
/** Figures out which queries are static and which ones are dynamic. */
|
||||
private findStaticQueryIds(
|
||||
nodes: TemplateAst[], result = new Map<TemplateAst, StaticAndDynamicQueryIds>()):
|
||||
Map<TemplateAst, StaticAndDynamicQueryIds> {
|
||||
nodes.forEach((node) => {
|
||||
const staticQueryIds = new Set<number>();
|
||||
const dynamicQueryIds = new Set<number>();
|
||||
let queryMatches: QueryMatch[] = undefined!;
|
||||
if (node instanceof this.compilerModule.ElementAst) {
|
||||
this.findStaticQueryIds(node.children, result);
|
||||
node.children.forEach((child) => {
|
||||
const childData = result.get(child)!;
|
||||
childData.staticQueryIds.forEach(queryId => staticQueryIds.add(queryId));
|
||||
childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
|
||||
});
|
||||
queryMatches = node.queryMatches;
|
||||
} else if (node instanceof this.compilerModule.EmbeddedTemplateAst) {
|
||||
this.findStaticQueryIds(node.children, result);
|
||||
node.children.forEach((child) => {
|
||||
const childData = result.get(child)!;
|
||||
childData.staticQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
|
||||
childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
|
||||
});
|
||||
queryMatches = node.queryMatches;
|
||||
}
|
||||
if (queryMatches) {
|
||||
queryMatches.forEach((match) => staticQueryIds.add(match.queryId));
|
||||
}
|
||||
dynamicQueryIds.forEach(queryId => staticQueryIds.delete(queryId));
|
||||
result.set(node, {staticQueryIds, dynamicQueryIds});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
interface StaticAndDynamicQueryIds {
|
||||
staticQueryIds: Set<number>;
|
||||
dynamicQueryIds: Set<number>;
|
||||
}
|
||||
|
||||
/** Splits queries into static and dynamic. */
|
||||
function staticViewQueryIds(nodeStaticQueryIds: Map<TemplateAst, StaticAndDynamicQueryIds>):
|
||||
StaticAndDynamicQueryIds {
|
||||
const staticQueryIds = new Set<number>();
|
||||
const dynamicQueryIds = new Set<number>();
|
||||
Array.from(nodeStaticQueryIds.values()).forEach((entry) => {
|
||||
entry.staticQueryIds.forEach(queryId => staticQueryIds.add(queryId));
|
||||
entry.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
|
||||
});
|
||||
dynamicQueryIds.forEach(queryId => staticQueryIds.delete(queryId));
|
||||
return {staticQueryIds, dynamicQueryIds};
|
||||
}
|
||||
|
|
@ -1,28 +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 {NgQueryDefinition} from '../../angular/query-definition';
|
||||
import {TimingResult, TimingStrategy} from '../timing-strategy';
|
||||
|
||||
/**
|
||||
* Query timing strategy that is used for queries used within test files. The query
|
||||
* timing is not analyzed for test files as the template strategy cannot work within
|
||||
* spec files (due to missing component modules) and the usage strategy is not capable
|
||||
* of detecting the timing of queries based on how they are used in tests.
|
||||
*/
|
||||
export class QueryTestStrategy implements TimingStrategy {
|
||||
setup() {}
|
||||
|
||||
/**
|
||||
* Detects the timing for a given query. For queries within tests, we always
|
||||
* add a TODO and print a message saying that the timing can't be detected for tests.
|
||||
*/
|
||||
detectTiming(query: NgQueryDefinition): TimingResult {
|
||||
return {timing: null, message: 'Timing within tests cannot be detected.'};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +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 {NgQueryDefinition, QueryTiming} from '../angular/query-definition';
|
||||
|
||||
export interface TimingStrategy {
|
||||
/** Sets up the given strategy. Throws if the strategy could not be set up. */
|
||||
setup(): void;
|
||||
/** Detects the timing result for a given query. */
|
||||
detectTiming(query: NgQueryDefinition): TimingResult;
|
||||
}
|
||||
|
||||
export type TimingResult = {
|
||||
timing: QueryTiming|null;
|
||||
message?: string;
|
||||
};
|
||||
|
|
@ -1,420 +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 ts from 'typescript';
|
||||
import {isFunctionLikeDeclaration, unwrapExpression} from '../../../../utils/typescript/functions';
|
||||
import {getPropertyNameText} from '../../../../utils/typescript/property_name';
|
||||
|
||||
export type FunctionContext = Map<ts.Node, ts.Node>;
|
||||
|
||||
export enum ResolvedUsage {
|
||||
SYNCHRONOUS,
|
||||
ASYNCHRONOUS,
|
||||
AMBIGUOUS,
|
||||
}
|
||||
|
||||
/**
|
||||
* List of TypeScript syntax tokens that can be used within a binary expression as
|
||||
* compound assignment. These imply a read and write of the left-side expression.
|
||||
*/
|
||||
const BINARY_COMPOUND_TOKENS = [
|
||||
ts.SyntaxKind.CaretEqualsToken,
|
||||
ts.SyntaxKind.AsteriskEqualsToken,
|
||||
ts.SyntaxKind.AmpersandEqualsToken,
|
||||
ts.SyntaxKind.BarEqualsToken,
|
||||
ts.SyntaxKind.AsteriskAsteriskEqualsToken,
|
||||
ts.SyntaxKind.PlusEqualsToken,
|
||||
ts.SyntaxKind.MinusEqualsToken,
|
||||
ts.SyntaxKind.SlashEqualsToken,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of known asynchronous external call expressions which aren't analyzable
|
||||
* but are guaranteed to not execute the passed argument synchronously.
|
||||
*/
|
||||
const ASYNC_EXTERNAL_CALLS = [
|
||||
{parent: ['Promise'], name: 'then'},
|
||||
{parent: ['Promise'], name: 'catch'},
|
||||
{parent: [null, 'Window'], name: 'requestAnimationFrame'},
|
||||
{parent: [null, 'Window'], name: 'setTimeout'},
|
||||
{parent: [null, 'Window'], name: 'setInterval'},
|
||||
{parent: ['*'], name: 'addEventListener'},
|
||||
];
|
||||
|
||||
/**
|
||||
* Class that can be used to determine if a given TypeScript node is used within
|
||||
* other given TypeScript nodes. This is achieved by walking through all children
|
||||
* of the given node and checking for usages of the given declaration. The visitor
|
||||
* also handles potential control flow changes caused by call/new expressions.
|
||||
*/
|
||||
export class DeclarationUsageVisitor {
|
||||
/** Set of visited symbols that caused a jump in control flow. */
|
||||
private visitedJumpExprNodes = new Set<ts.Node>();
|
||||
|
||||
/**
|
||||
* Queue of nodes that need to be checked for declaration usage and
|
||||
* are guaranteed to be executed synchronously.
|
||||
*/
|
||||
private nodeQueue: ts.Node[] = [];
|
||||
|
||||
/**
|
||||
* Nodes which need to be checked for declaration usage but aren't
|
||||
* guaranteed to execute synchronously.
|
||||
*/
|
||||
private ambiguousNodeQueue: ts.Node[] = [];
|
||||
|
||||
/**
|
||||
* Function context that holds the TypeScript node values for all parameters
|
||||
* of the currently analyzed function block.
|
||||
*/
|
||||
private context: FunctionContext = new Map();
|
||||
|
||||
constructor(
|
||||
private declaration: ts.Node, private typeChecker: ts.TypeChecker,
|
||||
private baseContext: FunctionContext = new Map()) {}
|
||||
|
||||
private isReferringToSymbol(node: ts.Node): boolean {
|
||||
const symbol = this.typeChecker.getSymbolAtLocation(node);
|
||||
return !!symbol && symbol.valueDeclaration === this.declaration;
|
||||
}
|
||||
|
||||
private addJumpExpressionToQueue(callExpression: ts.CallExpression) {
|
||||
const node = unwrapExpression(callExpression.expression);
|
||||
|
||||
// In case the given expression is already referring to a function-like declaration,
|
||||
// we don't need to resolve the symbol of the expression as the jump expression is
|
||||
// defined inline and we can just add the given node to the queue.
|
||||
if (isFunctionLikeDeclaration(node) && node.body) {
|
||||
this.nodeQueue.push(node.body);
|
||||
return;
|
||||
}
|
||||
|
||||
const callExprSymbol = this._getDeclarationSymbolOfNode(node);
|
||||
|
||||
if (!callExprSymbol || !callExprSymbol.valueDeclaration) {
|
||||
this.peekIntoJumpExpression(callExpression);
|
||||
return;
|
||||
}
|
||||
|
||||
const expressionDecl = this._resolveNodeFromContext(callExprSymbol.valueDeclaration);
|
||||
|
||||
// Note that we should not add previously visited symbols to the queue as
|
||||
// this could cause cycles.
|
||||
if (!isFunctionLikeDeclaration(expressionDecl) ||
|
||||
this.visitedJumpExprNodes.has(expressionDecl) || !expressionDecl.body) {
|
||||
this.peekIntoJumpExpression(callExpression);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the context for the new jump expression and its specified arguments.
|
||||
this._updateContext(callExpression.arguments, expressionDecl.parameters);
|
||||
|
||||
this.visitedJumpExprNodes.add(expressionDecl);
|
||||
this.nodeQueue.push(expressionDecl.body);
|
||||
}
|
||||
|
||||
private addNewExpressionToQueue(node: ts.NewExpression) {
|
||||
const newExprSymbol = this._getDeclarationSymbolOfNode(unwrapExpression(node.expression));
|
||||
|
||||
// Only handle new expressions which resolve to classes. Technically "new" could
|
||||
// also call void functions or objects with a constructor signature. Also note that
|
||||
// we should not visit already visited symbols as this could cause cycles.
|
||||
if (!newExprSymbol || !newExprSymbol.valueDeclaration ||
|
||||
!ts.isClassDeclaration(newExprSymbol.valueDeclaration)) {
|
||||
this.peekIntoJumpExpression(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetConstructor =
|
||||
newExprSymbol.valueDeclaration.members.find(ts.isConstructorDeclaration);
|
||||
|
||||
if (targetConstructor && targetConstructor.body &&
|
||||
!this.visitedJumpExprNodes.has(targetConstructor)) {
|
||||
// Update the context for the new expression and its specified constructor
|
||||
// parameters if arguments are passed to the class constructor.
|
||||
if (node.arguments) {
|
||||
this._updateContext(node.arguments, targetConstructor.parameters);
|
||||
}
|
||||
|
||||
this.visitedJumpExprNodes.add(targetConstructor);
|
||||
this.nodeQueue.push(targetConstructor.body);
|
||||
} else {
|
||||
this.peekIntoJumpExpression(node);
|
||||
}
|
||||
}
|
||||
|
||||
private visitPropertyAccessors(
|
||||
node: ts.PropertyAccessExpression, checkSetter: boolean, checkGetter: boolean) {
|
||||
const propertySymbol = this._getPropertyAccessSymbol(node);
|
||||
|
||||
if (propertySymbol?.declarations === undefined || propertySymbol.declarations.length === 0 ||
|
||||
(propertySymbol.getFlags() & ts.SymbolFlags.Accessor) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we checked the symbol flags and the symbol is describing an accessor, the
|
||||
// declarations are guaranteed to only contain the getters and setters.
|
||||
const accessors = propertySymbol.declarations as ts.AccessorDeclaration[];
|
||||
|
||||
accessors
|
||||
.filter(
|
||||
d => (checkSetter && ts.isSetAccessor(d) || checkGetter && ts.isGetAccessor(d)) &&
|
||||
d.body && !this.visitedJumpExprNodes.has(d))
|
||||
.forEach(d => {
|
||||
this.visitedJumpExprNodes.add(d);
|
||||
this.nodeQueue.push(d.body!);
|
||||
});
|
||||
}
|
||||
|
||||
private visitBinaryExpression(node: ts.BinaryExpression): boolean {
|
||||
const leftExpr = unwrapExpression(node.left);
|
||||
|
||||
if (!ts.isPropertyAccessExpression(leftExpr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BINARY_COMPOUND_TOKENS.indexOf(node.operatorToken.kind) !== -1) {
|
||||
// Compound assignments always cause the getter and setter to be called.
|
||||
// Therefore we need to check the setter and getter of the property access.
|
||||
this.visitPropertyAccessors(leftExpr, /* setter */ true, /* getter */ true);
|
||||
} else if (node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
// Value assignments using the equals token only cause the "setter" to be called.
|
||||
// Therefore we need to analyze the setter declaration of the property access.
|
||||
this.visitPropertyAccessors(leftExpr, /* setter */ true, /* getter */ false);
|
||||
} else {
|
||||
// If the binary expression is not an assignment, it's a simple property read and
|
||||
// we need to check the getter declaration if present.
|
||||
this.visitPropertyAccessors(leftExpr, /* setter */ false, /* getter */ true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getResolvedNodeUsage(searchNode: ts.Node): ResolvedUsage {
|
||||
this.nodeQueue = [searchNode];
|
||||
this.visitedJumpExprNodes.clear();
|
||||
this.context.clear();
|
||||
|
||||
// Copy base context values into the current function block context. The
|
||||
// base context is useful if nodes need to be mapped to other nodes. e.g.
|
||||
// abstract super class methods are mapped to their implementation node of
|
||||
// the derived class.
|
||||
this.baseContext.forEach((value, key) => this.context.set(key, value));
|
||||
|
||||
return this.isSynchronouslyUsedInNode(searchNode);
|
||||
}
|
||||
|
||||
private isSynchronouslyUsedInNode(searchNode: ts.Node): ResolvedUsage {
|
||||
this.ambiguousNodeQueue = [];
|
||||
|
||||
while (this.nodeQueue.length) {
|
||||
const node = this.nodeQueue.shift()!;
|
||||
|
||||
if (ts.isIdentifier(node) && this.isReferringToSymbol(node)) {
|
||||
return ResolvedUsage.SYNCHRONOUS;
|
||||
}
|
||||
|
||||
// Handle call expressions within TypeScript nodes that cause a jump in control
|
||||
// flow. We resolve the call expression value declaration and add it to the node queue.
|
||||
if (ts.isCallExpression(node)) {
|
||||
this.addJumpExpressionToQueue(node);
|
||||
}
|
||||
|
||||
// Handle new expressions that cause a jump in control flow. We resolve the
|
||||
// constructor declaration of the target class and add it to the node queue.
|
||||
if (ts.isNewExpression(node)) {
|
||||
this.addNewExpressionToQueue(node);
|
||||
}
|
||||
|
||||
// We also need to handle binary expressions where a value can be either assigned to
|
||||
// the property, or a value is read from a property expression. Depending on the
|
||||
// binary expression operator, setters or getters need to be analyzed.
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
// In case the binary expression contained a property expression on the left side, we
|
||||
// don't want to continue visiting this property expression on its own. This is necessary
|
||||
// because visiting the expression on its own causes a loss of context. e.g. property
|
||||
// access expressions *do not* always cause a value read (e.g. property assignments)
|
||||
if (this.visitBinaryExpression(node)) {
|
||||
this.nodeQueue.push(node.right);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle property access expressions. Property expressions which are part of binary
|
||||
// expressions won't be added to the node queue, so these access expressions are
|
||||
// guaranteed to be "read" accesses and we need to check the "getter" declaration.
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
this.visitPropertyAccessors(node, /* setter */ false, /* getter */ true);
|
||||
}
|
||||
|
||||
// Do not visit nodes that declare a block of statements but are not executed
|
||||
// synchronously (e.g. function declarations). We only want to check TypeScript
|
||||
// nodes which are synchronously executed in the control flow.
|
||||
if (!isFunctionLikeDeclaration(node)) {
|
||||
this.nodeQueue.push(...node.getChildren());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ambiguousNodeQueue.length) {
|
||||
// Update the node queue to all stored ambiguous nodes. These nodes are not
|
||||
// guaranteed to be executed and therefore in case of a synchronous usage
|
||||
// within one of those nodes, the resolved usage is ambiguous.
|
||||
this.nodeQueue = this.ambiguousNodeQueue;
|
||||
const usage = this.isSynchronouslyUsedInNode(searchNode);
|
||||
return usage === ResolvedUsage.SYNCHRONOUS ? ResolvedUsage.AMBIGUOUS : usage;
|
||||
}
|
||||
return ResolvedUsage.ASYNCHRONOUS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peeks into the given jump expression by adding all function like declarations
|
||||
* which are referenced in the jump expression arguments to the ambiguous node
|
||||
* queue. These arguments could technically access the given declaration but it's
|
||||
* not guaranteed that the jump expression is executed. In that case the resolved
|
||||
* usage is ambiguous.
|
||||
*/
|
||||
private peekIntoJumpExpression(jumpExp: ts.CallExpression|ts.NewExpression) {
|
||||
if (!jumpExp.arguments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For some call expressions we don't want to add the arguments to the
|
||||
// ambiguous node queue. e.g. "setTimeout" is not analyzable but is
|
||||
// guaranteed to execute its argument asynchronously. We handle a subset
|
||||
// of these call expressions by having a hardcoded list of some.
|
||||
if (ts.isCallExpression(jumpExp)) {
|
||||
const symbol = this._getDeclarationSymbolOfNode(jumpExp.expression);
|
||||
if (symbol && symbol.valueDeclaration) {
|
||||
const parentNode = symbol.valueDeclaration.parent;
|
||||
if (parentNode && (ts.isInterfaceDeclaration(parentNode) || ts.isSourceFile(parentNode)) &&
|
||||
(ts.isMethodSignature(symbol.valueDeclaration) ||
|
||||
ts.isFunctionDeclaration(symbol.valueDeclaration)) &&
|
||||
symbol.valueDeclaration.name) {
|
||||
const parentName = ts.isInterfaceDeclaration(parentNode) ? parentNode.name.text : null;
|
||||
const callName = getPropertyNameText(symbol.valueDeclaration.name);
|
||||
if (ASYNC_EXTERNAL_CALLS.some(
|
||||
c =>
|
||||
(c.name === callName &&
|
||||
(c.parent.indexOf(parentName) !== -1 || c.parent.indexOf('*') !== -1)))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jumpExp.arguments!.forEach((node: ts.Node) => {
|
||||
node = this._resolveDeclarationOfNode(node);
|
||||
|
||||
if (ts.isVariableDeclaration(node) && node.initializer) {
|
||||
node = node.initializer;
|
||||
}
|
||||
|
||||
if (isFunctionLikeDeclaration(node) && !!node.body) {
|
||||
this.ambiguousNodeQueue.push(node.body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a given node from the context. In case the node is not mapped in
|
||||
* the context, the original node is returned.
|
||||
*/
|
||||
private _resolveNodeFromContext(node: ts.Node): ts.Node {
|
||||
if (this.context.has(node)) {
|
||||
return this.context.get(node)!;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the context to reflect the newly set parameter values. This allows future
|
||||
* references to function parameters to be resolved to the actual node through the context.
|
||||
*/
|
||||
private _updateContext(
|
||||
callArgs: ts.NodeArray<ts.Expression>, parameters: ts.NodeArray<ts.ParameterDeclaration>) {
|
||||
parameters.forEach((parameter, index) => {
|
||||
let argumentNode: ts.Node = callArgs[index];
|
||||
|
||||
if (!argumentNode) {
|
||||
if (!parameter.initializer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Argument can be undefined in case the function parameter has a default
|
||||
// value. In that case we want to store the parameter default value in the context.
|
||||
argumentNode = parameter.initializer;
|
||||
}
|
||||
|
||||
if (ts.isIdentifier(argumentNode)) {
|
||||
this.context.set(parameter, this._resolveDeclarationOfNode(argumentNode));
|
||||
} else {
|
||||
this.context.set(parameter, argumentNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the declaration of a given TypeScript node. For example an identifier can
|
||||
* refer to a function parameter. This parameter can then be resolved through the
|
||||
* function context.
|
||||
*/
|
||||
private _resolveDeclarationOfNode(node: ts.Node): ts.Node {
|
||||
const symbol = this._getDeclarationSymbolOfNode(node);
|
||||
|
||||
if (!symbol || !symbol.valueDeclaration) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return this._resolveNodeFromContext(symbol.valueDeclaration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the declaration symbol of a given TypeScript node. Resolves aliased
|
||||
* symbols to the symbol containing the value declaration.
|
||||
*/
|
||||
private _getDeclarationSymbolOfNode(node: ts.Node): ts.Symbol|null {
|
||||
let symbol = this.typeChecker.getSymbolAtLocation(node);
|
||||
|
||||
if (!symbol) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve the symbol to it's original declaration symbol.
|
||||
while (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = this.typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/** Gets the symbol of the given property access expression. */
|
||||
private _getPropertyAccessSymbol(node: ts.PropertyAccessExpression): ts.Symbol|null {
|
||||
let propertySymbol = this._getDeclarationSymbolOfNode(node.name);
|
||||
|
||||
if (!propertySymbol || !propertySymbol.valueDeclaration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.context.has(propertySymbol.valueDeclaration)) {
|
||||
return propertySymbol;
|
||||
}
|
||||
|
||||
// In case the context has the value declaration of the given property access
|
||||
// name identifier, we need to replace the "propertySymbol" with the symbol
|
||||
// referring to the resolved symbol based on the context. e.g. abstract properties
|
||||
// can ultimately resolve into an accessor declaration based on the implementation.
|
||||
const contextNode = this._resolveNodeFromContext(propertySymbol.valueDeclaration);
|
||||
|
||||
if (!ts.isAccessor(contextNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve the symbol referring to the "accessor" using the name identifier
|
||||
// of the accessor declaration.
|
||||
return this._getDeclarationSymbolOfNode(contextNode.name);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +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 ts from 'typescript';
|
||||
|
||||
import {isFunctionLikeDeclaration} from '../../../../utils/typescript/functions';
|
||||
import {hasModifier} from '../../../../utils/typescript/nodes';
|
||||
import {getPropertyNameText} from '../../../../utils/typescript/property_name';
|
||||
import {ClassMetadataMap} from '../../angular/ng_query_visitor';
|
||||
import {getSuperClassDeclarations} from '../../angular/super_class';
|
||||
|
||||
import {FunctionContext} from './declaration_usage_visitor';
|
||||
|
||||
|
||||
/**
|
||||
* Updates the specified function context to map abstract super-class class members
|
||||
* to their implementation TypeScript nodes. This allows us to run the declaration visitor
|
||||
* for the super class with the context of the "baseClass" (e.g. with implemented abstract
|
||||
* class members)
|
||||
*/
|
||||
export function updateSuperClassAbstractMembersContext(
|
||||
baseClass: ts.ClassDeclaration, context: FunctionContext, classMetadataMap: ClassMetadataMap) {
|
||||
getSuperClassDeclarations(baseClass, classMetadataMap).forEach(superClassDecl => {
|
||||
superClassDecl.members.forEach(superClassMember => {
|
||||
if (!superClassMember.name || !hasModifier(superClassMember, ts.SyntaxKind.AbstractKeyword)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the matching implementation of the abstract declaration from the super class.
|
||||
const baseClassImpl = baseClass.members.find(
|
||||
baseClassMethod => !!baseClassMethod.name &&
|
||||
getPropertyNameText(baseClassMethod.name) ===
|
||||
getPropertyNameText(superClassMember.name!));
|
||||
|
||||
if (!baseClassImpl || !isFunctionLikeDeclaration(baseClassImpl) || !baseClassImpl.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.has(superClassMember)) {
|
||||
context.set(superClassMember, baseClassImpl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,94 +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 type {ParseSourceSpan, PropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||
import {TemplateAstVisitor} from '../../../../utils/template_ast_visitor';
|
||||
|
||||
/**
|
||||
* AST visitor that traverses the Render3 HTML AST in order to check if the given
|
||||
* query property is accessed statically in the template.
|
||||
*/
|
||||
export class TemplateUsageVisitor extends TemplateAstVisitor {
|
||||
private hasQueryTemplateReference = false;
|
||||
private expressionAstVisitor;
|
||||
|
||||
constructor(
|
||||
public queryPropertyName: string, compilerModule: typeof import('@angular/compiler')) {
|
||||
super(compilerModule);
|
||||
|
||||
// AST visitor that checks if the given expression contains property reads that
|
||||
// refer to the specified query property name.
|
||||
// This class must be defined within the template visitor due to the need to extend from a class
|
||||
// value found within `@angular/compiler` which is dynamically imported and provided to the
|
||||
// visitor.
|
||||
this.expressionAstVisitor = new (class extends compilerModule.RecursiveAstVisitor {
|
||||
hasQueryPropertyRead = false;
|
||||
|
||||
|
||||
override visitPropertyRead(node: PropertyRead, span: ParseSourceSpan): any {
|
||||
// The receiver of the property read needs to be "implicit" as queries are accessed
|
||||
// from the component instance and not from other objects.
|
||||
if (node.receiver instanceof compilerModule.ImplicitReceiver &&
|
||||
node.name === queryPropertyName) {
|
||||
this.hasQueryPropertyRead = true;
|
||||
return;
|
||||
}
|
||||
|
||||
super.visitPropertyRead(node, span);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/** Checks whether the given query is statically accessed within the specified HTML nodes. */
|
||||
isQueryUsedStatically(htmlNodes: TmplAstNode[]): boolean {
|
||||
this.hasQueryTemplateReference = false;
|
||||
this.expressionAstVisitor.hasQueryPropertyRead = false;
|
||||
|
||||
// Visit all AST nodes and check if the query property is used statically.
|
||||
this.visitAll(htmlNodes);
|
||||
|
||||
return !this.hasQueryTemplateReference && this.expressionAstVisitor.hasQueryPropertyRead;
|
||||
}
|
||||
|
||||
override visitElement(element: TmplAstElement): void {
|
||||
// In case there is a template references variable that matches the query property
|
||||
// name, we can finish this visitor as such a template variable can be used in the
|
||||
// entire template and the query therefore can't be accessed from the template.
|
||||
if (element.references.some(r => r.name === this.queryPropertyName)) {
|
||||
this.hasQueryTemplateReference = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.visitAll(element.attributes);
|
||||
this.visitAll(element.inputs);
|
||||
this.visitAll(element.outputs);
|
||||
this.visitAll(element.children);
|
||||
}
|
||||
|
||||
override visitTemplate(template: TmplAstTemplate): void {
|
||||
this.visitAll(template.attributes);
|
||||
this.visitAll(template.inputs);
|
||||
this.visitAll(template.outputs);
|
||||
|
||||
// We don't want to visit any children of the template as these never can't
|
||||
// access a query statically. The templates can be rendered in the ngAfterViewInit"
|
||||
// lifecycle hook at the earliest.
|
||||
}
|
||||
|
||||
override visitBoundAttribute(attribute: TmplAstBoundAttribute) {
|
||||
attribute.value.visit(this.expressionAstVisitor, attribute.sourceSpan);
|
||||
}
|
||||
|
||||
override visitBoundText(text: TmplAstBoundText) {
|
||||
text.value.visit(this.expressionAstVisitor, text.sourceSpan);
|
||||
}
|
||||
|
||||
override visitBoundEvent(node: TmplAstBoundEvent) {
|
||||
node.handler.visit(this.expressionAstVisitor, node.handlerSpan);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue