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:
Alan Agius 2021-10-26 13:25:34 +02:00 committed by Jessica Janiuk
parent e2f9dcff68
commit 57cbb4f2e8
166 changed files with 23 additions and 21626 deletions

View file

@ -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",
],
)

View file

@ -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.",

View file

@ -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",
],
)

View file

@ -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.
}
}
```

View file

@ -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);
});
});
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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;
}
}
```

View file

@ -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);
}
});
}

View file

@ -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);
}

View file

@ -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",
],
)

View file

@ -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'}
];
```

View file

@ -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);
});
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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;
}
```

View file

@ -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 '));
}
}
};
}

View file

@ -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",
],
)

View file

@ -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;
}
```

View file

@ -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);
}
});
}

View file

@ -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'));
}

View file

@ -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",

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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",
],
)

View file

@ -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)

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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() {}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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,
});
}
}

View file

@ -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",
],
)

View file

@ -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() {}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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 [];
}
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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
}
}
```

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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} `
}]);
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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);
}
}

View file

@ -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);
});
}

View file

@ -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);
}

View file

@ -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",
],
)

View file

@ -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 {
}
```

View file

@ -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);
});
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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 });
}
}
```

View file

@ -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);
});
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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 {
}
```

View file

@ -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;
}
}

View file

@ -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",
],
)

View file

@ -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() {}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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');
}
}
```

View file

@ -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));
}

View file

@ -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);
});
}

View file

@ -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]);
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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' });
}
}
```

View file

@ -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);
});
}

View file

@ -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;
}

View file

@ -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",
],
)

View file

@ -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());
}

View file

@ -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: []};
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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!['_parseTemplate'](component, ngModule, ngModule.transitiveModule.directives)
.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};
}

View file

@ -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.'};
}
}

View file

@ -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;
};

View file

@ -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);
}
}

View file

@ -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);
}
});
});
}

View file

@ -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