diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8439ba57..15c3b68067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/docs/cmake-settings.md b/docs/cmake-settings.md index 36b9e37247..a9f85367f4 100644 --- a/docs/cmake-settings.md +++ b/docs/cmake-settings.md @@ -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 | diff --git a/package.json b/package.json index 32bf8654fd..778405ea0d 100644 --- a/package.json +++ b/package.json @@ -2295,7 +2295,8 @@ "msvc", "ghs", "diab", - "iar" + "iar", + "iwyu" ] }, "default": [ diff --git a/package.nls.json b/package.nls.json index 09b3e8451f..a1fd8d1b58 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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." ] diff --git a/src/diagnostics/build.ts b/src/diagnostics/build.ts index fa0089d221..d4b7f0c1d5 100644 --- a/src/diagnostics/build.ts +++ b/src/diagnostics/build.ts @@ -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'; @@ -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 { @@ -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); diff --git a/src/diagnostics/iwyu.ts b/src/diagnostics/iwyu.ts new file mode 100644 index 0000000000..371b9e7ab3 --- /dev/null +++ b/src/diagnostics/iwyu.ts @@ -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'); +} diff --git a/test/unit-tests/diagnostics.test.ts b/test/unit-tests/diagnostics.test.ts index f67c288633..b097b27b81 100644 --- a/test/unit-tests/diagnostics.test.ts +++ b/test/unit-tests/diagnostics.test.ts @@ -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 // for bool', + '#include // for uint32_t, uint8_t', + '', + '/home/user/src/project/main.c should remove these lines:', + '- #include // lines 24-24', + '- #include // lines 25-26', + '', + 'The full include-list for /home/user/src/project/main.c:', + '#include // for bool', + '#include // for uint32_t, uint8_t', + '#include // 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 // for bool\n#include // 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 '); + 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 '); + 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 // for bool\n#include // for uint32_t, uint8_t\n#include // 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 // for bool', + '', + '/home/user/src/project/main.c should remove these lines:', + '', + 'The full include-list for /home/user/src/project/main.c:', + '#include // 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 // 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 // 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 // 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 '); + 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 // for bool', + '', + '/home/user/src/project/main.c should remove these lines:', + '', + 'The full include-list for /home/user/src/project/main.c:', + '#include // for bool', + '---', + '/home/user/src/project/module.c should add these lines:', + '', + '/home/user/src/project/module.c should remove these lines:', + '- #include // 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 // 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 // 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 '); + 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'); + }); });