Skip to content

Commit 0802573

Browse files
bradphelanBrad PhelanHannia Valera
authored
Add a regexp for problem matching on the msvc linker output (#4675)
* add msvc linker problem matchers * Update diagnostics tests to use regex for message validation. Chai doesn't have endsWith * vscode copilot get_errors will only process errors that point to a file. link errors need a dummy file to point to. This is a work around. Perhaps the copilot, vscode and cmake-tools-teams need to talk to ensure llm agent workflow is robust * Add support for collecting and writing linker errors to a file * Add linker diagnostics handling and unit test for unique line numbers in linkerrors.txt * changed things so lint passes * changelog entry --------- Co-authored-by: Brad Phelan <[email protected]> Co-authored-by: Hannia Valera <[email protected]>
1 parent 5201864 commit 0802573

4 files changed

Lines changed: 449 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Features:
66
- triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate)
77
- Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691)
88

9+
Improvements:
10+
- Add MSVC linker error problem matching to the Problems pane. [#4675](https://github.com/microsoft/vscode-cmake-tools/pull/4675) [@bradphelan](https://github.com/bradphelan)
11+
912
Bug Fixes:
1013
- Clarify that semicolons in `cmake.configureSettings` string values are escaped, and array notation should be used for CMake lists. [#4585](https://github.com/microsoft/vscode-cmake-tools/issues/4585)
1114
- Fix "CMake: Quick Start" command failing silently when no folder is open. Now shows an error message with an option to open a folder. [#4504](https://github.com/microsoft/vscode-cmake-tools/issues/4504)

src/diagnostics/build.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import * as gnu_ld from '@cmt/diagnostics/gnu-ld';
1515
import * as mvsc from '@cmt/diagnostics/msvc';
1616
import * as iar from '@cmt/diagnostics/iar';
1717
import * as iwyu from '@cmt/diagnostics/iwyu';
18-
import { FileDiagnostic, RawDiagnosticParser } from '@cmt/diagnostics/util';
18+
import { FileDiagnostic, RawDiagnostic, RawDiagnosticParser } from '@cmt/diagnostics/util';
1919
import { ConfigurationReader } from '@cmt/config';
20+
import { fs } from '@cmt/pr';
2021

2122
export class Compilers {
2223
[compiler: string]: RawDiagnosticParser;
@@ -60,6 +61,7 @@ export class CompileOutputConsumer implements OutputConsumer {
6061

6162
async resolveDiagnostics(...basePaths: string[]): Promise<FileDiagnostic[]> {
6263
const diags_by_file = new Map<string, vscode.Diagnostic[]>();
64+
const linkerHandler = this.createLinkerDiagnosticsHandler(basePaths);
6365

6466
const severity_of = (p: string) => {
6567
switch (p) {
@@ -91,13 +93,17 @@ export class CompileOutputConsumer implements OutputConsumer {
9193
const parsers = util.objectPairs(by_source)
9294
.filter(([source, _]) => this.config.enableOutputParsers?.includes(source.toLowerCase()) ?? false);
9395
const arrs: FileDiagnostic[] = [];
96+
97+
await linkerHandler.maybeEnsureFileFromDiagnostics(this.compilers.msvc.diagnostics);
9498
for (const [ source, diags ] of parsers) {
9599
for (const raw_diag of diags) {
100+
await linkerHandler.collect(raw_diag, source, arrs.length);
96101
const filepath = await this.resolvePath(raw_diag.file, basePaths);
97102
const severity = severity_of(raw_diag.severity);
98103
if (severity === undefined) {
99104
continue;
100105
}
106+
101107
const diag = new vscode.Diagnostic(raw_diag.location, raw_diag.message, severity);
102108
diag.source = source;
103109
if (raw_diag.code) {
@@ -119,8 +125,132 @@ export class CompileOutputConsumer implements OutputConsumer {
119125
});
120126
}
121127
}
128+
129+
await linkerHandler.finalize(arrs);
130+
122131
return arrs;
123132
}
133+
134+
/**
135+
* Creates a handler that centralizes linker-only diagnostics logic.
136+
*
137+
* Why this exists:
138+
* - MSVC linker diagnostics are emitted without a real source file path.
139+
* - Copilot `get_errors` drops diagnostics that do not map to a file on disk.
140+
* - We synthesize a real file (`linkerrors.txt`) and map all linker diagnostics
141+
* to it so tooling can surface the errors consistently.
142+
*
143+
* Responsibilities:
144+
* - Ensure `linkerrors.txt` exists when linker diagnostics are present.
145+
* - Collect linker diagnostics and keep a stable index mapping to `FileDiagnostic` entries.
146+
* - Write a deterministic, line-based report file and then update diagnostic ranges
147+
* to point at the correct lines in that file.
148+
*
149+
* Line mapping details:
150+
* - The file starts with a fixed header (5 lines plus a blank line).
151+
* - Each linker diagnostic then occupies three lines: header, message, blank.
152+
* - We store 1-based line numbers while building content and convert to 0-based
153+
* `vscode.Range` indices when updating diagnostics.
154+
*
155+
* This is intentionally isolated to keep non-linker diagnostic flow readable.
156+
*/
157+
private createLinkerDiagnosticsHandler(basePaths: string[]) {
158+
const linkErrorsFilename = 'linkerrors.txt';
159+
const linkerErrors: { code: string; message: string; source: string; lineNumber: number }[] = [];
160+
const linkerDiagIndexMap = new Map<number, number>(); // Maps linkerErrors index to arrs index
161+
let ensuredLinkErrorsFile = false;
162+
163+
const ensureLinkErrorsFile = async () => {
164+
if (ensuredLinkErrorsFile) {
165+
return;
166+
}
167+
const buildDir = basePaths[0];
168+
if (!buildDir) {
169+
return;
170+
}
171+
const linkErrorsPath = util.resolvePath(linkErrorsFilename, buildDir);
172+
if (!await util.checkFileExists(linkErrorsPath)) {
173+
try {
174+
await fs.writeFile(linkErrorsPath, '');
175+
} catch {
176+
// Best-effort: if this fails, diagnostics will still resolve to the path.
177+
}
178+
}
179+
ensuredLinkErrorsFile = true;
180+
};
181+
182+
const maybeEnsureFileFromDiagnostics = async (diagnostics: readonly RawDiagnostic[]) => {
183+
if (diagnostics.some(diag => diag.file === linkErrorsFilename)) {
184+
await ensureLinkErrorsFile();
185+
}
186+
};
187+
188+
const collect = async (raw_diag: RawDiagnostic, source: string, arrsIndex: number) => {
189+
if (raw_diag.file !== linkErrorsFilename) {
190+
return;
191+
}
192+
await ensureLinkErrorsFile();
193+
const linkerErrorIndex = linkerErrors.length;
194+
linkerErrors.push({
195+
code: raw_diag.code || 'LNK0000',
196+
message: raw_diag.message,
197+
source: source,
198+
lineNumber: -1 // Will be set when building file content
199+
});
200+
linkerDiagIndexMap.set(linkerErrorIndex, arrsIndex);
201+
};
202+
203+
const finalize = async (arrs: FileDiagnostic[]) => {
204+
if (linkerErrors.length === 0) {
205+
return;
206+
}
207+
208+
const buildDir = basePaths[0];
209+
if (!buildDir) {
210+
return;
211+
}
212+
213+
const linkErrorsPath = util.resolvePath(linkErrorsFilename, buildDir);
214+
const timestamp = new Date().toISOString();
215+
const lines: string[] = [
216+
'================================================================================',
217+
'Linker Errors',
218+
`Generated: ${timestamp}`,
219+
`Total Errors: ${linkerErrors.length}`,
220+
'================================================================================',
221+
''
222+
];
223+
224+
// Hack: write a real file so Copilot get_errors does not drop diagnostics without a source file.
225+
for (const err of linkerErrors) {
226+
err.lineNumber = lines.length + 1;
227+
lines.push(`[${err.code}] (${err.source})`);
228+
lines.push(err.message);
229+
lines.push('');
230+
}
231+
232+
try {
233+
await fs.writeFile(linkErrorsPath, lines.join('\n'));
234+
235+
// Now update the line numbers in the diagnostics using the index mapping
236+
linkerDiagIndexMap.forEach((arrsIndex, linkerErrorIndex) => {
237+
const errorInfo = linkerErrors[linkerErrorIndex];
238+
if (errorInfo && errorInfo.lineNumber >= 0 && arrsIndex < arrs.length) {
239+
const line = errorInfo.lineNumber - 1; // VS Code uses 0-based line numbers
240+
arrs[arrsIndex].diag.range = new vscode.Range(line, 0, line, 999);
241+
}
242+
});
243+
} catch {
244+
// Best-effort: if writing fails, diagnostics are still available in Problems panel
245+
}
246+
};
247+
248+
return {
249+
maybeEnsureFileFromDiagnostics,
250+
collect,
251+
finalize
252+
};
253+
}
124254
}
125255

126256
/**

src/diagnostics/msvc.ts

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,69 @@ import { oneLess, RawDiagnosticParser, FeedLineResult } from '@cmt/diagnostics/u
88

99
export const REGEX = /^\s*(\d+>)?\s*([^\s>].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\)\s*:\s+((?:fatal )?error|warning|info)\s*(\w{1,2}\d+)?\s*:\s*(.*)$/;
1010

11+
// Regex for MSVC linker errors with optional file prefix, e.g.
12+
// '[build] LINK : error LNK2001: message'
13+
// '[build] foo.obj : error LNK2019: message'
14+
// 'fatal error LNK1104: cannot open file'
15+
// Matches MSVC linker errors in multiple formats:
16+
// '[build] LINK : error LNK####: message'
17+
// '[build] foo.obj : error LNK####: message'
18+
// 'fatal error LNK####: message'
19+
// Handles flexible spacing: colons with or without spaces
20+
export const LINKER_REGEX =
21+
/^\s*(?:\[[^\]]*\])?\s*(?:(.+?)\s*:\s*)?((?:fatal\s+)?error|warning|info)\s+(LNK\d+)\s*:\s*(.*)$/;
22+
1123
export class Parser extends RawDiagnosticParser {
1224
doHandleLine(line: string) {
13-
const res = REGEX.exec(line);
14-
if (!res) {
15-
return FeedLineResult.NotMine;
25+
// Try the linker error regex first (handles LINK prefix, file prefix, or standalone)
26+
// Must be checked before compiler regex since linker errors can look similar
27+
let res = LINKER_REGEX.exec(line);
28+
if (res) {
29+
const [full, _file, severity, code, message] = res;
30+
return {
31+
full,
32+
file: 'linkerrors.txt',
33+
location: new vscode.Range(0, 0, 0, 999),
34+
severity,
35+
message,
36+
code,
37+
related: []
38+
};
39+
}
40+
41+
// Then try the standard compiler diagnostic regex
42+
res = REGEX.exec(line);
43+
if (res) {
44+
const [full, /* proc*/, file, location, severity, code, message] = res;
45+
const range = (() => {
46+
const parts = location.split(',');
47+
const n0 = oneLess(parts[0]);
48+
if (parts.length === 1) {
49+
return new vscode.Range(n0, 0, n0, 999);
50+
}
51+
if (parts.length === 2) {
52+
const n1 = oneLess(parts[1]);
53+
return new vscode.Range(n0, n1, n0, n1);
54+
}
55+
if (parts.length === 4) {
56+
const n1 = oneLess(parts[1]);
57+
const n2 = oneLess(parts[2]);
58+
const n3 = oneLess(parts[3]);
59+
return new vscode.Range(n0, n1, n2, n3);
60+
}
61+
throw new Error('Unable to determine location of MSVC diagnostic');
62+
})();
63+
return {
64+
full,
65+
file,
66+
location: range,
67+
severity,
68+
message,
69+
code,
70+
related: []
71+
};
1672
}
17-
const [full, /* proc*/, file, location, severity, code, message] = res;
18-
const range = (() => {
19-
const parts = location.split(',');
20-
const n0 = oneLess(parts[0]);
21-
if (parts.length === 1) {
22-
return new vscode.Range(n0, 0, n0, 999);
23-
}
24-
if (parts.length === 2) {
25-
const n1 = oneLess(parts[1]);
26-
return new vscode.Range(n0, n1, n0, n1);
27-
}
28-
if (parts.length === 4) {
29-
const n1 = oneLess(parts[1]);
30-
const n2 = oneLess(parts[2]);
31-
const n3 = oneLess(parts[3]);
32-
return new vscode.Range(n0, n1, n2, n3);
33-
}
34-
throw new Error('Unable to determine location of MSVC diagnostic');
35-
})();
36-
return {
37-
full,
38-
file,
39-
location: range,
40-
severity,
41-
message,
42-
code,
43-
related: []
44-
};
73+
74+
return FeedLineResult.NotMine;
4575
}
4676
}

0 commit comments

Comments
 (0)