Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# What's New?

# 1.22

Features:

- Add output parser for [include-what-you-use](https://github.com/include-what-you-use). [PR #4548](https://github.com/microsoft/vscode-cmake-tools/pull/4548) [@malsyned](https://github.com/malsyned)

## 1.21

Features:
Expand Down
2 changes: 1 addition & 1 deletion docs/cmake-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Options that support substitution, in the table below, allow variable references
| `cmake.deleteBuildDirOnCleanConfigure` | If `true`, delete build directory during clean configure. | `false` | no |
| `cmake.emscriptenSearchDirs` | List of paths to search for Emscripten. | `[]` | no |
| `cmake.enableAutomaticKitScan` | Enable automatic kit scanning. | `true` | no |
| `cmake.enabledOutputParsers` | List of enabled output parsers. | `["cmake", "gcc", "gnuld", "msvc", "ghs", "diab"]` | no |
| `cmake.enabledOutputParsers` | List of enabled output parsers. | `["cmake", "gcc", "gnuld", "msvc", "ghs", "diab", "iwyu"]` | no |
| `cmake.enableLanguageServices` | If `true`, enable CMake language services. | `true` | no |
| `cmake.enableTraceLogging` | If `true`, enable trace logging. | `false` | no |
| `cmake.environment` | An object containing `key:value` pairs of environment variables, which will be available when configuring, building, or testing with CTest. | `{}` (no environment variables) | yes |
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2295,7 +2295,8 @@
"msvc",
"ghs",
"diab",
"iar"
"iar",
"iwyu"
]
},
"default": [
Expand Down
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
"cmake-tools.configuration.cmake.ctest.debugLaunchTarget.description": "Target name from launch.json to start when debugging a test with CTest. By default and in case of a non-existing target, this will show a picker with all available targets.",
"cmake-tools.configuration.cmake.parseBuildDiagnostics.description": "Parse compiler output for warnings and errors.",
"cmake-tools.configuration.cmake.enabledOutputParsers.description": {
"message": "Output parsers to use. Supported parsers `cmake`, `gcc`, `gnuld` for GNULD-style linker output, `msvc` for Microsoft Visual C++, `ghs` for the Green Hills compiler with --no_wrap_diagnostics --brief_diagnostics, and `diab` for the Wind River Diab compiler.",
"message": "Output parsers to use. Supported parsers `cmake`, `gcc`, `gnuld` for GNULD-style linker output, `msvc` for Microsoft Visual C++, `ghs` for the Green Hills compiler with --no_wrap_diagnostics --brief_diagnostics, `diab` for the Wind River Diab compiler, and `iwyu` for include-what-you-use diagnostics.",
"comment": [
"Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered."
]
Expand Down
5 changes: 4 additions & 1 deletion src/diagnostics/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as diab from '@cmt/diagnostics/diab';
import * as gnu_ld from '@cmt/diagnostics/gnu-ld';
import * as mvsc from '@cmt/diagnostics/msvc';
import * as iar from '@cmt/diagnostics/iar';
import * as iwyu from '@cmt/diagnostics/iwyu';
import { FileDiagnostic, RawDiagnosticParser } from '@cmt/diagnostics/util';
import { ConfigurationReader } from '@cmt/config';

Expand All @@ -26,6 +27,7 @@ export class Compilers {
diab = new diab.Parser();
msvc = new mvsc.Parser();
iar = new iar.Parser();
iwyu = new iwyu.Parser();
}

export class CompileOutputConsumer implements OutputConsumer {
Expand Down Expand Up @@ -83,7 +85,8 @@ export class CompileOutputConsumer implements OutputConsumer {
GHS: this.compilers.ghs.diagnostics,
DIAB: this.compilers.diab.diagnostics,
GNULD: this.compilers.gnuld.diagnostics,
IAR: this.compilers.iar.diagnostics
IAR: this.compilers.iar.diagnostics,
IWYU: this.compilers.iwyu.diagnostics
};
const parsers = util.objectPairs(by_source)
.filter(([source, _]) => this.config.enableOutputParsers?.includes(source.toLowerCase()) ?? false);
Expand Down
113 changes: 113 additions & 0 deletions src/diagnostics/iwyu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Module for handling include-what-you-use diagnostics
*/ /** */

import * as vscode from 'vscode';

import { RawDiagnosticParser, RawDiagnostic, FeedLineResult, oneLess } from '@cmt/diagnostics/util';

const HEADER_RE = /^(.*)\s+((should\s+(add|remove))\s+these\slines:)/i;
const FULL_HEADER_RE = /^\s*(the\s+full\s+include[-\s]+list)\s+for\s+(.*):\s*$/i;
const REMOVE_RE = /^\s*-\s*(.*?)\s*\/\/\s*lines\s(\d+)-(\d+)/i;
const SUGGESTION_RE = /^\s*(.*?[^\s].*?)\s*$/;
const END_RE = /^\s*-+\s*$/;

type State = 'wait-header' | 'collect' | 'collect-removes';

export class Parser extends RawDiagnosticParser {
private state: State = 'wait-header';
private file = '';
private messagePrefix = '';
private suggestedLines: string[] = [];
private full: string[] = [];
private severity: string = '';

protected doHandleLine(line: string): RawDiagnostic | FeedLineResult {
let mat: RegExpExecArray | null;
let change: string;
let addPrefix: string;
let removePrefix: string;

switch (this.state) {
case 'wait-header':
this.full = [line];
this.suggestedLines = [];
mat = HEADER_RE.exec(line);
if (mat) {
[, this.file, addPrefix, removePrefix, change] = mat;
this.severity = 'warning';
if (change === 'add') {
this.messagePrefix = addPrefix;
this.state = 'collect';
} else {
this.messagePrefix = removePrefix;
this.state = 'collect-removes';
}
return FeedLineResult.Ok;
}
mat = FULL_HEADER_RE.exec(line);
if (mat) {
[, this.messagePrefix, this.file] = mat;
this.severity = 'note';
this.messagePrefix += ':';
this.state = 'collect';
return FeedLineResult.Ok;
}
return FeedLineResult.NotMine;

case 'collect-removes':
mat = REMOVE_RE.exec(line);
if (mat) {
const [, msg, start, end] = mat;
return this.makeDiagnostic(
this.messagePrefix + ': ' + msg,
new vscode.Range(oneLess(start), 0, oneLess(end), 999),
join(this.full, line)
);
} else {
this.state = 'wait-header';
return FeedLineResult.Ok;
}

case 'collect':
mat = SUGGESTION_RE.exec(line);
if (mat && !END_RE.exec(line)) {
const [, msg] = mat;
this.full.push(line);
this.suggestedLines.push(msg);
return FeedLineResult.Ok;
} else {
this.state = 'wait-header';
if (this.suggestedLines.length) {
return this.makeDiagnostic(
join(this.messagePrefix, this.suggestedLines),
new vscode.Range(0, 0, 0, 999),
join(this.full)
);
} else {
return FeedLineResult.Ok;
}
}
}
}

private makeDiagnostic(
message: string, location: vscode.Range, full: string
): RawDiagnostic {
return {
message: message,
location: location,
full: full,
file: this.file,
related: [],
severity: this.severity
};
}
}

/** join a grab bag of strings and string[]s with \n */
function join(...lines: (string|string[])[]): string {
return lines.map(
(v) => typeof(v) === 'string' ? v : v.join('\n')
).join('\n');
}
191 changes: 191 additions & 0 deletions test/unit-tests/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,4 +784,195 @@ suite('Diagnostics', () => {
diagnostic = resolved[0];
expect(diagnostic.filepath).to.eq(resolvePath('main.cpp', project_dir));
});

test('Parse IWYU', () => {
const lines = [
'/home/user/src/project/main.c should add these lines:',
'#include <stdbool.h> // for bool',
'#include <stdint.h> // for uint32_t, uint8_t',
'',
'/home/user/src/project/main.c should remove these lines:',
'- #include <alloca.h> // lines 24-24',
'- #include <stdalign.h> // lines 25-26',
'',
'The full include-list for /home/user/src/project/main.c:',
'#include <stdbool.h> // for bool',
'#include <stdint.h> // for uint32_t, uint8_t',
'#include <stdio.h> // for fprintf, FILE, printf, NULL, stdout',
'#include "array.h" // for ARRAY_SIZE',
'---'
];

feedLines(build_consumer, [], lines);
expect(build_consumer.compilers.iwyu.diagnostics).to.have.length(4);
const [add, rem1, rem2, all] = build_consumer.compilers.iwyu.diagnostics;

expect(add.file).to.eq('/home/user/src/project/main.c');
expect(add.location.start.line).to.eq(0);
expect(add.location.start.character).to.eq(0);
expect(add.location.end.line).to.eq(0);
expect(add.location.end.character).to.eq(999);
expect(add.code).to.eq(undefined);
expect(add.message).to.eq('should add these lines:\n#include <stdbool.h> // for bool\n#include <stdint.h> // for uint32_t, uint8_t');
expect(add.severity).to.eq('warning');

expect(rem1.file).to.eq('/home/user/src/project/main.c');
expect(rem1.location.start.line).to.eq(23);
expect(rem1.location.start.character).to.eq(0);
expect(rem1.location.end.line).to.eq(23);
expect(rem1.location.end.character).to.eq(999);
expect(rem1.code).to.eq(undefined);
expect(rem1.message).to.eq('should remove: #include <alloca.h>');
expect(rem1.severity).to.eq('warning');

expect(rem2.file).to.eq('/home/user/src/project/main.c');
expect(rem2.location.start.line).to.eq(24);
expect(rem2.location.start.character).to.eq(0);
expect(rem2.location.end.line).to.eq(25);
expect(rem2.location.end.character).to.eq(999);
expect(rem2.code).to.eq(undefined);
expect(rem2.message).to.eq('should remove: #include <stdalign.h>');
expect(rem2.severity).to.eq('warning');

expect(all.file).to.eq('/home/user/src/project/main.c');
expect(all.location.start.line).to.eq(0);
expect(all.location.start.character).to.eq(0);
expect(all.location.end.line).to.eq(0);
expect(all.location.end.character).to.eq(999);
expect(all.code).to.eq(undefined);
expect(all.message).to.eq('The full include-list:\n#include <stdbool.h> // for bool\n#include <stdint.h> // for uint32_t, uint8_t\n#include <stdio.h> // for fprintf, FILE, printf, NULL, stdout\n#include "array.h" // for ARRAY_SIZE');
expect(all.severity).to.eq('note');
});

test('Parse IWYU with only additions', () => {
const lines = [
'/home/user/src/project/main.c should add these lines:',
'#include <stdbool.h> // for bool',
'',
'/home/user/src/project/main.c should remove these lines:',
'',
'The full include-list for /home/user/src/project/main.c:',
'#include <stdbool.h> // for bool',
'#include "array.h" // for ARRAY_SIZE',
'---'
];

feedLines(build_consumer, [], lines);
expect(build_consumer.compilers.iwyu.diagnostics).to.have.length(2);
const [add, all] = build_consumer.compilers.iwyu.diagnostics;

expect(add.file).to.eq('/home/user/src/project/main.c');
expect(add.location.start.line).to.eq(0);
expect(add.location.start.character).to.eq(0);
expect(add.location.end.line).to.eq(0);
expect(add.location.end.character).to.eq(999);
expect(add.code).to.eq(undefined);
expect(add.message).to.eq('should add these lines:\n#include <stdbool.h> // for bool');
expect(add.severity).to.eq('warning');

expect(all.file).to.eq('/home/user/src/project/main.c');
expect(all.location.start.line).to.eq(0);
expect(all.location.start.character).to.eq(0);
expect(all.location.end.line).to.eq(0);
expect(all.location.end.character).to.eq(999);
expect(all.code).to.eq(undefined);
expect(all.message).to.eq('The full include-list:\n#include <stdbool.h> // for bool\n#include "array.h" // for ARRAY_SIZE');
expect(all.severity).to.eq('note');
});

test('Parse IWYU with only removals', () => {
const lines = [
'/home/user/src/project/main.c should add these lines:',
'',
'/home/user/src/project/main.c should remove these lines:',
'- #include <alloca.h> // lines 24-24',
'',
'The full include-list for /home/user/src/project/main.c:',
'#include "array.h" // for ARRAY_SIZE',
'---'
];

feedLines(build_consumer, [], lines);
expect(build_consumer.compilers.iwyu.diagnostics).to.have.length(2);
const [rem, all] = build_consumer.compilers.iwyu.diagnostics;

expect(rem.file).to.eq('/home/user/src/project/main.c');
expect(rem.location.start.line).to.eq(23);
expect(rem.location.start.character).to.eq(0);
expect(rem.location.end.line).to.eq(23);
expect(rem.location.end.character).to.eq(999);
expect(rem.code).to.eq(undefined);
expect(rem.message).to.eq('should remove: #include <alloca.h>');
expect(rem.severity).to.eq('warning');

expect(all.file).to.eq('/home/user/src/project/main.c');
expect(all.location.start.line).to.eq(0);
expect(all.location.start.character).to.eq(0);
expect(all.location.end.line).to.eq(0);
expect(all.location.end.character).to.eq(999);
expect(all.code).to.eq(undefined);
expect(all.message).to.eq('The full include-list:\n#include "array.h" // for ARRAY_SIZE');
expect(all.severity).to.eq('note');
});

test('Parse IWYU with multiple files', () => {
const lines = [
'/home/user/src/project/main.c should add these lines:',
'#include <stdbool.h> // for bool',
'',
'/home/user/src/project/main.c should remove these lines:',
'',
'The full include-list for /home/user/src/project/main.c:',
'#include <stdbool.h> // for bool',
'---',
'/home/user/src/project/module.c should add these lines:',
'',
'/home/user/src/project/module.c should remove these lines:',
'- #include <alloca.h> // lines 24-24',
'',
'The full include-list for /home/user/src/project/module.c:',
'#include "array.h" // for ARRAY_SIZE',
'---'
];

feedLines(build_consumer, [], lines);
expect(build_consumer.compilers.iwyu.diagnostics).to.have.length(4);
const [add, all1, rem, all2] = build_consumer.compilers.iwyu.diagnostics;

expect(add.file).to.eq('/home/user/src/project/main.c');
expect(add.location.start.line).to.eq(0);
expect(add.location.start.character).to.eq(0);
expect(add.location.end.line).to.eq(0);
expect(add.location.end.character).to.eq(999);
expect(add.code).to.eq(undefined);
expect(add.message).to.eq('should add these lines:\n#include <stdbool.h> // for bool');
expect(add.severity).to.eq('warning');

expect(all1.file).to.eq('/home/user/src/project/main.c');
expect(all1.location.start.line).to.eq(0);
expect(all1.location.start.character).to.eq(0);
expect(all1.location.end.line).to.eq(0);
expect(all1.location.end.character).to.eq(999);
expect(all1.code).to.eq(undefined);
expect(all1.message).to.eq('The full include-list:\n#include <stdbool.h> // for bool');
expect(all1.severity).to.eq('note');

expect(rem.file).to.eq('/home/user/src/project/module.c');
expect(rem.location.start.line).to.eq(23);
expect(rem.location.start.character).to.eq(0);
expect(rem.location.end.line).to.eq(23);
expect(rem.location.end.character).to.eq(999);
expect(rem.code).to.eq(undefined);
expect(rem.message).to.eq('should remove: #include <alloca.h>');
expect(rem.severity).to.eq('warning');

expect(all2.file).to.eq('/home/user/src/project/module.c');
expect(all2.location.start.line).to.eq(0);
expect(all2.location.start.character).to.eq(0);
expect(all2.location.end.line).to.eq(0);
expect(all2.location.end.character).to.eq(999);
expect(all2.code).to.eq(undefined);
expect(all2.message).to.eq('The full include-list:\n#include "array.h" // for ARRAY_SIZE');
expect(all2.severity).to.eq('note');
});
});