Skip to content

Commit

Permalink
feat(core): update schematic to migrate to explicit query timing
Browse files Browse the repository at this point in the history
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:
#28810
  • Loading branch information
devversion committed Feb 26, 2019
1 parent 9f7a9c6 commit a7f19f2
Show file tree
Hide file tree
Showing 21 changed files with 980 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/core/BUILD.bazel
Expand Up @@ -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",
],
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Expand Up @@ -24,6 +24,7 @@
"url": "https://github.com/angular/angular.git"
},
"ng-update": {
"migrations":"./schematics/migrations.json",
"packageGroup": "NG_UPDATE_PACKAGE_GROUP"
},
"sideEffects": false
Expand Down
13 changes: 13 additions & 0 deletions packages/core/schematics/BUILD.bazel
@@ -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"],
)
9 changes: 9 additions & 0 deletions packages/core/schematics/migrations.json
@@ -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"
}
}
}
37 changes: 37 additions & 0 deletions packages/core/schematics/migrations/static-queries/BUILD.bazel
@@ -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,71 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';
import {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,86 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';

/**
* 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,22 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';
import {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,96 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';

import {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,21 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';
import {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 comments on commit a7f19f2

Please sign in to comment.