Skip to content

Commit c0ad276

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 17bfc2f commit c0ad276

File tree

21 files changed

+462
-68
lines changed

21 files changed

+462
-68
lines changed

packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareUsedDirectiveMetadata, R3PartialDeclaration, R3UsedDirectiveMetadata, ViewEncapsulation} from '@angular/compiler';
8+
import {ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareUsedDirectiveMetadata, R3PartialDeclaration, R3UsedDirectiveMetadata, ViewEncapsulation} from '@angular/compiler';
99

1010
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
1111
import {Range} from '../../ast/ast_host';
@@ -69,8 +69,8 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
6969
const type = directiveExpr.getValue('type');
7070
const selector = directiveExpr.getString('selector');
7171

72-
const {expression: typeExpr, isForwardRef} = extractForwardRef(type);
73-
if (isForwardRef) {
72+
const {expression: typeExpr, forwardRef} = extractForwardRef(type);
73+
if (forwardRef === ForwardRefHandling.Unwrapped) {
7474
declarationListEmitMode = DeclarationListEmitMode.Closure;
7575
}
7676

@@ -101,8 +101,8 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
101101
let pipes = new Map<string, o.Expression>();
102102
if (metaObj.has('pipes')) {
103103
pipes = metaObj.getObject('pipes').toMap(pipe => {
104-
const {expression: pipeType, isForwardRef} = extractForwardRef(pipe);
105-
if (isForwardRef) {
104+
const {expression: pipeType, forwardRef} = extractForwardRef(pipe);
105+
if (forwardRef === ForwardRefHandling.Unwrapped) {
106106
declarationListEmitMode = DeclarationListEmitMode.Closure;
107107
}
108108
return pipeType;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {AstObject, AstValue} from '../../ast/ast_value';
1313
import {FatalLinkerError} from '../../fatal_linker_error';
1414

1515
import {PartialLinker} from './partial_linker';
16-
import {wrapReference} from './util';
16+
import {extractForwardRef, wrapReference} from './util';
1717

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

packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {compileInjectable, ConstantPool, createMayBeForwardRefExpression, outputAst as o, R3DeclareInjectableMetadata, R3InjectableMetadata, R3PartialDeclaration} from '@angular/compiler';
8+
import {compileInjectable, ConstantPool, createMayBeForwardRefExpression, ForwardRefHandling, outputAst as o, R3DeclareInjectableMetadata, R3InjectableMetadata, R3PartialDeclaration} from '@angular/compiler';
99

1010
import {AstObject} from '../../ast/ast_value';
1111
import {FatalLinkerError} from '../../fatal_linker_error';
@@ -43,8 +43,9 @@ export function toR3InjectableMeta<TExpression>(
4343
type: wrapReference(typeExpr.getOpaque()),
4444
internalType: typeExpr.getOpaque(),
4545
typeArgumentCount: 0,
46-
providedIn: metaObj.has('providedIn') ? extractForwardRef(metaObj.getValue('providedIn')) :
47-
createMayBeForwardRefExpression(o.literal(null), false),
46+
providedIn: metaObj.has('providedIn') ?
47+
extractForwardRef(metaObj.getValue('providedIn')) :
48+
createMayBeForwardRefExpression(o.literal(null), ForwardRefHandling.None),
4849
};
4950

5051
if (metaObj.has('useClass')) {

packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {createMayBeForwardRefExpression, MaybeForwardRefExpression, outputAst as o, R3DeclareDependencyMetadata, R3DependencyMetadata, R3Reference} from '@angular/compiler';
8+
import {createMayBeForwardRefExpression, ForwardRefHandling, MaybeForwardRefExpression, outputAst as o, R3DeclareDependencyMetadata, R3DependencyMetadata, R3Reference} from '@angular/compiler';
99

1010
import {AstObject, AstValue} from '../../ast/ast_value';
1111
import {FatalLinkerError} from '../../fatal_linker_error';
@@ -68,7 +68,7 @@ export function getDependency<TExpression>(
6868
export function extractForwardRef<TExpression>(expr: AstValue<unknown, TExpression>):
6969
MaybeForwardRefExpression<o.WrappedNodeExpr<TExpression>> {
7070
if (!expr.isCallExpression()) {
71-
return createMayBeForwardRefExpression(expr.getOpaque(), /* isForwardRef */ false);
71+
return createMayBeForwardRefExpression(expr.getOpaque(), ForwardRefHandling.None);
7272
}
7373

7474
const callee = expr.getCallee();
@@ -90,5 +90,6 @@ export function extractForwardRef<TExpression>(expr: AstValue<unknown, TExpressi
9090
wrapperFn, 'Unsupported `forwardRef(fn)` call, expected its argument to be a function');
9191
}
9292

93-
return createMayBeForwardRefExpression(wrapperFn.getFunctionReturnValue().getOpaque(), true);
93+
return createMayBeForwardRefExpression(
94+
wrapperFn.getFunctionReturnValue().getOpaque(), ForwardRefHandling.Unwrapped);
9495
}

packages/compiler-cli/src/ngtsc/annotations/src/directive.ts

Lines changed: 8 additions & 4 deletions
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, emitDistinctChangesOnlyDefaultValue, 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, emitDistinctChangesOnlyDefaultValue, Expression, ExternalExpr, FactoryTarget, ForwardRefHandling, getSafePropertyAccessString, makeBindingParser, MaybeForwardRefExpression, ParsedHostBindings, ParseError, parseHostBindings, R3ClassMetadata, R3DirectiveMetadata, R3FactoryMetadata, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@@ -531,17 +531,21 @@ export function extractQueryMetadata(
531531
ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`);
532532
}
533533
const first = name === 'ViewChild' || name === 'ContentChild';
534-
const node = tryUnwrapForwardRef(args[0], reflector) ?? args[0];
534+
const forwardReferenceTarget = tryUnwrapForwardRef(args[0], reflector);
535+
const node = forwardReferenceTarget ?? args[0];
536+
535537
const arg = evaluator.evaluate(node);
536538

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

540542
// Extract the predicate
541-
let predicate: Expression|string[]|null = null;
543+
let predicate: MaybeForwardRefExpression|string[]|null = null;
542544
if (arg instanceof Reference || arg instanceof DynamicValue) {
543545
// References and predicates that could not be evaluated statically are emitted as is.
544-
predicate = new WrappedNodeExpr(node);
546+
predicate = createMayBeForwardRefExpression(
547+
new WrappedNodeExpr(node),
548+
forwardReferenceTarget !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None);
545549
} else if (typeof arg === 'string') {
546550
predicate = [arg];
547551
} else if (isStringArrayOrDie(arg, `@${name} predicate`, node)) {

packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts

Lines changed: 5 additions & 4 deletions
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, CompileClassMetadataFn, compileDeclareClassMetadata, compileDeclareInjectableFromMetadata, compileInjectable, createMayBeForwardRefExpression, FactoryTarget, LiteralExpr, MaybeForwardRefExpression, R3ClassMetadata, R3CompiledExpression, R3DependencyMetadata, R3InjectableMetadata, WrappedNodeExpr} from '@angular/compiler';
9+
import {compileClassMetadata, CompileClassMetadataFn, compileDeclareClassMetadata, compileDeclareInjectableFromMetadata, compileInjectable, createMayBeForwardRefExpression, FactoryTarget, ForwardRefHandling, LiteralExpr, MaybeForwardRefExpression, R3ClassMetadata, R3CompiledExpression, R3DependencyMetadata, R3InjectableMetadata, WrappedNodeExpr} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@@ -162,7 +162,7 @@ function extractInjectableMetadata(
162162
type,
163163
typeArgumentCount,
164164
internalType,
165-
providedIn: createMayBeForwardRefExpression(new LiteralExpr(null), false),
165+
providedIn: createMayBeForwardRefExpression(new LiteralExpr(null), ForwardRefHandling.None),
166166
};
167167
} else if (decorator.args.length === 1) {
168168
const metaNode = decorator.args[0];
@@ -180,7 +180,7 @@ function extractInjectableMetadata(
180180

181181
const providedIn = meta.has('providedIn') ?
182182
getProviderExpression(meta.get('providedIn')!, reflector) :
183-
createMayBeForwardRefExpression(new LiteralExpr(null), false);
183+
createMayBeForwardRefExpression(new LiteralExpr(null), ForwardRefHandling.None);
184184

185185
let deps: R3DependencyMetadata[]|undefined = undefined;
186186
if ((meta.has('useClass') || meta.has('useFactory')) && meta.has('deps')) {
@@ -223,7 +223,8 @@ function getProviderExpression(
223223
expression: ts.Expression, reflector: ReflectionHost): MaybeForwardRefExpression {
224224
const forwardRefValue = tryUnwrapForwardRef(expression, reflector);
225225
return createMayBeForwardRefExpression(
226-
new WrappedNodeExpr(forwardRefValue ?? expression), forwardRefValue !== null);
226+
new WrappedNodeExpr(forwardRefValue ?? expression),
227+
forwardRefValue !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None);
227228
}
228229

229230
function extractInjectableCtorDeps(

0 commit comments

Comments
 (0)