Skip to content
Permalink
Browse files

feat(core): update schematic to migrate to explicit query timing

Introduces an update schematic for the "@angular/core" package
that automatically migrates pre-V8 "ViewChild" and "ContentChild"
queries to the new explicit timing syntax. This is not required
yet, but with Ivy, queries will be "dynamic" by default. Therefore
specifying an explicit query timing ensures that developers can
smoothly migrate to Ivy (once it's the default).

Read more about the explicit timing API here:
angular#28810
  • Loading branch information...
devversion committed Feb 26, 2019
1 parent 9f7a9c6 commit 95945c8c6921625b1c399cb920b16b96ad5ab55d
Showing with 884 additions and 2 deletions.
  1. +1 −1 .bazelrc
  2. +3 −0 packages/core/BUILD.bazel
  3. +2 −1 packages/core/package.json
  4. +13 −0 packages/core/schematics/BUILD.bazel
  5. +9 −0 packages/core/schematics/migrations.json
  6. +37 −0 packages/core/schematics/migrations/static-queries/BUILD.bazel
  7. +65 −0 packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts
  8. +78 −0 packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts
  9. +14 −0 packages/core/schematics/migrations/static-queries/angular/decorators.ts
  10. +89 −0 packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts
  11. +13 −0 packages/core/schematics/migrations/static-queries/angular/query-definition.ts
  12. +17 −0 packages/core/schematics/migrations/static-queries/index.ts
  13. +225 −0 packages/core/schematics/migrations/static-queries/index_spec.ts
  14. +85 −0 packages/core/schematics/migrations/static-queries/migration.ts
  15. +26 −0 packages/core/schematics/migrations/static-queries/typescript/class_declaration.ts
  16. +16 −0 packages/core/schematics/migrations/static-queries/typescript/decorators.ts
  17. +34 −0 packages/core/schematics/migrations/static-queries/typescript/imports.ts
  18. +14 −0 packages/core/schematics/migrations/static-queries/typescript/tsconfig.ts
  19. +7 −0 packages/core/schematics/tsconfig.json
  20. +27 −0 packages/core/schematics/utils/BUILD.bazel
  21. +70 −0 packages/core/schematics/utils/project_tsconfig_paths.ts
  22. +39 −0 packages/core/schematics/utils/project_tsconfig_paths_spec.ts
@@ -33,7 +33,7 @@ test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test
# eventually a surprising failure with auto-discovery of the C++ toolchain in
# MacOS High Sierra.
# See https://github.com/bazelbuild/bazel/issues/4603
build --symlink_prefix=dist/
build --symlink_prefix=/

# Performance: avoid stat'ing input files
build --watchfs
@@ -29,6 +29,9 @@ ng_package(
"//packages/core/testing:package.json",
],
entry_point = "packages/core/index.js",
packages = [
"//packages/core/schematics:npm_package",
],
tags = [
"release-with-framework",
],
@@ -24,7 +24,8 @@
"url": "https://github.com/angular/angular.git"
},
"ng-update": {
"migrations":"./schematics/migrations.json",
"packageGroup": "NG_UPDATE_PACKAGE_GROUP"
},
"sideEffects": false
}
}
@@ -0,0 +1,13 @@
load("//tools:defaults.bzl", "npm_package")

exports_files([
"tsconfig.json",
"migrations.json",
])

npm_package(
name = "npm_package",
srcs = ["migrations.json"],
visibility = ["//packages/core:__pkg__"],
deps = ["//packages/core/schematics/migrations/static-queries"],
)
@@ -0,0 +1,9 @@
{
"schematics": {
"migration-v8-static-queries": {
"version": "8",
"description": "Migrates ViewChild and ContentChild to explicit query timing",
"factory": "./migrations/static-queries/index"
}
}
}
@@ -0,0 +1,37 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")

ts_library(
name = "static-queries",
srcs = glob(
["**/*.ts"],
exclude = ["index_spec.ts"],
),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = ["//packages/core/schematics:__pkg__"],
deps = [
"//packages/core/schematics/utils",
"@ngdeps//@angular-devkit/schematics",
"@ngdeps//@types/node",
"@ngdeps//typescript",
],
)

ts_library(
name = "test_lib",
testonly = True,
srcs = ["index_spec.ts"],
data = [
"//packages/core/schematics:migration.json",
"@ngdeps//shelljs",
],
deps = [
":static-queries",
"@ngdeps//@angular-devkit/schematics",
"@ngdeps//@types/shelljs",
],
)

jasmine_node_test(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,65 @@
import * as ts from 'typescript';

import {DeclarationUsageVisitor} from './declaration_usage_visitor';
import {DerivedClassesMap} from './ng_query_visitor';
import {NgQueryDefinition} from './query-definition';


/** Type of a given query. Either static or dynamic. */
export enum QueryType {
STATIC,
DYNAMIC
}

/** Name of Angular lifecycle hooks that could access queries statically. */
const STATIC_QUERY_LIFECYCLE_HOOKS = ['ngOnInit', 'ngAfterContentInit', 'ngAfterContentChecked'];

/** Analyzes the usage of the given query and determines the query type. */
export function analyzeNgQueryUsage(
query: NgQueryDefinition, derivedClassesMap: DerivedClassesMap,
typeChecker: ts.TypeChecker): QueryType {
const classDecl = query.container;

// List of classes that derive from the query container and need to be analyzed as well.
// e.g. a ViewQuery could be used statically in a derived class.
const derivedClasses = derivedClassesMap.get(classDecl);
let isStatic = isQueryUsedStatically(classDecl, query, typeChecker);

// We don't need to check the derived classes if the container class already
// uses the query statically. This improves performances for a large chain of
// derived classes.
if (derivedClasses && !isStatic) {
derivedClasses.forEach(derivedClass => {
isStatic = isStatic || isQueryUsedStatically(derivedClass, query, typeChecker);
});
}

return isStatic ? QueryType.STATIC : QueryType.DYNAMIC;
}

/** Checks whether the given class uses the specified query statically. */
function isQueryUsedStatically(
classDecl: ts.ClassDeclaration, query: NgQueryDefinition,
typeChecker: ts.TypeChecker): boolean {
const staticQueryHooks = classDecl.members.filter(
m => ts.isMethodDeclaration(m) &&
(ts.isStringLiteralLike(m.name) || ts.isIdentifier(m.name)) &&
STATIC_QUERY_LIFECYCLE_HOOKS.indexOf(m.name.text) !== -1);

// In case there is no are lifecycle hooks defined which could access a query
// statically, we can consider the query as dynamic as nothing in the class declaration
// could reasonably access the query in a static way.
if (!staticQueryHooks.length) {
return false;
}

const usageVisitor = new DeclarationUsageVisitor(query.property, typeChecker);
let isStatic = false;

// Visit each defined lifecycle hook and check whether the query property is used
// inside the method declaration.
staticQueryHooks.forEach(
hookDeclNode => isStatic = isStatic || usageVisitor.isUsedInNode(hookDeclNode));

return isStatic;
}
@@ -0,0 +1,78 @@
import * as ts from 'typescript';

/**
* 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 visitedJumpExprSymbols = new Set<ts.Symbol>();

constructor(private declaration: ts.Node, private typeChecker: ts.TypeChecker) {}

private isReferringToSymbol(node: ts.Node): boolean {
const symbol = this.typeChecker.getSymbolAtLocation(node);
return !!symbol && symbol.valueDeclaration === this.declaration;
}

private addJumpExpressionToQueue(node: ts.Expression, nodeQueue: ts.Node[]) {
const callExprSymbol = this.typeChecker.getSymbolAtLocation(node);

// Note that we should not add previously visited symbols to the queue as this
// could cause cycles.
if (callExprSymbol && !this.visitedJumpExprSymbols.has(callExprSymbol)) {
this.visitedJumpExprSymbols.add(callExprSymbol);
nodeQueue.push(callExprSymbol.valueDeclaration);
}
}

private addNewExpressionToQueue(node: ts.NewExpression, nodeQueue: ts.Node[]) {
const newExprSymbol = this.typeChecker.getSymbolAtLocation(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 || !ts.isClassDeclaration(newExprSymbol.valueDeclaration) ||
this.visitedJumpExprSymbols.has(newExprSymbol)) {
return;
}

const targetConstructor =
newExprSymbol.valueDeclaration.members.find(d => ts.isConstructorDeclaration(d));

if (targetConstructor) {
this.visitedJumpExprSymbols.add(newExprSymbol);
nodeQueue.push(targetConstructor);
}
}

isUsedInNode(searchNode: ts.Node): boolean {
const nodeQueue: ts.Node[] = [searchNode];
this.visitedJumpExprSymbols.clear();

while (nodeQueue.length) {
const node = nodeQueue.shift() !;

if (ts.isIdentifier(node) && this.isReferringToSymbol(node)) {
return true;
}

// 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.expression, nodeQueue);
}

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

nodeQueue.push(...node.getChildren());
}
return false;
}
}
@@ -0,0 +1,14 @@
import * as ts from 'typescript';
import {getCallDecoratorImport} from '../typescript/decorators';

export interface NgDecorator {
name: string;
node: ts.Decorator;
}

export function getAngularDecorators(
typeChecker: ts.TypeChecker, decorators: ReadonlyArray<ts.Decorator>): NgDecorator[] {
return decorators.map(node => ({node, importData: getCallDecoratorImport(typeChecker, node)}))
.filter(({importData}) => importData && importData.importModule.startsWith('@angular/'))
.map(({node, importData}) => ({node, name: importData !.name}))
}
@@ -0,0 +1,89 @@
import * as ts from 'typescript';

import {findParentClassDeclaration, getBaseTypeIdentifiers} from '../typescript/class_declaration';

import {getAngularDecorators} from './decorators';
import {NgQueryDefinition} from './query-definition';

export type DerivedClassesMap = Map<ts.ClassDeclaration, ts.ClassDeclaration[]>;

export class NgQueryResolveVisitor {
/** Resolved Angular query definitions. */
resolvedQueries = new Map<ts.SourceFile, NgQueryDefinition[]>();

/** Maps a class declaration to all class declarations that derive from it. */
derivedClasses: DerivedClassesMap = new Map<ts.ClassDeclaration, ts.ClassDeclaration[]>();

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

ts.forEachChild(node, node => this.visitNode(node));
}

visitPropertyDeclaration(node: ts.PropertyDeclaration) {
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({
property: node,
decorator: queryDecorator,
container: queryContainer,
}));
}

visitClassDeclaration(node: ts.ClassDeclaration) {
const baseTypes = getBaseTypeIdentifiers(node);

if (!baseTypes || !baseTypes.length) {
return;
}

baseTypes.forEach(baseTypeIdentifier => {
// 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(baseTypeIdentifier).getSymbol();

if (symbol && symbol.valueDeclaration) {
this._recordClassInheritance(node, symbol.valueDeclaration as ts.ClassDeclaration);
}
});
}

private _recordClassInheritance(node: ts.ClassDeclaration, superClass: ts.ClassDeclaration) {
const existingInheritances = this.derivedClasses.get(superClass) || [];

// Record all classes that derive from a given class. This makes it easy to
// determine all classes that could potentially use inherited queries statically.
this.derivedClasses.set(superClass, existingInheritances.concat(node))
}
}
@@ -0,0 +1,13 @@
import * as ts from 'typescript';
import {NgDecorator} from './decorators';

export interface NgQueryDefinition {
/** Property that declares the query. */
property: ts.PropertyDeclaration;

/** Decorator that declares this as a query. */
decorator: NgDecorator;

/** Class declaration that holds this query. */
container: ts.ClassDeclaration;
}
@@ -0,0 +1,17 @@
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {runStaticQueryMigration} from './migration';

export default function(): Rule {
return (tree: Tree) => {
const projectTsConfigPaths = getProjectTsConfigPaths(tree);

if (!projectTsConfigPaths.length) {
throw new SchematicsException('Could not find any tsconfig file. Aborting.');
}

for (const tsconfigPath of projectTsConfigPaths) {
runStaticQueryMigration(tree, tsconfigPath);
}
};
}
Oops, something went wrong.

0 comments on commit 95945c8

Please sign in to comment.
You can’t perform that action at this time.