From 17db18aa87c33fa2817ed83f8c01afe9b57b5a50 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:57:18 +0000 Subject: [PATCH] fix(@angular/build): preserve diagnostic locations at offset 0 and guard empty files Ensure `convertTypeScriptDiagnosticInfo` checks `info.start !== undefined` so diagnostics and related information starting at file offset `0` retain their line, column, and lineText location properties. Also clamp `info.file.text.length - 1` to `0` when computing `lastLineOfFile` to prevent a TypeScript `Debug Failure` when reporting diagnostics on empty files. --- .../tools/angular/compilation/diagnostics.ts | 4 +- .../angular/compilation/diagnostics_spec.ts | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 packages/angular/build/src/tools/angular/compilation/diagnostics_spec.ts diff --git a/packages/angular/build/src/tools/angular/compilation/diagnostics.ts b/packages/angular/build/src/tools/angular/compilation/diagnostics.ts index bf85b69f707c..41563b90ebbd 100644 --- a/packages/angular/build/src/tools/angular/compilation/diagnostics.ts +++ b/packages/angular/build/src/tools/angular/compilation/diagnostics.ts @@ -37,7 +37,7 @@ function convertTypeScriptDiagnosticInfo( }; // Calculate the line/column location and extract the full line text that has the diagnostic - if (info.start) { + if (info.start !== undefined) { const { line, character } = typescript.getLineAndCharacterOfPosition(info.file, info.start); note.location.line = line + 1; note.location.column = character; @@ -50,7 +50,7 @@ function convertTypeScriptDiagnosticInfo( // will error if a nonexistent line is passed). const { line: lastLineOfFile } = typescript.getLineAndCharacterOfPosition( info.file, - info.file.text.length - 1, + Math.max(0, info.file.text.length - 1), ); const lineEndPosition = line < lastLineOfFile diff --git a/packages/angular/build/src/tools/angular/compilation/diagnostics_spec.ts b/packages/angular/build/src/tools/angular/compilation/diagnostics_spec.ts new file mode 100644 index 000000000000..8ea79c20ba43 --- /dev/null +++ b/packages/angular/build/src/tools/angular/compilation/diagnostics_spec.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import ts from 'typescript'; +import { convertTypeScriptDiagnostic } from './diagnostics'; + +describe('convertTypeScriptDiagnostic', () => { + it('populates line, column, and lineText when diagnostic or relatedInformation starts at offset 0', () => { + const sourceFile = ts.createSourceFile( + '/src/app/app.component.ts', + 'const invalid: number = "string";\nconst secondLine = 2;\n', + ts.ScriptTarget.Latest, + ); + const templateFile = ts.createSourceFile( + '/src/app/app.component.html', + '\n
Valid
\n', + ts.ScriptTarget.Latest, + ); + + const diagnostic: ts.Diagnostic = { + category: ts.DiagnosticCategory.Error, + code: -998001, + file: templateFile, + start: 0, + length: 17, + messageText: "'unknown-element' is not a known element.", + relatedInformation: [ + { + category: ts.DiagnosticCategory.Message, + code: 0, + file: sourceFile, + start: 0, + length: 5, + messageText: 'Error occurs in the template of component AppComponent.', + }, + ], + }; + + const result = convertTypeScriptDiagnostic(ts, diagnostic); + + expect(result.text).toBe("NG8001: 'unknown-element' is not a known element."); + expect(result.location).toEqual({ + file: '/src/app/app.component.html', + length: 17, + line: 1, + column: 0, + lineText: '', + }); + + expect(result.notes).toEqual([ + { + text: 'Error occurs in the template of component AppComponent.', + location: { + file: '/src/app/app.component.ts', + length: 5, + line: 1, + column: 0, + lineText: 'const invalid: number = "string";', + }, + }, + ]); + }); + + it('does not throw a TypeScript Debug Failure when diagnostic is reported on an empty file', () => { + const emptyFile = ts.createSourceFile('/src/empty.ts', '', ts.ScriptTarget.Latest); + + const diagnosticAtZero: ts.Diagnostic = { + category: ts.DiagnosticCategory.Error, + code: 2306, + file: emptyFile, + start: 0, + length: 0, + messageText: "File '/src/empty.ts' is not a module.", + }; + + expect(() => convertTypeScriptDiagnostic(ts, diagnosticAtZero)).not.toThrow(); + expect(convertTypeScriptDiagnostic(ts, diagnosticAtZero)).toEqual({ + text: "TS2306: File '/src/empty.ts' is not a module.", + location: { + file: '/src/empty.ts', + length: 0, + line: 1, + column: 0, + lineText: '', + }, + }); + + const diagnosticPastEnd: ts.Diagnostic = { + category: ts.DiagnosticCategory.Error, + code: 1005, + file: emptyFile, + start: 1, + length: 0, + messageText: 'Unexpected end of text.', + }; + + expect(() => convertTypeScriptDiagnostic(ts, diagnosticPastEnd)).not.toThrow(); + expect(convertTypeScriptDiagnostic(ts, diagnosticPastEnd).location).toEqual({ + file: '/src/empty.ts', + length: 0, + line: 1, + column: 1, + lineText: '', + }); + }); +});