Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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: #28810
- Loading branch information
1 parent
9f7a9c6
commit a7f19f2
Showing
21 changed files
with
980 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
37
packages/core/schematics/migrations/static-queries/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"], | ||
) |
71 changes: 71 additions & 0 deletions
71
packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
86 changes: 86 additions & 0 deletions
86
packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/core/schematics/migrations/static-queries/angular/decorators.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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})); | ||
} |
96 changes: 96 additions & 0 deletions
96
packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
packages/core/schematics/migrations/static-queries/angular/query-definition.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Oops, something went wrong.