Skip to content

Commit a8536d5

Browse files
Copilotsnehara99
andauthored
Clear Problems pane on build start and populate diagnostics incrementally (#4711)
* Initial plan * feat: clear Problems tab on build start and populate diagnostics incrementally (#4608) Co-authored-by: snehara99 <[email protected]> * Address code review feedback: simplify enableOutputParsers check and add comment Co-authored-by: snehara99 <[email protected]> * Simplify to minimal change: only clear build diagnostics at build start Co-authored-by: snehara99 <[email protected]> * Add back incremental build diagnostics without source tracking Co-authored-by: snehara99 <[email protected]> * Add source tracking to incremental diagnostics for enableOutputParsers filtering and source labels Co-authored-by: snehara99 <[email protected]> * Derive source names dynamically from Compilers properties instead of hardcoded list Co-authored-by: snehara99 <[email protected]> * merge fixes --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: snehara99 <[email protected]> Co-authored-by: Sneha Ramachandran <[email protected]>
1 parent 2420d3e commit a8536d5

5 files changed

Lines changed: 173 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
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)
8+
- Clear build diagnostics from the Problems pane when a new build starts and populate them incrementally during the build. [#4608](https://github.com/microsoft/vscode-cmake-tools/issues/4608)
89
- Add individual CTest test nodes to the Project Outline with inline run/debug buttons, and enable debugging tests from both the Outline and Test Explorer without requiring a launch.json. [#4721](https://github.com/microsoft/vscode-cmake-tools/pull/4721)
910
- Add "Delete Cache, Reconfigure and Build" command that chains cache deletion, reconfiguration, and build into a single action. [#4723](https://github.com/microsoft/vscode-cmake-tools/pull/4723)
1011
- Add "Set Build and Launch/Debug Target" command that sets both the build target and launch target simultaneously. [#4732](https://github.com/microsoft/vscode-cmake-tools/pull/4732)

src/cmakeProject.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { CPackDriver } from '@cmt/cpack';
2828
import { WorkflowDriver } from '@cmt/workflow';
2929
import { CMakeBuildConsumer } from '@cmt/diagnostics/build';
3030
import { CMakeOutputConsumer } from '@cmt/diagnostics/cmake';
31-
import { FileDiagnostic, populateCollection } from '@cmt/diagnostics/util';
31+
import { FileDiagnostic, addDiagnosticToCollection, diagnosticSeverity, populateCollection } from '@cmt/diagnostics/util';
3232
import { expandStrings, expandString, ExpansionOptions } from '@cmt/expand';
3333
import { CMakeGenerator, Kit, SpecialKits } from '@cmt/kits/kit';
3434
import * as logging from '@cmt/logging';
@@ -2106,6 +2106,9 @@ export class CMakeProject {
21062106
try {
21072107
this.statusMessage.set(localize('building.status', 'Building'));
21082108
this.isBusy.set(true);
2109+
if (drv.config.parseBuildDiagnostics) {
2110+
collections.build.clear();
2111+
}
21092112
let rc: number | null;
21102113
if (taskConsumer) {
21112114
buildLogger.info(localize('starting.build', 'Starting build'));
@@ -2125,6 +2128,24 @@ export class CMakeProject {
21252128
};
21262129
} else {
21272130
consumer = new CMakeBuildConsumer(buildLogger, drv.config);
2131+
if (drv.config.parseBuildDiagnostics) {
2132+
consumer.onDiagnostic(({ source, diagnostic: rawDiag }) => {
2133+
if (!drv!.config.enableOutputParsers?.includes(source.toLowerCase())) {
2134+
return;
2135+
}
2136+
const severity = diagnosticSeverity(rawDiag.severity);
2137+
if (severity === undefined) {
2138+
return;
2139+
}
2140+
const filepath = util.resolvePath(rawDiag.file, drv!.binaryDir);
2141+
const diag = new vscode.Diagnostic(rawDiag.location, rawDiag.message, severity);
2142+
diag.source = source;
2143+
if (rawDiag.code) {
2144+
diag.code = rawDiag.code;
2145+
}
2146+
addDiagnosticToCollection(collections.build, { filepath, diag });
2147+
});
2148+
}
21282149
return await vscode.window.withProgress(
21292150
{
21302151
location: vscode.ProgressLocation.Window,

src/diagnostics/build.ts

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ 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, RawDiagnostic, RawDiagnosticParser, diagnosticSeverity } from '@cmt/diagnostics/util';
1819
import { CustomParser } from '@cmt/diagnostics/custom';
19-
import { FileDiagnostic, RawDiagnostic, RawDiagnosticParser } from '@cmt/diagnostics/util';
2020
import { ConfigurationReader } from '@cmt/config';
2121
import { fs } from '@cmt/pr';
2222

@@ -32,6 +32,12 @@ export class Compilers {
3232
iwyu = new iwyu.Parser();
3333
}
3434

35+
export interface RawDiagnosticWithSource {
36+
/** The parser that produced this diagnostic (e.g. 'GCC', 'MSVC'). Matches the keys used in `enableOutputParsers`. */
37+
source: string;
38+
diagnostic: RawDiagnostic;
39+
}
40+
3541
export class CompileOutputConsumer implements OutputConsumer {
3642
readonly customParsers: Map<string, CustomParser> = new Map();
3743

@@ -45,6 +51,21 @@ export class CompileOutputConsumer implements OutputConsumer {
4551

4652
compilers = new Compilers();
4753

54+
private readonly _onDiagnosticEmitter = new vscode.EventEmitter<RawDiagnosticWithSource>();
55+
56+
/**
57+
* Event fired when a new diagnostic is parsed from compiler output.
58+
* Includes the source parser name so subscribers can filter by `enableOutputParsers`
59+
* and set `diag.source` for display in the Problems pane.
60+
*/
61+
get onDiagnostic() {
62+
return this._onDiagnosticEmitter.event;
63+
}
64+
65+
dispose() {
66+
this._onDiagnosticEmitter.dispose();
67+
}
68+
4869
// Defer all output to the `error` method
4970
output(line: string) {
5071
this.error(line);
@@ -53,13 +74,28 @@ export class CompileOutputConsumer implements OutputConsumer {
5374
error(line: string) {
5475
// Built-in parsers get first priority
5576
for (const cand in this.compilers) {
56-
if (this.compilers[cand].handleLine(line)) {
77+
const parser = this.compilers[cand];
78+
const countBefore = parser.diagnostics.length;
79+
if (parser.handleLine(line)) {
80+
if (parser.diagnostics.length > countBefore) {
81+
this._onDiagnosticEmitter.fire({
82+
source: cand.toUpperCase(),
83+
diagnostic: parser.diagnostics[parser.diagnostics.length - 1]
84+
});
85+
}
5786
return;
5887
}
5988
}
6089
// Custom user-defined parsers run after built-ins
61-
for (const [, parser] of this.customParsers) {
90+
for (const [name, parser] of this.customParsers) {
91+
const countBefore = parser.diagnostics.length;
6292
if (parser.handleLine(line)) {
93+
if (parser.diagnostics.length > countBefore) {
94+
this._onDiagnosticEmitter.fire({
95+
source: name,
96+
diagnostic: parser.diagnostics[parser.diagnostics.length - 1]
97+
});
98+
}
6399
return;
64100
}
65101
}
@@ -79,33 +115,10 @@ export class CompileOutputConsumer implements OutputConsumer {
79115
const diags_by_file = new Map<string, vscode.Diagnostic[]>();
80116
const linkerHandler = this.createLinkerDiagnosticsHandler(basePaths);
81117

82-
const severity_of = (p: string) => {
83-
switch (p) {
84-
case 'warning':
85-
return vscode.DiagnosticSeverity.Warning;
86-
case 'catastrophic error':
87-
case 'fatal error':
88-
case 'error':
89-
return vscode.DiagnosticSeverity.Error;
90-
case 'note':
91-
case 'info':
92-
case 'remark':
93-
return vscode.DiagnosticSeverity.Information;
94-
}
95-
// tslint:disable-next-line
96-
console.warn('Unknown diagnostic severity level: ' + p);
97-
return undefined;
98-
};
99-
100-
const by_source = {
101-
GCC: this.compilers.gcc.diagnostics,
102-
MSVC: this.compilers.msvc.diagnostics,
103-
GHS: this.compilers.ghs.diagnostics,
104-
DIAB: this.compilers.diab.diagnostics,
105-
GNULD: this.compilers.gnuld.diagnostics,
106-
IAR: this.compilers.iar.diagnostics,
107-
IWYU: this.compilers.iwyu.diagnostics
108-
};
118+
const by_source: Record<string, readonly RawDiagnostic[]> = {};
119+
for (const name in this.compilers) {
120+
by_source[name.toUpperCase()] = this.compilers[name].diagnostics;
121+
}
109122
const parsers = util.objectPairs(by_source)
110123
.filter(([source, _]) => this.config.enableOutputParsers?.includes(source.toLowerCase()) ?? false);
111124
const arrs: FileDiagnostic[] = [];
@@ -115,7 +128,7 @@ export class CompileOutputConsumer implements OutputConsumer {
115128
for (const raw_diag of diags) {
116129
await linkerHandler.collect(raw_diag, source, arrs.length);
117130
const filepath = await this.resolvePath(raw_diag.file, basePaths);
118-
const severity = severity_of(raw_diag.severity);
131+
const severity = diagnosticSeverity(raw_diag.severity);
119132
if (severity === undefined) {
120133
continue;
121134
}
@@ -148,7 +161,7 @@ export class CompileOutputConsumer implements OutputConsumer {
148161
for (const [name, parser] of this.customParsers) {
149162
for (const raw_diag of parser.diagnostics) {
150163
const filepath = await this.resolvePath(raw_diag.file, basePaths);
151-
const severity = severity_of(raw_diag.severity);
164+
const severity = diagnosticSeverity(raw_diag.severity);
152165
if (severity === undefined) {
153166
continue;
154167
}
@@ -323,8 +336,16 @@ export class CMakeBuildConsumer extends proc.CommandConsumer implements vscode.D
323336

324337
readonly compileConsumer: CompileOutputConsumer;
325338

339+
/**
340+
* Event fired when a new diagnostic is parsed from compiler output
341+
*/
342+
get onDiagnostic() {
343+
return this.compileConsumer.onDiagnostic;
344+
}
345+
326346
dispose() {
327347
this._onProgressEmitter.dispose();
348+
this.compileConsumer.dispose();
328349
}
329350

330351
error(line: string) {

src/diagnostics/util.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ export function oneLess(num: number | string): number {
5858
}
5959
}
6060

61+
/**
62+
* Convert a severity string from compiler output to a `vscode.DiagnosticSeverity`.
63+
* @param severity The severity string (e.g. 'warning', 'error', 'note')
64+
* @returns The corresponding `vscode.DiagnosticSeverity`, or `undefined` if unknown
65+
*/
66+
export function diagnosticSeverity(severity: string): vscode.DiagnosticSeverity | undefined {
67+
switch (severity) {
68+
case 'warning':
69+
return vscode.DiagnosticSeverity.Warning;
70+
case 'catastrophic error':
71+
case 'fatal error':
72+
case 'error':
73+
return vscode.DiagnosticSeverity.Error;
74+
case 'note':
75+
case 'info':
76+
case 'remark':
77+
return vscode.DiagnosticSeverity.Information;
78+
}
79+
return undefined;
80+
}
81+
6182
/**
6283
* Inserts a list of `FileDiagnostic` instances into a diagnostic collection.
6384
* @param coll The `vscode.DiagnosticCollecion` to populate.
@@ -82,6 +103,18 @@ export function populateCollection(coll: vscode.DiagnosticCollection, fdiags: It
82103
});
83104
}
84105

106+
/**
107+
* Adds a single `FileDiagnostic` to a diagnostic collection without clearing existing entries.
108+
* @param coll The `vscode.DiagnosticCollection` to add to.
109+
* @param fdiag The `FileDiagnostic` to add.
110+
*/
111+
export function addDiagnosticToCollection(coll: vscode.DiagnosticCollection, fdiag: FileDiagnostic) {
112+
const uri = vscode.Uri.file(fdiag.filepath);
113+
const existing = coll.get(uri);
114+
const diags = existing ? [...existing, fdiag.diag] : [fdiag.diag];
115+
coll.set(uri, diags);
116+
}
117+
85118
/**
86119
* Base class for parsing raw diagnostic information on a line-by-line basis
87120
*/

test/unit-tests/diagnostics.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { ExtensionConfigurationSettings, ConfigurationReader } from '../../src/c
1414
import { CustomParser, BuildProblemMatcherConfig } from '@cmt/diagnostics/custom';
1515
import { platformPathEquivalent, resolvePath } from '@cmt/util';
1616
import { CMakeOutputConsumer } from '@cmt/diagnostics/cmake';
17-
import { populateCollection } from '@cmt/diagnostics/util';
17+
import { populateCollection, addDiagnosticToCollection, diagnosticSeverity } from '@cmt/diagnostics/util';
1818
import collections from '@cmt/diagnostics/collections';
1919
import { getTestResourceFilePath } from '@test/util';
2020
import { Logger } from '@cmt/logging';
@@ -1295,6 +1295,69 @@ suite('Diagnostics', () => {
12951295
expect(collections.presets.has(testUri)).to.be.false;
12961296
});
12971297

1298+
test('diagnosticSeverity returns correct severities', () => {
1299+
expect(diagnosticSeverity('warning')).to.eq(vscode.DiagnosticSeverity.Warning);
1300+
expect(diagnosticSeverity('error')).to.eq(vscode.DiagnosticSeverity.Error);
1301+
expect(diagnosticSeverity('fatal error')).to.eq(vscode.DiagnosticSeverity.Error);
1302+
expect(diagnosticSeverity('catastrophic error')).to.eq(vscode.DiagnosticSeverity.Error);
1303+
expect(diagnosticSeverity('note')).to.eq(vscode.DiagnosticSeverity.Information);
1304+
expect(diagnosticSeverity('info')).to.eq(vscode.DiagnosticSeverity.Information);
1305+
expect(diagnosticSeverity('remark')).to.eq(vscode.DiagnosticSeverity.Information);
1306+
expect(diagnosticSeverity('unknown')).to.be.undefined;
1307+
});
1308+
1309+
test('addDiagnosticToCollection adds to empty collection', () => {
1310+
const coll = vscode.languages.createDiagnosticCollection('test-add-diag');
1311+
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), 'test error', vscode.DiagnosticSeverity.Error);
1312+
addDiagnosticToCollection(coll, { filepath: '/test/file.cpp', diag });
1313+
const uri = vscode.Uri.file('/test/file.cpp');
1314+
expect(coll.has(uri)).to.be.true;
1315+
expect(coll.get(uri)!.length).to.eq(1);
1316+
coll.dispose();
1317+
});
1318+
1319+
test('addDiagnosticToCollection appends to existing diagnostics', () => {
1320+
const coll = vscode.languages.createDiagnosticCollection('test-add-diag-append');
1321+
const diag1 = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 10), 'error 1', vscode.DiagnosticSeverity.Error);
1322+
const diag2 = new vscode.Diagnostic(new vscode.Range(1, 0, 1, 10), 'error 2', vscode.DiagnosticSeverity.Error);
1323+
addDiagnosticToCollection(coll, { filepath: '/test/file.cpp', diag: diag1 });
1324+
addDiagnosticToCollection(coll, { filepath: '/test/file.cpp', diag: diag2 });
1325+
const uri = vscode.Uri.file('/test/file.cpp');
1326+
expect(coll.has(uri)).to.be.true;
1327+
expect(coll.get(uri)!.length).to.eq(2);
1328+
coll.dispose();
1329+
});
1330+
1331+
test('CompileOutputConsumer fires onDiagnostic event with source', () => {
1332+
const consumer = new diags.CompileOutputConsumer(new ConfigurationReader({} as ExtensionConfigurationSettings));
1333+
const fired: diags.RawDiagnosticWithSource[] = [];
1334+
consumer.onDiagnostic(e => fired.push(e));
1335+
feedLines(consumer, [], [
1336+
'/some/path/here:4:26: error: some error message'
1337+
]);
1338+
expect(fired).to.have.length(1);
1339+
expect(fired[0].source).to.eq('GCC');
1340+
expect(fired[0].diagnostic.severity).to.eq('error');
1341+
expect(fired[0].diagnostic.message).to.eq('some error message');
1342+
consumer.dispose();
1343+
});
1344+
1345+
test('CompileOutputConsumer fires onDiagnostic for each diagnostic with source', () => {
1346+
const consumer = new diags.CompileOutputConsumer(new ConfigurationReader({} as ExtensionConfigurationSettings));
1347+
const fired: diags.RawDiagnosticWithSource[] = [];
1348+
consumer.onDiagnostic(e => fired.push(e));
1349+
feedLines(consumer, [], [
1350+
'/path/a.cpp:1:1: warning: first warning',
1351+
'/path/b.cpp:2:1: error: first error'
1352+
]);
1353+
expect(fired).to.have.length(2);
1354+
expect(fired[0].source).to.eq('GCC');
1355+
expect(fired[0].diagnostic.severity).to.eq('warning');
1356+
expect(fired[1].source).to.eq('GCC');
1357+
expect(fired[1].diagnostic.severity).to.eq('error');
1358+
consumer.dispose();
1359+
});
1360+
12981361
test('CMakeOutputConsumer logs stdout lines at trace level, not info', () => {
12991362
const spy = new SpyLogger();
13001363
const consumerWithLogger = new CMakeOutputConsumer('dummyPath', spy);

0 commit comments

Comments
 (0)