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 ab21246052..5a5535057c 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 @@ -355,7 +355,10 @@ class TcbDirectiveTypeOp extends TcbOp { } get optional() { - return false; + // The statement generated by this operation is only used to declare the directive's type and + // won't report diagnostics by itself, so the operation is marked as optional to avoid + // generating declarations for directives that don't have any inputs/outputs. + return true; } execute(): ts.Identifier { @@ -387,7 +390,9 @@ class TcbDirectiveCtorOp extends TcbOp { } get optional() { - return false; + // The statement generated by this operation is only used to infer the directive's type and + // won't report diagnostics by itself, so the operation is marked as optional. + return true; } execute(): ts.Identifier { @@ -452,7 +457,7 @@ class TcbDirectiveInputsOp extends TcbOp { } execute(): null { - const dirId = this.scope.resolve(this.node, this.dir); + let dirId: ts.Expression|null = null; // TODO(joost): report duplicate properties @@ -502,6 +507,10 @@ class TcbDirectiveInputsOp extends TcbOp { // (i.e. private/protected/readonly), generate an assignment into a temporary variable // that has the type of the field. This achieves type-checking but circumvents the access // modifiers. + if (dirId === null) { + dirId = this.scope.resolve(this.node, this.dir); + } + const id = this.tcb.allocateId(); const dirTypeRef = this.tcb.env.referenceType(this.dir.ref); if (!ts.isTypeReferenceNode(dirTypeRef)) { @@ -515,6 +524,10 @@ class TcbDirectiveInputsOp extends TcbOp { this.scope.addStatement(temp); target = id; } else { + if (dirId === null) { + dirId = this.scope.resolve(this.node, this.dir); + } + // To get errors assign directly to the fields on the instance, using property access // when possible. String literal fields may not be valid JS identifiers so we use // literal element access instead for those cases. @@ -718,7 +731,8 @@ class TcbDirectiveOutputsOp extends TcbOp { } execute(): null { - const dirId = this.scope.resolve(this.node, this.dir); + let dirId: ts.Expression|null = null; + // `dir.outputs` is an object map of field names on the directive class to event names. // This is backwards from what's needed to match event handlers - a map of event names to field @@ -748,6 +762,9 @@ class TcbDirectiveOutputsOp extends TcbOp { // that has a `subscribe` method that properly carries the `T` into the handler function. const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer); + if (dirId === null) { + dirId = this.scope.resolve(this.node, this.dir); + } const outputField = ts.createElementAccess(dirId, ts.createStringLiteral(field)); const outputHelper = ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 27cb1ac6e3..ab7c6e73aa 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -156,8 +156,7 @@ describe('type check blocks', () => { isGeneric: true, }]; const block = tcb(TEMPLATE, DIRECTIVES); - expect(block).toContain( - 'var _t1 = Dir.ngTypeCtor({ "color": (null as any), "strong": (null as any), "enabled": (null as any) });'); + expect(block).not.toContain('Dir.ngTypeCtor'); expect(block).toContain('"blue"; false; true;'); }); @@ -204,9 +203,9 @@ describe('type check blocks', () => { ]; expect(tcb(TEMPLATE, DIRECTIVES)) .toContain( - 'var _t3 = DirA.ngTypeCtor((null!)); ' + - 'var _t2 = DirB.ngTypeCtor({ "inputB": (_t3) }); ' + - 'var _t1 = DirA.ngTypeCtor({ "inputA": (_t2) });'); + 'var _t3 = DirB.ngTypeCtor((null!)); ' + + 'var _t2 = DirA.ngTypeCtor({ "inputA": (_t3) }); ' + + 'var _t1 = DirB.ngTypeCtor({ "inputB": (_t2) });'); }); it('should handle empty bindings', () => { @@ -247,9 +246,8 @@ describe('type check blocks', () => { }]; expect(tcb(TEMPLATE, DIRECTIVES)) .toContain( - 'var _t1 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)) }); ' + - 'var _t2: typeof Dir.ngAcceptInputType_fieldA = (null!); ' + - '_t2 = (((ctx).foo));'); + 'var _t1: typeof Dir.ngAcceptInputType_fieldA = (null!); ' + + '_t1 = (((ctx).foo));'); }); }); @@ -264,6 +262,58 @@ describe('type check blocks', () => { expect(block).toContain('(ctx).handle(_t1);'); }); + it('should only generate directive declarations that have bindings or are referenced', () => { + const TEMPLATE = ` +
{{ref.a}}
+ `; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'HasInput', + selector: '[hasInput]', + inputs: {input: 'input'}, + }, + { + type: 'directive', + name: 'HasOutput', + selector: '[hasOutput]', + outputs: {output: 'output'}, + }, + { + type: 'directive', + name: 'HasReference', + selector: '[hasReference]', + exportAs: ['ref'], + }, + { + type: 'directive', + name: 'NoReference', + selector: '[noReference]', + exportAs: ['no-ref'], + }, + { + type: 'directive', + name: 'NoBindings', + selector: '[noBindings]', + inputs: {unset: 'unset'}, + }, + ]; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).toContain('var _t1: HasInput = (null!)'); + expect(block).toContain('_t1.input = (((ctx).value));'); + expect(block).toContain('var _t2: HasOutput = (null!)'); + expect(block).toContain('_t2["output"]'); + expect(block).toContain('var _t3: HasReference = (null!)'); + expect(block).toContain('(_t3).a'); + expect(block).not.toContain('NoBindings'); + expect(block).not.toContain('NoReference'); + }); + it('should generate a forward element reference correctly', () => { const TEMPLATE = ` {{ i.value }} @@ -310,7 +360,7 @@ describe('type check blocks', () => { inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'}, }]; const block = tcb(TEMPLATE, DIRECTIVES); - expect(block).toContain('var _t1: Dir = (null!);'); + expect(block).not.toContain('var _t1: Dir = (null!);'); expect(block).not.toContain('"color"'); expect(block).not.toContain('"strong"'); expect(block).not.toContain('"enabled"'); @@ -357,10 +407,10 @@ describe('type check blocks', () => { ]; expect(tcb(TEMPLATE, DIRECTIVES)) .toContain( - 'var _t1: DirA = (null!); ' + - 'var _t2: DirB = (null!); ' + - '_t1.inputA = (_t2); ' + - '_t2.inputA = (_t1);'); + 'var _t1: DirB = (null!); ' + + 'var _t2: DirA = (null!); ' + + '_t2.inputA = (_t1); ' + + '_t1.inputA = (_t2);'); }); it('should handle undeclared properties', () => { @@ -374,10 +424,9 @@ describe('type check blocks', () => { }, undeclaredInputFields: ['fieldA'] }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1: Dir = (null!); ' + - '(((ctx).foo)); '); + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).not.toContain('var _t1: Dir = (null!);'); + expect(block).toContain('(((ctx).foo)); '); }); it('should assign restricted properties to temp variables by default', () => { @@ -448,9 +497,9 @@ describe('type check blocks', () => { }]; expect(tcb(TEMPLATE, DIRECTIVES)) .toContain( - 'var _t1: Dir = (null!); ' + - 'var _t2: typeof Dir.ngAcceptInputType_field1 = (null!); ' + - '_t1.field2 = _t2 = (((ctx).foo));'); + 'var _t1: typeof Dir.ngAcceptInputType_field1 = (null!); ' + + 'var _t2: Dir = (null!); ' + + '_t2.field2 = _t1 = (((ctx).foo));'); }); it('should handle a single property bound to multiple fields, where one of them is undeclared', @@ -483,11 +532,11 @@ describe('type check blocks', () => { }, coercedInputFields: ['fieldA'], }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1: Dir = (null!); ' + - 'var _t2: typeof Dir.ngAcceptInputType_fieldA = (null!); ' + - '_t2 = (((ctx).foo));'); + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).not.toContain('var _t1: Dir = (null!);'); + expect(block).toContain( + 'var _t1: typeof Dir.ngAcceptInputType_fieldA = (null!); ' + + '_t1 = (((ctx).foo));'); }); it('should use coercion types if declared, even when backing field is not declared', () => { @@ -502,11 +551,11 @@ describe('type check blocks', () => { coercedInputFields: ['fieldA'], undeclaredInputFields: ['fieldA'], }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1: Dir = (null!); ' + - 'var _t2: typeof Dir.ngAcceptInputType_fieldA = (null!); ' + - '_t2 = (((ctx).foo));'); + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).not.toContain('var _t1: Dir = (null!);'); + expect(block).toContain( + 'var _t1: typeof Dir.ngAcceptInputType_fieldA = (null!); ' + + '_t1 = (((ctx).foo));'); }); it('should handle $any casts', () => { @@ -721,7 +770,7 @@ describe('type check blocks', () => { expect(block).toContain('function ($event: any): any { (ctx).foo($event); }'); // Note that DOM events are still checked, that is controlled by `checkTypeOfDomEvents` expect(block).toContain( - '_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });'); + '_t1.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });'); }); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts index e2a14bce68..c6e148b961 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts @@ -276,11 +276,13 @@ runInEachFileSystem(os => { selector: '[dir]', file: dirFile, type: 'directive', + inputs: {'input': 'input'}, + isGeneric: true, }] }, { fileName: dirFile, - source: `export class TestDir {}`, + source: `export class TestDir {}`, templates: {}, } ], @@ -294,7 +296,7 @@ runInEachFileSystem(os => { const tcbReal = templateTypeChecker.getTypeCheckBlock(cmp)!; expect(tcbReal.getSourceFile().text).not.toContain('TestDir'); - templateTypeChecker.overrideComponentTemplate(cmp, '
'); + templateTypeChecker.overrideComponentTemplate(cmp, '
'); const tcbOverridden = templateTypeChecker.getTypeCheckBlock(cmp); expect(tcbOverridden).not.toBeNull();