From 3e3a1ef30dd9aa97fc2c0a0de12f0e6dc0eff13a Mon Sep 17 00:00:00 2001 From: JoostK Date: Mon, 10 Feb 2020 20:53:13 +0100 Subject: [PATCH] fix(ivy): support dynamic query tokens in AOT mode (#35307) For view and content queries, the Ivy compiler attempts to statically evaluate the predicate token so that string predicates containing comma-separated reference names can be split into an array of strings during compilation. When the predicate is a dynamic value that cannot be statically interpreted at compile time, the compiler would previously produce an error. This behavior breaks a use-case where an `InjectionToken` is being used as query predicate, as the usage of the `new` keyword prevents such predicates from being statically evaluated. This commit changes the behavior to no longer produce an error for dynamic values. Instead, the expression is emitted as is into the generated code, postponing the evaluation to happen at runtime. Fixes #34267 Resolves FW-1828 PR Close #35307 --- .../src/ngtsc/annotations/src/directive.ts | 3 +- .../test/ngtsc/fake_core/index.ts | 4 +++ .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 32 ++++++++++++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index fdc508a398..a0caee7461 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -341,7 +341,8 @@ export function extractQueryMetadata( // Extract the predicate let predicate: Expression|string[]|null = null; - if (arg instanceof Reference) { + if (arg instanceof Reference || arg instanceof DynamicValue) { + // References and predicates that could not be evaluated statically are emitted as is. predicate = new WrappedNodeExpr(node); } else if (typeof arg === 'string') { predicate = [arg]; diff --git a/packages/compiler-cli/test/ngtsc/fake_core/index.ts b/packages/compiler-cli/test/ngtsc/fake_core/index.ts index d0ed030493..6c296d2137 100644 --- a/packages/compiler-cli/test/ngtsc/fake_core/index.ts +++ b/packages/compiler-cli/test/ngtsc/fake_core/index.ts @@ -57,6 +57,10 @@ export class ɵNgModuleFactory { constructor(public clazz: T) {} } +export class InjectionToken { + constructor(description: string) {} +} + export function forwardRef(fn: () => T): T { return fn(); } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 695c4cc2b9..76c840ab42 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -22,9 +22,9 @@ const trim = (input: string): string => input.replace(/\s+/g, ' ').trim(); const varRegExp = (name: string): RegExp => new RegExp(`var \\w+ = \\[\"${name}\"\\];`); -const viewQueryRegExp = (descend: boolean, ref?: string): RegExp => { +const viewQueryRegExp = (predicate: string, descend: boolean, ref?: string): RegExp => { const maybeRef = ref ? `, ${ref}` : ``; - return new RegExp(`i0\\.ɵɵviewQuery\\(\\w+, ${descend}${maybeRef}\\)`); + return new RegExp(`i0\\.ɵɵviewQuery\\(${predicate}, ${descend}${maybeRef}\\)`); }; const contentQueryRegExp = (predicate: string, descend: boolean, ref?: string): RegExp => { @@ -2396,7 +2396,7 @@ runInEachFileSystem(os => { // match `i0.ɵɵcontentQuery(dirIndex, _c1, true, TemplateRef)` expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef')); // match `i0.ɵɵviewQuery(_c2, true, null)` - expect(jsContents).toMatch(viewQueryRegExp(true)); + expect(jsContents).toMatch(viewQueryRegExp('\\w+', true)); }); it('should generate queries for directives', () => { @@ -2430,7 +2430,7 @@ runInEachFileSystem(os => { // match `i0.ɵɵviewQuery(_c2, true)` // Note that while ViewQuery doesn't necessarily make sense on a directive, because it doesn't // have a view, we still need to handle it because a component could extend the directive. - expect(jsContents).toMatch(viewQueryRegExp(true)); + expect(jsContents).toMatch(viewQueryRegExp('\\w+', true)); }); it('should handle queries that use forwardRef', () => { @@ -2461,6 +2461,30 @@ runInEachFileSystem(os => { expect(jsContents).toMatch(contentQueryRegExp('_c0', true)); }); + it('should handle queries that use an InjectionToken', () => { + env.write(`test.ts`, ` + import {Component, ContentChild, InjectionToken, ViewChild} from '@angular/core'; + + const TOKEN = new InjectionToken('token'); + + @Component({ + selector: 'test', + template: '
', + }) + class FooCmp { + @ViewChild(TOKEN as any) viewChild: any; + @ContentChild(TOKEN as any) contentChild: any; + } + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + // match `i0.ɵɵviewQuery(TOKEN, true, null)` + expect(jsContents).toMatch(viewQueryRegExp('TOKEN', true)); + // match `i0.ɵɵcontentQuery(dirIndex, TOKEN, true, null)` + expect(jsContents).toMatch(contentQueryRegExp('TOKEN', true)); + }); + it('should compile expressions that write keys', () => { env.write(`test.ts`, ` import {Component, ContentChild, TemplateRef, ViewContainerRef, forwardRef} from '@angular/core';