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
This commit is contained in:
JoostK 2020-11-22 15:06:32 +01:00 committed by Andrew Kushnir
parent b5c0f9d7d1
commit 453b32f4b9
2 changed files with 50 additions and 5 deletions

View File

@ -458,6 +458,26 @@ class TcbReferenceOp extends TcbOp {
} }
} }
/**
* A `TcbOp` which is used when the target of a reference is missing. This operation generates a
* variable of type any for usages of the invalid reference to resolve to. The invalid reference
* itself is recorded out-of-band.
*/
class TcbInvalidReferenceOp extends TcbOp {
constructor(private readonly tcb: Context, private readonly scope: Scope) {
super();
}
// The declaration of a missing reference is only needed when the reference is resolved.
readonly optional = true;
execute(): ts.Identifier {
const id = this.tcb.allocateId();
this.scope.addStatement(tsCreateVariable(id, NULL_AS_ANY));
return id;
}
}
/** /**
* A `TcbOp` which constructs an instance of a directive with types inferred from its inputs. The * A `TcbOp` which constructs an instance of a directive with types inferred from its inputs. The
* inputs themselves are not checked here; checking of inputs is achieved in `TcbDirectiveInputsOp`. * inputs themselves are not checked here; checking of inputs is achieved in `TcbDirectiveInputsOp`.
@ -1353,13 +1373,15 @@ class Scope {
private checkAndAppendReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void { private checkAndAppendReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void {
for (const ref of node.references) { for (const ref of node.references) {
const target = this.tcb.boundTarget.getReferenceTarget(ref); const target = this.tcb.boundTarget.getReferenceTarget(ref);
if (target === null) {
this.tcb.oobRecorder.missingReferenceTarget(this.tcb.id, ref);
continue;
}
let ctxIndex: number; let ctxIndex: number;
if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) { if (target === null) {
// The reference is invalid if it doesn't have a target, so report it as an error.
this.tcb.oobRecorder.missingReferenceTarget(this.tcb.id, ref);
// Any usages of the invalid reference will be resolved to a variable of type any.
ctxIndex = this.opQueue.push(new TcbInvalidReferenceOp(this.tcb, this)) - 1;
} else if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) {
ctxIndex = this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target)) - 1; ctxIndex = this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target)) - 1;
} else { } else {
ctxIndex = ctxIndex =

View File

@ -1080,6 +1080,29 @@ export declare class AnimationEvent {
expect(getSourceCodeForDiagnostic(diags[0])).toBe('unknownTarget'); expect(getSourceCodeForDiagnostic(diags[0])).toBe('unknownTarget');
}); });
it('should treat an unknown local ref target as type any', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'test',
template: '<div #ref="unknownTarget">{{ use(ref) }}</div>',
})
class TestCmp {
use(ref: string): string { return ref; }
}
@NgModule({
declarations: [TestCmp],
})
class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`No directive found with exportAs 'unknownTarget'.`);
expect(getSourceCodeForDiagnostic(diags[0])).toBe('unknownTarget');
});
it('should report an error with an unknown pipe', () => { it('should report an error with an unknown pipe', () => {
env.write('test.ts', ` env.write('test.ts', `
import {Component, NgModule} from '@angular/core'; import {Component, NgModule} from '@angular/core';