Skip to content

Commit 453b32f

Browse files
JoostKAndrewKushnir
authored andcommitted
fix(compiler-cli): report error when a reference target is missing instead of crashing (#39805)
If a template declares a reference to a missing target then referring to that reference from elsewhere in the template would crash the template type checker, due to a regression introduced in #38618. This commit fixes the crash by ensuring that the invalid reference will resolve to a variable of type any. Fixes #39744 PR Close #39805
1 parent b5c0f9d commit 453b32f

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,26 @@ class TcbReferenceOp extends TcbOp {
458458
}
459459
}
460460

461+
/**
462+
* A `TcbOp` which is used when the target of a reference is missing. This operation generates a
463+
* variable of type any for usages of the invalid reference to resolve to. The invalid reference
464+
* itself is recorded out-of-band.
465+
*/
466+
class TcbInvalidReferenceOp extends TcbOp {
467+
constructor(private readonly tcb: Context, private readonly scope: Scope) {
468+
super();
469+
}
470+
471+
// The declaration of a missing reference is only needed when the reference is resolved.
472+
readonly optional = true;
473+
474+
execute(): ts.Identifier {
475+
const id = this.tcb.allocateId();
476+
this.scope.addStatement(tsCreateVariable(id, NULL_AS_ANY));
477+
return id;
478+
}
479+
}
480+
461481
/**
462482
* A `TcbOp` which constructs an instance of a directive with types inferred from its inputs. The
463483
* inputs themselves are not checked here; checking of inputs is achieved in `TcbDirectiveInputsOp`.
@@ -1353,13 +1373,15 @@ class Scope {
13531373
private checkAndAppendReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void {
13541374
for (const ref of node.references) {
13551375
const target = this.tcb.boundTarget.getReferenceTarget(ref);
1376+
1377+
let ctxIndex: number;
13561378
if (target === null) {
1379+
// The reference is invalid if it doesn't have a target, so report it as an error.
13571380
this.tcb.oobRecorder.missingReferenceTarget(this.tcb.id, ref);
1358-
continue;
1359-
}
13601381

1361-
let ctxIndex: number;
1362-
if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) {
1382+
// Any usages of the invalid reference will be resolved to a variable of type any.
1383+
ctxIndex = this.opQueue.push(new TcbInvalidReferenceOp(this.tcb, this)) - 1;
1384+
} else if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) {
13631385
ctxIndex = this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target)) - 1;
13641386
} else {
13651387
ctxIndex =

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,29 @@ export declare class AnimationEvent {
10801080
expect(getSourceCodeForDiagnostic(diags[0])).toBe('unknownTarget');
10811081
});
10821082

1083+
it('should treat an unknown local ref target as type any', () => {
1084+
env.write('test.ts', `
1085+
import {Component, NgModule} from '@angular/core';
1086+
1087+
@Component({
1088+
selector: 'test',
1089+
template: '<div #ref="unknownTarget">{{ use(ref) }}</div>',
1090+
})
1091+
class TestCmp {
1092+
use(ref: string): string { return ref; }
1093+
}
1094+
1095+
@NgModule({
1096+
declarations: [TestCmp],
1097+
})
1098+
class Module {}
1099+
`);
1100+
const diags = env.driveDiagnostics();
1101+
expect(diags.length).toBe(1);
1102+
expect(diags[0].messageText).toBe(`No directive found with exportAs 'unknownTarget'.`);
1103+
expect(getSourceCodeForDiagnostic(diags[0])).toBe('unknownTarget');
1104+
});
1105+
10831106
it('should report an error with an unknown pipe', () => {
10841107
env.write('test.ts', `
10851108
import {Component, NgModule} from '@angular/core';

0 commit comments

Comments
 (0)