diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts index 2b7cbd02e5..75170a918a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts @@ -73,6 +73,12 @@ export interface OutOfBandDiagnosticRecorder { export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder { private _diagnostics: TemplateDiagnostic[] = []; + /** + * Tracks which `BindingPipe` nodes have already been recorded as invalid, so only one diagnostic + * is ever produced per node. + */ + private recordedPipes = new Set(); + constructor(private resolver: TemplateSourceResolver) {} get diagnostics(): ReadonlyArray { @@ -90,6 +96,10 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor } missingPipe(templateId: TemplateId, ast: BindingPipe): void { + if (this.recordedPipes.has(ast)) { + return; + } + const mapping = this.resolver.getSourceMapping(templateId); const errorMsg = `No pipe found with name '${ast.name}'.`; @@ -101,6 +111,7 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor this._diagnostics.push(makeTemplateDiagnostic( templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.MISSING_PIPE), errorMsg)); + this.recordedPipes.add(ast); } illegalAssignmentToTemplateVar( diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts index 0104b024a2..6662032296 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts @@ -210,6 +210,32 @@ runInEachFileSystem(() => { ]); }); + it('does not repeat diagnostics for missing pipes in directive inputs', () => { + // The directive here is structured so that a type constructor is used, which resuts in each + // input binding being processed twice. This results in the 'uppercase' pipe being resolved + // twice, and since it doesn't exist this operation will fail. The test is here to verify that + // failing to resolve the pipe twice only produces a single diagnostic (no duplicates). + const messages = diagnose( + '
', ` + class Dir { + dirOf: T; + } + + class TestComponent { + name: string; + }`, + [{ + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: {'dirOf': 'dirOf'}, + isGeneric: true, + }]); + + expect(messages.length).toBe(1); + expect(messages[0]).toContain(`No pipe found with name 'uppercase'.`); + }); + it('does not repeat diagnostics for errors within LHS of safe-navigation operator', () => { const messages = diagnose(`{{ personn?.name }} {{ personn?.getName() }}`, ` class TestComponent {