Skip to content

Commit 6215799

Browse files
devversionAndrewKushnir
authored andcommitted
feat(core): update schematic to migrate to explicit query timing (angular#28983)
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 PR Close angular#28983
1 parent ff95505 commit 6215799

24 files changed

+1456
-0
lines changed

packages/core/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ ng_package(
2929
"//packages/core/testing:package.json",
3030
],
3131
entry_point = "packages/core/index.js",
32+
packages = [
33+
"//packages/core/schematics:npm_package",
34+
],
3235
tags = [
3336
"release-with-framework",
3437
],

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"url": "https://github.com/angular/angular.git"
2525
},
2626
"ng-update": {
27+
"migrations":"./schematics/migrations.json",
2728
"packageGroup": "NG_UPDATE_PACKAGE_GROUP"
2829
},
2930
"sideEffects": false

packages/core/schematics/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("//tools:defaults.bzl", "npm_package")
2+
3+
exports_files([
4+
"tsconfig.json",
5+
"migrations.json",
6+
])
7+
8+
npm_package(
9+
name = "npm_package",
10+
srcs = ["migrations.json"],
11+
visibility = ["//packages/core:__pkg__"],
12+
deps = ["//packages/core/schematics/migrations/static-queries"],
13+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"schematics": {
3+
"migration-v8-static-queries": {
4+
"version": "8",
5+
"description": "Migrates ViewChild and ContentChild to explicit query timing",
6+
"factory": "./migrations/static-queries/index"
7+
}
8+
}
9+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "static-queries",
5+
srcs = glob(
6+
["**/*.ts"],
7+
exclude = ["index_spec.ts"],
8+
),
9+
tsconfig = "//packages/core/schematics:tsconfig.json",
10+
visibility = [
11+
"//packages/core/schematics:__pkg__",
12+
"//packages/core/schematics/test:__pkg__",
13+
],
14+
deps = [
15+
"//packages/core/schematics/utils",
16+
"@npm//@angular-devkit/schematics",
17+
"@npm//@types/node",
18+
"@npm//typescript",
19+
],
20+
)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {hasPropertyNameText} from '../typescript/property_name';
12+
import {DeclarationUsageVisitor} from './declaration_usage_visitor';
13+
import {ClassMetadataMap} from './ng_query_visitor';
14+
import {NgQueryDefinition, QueryTiming, QueryType} from './query-definition';
15+
16+
17+
/**
18+
* Object that maps a given type of query to a list of lifecycle hooks that
19+
* could be used to access such a query statically.
20+
*/
21+
const STATIC_QUERY_LIFECYCLE_HOOKS = {
22+
[QueryType.ViewChild]: ['ngOnInit', 'ngAfterContentInit', 'ngAfterContentChecked'],
23+
[QueryType.ContentChild]: ['ngOnInit'],
24+
};
25+
26+
/**
27+
* Analyzes the usage of the given query and determines the query timing based
28+
* on the current usage of the query.
29+
*/
30+
export function analyzeNgQueryUsage(
31+
query: NgQueryDefinition, classMetadata: ClassMetadataMap,
32+
typeChecker: ts.TypeChecker): QueryTiming {
33+
return isQueryUsedStatically(query.container, query, classMetadata, typeChecker, []) ?
34+
QueryTiming.STATIC :
35+
QueryTiming.DYNAMIC;
36+
}
37+
38+
/** Checks whether a given class or it's derived classes use the specified query statically. */
39+
function isQueryUsedStatically(
40+
classDecl: ts.ClassDeclaration, query: NgQueryDefinition, classMetadataMap: ClassMetadataMap,
41+
typeChecker: ts.TypeChecker, knownInputNames: string[]): boolean {
42+
const usageVisitor = new DeclarationUsageVisitor(query.property, typeChecker);
43+
const classMetadata = classMetadataMap.get(classDecl);
44+
45+
// In case there is metadata for the current class, we collect all resolved Angular input
46+
// names and add them to the list of known inputs that need to be checked for usages of
47+
// the current query. e.g. queries used in an @Input() *setter* are always static.
48+
if (classMetadata) {
49+
knownInputNames.push(...classMetadata.ngInputNames);
50+
}
51+
52+
// List of TypeScript nodes which can contain usages of the given query in order to
53+
// access it statically. e.g.
54+
// (1) queries used in the "ngOnInit" lifecycle hook are static.
55+
// (2) inputs with setters can access queries statically.
56+
const possibleStaticQueryNodes: ts.Node[] = classDecl.members.filter(m => {
57+
if (ts.isMethodDeclaration(m) && hasPropertyNameText(m.name) &&
58+
STATIC_QUERY_LIFECYCLE_HOOKS[query.type].indexOf(m.name.text) !== -1) {
59+
return true;
60+
} else if (
61+
knownInputNames && ts.isSetAccessor(m) && hasPropertyNameText(m.name) &&
62+
knownInputNames.indexOf(m.name.text) !== -1) {
63+
return true;
64+
}
65+
return false;
66+
});
67+
68+
// In case nodes that can possibly access a query statically have been found, check
69+
// if the query declaration is used within any of these nodes.
70+
if (possibleStaticQueryNodes.length &&
71+
possibleStaticQueryNodes.some(hookNode => usageVisitor.isUsedInNode(hookNode))) {
72+
return true;
73+
}
74+
75+
// In case there are classes that derive from the current class, visit each
76+
// derived class as inherited queries could be used statically.
77+
if (classMetadata) {
78+
return classMetadata.derivedClasses.some(
79+
derivedClass => isQueryUsedStatically(
80+
derivedClass, query, classMetadataMap, typeChecker, knownInputNames));
81+
}
82+
83+
return false;
84+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
/**
12+
* Class that can be used to determine if a given TypeScript node is used within
13+
* other given TypeScript nodes. This is achieved by walking through all children
14+
* of the given node and checking for usages of the given declaration. The visitor
15+
* also handles potential control flow changes caused by call/new expressions.
16+
*/
17+
export class DeclarationUsageVisitor {
18+
/** Set of visited symbols that caused a jump in control flow. */
19+
private visitedJumpExprSymbols = new Set<ts.Symbol>();
20+
21+
constructor(private declaration: ts.Node, private typeChecker: ts.TypeChecker) {}
22+
23+
private isReferringToSymbol(node: ts.Node): boolean {
24+
const symbol = this.typeChecker.getSymbolAtLocation(node);
25+
return !!symbol && symbol.valueDeclaration === this.declaration;
26+
}
27+
28+
private addJumpExpressionToQueue(node: ts.Expression, nodeQueue: ts.Node[]) {
29+
const callExprSymbol = this.typeChecker.getSymbolAtLocation(node);
30+
31+
// Note that we should not add previously visited symbols to the queue as this
32+
// could cause cycles.
33+
if (callExprSymbol && callExprSymbol.valueDeclaration &&
34+
!this.visitedJumpExprSymbols.has(callExprSymbol)) {
35+
this.visitedJumpExprSymbols.add(callExprSymbol);
36+
nodeQueue.push(callExprSymbol.valueDeclaration);
37+
}
38+
}
39+
40+
private addNewExpressionToQueue(node: ts.NewExpression, nodeQueue: ts.Node[]) {
41+
const newExprSymbol = this.typeChecker.getSymbolAtLocation(node.expression);
42+
43+
// Only handle new expressions which resolve to classes. Technically "new" could
44+
// also call void functions or objects with a constructor signature. Also note that
45+
// we should not visit already visited symbols as this could cause cycles.
46+
if (!newExprSymbol || !newExprSymbol.valueDeclaration ||
47+
!ts.isClassDeclaration(newExprSymbol.valueDeclaration) ||
48+
this.visitedJumpExprSymbols.has(newExprSymbol)) {
49+
return;
50+
}
51+
52+
const targetConstructor =
53+
newExprSymbol.valueDeclaration.members.find(d => ts.isConstructorDeclaration(d));
54+
55+
if (targetConstructor) {
56+
this.visitedJumpExprSymbols.add(newExprSymbol);
57+
nodeQueue.push(targetConstructor);
58+
}
59+
}
60+
61+
isUsedInNode(searchNode: ts.Node): boolean {
62+
const nodeQueue: ts.Node[] = [searchNode];
63+
this.visitedJumpExprSymbols.clear();
64+
65+
while (nodeQueue.length) {
66+
const node = nodeQueue.shift() !;
67+
68+
if (ts.isIdentifier(node) && this.isReferringToSymbol(node)) {
69+
return true;
70+
}
71+
72+
// Handle call expressions within TypeScript nodes that cause a jump in control
73+
// flow. We resolve the call expression value declaration and add it to the node queue.
74+
if (ts.isCallExpression(node)) {
75+
this.addJumpExpressionToQueue(node.expression, nodeQueue);
76+
}
77+
78+
// Handle new expressions that cause a jump in control flow. We resolve the
79+
// constructor declaration of the target class and add it to the node queue.
80+
if (ts.isNewExpression(node)) {
81+
this.addNewExpressionToQueue(node, nodeQueue);
82+
}
83+
84+
nodeQueue.push(...node.getChildren());
85+
}
86+
return false;
87+
}
88+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
import {getCallDecoratorImport} from '../typescript/decorators';
11+
12+
export interface NgDecorator {
13+
name: string;
14+
node: ts.Decorator;
15+
}
16+
17+
/**
18+
* Gets all decorators which are imported from an Angular package (e.g. "@angular/core")
19+
* from a list of decorators.
20+
*/
21+
export function getAngularDecorators(
22+
typeChecker: ts.TypeChecker, decorators: ReadonlyArray<ts.Decorator>): NgDecorator[] {
23+
return decorators.map(node => ({node, importData: getCallDecoratorImport(typeChecker, node)}))
24+
.filter(({importData}) => importData && importData.importModule.startsWith('@angular/'))
25+
.map(({node, importData}) => ({node, name: importData !.name}));
26+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {getPropertyNameText, hasPropertyNameText} from '../typescript/property_name';
12+
import {getAngularDecorators} from './decorators';
13+
14+
/** Analyzes the given class and resolves the name of all inputs which are declared. */
15+
export function getInputNamesOfClass(
16+
node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): string[] {
17+
const resolvedInputSetters: string[] = [];
18+
19+
// Determines the names of all inputs defined in the current class declaration by
20+
// checking whether a given property/getter/setter has the "@Input" decorator applied.
21+
node.members.forEach(m => {
22+
if (!m.decorators || !m.decorators.length ||
23+
!ts.isPropertyDeclaration(m) && !ts.isSetAccessor(m) && !ts.isGetAccessor(m)) {
24+
return;
25+
}
26+
27+
const inputDecorator =
28+
getAngularDecorators(typeChecker, m.decorators !).find(d => d.name === 'Input');
29+
30+
if (inputDecorator && hasPropertyNameText(m.name)) {
31+
resolvedInputSetters.push(m.name.text);
32+
}
33+
});
34+
35+
// Besides looking for immediate setters in the current class declaration, developers
36+
// can also define inputs in the directive metadata using the "inputs" property. We
37+
// also need to determine these inputs which are declared in the directive metadata.
38+
const metadataInputs = getInputNamesFromMetadata(node, typeChecker);
39+
40+
if (metadataInputs) {
41+
resolvedInputSetters.push(...metadataInputs);
42+
}
43+
44+
return resolvedInputSetters;
45+
}
46+
47+
/**
48+
* Determines the names of all inputs declared in the directive/component metadata
49+
* of the given class.
50+
*/
51+
function getInputNamesFromMetadata(
52+
node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): string[]|null {
53+
if (!node.decorators || !node.decorators.length) {
54+
return null;
55+
}
56+
57+
const decorator = getAngularDecorators(typeChecker, node.decorators)
58+
.find(d => d.name === 'Directive' || d.name === 'Component');
59+
60+
// In case no directive/component decorator could be found for this class, just
61+
// return null as there is no metadata where an input could be declared.
62+
if (!decorator) {
63+
return null;
64+
}
65+
66+
const decoratorCall = decorator.node.expression as ts.CallExpression;
67+
68+
// In case the decorator does define any metadata, there is no metadata
69+
// where inputs could be declared. This is an edge case because there
70+
// always needs to be an object literal, but in case there isn't we just
71+
// want to skip the invalid decorator and return null.
72+
if (!ts.isObjectLiteralExpression(decoratorCall.arguments[0])) {
73+
return null;
74+
}
75+
76+
const metadata = decoratorCall.arguments[0] as ts.ObjectLiteralExpression;
77+
const inputs = metadata.properties.filter(ts.isPropertyAssignment)
78+
.find(p => getPropertyNameText(p.name) === 'inputs');
79+
80+
// In case there is no "inputs" property in the directive metadata,
81+
// just return "null" as no inputs can be declared for this class.
82+
if (!inputs || !ts.isArrayLiteralExpression(inputs.initializer)) {
83+
return null;
84+
}
85+
86+
return inputs.initializer.elements.filter(ts.isStringLiteralLike)
87+
.map(element => element.text.split(':')[0].trim());
88+
}

0 commit comments

Comments
 (0)