Skip to content

Commit

Permalink
refactor(core): add static-query template strategy
Browse files Browse the repository at this point in the history
Introduces a new strategy for the `static-query` schematic that
is enabled by default. In order to provide a migration that works
for the most Angular applications and makes the upgrade as easy
as possible, the template strategy leverages the view engine
Angular compiler logic in order to determine the query timing
that is currently used within applications using view engine.
  • Loading branch information
devversion committed Apr 12, 2019
1 parent bb710bd commit 4868374
Show file tree
Hide file tree
Showing 12 changed files with 812 additions and 52 deletions.
Expand Up @@ -11,6 +11,7 @@ ts_library(
],
deps = [
"//packages/compiler",
"//packages/compiler-cli",
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
Expand Down
Expand Up @@ -12,7 +12,7 @@ import {NgDecorator} from '../../../utils/ng_decorators';
/** Timing of a given query. Either static or dynamic. */
export enum QueryTiming {
STATIC,
DYNAMIC
DYNAMIC,
}

/** Type of a given query. */
Expand All @@ -24,13 +24,10 @@ export enum QueryType {
export interface NgQueryDefinition {
/** Type of the query definition. */
type: QueryType;

/** 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;
}
Expand Up @@ -63,8 +63,8 @@ export class Rule extends Rules.TypedRule {
// query definitions to explicitly declare the query timing (static or dynamic)
queries.forEach(q => {
const queryExpr = q.decorator.node.expression;
const timing = usageStrategy.detectTiming(q);
const transformedNode = getTransformedQueryCallExpr(q, timing);
const {timing, message} = usageStrategy.detectTiming(q);
const transformedNode = getTransformedQueryCallExpr(q, timing, !!message);

if (!transformedNode) {
return;
Expand Down
85 changes: 63 additions & 22 deletions packages/core/schematics/migrations/static-queries/index.ts
Expand Up @@ -6,24 +6,27 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {logging} from '@angular-devkit/core';
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import {dirname, relative} from 'path';
import * as ts from 'typescript';

import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
import {visitAllNodes} from '../../utils/typescript/visit_nodes';
import {TypeScriptVisitor, visitAllNodes} from '../../utils/typescript/visit_nodes';

import {NgQueryResolveVisitor} from './angular/ng_query_visitor';
import {QueryTemplateStrategy} from './strategies/template_strategy/template_strategy';
import {TimingStrategy} from './strategies/timing-strategy';
import {QueryUsageStrategy} from './strategies/usage_strategy/usage_strategy';
import {getTransformedQueryCallExpr} from './transform';


type Logger = logging.LoggerApi;

/** Entry point for the V8 static-query migration. */
export default function(): Rule {
return (tree: Tree) => {
return (tree: Tree, context: SchematicContext) => {
const projectTsConfigPaths = getProjectTsConfigPaths(tree);
const basePath = process.cwd();

Expand All @@ -34,7 +37,7 @@ export default function(): Rule {
}

for (const tsconfigPath of projectTsConfigPaths) {
runStaticQueryMigration(tree, tsconfigPath, basePath);
runStaticQueryMigration(tree, tsconfigPath, basePath, context.logger);
}
};
}
Expand All @@ -45,7 +48,8 @@ export default function(): Rule {
* the current usage of the query property. e.g. a view query that is not used in any
* lifecycle hook does not need to be static and can be set up with "static: false".
*/
function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: string) {
function runStaticQueryMigration(
tree: Tree, tsconfigPath: string, basePath: string, logger: Logger) {
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
const host = ts.createCompilerHost(parsed.options, true);

Expand All @@ -58,46 +62,67 @@ function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: str
return buffer ? buffer.toString() : undefined;
};

const isUsageStrategy = !!process.env['NG_STATIC_QUERY_USAGE_STRATEGY'];
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const typeChecker = program.getTypeChecker();
const queryVisitor = new NgQueryResolveVisitor(typeChecker);
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
const printer = ts.createPrinter();
const analysisVisitors: TypeScriptVisitor[] = [queryVisitor];

// If the "usage" strategy is selected, we also need to add the query visitor
// to the analysis visitors so that query usage in templates can be also checked.
if (isUsageStrategy) {
analysisVisitors.push(templateVisitor);
}

// Analyze source files by detecting queries, class relations and component templates.
rootSourceFiles.forEach(sourceFile => {
// The visit utility function only traverses the source file once. We don't want to
// The visit utility function only traverses a source file once. We don't want to
// traverse through all source files multiple times for each visitor as this could be
// slow.
visitAllNodes(sourceFile, [queryVisitor, templateVisitor]);
visitAllNodes(sourceFile, analysisVisitors);
});

const {resolvedQueries, classMetadata} = queryVisitor;
const {resolvedTemplates} = templateVisitor;

if (isUsageStrategy) {
// Add all resolved templates to the class metadata if the usage strategy is used. This
// is necessary in order to be able to check component templates for static query usage.
resolvedTemplates.forEach(template => {
if (classMetadata.has(template.container)) {
classMetadata.get(template.container) !.template = template;
}
});
}

// Add all resolved templates to the class metadata so that we can also
// check component templates for static query usage.
templateVisitor.resolvedTemplates.forEach(template => {
if (classMetadata.has(template.container)) {
classMetadata.get(template.container) !.template = template;
}
});
const strategy: TimingStrategy = isUsageStrategy ?
new QueryUsageStrategy(classMetadata, typeChecker) :
new QueryTemplateStrategy(tsconfigPath, classMetadata, host);
const detectionMessages: string[] = [];

const usageStrategy = new QueryUsageStrategy(classMetadata, typeChecker);
// In case the strategy could not be set up properly, we just exit the
// migration. We don't want to throw an exception as this could mean
// that other migrations are interrupted.
if (!strategy.setup()) {
return;
}

// Walk through all source files that contain resolved queries and update
// the source files if needed. Note that we need to update multiple queries
// within a source file within the same recorder in order to not throw off
// the TypeScript node offsets.
resolvedQueries.forEach((queries, sourceFile) => {
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
const relativePath = relative(basePath, sourceFile.fileName);
const update = tree.beginUpdate(relativePath);

// Compute the query usage for all resolved queries and update the
// query definitions to explicitly declare the query timing (static or dynamic)
// Compute the query timing for all resolved queries and update the
// query definitions to explicitly set the determined query timing.
queries.forEach(q => {
const queryExpr = q.decorator.node.expression;
const timing = usageStrategy.detectTiming(q);
const transformedNode = getTransformedQueryCallExpr(q, timing);
const {timing, message} = strategy.detectTiming(q);
const transformedNode = getTransformedQueryCallExpr(q, timing, !!message);

if (!transformedNode) {
return;
Expand All @@ -109,8 +134,24 @@ function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: str
// call expression node.
update.remove(queryExpr.getStart(), queryExpr.getWidth());
update.insertRight(queryExpr.getStart(), newText);

const {line, character} =
ts.getLineAndCharacterOfPosition(sourceFile, q.decorator.node.getStart());
detectionMessages.push(`${relativePath}@${line + 1}:${character + 1}: ${message}`);
});

tree.commitUpdate(update);
});

if (detectionMessages.length) {
logger.info('------ Static Query migration ------');
logger.info('In preparation for Ivy, developers can now explicitly specify the');
logger.info('timing of their queries. Read more about this here:');
logger.info('https://github.com/angular/angular/pull/28810');
logger.info('');
logger.info('Some queries cannot be migrated automatically. Please go through');
logger.info('those manually and apply the appropriate timing:');
detectionMessages.forEach(failure => logger.warn(`⮑ ${failure}`));
logger.info('------------------------------------------------');
}
}
@@ -0,0 +1,178 @@
/**
* @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 {AotCompiler, CompileDirectiveMetadata, CompileMetadataResolver, CompileNgModuleMetadata, NgAnalyzedModules, StaticSymbol, TemplateAst, findStaticQueryIds, staticViewQueryIds} from '@angular/compiler';
import {Diagnostic, createProgram, readConfiguration} from '@angular/compiler-cli';
import {resolve} from 'path';
import * as ts from 'typescript';

import {hasPropertyNameText} from '../../../../utils/typescript/property_name';
import {ClassMetadataMap} from '../../angular/ng_query_visitor';
import {NgQueryDefinition, QueryTiming, QueryType} from '../../angular/query-definition';
import {TimingResult, TimingStrategy} from '../timing-strategy';

const QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE = 'Timing could not be determined. This happens ' +
'if the query is not declared in any component.';

export class QueryTemplateStrategy implements TimingStrategy {
private compiler: AotCompiler|null = null;
private metadataResolver: CompileMetadataResolver|null = null;
private analyzedQueries = new Map<string, QueryTiming>();

constructor(
private projectPath: string, private classMetadata: ClassMetadataMap,
private host: ts.CompilerHost) {}

/**
* Sets up the template strategy by creating the AngularCompilerProgram. Returns false if
* the AOT compiler program could not be created due to failure diagnostics.
*/
setup() {
const {rootNames, options} = readConfiguration(this.projectPath);
const aotProgram = createProgram({rootNames, options, host: this.host});

// The "AngularCompilerProgram" does not expose the "AotCompiler" instance, nor does it
// expose the logic that is necessary to analyze the determined modules. We work around
// this by just accessing the necessary private properties using the bracket notation.
this.compiler = (aotProgram as any)['compiler'];
this.metadataResolver = this.compiler !['_metadataResolver'];
const analyzedModules = (aotProgram as any)['analyzedModules'] as NgAnalyzedModules;

const ngDiagnostics = [
...aotProgram.getNgStructuralDiagnostics(),
...aotProgram.getNgSemanticDiagnostics(),
];

if (ngDiagnostics.length) {
this._printDiagnosticFailures(ngDiagnostics);
return false;
}

analyzedModules.files.forEach(file => {
file.directives.forEach(directive => this._analyzeDirective(directive, analyzedModules));
});
return true;
}

/** Analyzes a given directive by determining the timing of all matched view queries. */
private _analyzeDirective(symbol: StaticSymbol, analyzedModules: NgAnalyzedModules) {
const metadata = this.metadataResolver !.getDirectiveMetadata(symbol);
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(symbol);

if (!metadata.isComponent || !ngModule) {
return;
}

const parsedTemplate = this._parseTemplate(metadata, ngModule);
const queryTimingMap = findStaticQueryIds(parsedTemplate);
const {staticQueryIds} = staticViewQueryIds(queryTimingMap);

metadata.viewQueries.forEach((query, index) => {
// Query ids are computed by adding "one" to the index. This is done within
// the "view_compiler.ts" in order to support using a bloom filter for queries.
const queryId = index + 1;
const queryKey =
this._getViewQueryUniqueKey(symbol.filePath, symbol.name, query.propertyName);
this.analyzedQueries.set(
queryKey, staticQueryIds.has(queryId) ? QueryTiming.STATIC : QueryTiming.DYNAMIC);
});
}

/** Detects the timing of the query definition. */
detectTiming(query: NgQueryDefinition): TimingResult {
if (query.type === QueryType.ContentChild) {
return {timing: null, message: 'Content queries cannot be migrated automatically.'};
} else if (!hasPropertyNameText(query.property.name)) {
// In case the query property name is not statically analyzable, we mark this
// query as unresolved. NGC currently skips these view queries as well.
return {timing: null, message: 'Query is not statically analyzable.'};
}

const propertyName = query.property.name.text;
const classMetadata = this.classMetadata.get(query.container);

// In case there is no class metadata or there are no derived classes that
// could access the current query, we just look for the query analysis of
// the class that declares the query. e.g. only the template of the class
// that declares the view query affects the query timing.
if (!classMetadata || !classMetadata.derivedClasses.length) {
const timing = this._getQueryTimingFromClass(query.container, propertyName);

if (timing === null) {
return {timing: null, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE};
}

return {timing};
}

let resolvedTiming: QueryTiming|null = null;
let timingMismatch = false;

// In case there are multiple components that use the same query (e.g. through inheritance),
// we need to check if all components use the query with the same timing. If that is not
// the case, the query timing is ambiguous and the developer needs to fix the query manually.
[query.container, ...classMetadata.derivedClasses].forEach(classDecl => {
const classTiming = this._getQueryTimingFromClass(classDecl, propertyName);

if (classTiming === null) {
return;
}

// In case there is no resolved timing yet, save the new timing. Timings from other
// components that use the query with a different timing, cause the timing to be
// mismatched. In that case we can't detect a working timing for all components.
if (resolvedTiming === null) {
resolvedTiming = classTiming;
} else if (resolvedTiming !== classTiming) {
timingMismatch = true;
}
});

if (resolvedTiming === null) {
return {timing: QueryTiming.DYNAMIC, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE};
} else if (timingMismatch) {
return {timing: null, message: 'Multiple components use the query with different timings.'};
}
return {timing: resolvedTiming};
}

/**
* Gets the timing that has been resolved for a given query when it's used within the
* specified class declaration. e.g. queries from an inherited class can be used.
*/
private _getQueryTimingFromClass(classDecl: ts.ClassDeclaration, queryName: string): QueryTiming
|null {
if (!classDecl.name) {
return null;
}
const filePath = classDecl.getSourceFile().fileName;
const queryKey = this._getViewQueryUniqueKey(filePath, classDecl.name.text, queryName);

if (this.analyzedQueries.has(queryKey)) {
return this.analyzedQueries.get(queryKey) !;
}
return null;
}

private _parseTemplate(component: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata):
TemplateAst[] {
return this
.compiler !['_parseTemplate'](component, ngModule, ngModule.transitiveModule.directives)
.template;
}

private _printDiagnosticFailures(diagnostics: (ts.Diagnostic|Diagnostic)[]) {
console.error('Could not create Angular AOT compiler to determine query timing.');
console.error('The following diagnostics were detected:\n');
console.error(diagnostics.map(d => d.messageText).join(`\n`));
}

private _getViewQueryUniqueKey(filePath: string, className: string, propName: string) {
return `${resolve(filePath)}#${className}-${propName}`;
}
}

0 comments on commit 4868374

Please sign in to comment.