diff --git a/packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts b/packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts index 62972e61fa..bf3feecc5d 100644 --- a/packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts +++ b/packages/core/schematics/migrations/static-queries/angular/declaration_usage_visitor.ts @@ -74,6 +74,25 @@ export class DeclarationUsageVisitor { } } + private visitPropertyAccessExpression(node: ts.PropertyAccessExpression, nodeQueue: ts.Node[]) { + const propertySymbol = this.typeChecker.getSymbolAtLocation(node.name); + + if (!propertySymbol || !propertySymbol.valueDeclaration || + this.visitedJumpExprSymbols.has(propertySymbol)) { + return; + } + + const valueDeclaration = propertySymbol.valueDeclaration; + + // In case the property access expression refers to a get accessor, we need to visit + // the body of the get accessor declaration as there could be logic that uses the + // given search node synchronously. + if (ts.isGetAccessorDeclaration(valueDeclaration) && valueDeclaration.body) { + this.visitedJumpExprSymbols.add(propertySymbol); + nodeQueue.push(valueDeclaration.body); + } + } + isSynchronouslyUsedInNode(searchNode: ts.Node): boolean { const nodeQueue: ts.Node[] = [searchNode]; this.visitedJumpExprSymbols.clear(); @@ -97,6 +116,12 @@ export class DeclarationUsageVisitor { this.addNewExpressionToQueue(node, nodeQueue); } + // Handle property access expressions. These could resolve to get-accessor declarations + // which can contain synchronous logic that accesses the search node. + if (ts.isPropertyAccessExpression(node)) { + this.visitPropertyAccessExpression(node, nodeQueue); + } + // Do not visit nodes that declare a block of statements but are not executed // synchronously (e.g. function declarations). We only want to check TypeScript // nodes which are synchronously executed in the control flow. diff --git a/packages/core/schematics/test/static_queries_migration_spec.ts b/packages/core/schematics/test/static_queries_migration_spec.ts index fbd94e5f11..7deac250f8 100644 --- a/packages/core/schematics/test/static_queries_migration_spec.ts +++ b/packages/core/schematics/test/static_queries_migration_spec.ts @@ -780,6 +780,67 @@ describe('static-queries migration', () => { .toContain(`@${queryType}('test', { static: true }) query: any;`); }); + it('should detect static queries used through getter property access', () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + private @${queryType}('test') query: any; + + get myProp() { + return this.query.myValue; + } + + ngOnInit() { + this.myProp.test(); + } + } + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: true }) query: any;`); + }); + + it('should detect static queries used through external getter access', () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + import {External} from './external'; + + @Component({template: ''}) + export class MyComp { + @${queryType}('test') query: any; + + private external = new External(this); + + get myProp() { + return this.query.myValue; + } + + ngOnInit() { + console.log(this.external.query); + } + } + `); + + writeFile('/external.ts', ` + import {MyComp} from './index'; + + export class External { + constructor(private comp: MyComp) {} + + get query() { return this.comp.query; } + } + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: true }) query: any;`); + }); + it('should properly handle multiple tsconfig files', () => { writeFile('/src/index.ts', ` import {Component, ${queryType}} from '@angular/core';