From 453b32f4b9936b78cfb2991b64d78c0509c94514 Mon Sep 17 00:00:00 2001 From: JoostK Date: Sun, 22 Nov 2020 15:06:32 +0100 Subject: [PATCH] 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 --- .../ngtsc/typecheck/src/type_check_block.ts | 30 ++++++++++++++++--- .../test/ngtsc/template_typecheck_spec.ts | 23 ++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 9b37884e49f27..59e87635b9b1c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -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 * 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 { for (const ref of node.references) { const target = this.tcb.boundTarget.getReferenceTarget(ref); + + let ctxIndex: number; 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); - continue; - } - let ctxIndex: number; - if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) { + // 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; } else { ctxIndex = diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 2e9164e297206..def7a8bf16aeb 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -1080,6 +1080,29 @@ export declare class AnimationEvent { 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: '
{{ use(ref) }}
', + }) + 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', () => { env.write('test.ts', ` import {Component, NgModule} from '@angular/core';