Skip to content

Commit 0a3edfb

Browse files
crisbetoalxhub
authored andcommitted
fix(compiler-cli): correctly detect deferred dependencies across scoped nodes (#54499)
This is based on an internal issue report. An earlier change introduced a diagnostic to report cases where a symbol is in the `deferredImports` array, but is used eagerly. The check worked by looking through the deferred blocks in a scope, resolving the scope for each and checking if the element is within the scope. The problem is that resolving the scope won't work across scoped node boundaries. For example, if there's a control flow statement around the block or within the block but around the deferred dependency, it won't be able to resolve the scope since it isn't a direct child, e.g. ``` @if (true) { @defer { <deferred-dep/> } } ``` To fix this the case where the deferred block is inside a scoped node, I've changed the `R3BoundTarget.deferBlocks` to be a `Map` holding both the deferred block and its corresponding scope. Then to resolve the case where the dependency is within a scoped node inside the deferred block, I've added a depth-first traversal through the scopes within the deferred block. PR Close #54499
1 parent f00336c commit 0a3edfb

File tree

2 files changed

+107
-13
lines changed

2 files changed

+107
-13
lines changed

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10063,6 +10063,86 @@ function allTests(os: string) {
1006310063
expect(diags.length).toBe(1);
1006410064
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY));
1006510065
});
10066+
10067+
it('should not produce an error when a deferred block is wrapped in a conditional',
10068+
() => {
10069+
env.write('deferred-a.ts', `
10070+
import {Component} from '@angular/core';
10071+
@Component({
10072+
standalone: true,
10073+
selector: 'deferred-cmp-a',
10074+
template: 'DeferredCmpA contents',
10075+
})
10076+
export class DeferredCmpA {
10077+
}
10078+
`);
10079+
10080+
env.write('test.ts', `
10081+
import {Component} from '@angular/core';
10082+
import {DeferredCmpA} from './deferred-a';
10083+
@Component({
10084+
standalone: true,
10085+
deferredImports: [DeferredCmpA],
10086+
template: \`
10087+
@if (true) {
10088+
@if (true) {
10089+
@if (true) {
10090+
@defer {
10091+
<deferred-cmp-a />
10092+
}
10093+
}
10094+
}
10095+
}
10096+
\`,
10097+
})
10098+
export class AppCmp {
10099+
condition = true;
10100+
}
10101+
`);
10102+
10103+
const diags = env.driveDiagnostics();
10104+
expect(diags).toEqual([]);
10105+
});
10106+
10107+
it('should not produce an error when a dependency is wrapped in a condition inside of a deferred block',
10108+
() => {
10109+
env.write('deferred-a.ts', `
10110+
import {Component} from '@angular/core';
10111+
@Component({
10112+
standalone: true,
10113+
selector: 'deferred-cmp-a',
10114+
template: 'DeferredCmpA contents',
10115+
})
10116+
export class DeferredCmpA {
10117+
}
10118+
`);
10119+
10120+
env.write('test.ts', `
10121+
import {Component} from '@angular/core';
10122+
import {DeferredCmpA} from './deferred-a';
10123+
@Component({
10124+
standalone: true,
10125+
deferredImports: [DeferredCmpA],
10126+
template: \`
10127+
@defer {
10128+
@if (true) {
10129+
@if (true) {
10130+
@if (true) {
10131+
<deferred-cmp-a />
10132+
}
10133+
}
10134+
}
10135+
}
10136+
\`,
10137+
})
10138+
export class AppCmp {
10139+
condition = true;
10140+
}
10141+
`);
10142+
10143+
const diags = env.driveDiagnostics();
10144+
expect(diags).toEqual([]);
10145+
});
1006610146
});
1006710147
});
1006810148

packages/compiler/src/render3/view/t2_binder.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
5151
TemplateBinder.applyWithScope(target.template, scope);
5252
return new R3BoundTarget(
5353
target, directives, eagerDirectives, bindings, references, expressions, symbols,
54-
nestingLevel, scopedNodeEntities, usedPipes, eagerPipes, deferBlocks, scope);
54+
nestingLevel, scopedNodeEntities, usedPipes, eagerPipes, deferBlocks);
5555
}
5656
}
5757

@@ -472,7 +472,7 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
472472
private constructor(
473473
private bindings: Map<AST, Reference|Variable>,
474474
private symbols: Map<Reference|Variable, ScopedNode>, private usedPipes: Set<string>,
475-
private eagerPipes: Set<string>, private deferBlocks: Set<DeferredBlock>,
475+
private eagerPipes: Set<string>, private deferBlocks: Map<DeferredBlock, Scope>,
476476
private nestingLevel: Map<ScopedNode, number>, private scope: Scope,
477477
private rootNode: ScopedNode|null, private level: number) {
478478
super();
@@ -510,15 +510,15 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
510510
nestingLevel: Map<ScopedNode, number>,
511511
usedPipes: Set<string>,
512512
eagerPipes: Set<string>,
513-
deferBlocks: Set<DeferredBlock>,
513+
deferBlocks: Map<DeferredBlock, Scope>,
514514
} {
515515
const expressions = new Map<AST, Reference|Variable>();
516516
const symbols = new Map<Variable|Reference, Template>();
517517
const nestingLevel = new Map<ScopedNode, number>();
518518
const usedPipes = new Set<string>();
519519
const eagerPipes = new Set<string>();
520520
const template = nodes instanceof Template ? nodes : null;
521-
const deferBlocks = new Set<DeferredBlock>();
521+
const deferBlocks = new Map<DeferredBlock, Scope>();
522522
// The top-level template has nesting level 0.
523523
const binder = new TemplateBinder(
524524
expressions, symbols, usedPipes, eagerPipes, deferBlocks, nestingLevel, scope, template, 0);
@@ -547,9 +547,17 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
547547
nodeOrNodes.trackBy.visit(this);
548548
nodeOrNodes.children.forEach(this.visitNode);
549549
this.nestingLevel.set(nodeOrNodes, this.level);
550+
} else if (nodeOrNodes instanceof DeferredBlock) {
551+
if (this.scope.rootNode !== nodeOrNodes) {
552+
throw new Error(
553+
`Assertion error: resolved incorrect scope for deferred block ${nodeOrNodes}`);
554+
}
555+
this.deferBlocks.set(nodeOrNodes, this.scope);
556+
nodeOrNodes.children.forEach(node => node.visit(this));
557+
this.nestingLevel.set(nodeOrNodes, this.level);
550558
} else if (
551559
nodeOrNodes instanceof SwitchBlockCase || nodeOrNodes instanceof ForLoopBlockEmpty ||
552-
nodeOrNodes instanceof DeferredBlock || nodeOrNodes instanceof DeferredBlockError ||
560+
nodeOrNodes instanceof DeferredBlockError ||
553561
nodeOrNodes instanceof DeferredBlockPlaceholder ||
554562
nodeOrNodes instanceof DeferredBlockLoading) {
555563
nodeOrNodes.children.forEach(node => node.visit(this));
@@ -616,9 +624,7 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
616624
}
617625

618626
visitDeferredBlock(deferred: DeferredBlock) {
619-
this.deferBlocks.add(deferred);
620627
this.ingestScopedNode(deferred);
621-
622628
deferred.triggers.when?.value.visit(this);
623629
deferred.prefetchTriggers.when?.value.visit(this);
624630
deferred.placeholder && this.visitNode(deferred.placeholder);
@@ -738,7 +744,7 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
738744
private nestingLevel: Map<ScopedNode, number>,
739745
private scopedNodeEntities: Map<ScopedNode|null, ReadonlySet<Reference|Variable>>,
740746
private usedPipes: Set<string>, private eagerPipes: Set<string>,
741-
private deferredBlocks: Set<DeferredBlock>, private rootScope: Scope) {}
747+
private deferBlocks: Map<DeferredBlock, Scope>) {}
742748

743749
getEntitiesInScope(node: ScopedNode|null): ReadonlySet<Reference|Variable> {
744750
return this.scopedNodeEntities.get(node) ?? new Set();
@@ -789,7 +795,7 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
789795
}
790796

791797
getDeferBlocks(): DeferredBlock[] {
792-
return Array.from(this.deferredBlocks);
798+
return Array.from(this.deferBlocks.keys());
793799
}
794800

795801
getDeferredTriggerTarget(block: DeferredBlock, trigger: DeferredTrigger): Element|null {
@@ -856,12 +862,20 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
856862
}
857863

858864
isDeferred(element: Element): boolean {
859-
for (const deferBlock of this.deferredBlocks) {
860-
const scope = this.rootScope.childScopes.get(deferBlock);
861-
if (scope && scope.elementsInScope.has(element)) {
862-
return true;
865+
for (const deferredScope of this.deferBlocks.values()) {
866+
const stack = [deferredScope];
867+
868+
while (stack.length > 0) {
869+
const current = stack.pop()!;
870+
871+
if (current.elementsInScope.has(element)) {
872+
return true;
873+
}
874+
875+
stack.push(...current.childScopes.values());
863876
}
864877
}
878+
865879
return false;
866880
}
867881

0 commit comments

Comments
 (0)