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:
parent
7a7781e925
commit
205a45e9a8
|
@ -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[] = [];
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>;
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
|
@ -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;
|
|
@ -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; }
|
Loading…
Reference in New Issue