diff --git a/packages/core/schematics/migrations/static-queries/angular/super_class.ts b/packages/core/schematics/migrations/static-queries/angular/super_class.ts index b86860a731..77e3cdb674 100644 --- a/packages/core/schematics/migrations/static-queries/angular/super_class.ts +++ b/packages/core/schematics/migrations/static-queries/angular/super_class.ts @@ -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[] = []; diff --git a/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts b/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts index e5d26a9634..73b301089d 100644 --- a/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts +++ b/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts @@ -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) { diff --git a/packages/core/schematics/migrations/static-queries/index.ts b/packages/core/schematics/migrations/static-queries/index.ts index 727820792a..b8b91ddbbc 100644 --- a/packages/core/schematics/migrations/static-queries/index.ts +++ b/packages/core/schematics/migrations/static-queries/index.ts @@ -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) { diff --git a/packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/declaration_usage_visitor.ts similarity index 99% rename from packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts rename to packages/core/schematics/migrations/static-queries/strategies/usage_strategy/declaration_usage_visitor.ts index cdbcddd0db..668a71b5e5 100644 --- a/packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts +++ b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/declaration_usage_visitor.ts @@ -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; diff --git a/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/super_class_context.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/super_class_context.ts new file mode 100644 index 0000000000..b48589e8a1 --- /dev/null +++ b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/super_class_context.ts @@ -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); + } + }); + }); +} diff --git a/packages/core/schematics/migrations/static-queries/angular/query_read_html_visitor.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/template_usage_visitor.ts similarity index 98% rename from packages/core/schematics/migrations/static-queries/angular/query_read_html_visitor.ts rename to packages/core/schematics/migrations/static-queries/strategies/usage_strategy/template_usage_visitor.ts index 7ff96a436d..3cfc79c2d2 100644 --- a/packages/core/schematics/migrations/static-queries/angular/query_read_html_visitor.ts +++ b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/template_usage_visitor.ts @@ -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); diff --git a/packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/usage_strategy.ts similarity index 81% rename from packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts rename to packages/core/schematics/migrations/static-queries/strategies/usage_strategy/usage_strategy.ts index 64f2dbad5f..3aa4801d2a 100644 --- a/packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts +++ b/packages/core/schematics/migrations/static-queries/strategies/usage_strategy/usage_strategy.ts @@ -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; diff --git a/packages/core/schematics/migrations/static-queries/timing-strategy.ts b/packages/core/schematics/migrations/static-queries/timing-strategy.ts new file mode 100644 index 0000000000..c8af666961 --- /dev/null +++ b/packages/core/schematics/migrations/static-queries/timing-strategy.ts @@ -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; }