refactor(core): move usage detection into usage strategy (#29815)

In order to support multiple strategies for detecting the query timing, the
query usage logic has been moved into a query usage strategy.

PR Close #29815
This commit is contained in:
Paul Gschwendtner 2019-04-08 19:37:31 +02:00 committed by Alex Rickabaugh
parent 7a7781e925
commit 205a45e9a8
8 changed files with 96 additions and 59 deletions

View File

@ -7,48 +7,13 @@
*/
import * as ts from 'typescript';
import {isFunctionLikeDeclaration} from '../../../utils/typescript/functions';
import {hasModifier} from '../../../utils/typescript/nodes';
import {getPropertyNameText} from '../../../utils/typescript/property_name';
import {FunctionContext} from './declaration_usage_visitor';
import {ClassMetadataMap} from './ng_query_visitor';
/**
* Updates the specified function context to map abstract super-class class members
* to their implementation TypeScript nodes. This allows us to run the declaration visitor
* for the super class with the context of the "baseClass" (e.g. with implemented abstract
* class members)
* Gets all chained super-class TypeScript declarations for the given class
* by using the specified class metadata map.
*/
export function updateSuperClassAbstractMembersContext(
baseClass: ts.ClassDeclaration, context: FunctionContext, classMetadataMap: ClassMetadataMap) {
getSuperClassDeclarations(baseClass, classMetadataMap).forEach(superClassDecl => {
superClassDecl.members.forEach(superClassMember => {
if (!superClassMember.name || !hasModifier(superClassMember, ts.SyntaxKind.AbstractKeyword)) {
return;
}
// Find the matching implementation of the abstract declaration from the super class.
const baseClassImpl = baseClass.members.find(
baseClassMethod => !!baseClassMethod.name &&
getPropertyNameText(baseClassMethod.name) ===
getPropertyNameText(superClassMember.name !));
if (!baseClassImpl || !isFunctionLikeDeclaration(baseClassImpl) || !baseClassImpl.body) {
return;
}
if (!context.has(superClassMember)) {
context.set(superClassMember, baseClassImpl);
}
});
});
}
/** Gets all super-class TypeScript declarations for the given class. */
function getSuperClassDeclarations(
export function getSuperClassDeclarations(
classDecl: ts.ClassDeclaration, classMetadataMap: ClassMetadataMap) {
const declarations: ts.ClassDeclaration[] = [];

View File

@ -11,9 +11,9 @@ import * as ts from 'typescript';
import {NgComponentTemplateVisitor} from '../../../utils/ng_component_template';
import {visitAllNodes} from '../../../utils/typescript/visit_nodes';
import {analyzeNgQueryUsage} from '../angular/analyze_query_usage';
import {NgQueryResolveVisitor} from '../angular/ng_query_visitor';
import {QueryTiming} from '../angular/query-definition';
import {QueryUsageStrategy} from '../strategies/usage_strategy/usage_strategy';
import {getTransformedQueryCallExpr} from '../transform';
const FAILURE_MESSAGE = 'Query does not explicitly specify its timing. Read more here: ' +
@ -52,6 +52,7 @@ export class Rule extends Rules.TypedRule {
});
const queries = resolvedQueries.get(sourceFile);
const usageStrategy = new QueryUsageStrategy(classMetadata, typeChecker);
// No queries detected for the given source file.
if (!queries) {
@ -62,7 +63,7 @@ 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 = analyzeNgQueryUsage(q, classMetadata, typeChecker);
const timing = usageStrategy.detectTiming(q);
const transformedNode = getTransformedQueryCallExpr(q, timing);
if (!transformedNode) {

View File

@ -15,8 +15,8 @@ import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
import {visitAllNodes} from '../../utils/typescript/visit_nodes';
import {analyzeNgQueryUsage} from './angular/analyze_query_usage';
import {NgQueryResolveVisitor} from './angular/ng_query_visitor';
import {QueryUsageStrategy} from './strategies/usage_strategy/usage_strategy';
import {getTransformedQueryCallExpr} from './transform';
@ -83,6 +83,8 @@ function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: str
}
});
const usageStrategy = new QueryUsageStrategy(classMetadata, typeChecker);
// 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
@ -94,7 +96,7 @@ function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: str
// query definitions to explicitly declare the query timing (static or dynamic)
queries.forEach(q => {
const queryExpr = q.decorator.node.expression;
const timing = analyzeNgQueryUsage(q, classMetadata, typeChecker);
const timing = usageStrategy.detectTiming(q);
const transformedNode = getTransformedQueryCallExpr(q, timing);
if (!transformedNode) {

View File

@ -7,7 +7,7 @@
*/
import * as ts from 'typescript';
import {isFunctionLikeDeclaration, unwrapExpression} from '../../../utils/typescript/functions';
import {isFunctionLikeDeclaration, unwrapExpression} from '../../../../utils/typescript/functions';
export type FunctionContext = Map<ts.Node, ts.Node>;

View File

@ -0,0 +1,49 @@
/**
* @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 {isFunctionLikeDeclaration} from '../../../../utils/typescript/functions';
import {hasModifier} from '../../../../utils/typescript/nodes';
import {getPropertyNameText} from '../../../../utils/typescript/property_name';
import {ClassMetadataMap} from '../../angular/ng_query_visitor';
import {getSuperClassDeclarations} from '../../angular/super_class';
import {FunctionContext} from './declaration_usage_visitor';
/**
* Updates the specified function context to map abstract super-class class members
* to their implementation TypeScript nodes. This allows us to run the declaration visitor
* for the super class with the context of the "baseClass" (e.g. with implemented abstract
* class members)
*/
export function updateSuperClassAbstractMembersContext(
baseClass: ts.ClassDeclaration, context: FunctionContext, classMetadataMap: ClassMetadataMap) {
getSuperClassDeclarations(baseClass, classMetadataMap).forEach(superClassDecl => {
superClassDecl.members.forEach(superClassMember => {
if (!superClassMember.name || !hasModifier(superClassMember, ts.SyntaxKind.AbstractKeyword)) {
return;
}
// Find the matching implementation of the abstract declaration from the super class.
const baseClassImpl = baseClass.members.find(
baseClassMethod => !!baseClassMethod.name &&
getPropertyNameText(baseClassMethod.name) ===
getPropertyNameText(superClassMember.name !));
if (!baseClassImpl || !isFunctionLikeDeclaration(baseClassImpl) || !baseClassImpl.body) {
return;
}
if (!context.has(superClassMember)) {
context.set(superClassMember, baseClassImpl);
}
});
});
}

View File

@ -13,7 +13,7 @@ import {BoundAttribute, BoundEvent, BoundText, Element, Node, NullVisitor, Templ
* AST visitor that traverses the Render3 HTML AST in order to check if the given
* query property is accessed statically in the template.
*/
export class QueryReadHtmlVisitor extends NullVisitor {
export class TemplateUsageVisitor extends NullVisitor {
private hasQueryTemplateReference = false;
private expressionAstVisitor = new ExpressionAstVisitor(this.queryPropertyName);

View File

@ -8,14 +8,15 @@
import * as ts from 'typescript';
import {parseHtmlGracefully} from '../../../utils/parse_html';
import {hasPropertyNameText} from '../../../utils/typescript/property_name';
import {parseHtmlGracefully} from '../../../../utils/parse_html';
import {hasPropertyNameText} from '../../../../utils/typescript/property_name';
import {ClassMetadataMap} from '../../angular/ng_query_visitor';
import {NgQueryDefinition, QueryTiming, QueryType} from '../../angular/query-definition';
import {TimingStrategy} from '../../timing-strategy';
import {DeclarationUsageVisitor, FunctionContext} from './declaration_usage_visitor';
import {ClassMetadataMap} from './ng_query_visitor';
import {NgQueryDefinition, QueryTiming, QueryType} from './query-definition';
import {QueryReadHtmlVisitor} from './query_read_html_visitor';
import {updateSuperClassAbstractMembersContext} from './super_class';
import {updateSuperClassAbstractMembersContext} from './super_class_context';
import {TemplateUsageVisitor} from './template_usage_visitor';
/**
@ -29,17 +30,25 @@ const STATIC_QUERY_LIFECYCLE_HOOKS = {
};
/**
* Analyzes the usage of the given query and determines the query timing based
* on the current usage of the query.
* Query timing strategy that determines the timing of a given query by inspecting how
* the query is accessed within the project's TypeScript source files. Read more about
* this strategy here: https://hackmd.io/s/Hymvc2OKE
*/
export function analyzeNgQueryUsage(
query: NgQueryDefinition, classMetadata: ClassMetadataMap,
typeChecker: ts.TypeChecker): QueryTiming {
return isQueryUsedStatically(query.container, query, classMetadata, typeChecker, []) ?
QueryTiming.STATIC :
QueryTiming.DYNAMIC;
export class QueryUsageStrategy implements TimingStrategy {
constructor(private classMetadata: ClassMetadataMap, private typeChecker: ts.TypeChecker) {}
/**
* Analyzes the usage of the given query and determines the query timing based
* on the current usage of the query.
*/
detectTiming(query: NgQueryDefinition): QueryTiming {
return isQueryUsedStatically(query.container, query, this.classMetadata, this.typeChecker, []) ?
QueryTiming.STATIC :
QueryTiming.DYNAMIC;
}
}
/**
* Checks whether a given query is used statically within the given class, its super
* class or derived classes.
@ -79,7 +88,7 @@ function isQueryUsedStatically(
if (classMetadata.template && hasPropertyNameText(query.property.name)) {
const template = classMetadata.template;
const parsedHtml = parseHtmlGracefully(template.content, template.filePath);
const htmlVisitor = new QueryReadHtmlVisitor(query.property.name.text);
const htmlVisitor = new TemplateUsageVisitor(query.property.name.text);
if (parsedHtml && htmlVisitor.isQueryUsedStatically(parsedHtml)) {
return true;

View File

@ -0,0 +1,11 @@
/**
* @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 {NgQueryDefinition, QueryTiming} from './angular/query-definition';
export interface TimingStrategy { detectTiming(query: NgQueryDefinition): QueryTiming; }