Skip to content

Commit

Permalink
fixup! feat(core): update schematic to migrate to explicit query timing
Browse files Browse the repository at this point in the history
Detect input setters with static query usage
  • Loading branch information
devversion committed Mar 1, 2019
1 parent 6ed43e1 commit ad88378
Show file tree
Hide file tree
Showing 10 changed files with 371 additions and 57 deletions.
12 changes: 6 additions & 6 deletions packages/core/schematics/migrations/static-queries/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ ts_library(
visibility = ["//packages/core/schematics:__pkg__"],
deps = [
"//packages/core/schematics/utils",
"@ngdeps//@angular-devkit/schematics",
"@ngdeps//@types/node",
"@ngdeps//typescript",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)

Expand All @@ -22,12 +22,12 @@ ts_library(
srcs = ["index_spec.ts"],
data = [
"//packages/core/schematics:migrations.json",
"@ngdeps//shelljs",
"@npm//shelljs",
],
deps = [
":static-queries",
"@ngdeps//@angular-devkit/schematics",
"@ngdeps//@types/shelljs",
"@npm//@angular-devkit/schematics",
"@npm//@types/shelljs",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
*/

import * as ts from 'typescript';

import {hasPropertyNameText} from '../typescript/property_name';
import {DeclarationUsageVisitor} from './declaration_usage_visitor';
import {DerivedClassesMap} from './ng_query_visitor';
import {ClassMetadataMap} from './ng_query_visitor';
import {NgQueryDefinition, QueryTiming, QueryType} from './query-definition';


/**
* Object that maps a given type of query to a list of lifecycle hooks that
* could be used to access such a query statically.
Expand All @@ -25,39 +28,56 @@ const STATIC_QUERY_LIFECYCLE_HOOKS = {
* on the current usage of the query.
*/
export function analyzeNgQueryUsage(
query: NgQueryDefinition, derivedClassesMap: DerivedClassesMap,
query: NgQueryDefinition, classMetadata: ClassMetadataMap,
typeChecker: ts.TypeChecker): QueryTiming {
return isQueryUsedStatically(query.container, query, derivedClassesMap, typeChecker) ?
return isQueryUsedStatically(query.container, query, classMetadata, typeChecker, []) ?
QueryTiming.STATIC :
QueryTiming.DYNAMIC;
}

/** Checks whether a given class or it's derived classes use the specified query statically. */
function isQueryUsedStatically(
classDecl: ts.ClassDeclaration, query: NgQueryDefinition, derivedClassesMap: DerivedClassesMap,
typeChecker: ts.TypeChecker): boolean {
classDecl: ts.ClassDeclaration, query: NgQueryDefinition, classMetadataMap: ClassMetadataMap,
typeChecker: ts.TypeChecker, knownInputNames: string[]): boolean {
const usageVisitor = new DeclarationUsageVisitor(query.property, typeChecker);
const staticQueryHooks = classDecl.members.filter(
m => ts.isMethodDeclaration(m) &&
(ts.isStringLiteralLike(m.name) || ts.isIdentifier(m.name)) &&
STATIC_QUERY_LIFECYCLE_HOOKS[query.type].indexOf(m.name.text) !== -1);

// In case there lifecycle hooks defined which could access this type of query
// statically, we look if the query declaration is statically accessed within
// one of the lifecycle hook declarations.
if (staticQueryHooks.length &&
staticQueryHooks.some(hookNode => usageVisitor.isUsedInNode(hookNode))) {
return true;
const classMetadata = classMetadataMap.get(classDecl);

// In case there is metadata for the current class, we collect all resolved Angular input
// names and add them to the list of known inputs that need to be checked for usages of
// the current query. e.g. queries used in an @Input() *setter* are always static.
if (classMetadata) {
knownInputNames.push(...classMetadata.ngInputNames);
}

// 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);
// List of TypeScript nodes which can contain usages of the given query in order to
// access it statically. e.g.
// (1) queries used in the "ngOnInit" lifecycle hook are static.
// (2) inputs with setters can access queries statically.
const possibleStaticQueryNodes: ts.Node[] = classDecl.members.filter(m => {
if (ts.isMethodDeclaration(m) && hasPropertyNameText(m.name) &&
STATIC_QUERY_LIFECYCLE_HOOKS[query.type].indexOf(m.name.text) !== -1) {
return true;
} else if (
knownInputNames && ts.isSetAccessor(m) && hasPropertyNameText(m.name) &&
knownInputNames.indexOf(m.name.text) !== -1) {
return true;
}
});

// In case nodes that can possibly access a query statically have been found, check
// if the query declaration is used within any of these nodes.
if (possibleStaticQueryNodes.length &&
possibleStaticQueryNodes.some(hookNode => usageVisitor.isUsedInNode(hookNode))) {
return true;
}

if (!derivedClasses) {
return false;
// In case there are classes that derive from the current class, visit each
// derived class as inherited queries could be used statically.
if (classMetadata) {
return classMetadata.derivedClasses.some(
derivedClass => isQueryUsedStatically(
derivedClass, query, classMetadataMap, typeChecker, knownInputNames));
}

return derivedClasses.some(
derivedClass => isQueryUsedStatically(derivedClass, query, derivedClassesMap, typeChecker));
return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export class DeclarationUsageVisitor {

// Note that we should not add previously visited symbols to the queue as this
// could cause cycles.
if (callExprSymbol && !this.visitedJumpExprSymbols.has(callExprSymbol)) {
if (callExprSymbol && callExprSymbol.valueDeclaration &&
!this.visitedJumpExprSymbols.has(callExprSymbol)) {
this.visitedJumpExprSymbols.add(callExprSymbol);
nodeQueue.push(callExprSymbol.valueDeclaration);
}
Expand All @@ -42,7 +43,8 @@ export class DeclarationUsageVisitor {
// 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) ||
if (!newExprSymbol || !newExprSymbol.valueDeclaration ||
!ts.isClassDeclaration(newExprSymbol.valueDeclaration) ||
this.visitedJumpExprSymbols.has(newExprSymbol)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @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 {getPropertyNameText, hasPropertyNameText} from '../typescript/property_name';
import {getAngularDecorators} from './decorators';

/** 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 as ts.CallExpression;

// 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 (!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());
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,33 @@
import * as ts from 'typescript';

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

import {getAngularDecorators} from './decorators';
import {getInputNamesOfClass} from './directive_inputs';
import {NgQueryDefinition, QueryType} from './query-definition';

export type DerivedClassesMap = Map<ts.ClassDeclaration, ts.ClassDeclaration[]>;
/** Resolved metadata of a given class. */
export interface ClassMetadata {
/** List of class declarations that derive from the given class. */
derivedClasses: ts.ClassDeclaration[];
/** List of property names that declare an Angular input within the given class. */
ngInputNames: string[];
}

/** 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 which can be used
* to analyze the usage of a given query.
* 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 all class declarations that derive from it. */
derivedClasses: DerivedClassesMap = new Map<ts.ClassDeclaration, ts.ClassDeclaration[]>();
/** Maps a class declaration to its class metadata. */
classMetadata: ClassMetadataMap = new Map();

constructor(public typeChecker: ts.TypeChecker) {}

Expand Down Expand Up @@ -74,6 +85,22 @@ export class NgQueryResolveVisitor {
}

private visitClassDeclaration(node: ts.ClassDeclaration) {
this._recordClassInputSetters(node);
this._recordClassInheritances(node);
}

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) {
Expand All @@ -86,18 +113,19 @@ export class NgQueryResolveVisitor {
// 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);
if (symbol && symbol.valueDeclaration && ts.isClassDeclaration(symbol.valueDeclaration)) {
const extendedClass = symbol.valueDeclaration;
const classMetadata = 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.
classMetadata.derivedClasses.push(node);
this.classMetadata.set(extendedClass, classMetadata);
}
});
}

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

// 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(baseClass, existingInheritances.concat(derivedClass));
private _getClassMetadata(node: ts.ClassDeclaration): ClassMetadata {
return this.classMetadata.get(node) || {derivedClasses: [], ngInputNames: []};
}
}
3 changes: 2 additions & 1 deletion packages/core/schematics/migrations/static-queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {runStaticQueryMigration} from './migration';
export default function(): Rule {
return (tree: Tree) => {
const projectTsConfigPaths = getProjectTsConfigPaths(tree);
const basePath = process.cwd();

if (!projectTsConfigPaths.length) {
throw new SchematicsException(
Expand All @@ -22,7 +23,7 @@ export default function(): Rule {
}

for (const tsconfigPath of projectTsConfigPaths) {
runStaticQueryMigration(tree, tsconfigPath);
runStaticQueryMigration(tree, tsconfigPath, basePath);
}
};
}
Loading

0 comments on commit ad88378

Please sign in to comment.