Skip to content

Commit d8751b1

Browse files
fix(compiler): ensure that partially compiled queries can handle forward references
When a partially compiled component or directive is "linked" in JIT mode, the body of its declaration is evaluated by the JavaScript runtime. If a class is referenced in a query (e.g. `ViewQuery` or `ContentQuery`) but its definition is later in the file, then the reference must be wrapped in a `forwardRef()` call. Previously, query predicates were not wrapped correctly in partial declarations causing the code to crash at runtime. In AOT mode, this code is never evaluated but instead transformed as part of the build, so this bug did not become apparent until Angular Material started running JIT mode tests on its distributable output. This change fixes this problem by noting when queries are wrapped in `forwardRef()` calls and ensuring that this gets passed through to partial compilation declarations and then suitably stripped during linking. See angular/components#23882 and angular/components#23907
1 parent 0c0698a commit d8751b1

File tree

14 files changed

+366
-18
lines changed

14 files changed

+366
-18
lines changed

Diff for: packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {AstObject, AstValue} from '../../ast/ast_value';
1414
import {FatalLinkerError} from '../../fatal_linker_error';
1515

1616
import {PartialLinker} from './partial_linker';
17-
import {wrapReference} from './util';
17+
import {extractForwardRef, wrapReference} from './util';
1818

1919
/**
2020
* A `PartialLinker` that is designed to process `ɵɵngDeclareDirective()` call expressions.
@@ -141,7 +141,7 @@ function toQueryMetadata<TExpression>(obj: AstObject<R3DeclareQueryMetadata, TEx
141141
if (predicateExpr.isArray()) {
142142
predicate = predicateExpr.getArray().map(entry => entry.getString());
143143
} else {
144-
predicate = predicateExpr.getOpaque();
144+
predicate = extractForwardRef(predicateExpr);
145145
}
146146
return {
147147
propertyName: obj.getString('propertyName'),

Diff for: packages/compiler-cli/src/ngtsc/annotations/src/directive.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, ExternalExpr, FactoryTarget, getSafePropertyAccessString, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3ClassMetadata, R3DirectiveMetadata, R3FactoryMetadata, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
9+
import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, createMayBeForwardRefExpression, Expression, ExternalExpr, FactoryTarget, getSafePropertyAccessString, makeBindingParser, MaybeForwardRefExpression, ParsedHostBindings, ParseError, parseHostBindings, R3ClassMetadata, R3DirectiveMetadata, R3QueryMetadata, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
1010
import {emitDistinctChangesOnlyDefaultValue} from '@angular/compiler/src/core';
1111
import * as ts from 'typescript';
1212

@@ -532,17 +532,20 @@ export function extractQueryMetadata(
532532
ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`);
533533
}
534534
const first = name === 'ViewChild' || name === 'ContentChild';
535-
const node = tryUnwrapForwardRef(args[0], reflector) ?? args[0];
535+
const forwardReferenceTarget = tryUnwrapForwardRef(args[0], reflector);
536+
const node = forwardReferenceTarget ?? args[0];
537+
536538
const arg = evaluator.evaluate(node);
537539

538540
/** Whether or not this query should collect only static results (see view/api.ts) */
539541
let isStatic: boolean = false;
540542

541543
// Extract the predicate
542-
let predicate: Expression|string[]|null = null;
544+
let predicate: MaybeForwardRefExpression|string[]|null = null;
543545
if (arg instanceof Reference || arg instanceof DynamicValue) {
544546
// References and predicates that could not be evaluated statically are emitted as is.
545-
predicate = new WrappedNodeExpr(node);
547+
predicate =
548+
createMayBeForwardRefExpression(new WrappedNodeExpr(node), forwardReferenceTarget !== null);
546549
} else if (typeof arg === 'string') {
547550
predicate = [arg];
548551
} else if (isStringArrayOrDie(arg, `@${name} predicate`, node)) {

Diff for: packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/GOLDEN_PARTIAL.js

+176
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,92 @@ export declare class MyModule {
7979
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
8080
}
8181

82+
/****************************************************************************************************
83+
* PARTIAL FILE: view_query_forward_ref.js
84+
****************************************************************************************************/
85+
import { Component, Directive, forwardRef, NgModule, ViewChild, ViewChildren } from '@angular/core';
86+
import * as i0 from "@angular/core";
87+
export class ViewQueryComponent {
88+
}
89+
ViewQueryComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ViewQueryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
90+
ViewQueryComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: ViewQueryComponent, selector: "view-query-component", viewQueries: [{ propertyName: "someDir", first: true, predicate: i0.forwardRef(function () { return SomeDirective; }), descendants: true }, { propertyName: "someDirList", predicate: i0.forwardRef(function () { return SomeDirective; }), descendants: true }], ngImport: i0, template: `
91+
<div someDir></div>
92+
`, isInline: true, directives: [{ type: i0.forwardRef(function () { return SomeDirective; }), selector: "[someDir]" }] });
93+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ViewQueryComponent, decorators: [{
94+
type: Component,
95+
args: [{
96+
selector: 'view-query-component',
97+
template: `
98+
<div someDir></div>
99+
`
100+
}]
101+
}], propDecorators: { someDir: [{
102+
type: ViewChild,
103+
args: [forwardRef(() => SomeDirective)]
104+
}], someDirList: [{
105+
type: ViewChildren,
106+
args: [forwardRef(() => SomeDirective)]
107+
}] } });
108+
export class MyApp {
109+
}
110+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
111+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "my-app", ngImport: i0, template: `
112+
<view-query-component></view-query-component>
113+
`, isInline: true, components: [{ type: ViewQueryComponent, selector: "view-query-component" }] });
114+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
115+
type: Component,
116+
args: [{
117+
selector: 'my-app',
118+
template: `
119+
<view-query-component></view-query-component>
120+
`
121+
}]
122+
}] });
123+
export class SomeDirective {
124+
}
125+
SomeDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
126+
SomeDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: SomeDirective, selector: "[someDir]", ngImport: i0 });
127+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, decorators: [{
128+
type: Directive,
129+
args: [{
130+
selector: '[someDir]',
131+
}]
132+
}] });
133+
export class MyModule {
134+
}
135+
MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
136+
MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [SomeDirective, ViewQueryComponent, MyApp] });
137+
MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule });
138+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{
139+
type: NgModule,
140+
args: [{ declarations: [SomeDirective, ViewQueryComponent, MyApp] }]
141+
}] });
142+
143+
/****************************************************************************************************
144+
* PARTIAL FILE: view_query_forward_ref.d.ts
145+
****************************************************************************************************/
146+
import { QueryList } from '@angular/core';
147+
import * as i0 from "@angular/core";
148+
export declare class ViewQueryComponent {
149+
someDir: SomeDirective;
150+
someDirList: QueryList<SomeDirective>;
151+
static ɵfac: i0.ɵɵFactoryDeclaration<ViewQueryComponent, never>;
152+
static ɵcmp: i0.ɵɵComponentDeclaration<ViewQueryComponent, "view-query-component", never, {}, {}, never, never>;
153+
}
154+
export declare class MyApp {
155+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
156+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never>;
157+
}
158+
export declare class SomeDirective {
159+
static ɵfac: i0.ɵɵFactoryDeclaration<SomeDirective, never>;
160+
static ɵdir: i0.ɵɵDirectiveDeclaration<SomeDirective, "[someDir]", never, {}, {}, never>;
161+
}
162+
export declare class MyModule {
163+
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
164+
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof SomeDirective, typeof ViewQueryComponent, typeof MyApp], never, never>;
165+
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
166+
}
167+
82168
/****************************************************************************************************
83169
* PARTIAL FILE: view_query_for_local_ref.js
84170
****************************************************************************************************/
@@ -410,6 +496,96 @@ export declare class MyModule {
410496
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
411497
}
412498

499+
/****************************************************************************************************
500+
* PARTIAL FILE: content_query_forward_ref.js
501+
****************************************************************************************************/
502+
import { Component, ContentChild, ContentChildren, Directive, forwardRef, NgModule } from '@angular/core';
503+
import * as i0 from "@angular/core";
504+
export class ContentQueryComponent {
505+
}
506+
ContentQueryComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ContentQueryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
507+
ContentQueryComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: ContentQueryComponent, selector: "content-query-component", queries: [{ propertyName: "someDir", first: true, predicate: i0.forwardRef(function () { return SomeDirective; }), descendants: true }, { propertyName: "someDirList", predicate: i0.forwardRef(function () { return SomeDirective; }) }], ngImport: i0, template: `
508+
<div><ng-content></ng-content></div>
509+
`, isInline: true });
510+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ContentQueryComponent, decorators: [{
511+
type: Component,
512+
args: [{
513+
selector: 'content-query-component',
514+
template: `
515+
<div><ng-content></ng-content></div>
516+
`
517+
}]
518+
}], propDecorators: { someDir: [{
519+
type: ContentChild,
520+
args: [forwardRef(() => SomeDirective)]
521+
}], someDirList: [{
522+
type: ContentChildren,
523+
args: [forwardRef(() => SomeDirective)]
524+
}] } });
525+
export class MyApp {
526+
}
527+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
528+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "my-app", ngImport: i0, template: `
529+
<content-query-component>
530+
<div someDir></div>
531+
</content-query-component>
532+
`, isInline: true, components: [{ type: i0.forwardRef(function () { return ContentQueryComponent; }), selector: "content-query-component" }], directives: [{ type: i0.forwardRef(function () { return SomeDirective; }), selector: "[someDir]" }] });
533+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
534+
type: Component,
535+
args: [{
536+
selector: 'my-app',
537+
template: `
538+
<content-query-component>
539+
<div someDir></div>
540+
</content-query-component>
541+
`
542+
}]
543+
}] });
544+
export class SomeDirective {
545+
}
546+
SomeDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
547+
SomeDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: SomeDirective, selector: "[someDir]", ngImport: i0 });
548+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, decorators: [{
549+
type: Directive,
550+
args: [{
551+
selector: '[someDir]',
552+
}]
553+
}] });
554+
export class MyModule {
555+
}
556+
MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
557+
MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [SomeDirective, ContentQueryComponent, MyApp] });
558+
MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule });
559+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{
560+
type: NgModule,
561+
args: [{ declarations: [SomeDirective, ContentQueryComponent, MyApp] }]
562+
}] });
563+
564+
/****************************************************************************************************
565+
* PARTIAL FILE: content_query_forward_ref.d.ts
566+
****************************************************************************************************/
567+
import { QueryList } from '@angular/core';
568+
import * as i0 from "@angular/core";
569+
export declare class ContentQueryComponent {
570+
someDir: SomeDirective;
571+
someDirList: QueryList<SomeDirective>;
572+
static ɵfac: i0.ɵɵFactoryDeclaration<ContentQueryComponent, never>;
573+
static ɵcmp: i0.ɵɵComponentDeclaration<ContentQueryComponent, "content-query-component", never, {}, {}, ["someDir", "someDirList"], ["*"]>;
574+
}
575+
export declare class MyApp {
576+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
577+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never>;
578+
}
579+
export declare class SomeDirective {
580+
static ɵfac: i0.ɵɵFactoryDeclaration<SomeDirective, never>;
581+
static ɵdir: i0.ɵɵDirectiveDeclaration<SomeDirective, "[someDir]", never, {}, {}, never>;
582+
}
583+
export declare class MyModule {
584+
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
585+
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof SomeDirective, typeof ContentQueryComponent, typeof MyApp], never, never>;
586+
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
587+
}
588+
413589
/****************************************************************************************************
414590
* PARTIAL FILE: content_query_for_local_ref.js
415591
****************************************************************************************************/

Diff for: packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/TEST_CASES.json

+28
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@
1616
}
1717
]
1818
},
19+
{
20+
"description": "should support view queries with forwardRefs",
21+
"inputFiles": [
22+
"view_query_forward_ref.ts"
23+
],
24+
"expectations": [
25+
{
26+
"failureMessage": "Invalid ViewQuery declaration",
27+
"files": [
28+
"view_query_forward_ref.js"
29+
]
30+
}
31+
]
32+
},
1933
{
2034
"description": "should support view queries with local refs",
2135
"inputFiles": [
@@ -75,6 +89,20 @@
7589
}
7690
]
7791
},
92+
{
93+
"description": "should support content queries with forwardRefs",
94+
"inputFiles": [
95+
"content_query_forward_ref.ts"
96+
],
97+
"expectations": [
98+
{
99+
"failureMessage": "Invalid ContentQuery declaration",
100+
"files": [
101+
"content_query_forward_ref.js"
102+
]
103+
}
104+
]
105+
},
78106
{
79107
"description": "should support content queries with local refs",
80108
"inputFiles": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
ContentQueryComponent.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
2+
type: ContentQueryComponent,
3+
selectors: [["content-query-component"]],
4+
contentQueries: function ContentQueryComponent_ContentQueries(rf, ctx, dirIndex) {
5+
if (rf & 1) {
6+
$r3$.ɵɵcontentQuery(dirIndex, SomeDirective, __QueryFlags.descendants__|__QueryFlags.emitDistinctChangesOnly__);
7+
$r3$.ɵɵcontentQuery(dirIndex, SomeDirective, __QueryFlags.emitDistinctChangesOnly__);
8+
}
9+
if (rf & 2) {
10+
let $tmp$;
11+
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDir = $tmp$.first);
12+
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDirList = $tmp$);
13+
}
14+
},
15+
ngContentSelectors: _c0,
16+
decls: 2,
17+
vars: 0,
18+
template: function ContentQueryComponent_Template(rf, ctx) {
19+
if (rf & 1) {
20+
$r3$.ɵɵprojectionDef();
21+
$r3$.ɵɵelementStart(0, "div");
22+
$r3$.ɵɵprojection(1);
23+
$r3$.ɵɵelementEnd();
24+
}
25+
},
26+
encapsulation: 2
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {Component, ContentChild, ContentChildren, Directive, forwardRef, NgModule, QueryList} from '@angular/core';
2+
3+
@Component({
4+
selector: 'content-query-component',
5+
template: `
6+
<div><ng-content></ng-content></div>
7+
`
8+
})
9+
export class ContentQueryComponent {
10+
@ContentChild(forwardRef(() => SomeDirective)) someDir!: SomeDirective;
11+
@ContentChildren(forwardRef(() => SomeDirective)) someDirList!: QueryList<SomeDirective>;
12+
}
13+
14+
@Component({
15+
selector: 'my-app',
16+
template: `
17+
<content-query-component>
18+
<div someDir></div>
19+
</content-query-component>
20+
`
21+
})
22+
export class MyApp {
23+
}
24+
25+
26+
@Directive({
27+
selector: '[someDir]',
28+
})
29+
export class SomeDirective {
30+
}
31+
32+
@NgModule({declarations: [SomeDirective, ContentQueryComponent, MyApp]})
33+
export class MyModule {
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
ViewQueryComponent.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
2+
type: ViewQueryComponent,
3+
selectors: [["view-query-component"]],
4+
viewQuery: function ViewQueryComponent_Query(rf, ctx) {
5+
if (rf & 1) {
6+
$r3$.ɵɵviewQuery(SomeDirective, __QueryFlags.descendants__|__QueryFlags.emitDistinctChangesOnly__);
7+
$r3$.ɵɵviewQuery(SomeDirective, __QueryFlags.descendants__|__QueryFlags.emitDistinctChangesOnly__);
8+
}
9+
if (rf & 2) {
10+
let $tmp$;
11+
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDir = $tmp$.first);
12+
$r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDirList = $tmp$);
13+
}
14+
},
15+
// ...
16+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {Component, Directive, forwardRef, NgModule, QueryList, ViewChild, ViewChildren} from '@angular/core';
2+
3+
@Component({
4+
selector: 'view-query-component',
5+
template: `
6+
<div someDir></div>
7+
`
8+
})
9+
export class ViewQueryComponent {
10+
@ViewChild(forwardRef(() => SomeDirective)) someDir!: SomeDirective;
11+
@ViewChildren(forwardRef(() => SomeDirective)) someDirList!: QueryList<SomeDirective>;
12+
}
13+
14+
@Component({
15+
selector: 'my-app',
16+
template: `
17+
<view-query-component></view-query-component>
18+
`
19+
})
20+
export class MyApp {
21+
}
22+
23+
24+
@Directive({
25+
selector: '[someDir]',
26+
})
27+
export class SomeDirective {
28+
}
29+
30+
@NgModule({declarations: [SomeDirective, ViewQueryComponent, MyApp]})
31+
export class MyModule {
32+
}

0 commit comments

Comments
 (0)