diff --git a/README.md b/README.md index 854c6b8..0c2e6e8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - **Warning notes**: Display notes for warnings when those are available - **Dynamic config**: The extension supports running a script to generate arguments to pass to cppcheck. This can be done by including the command in the argument field wrapped with \@(), e.g. `--suppress=memleak:src/file1.cpp @(bash path/to/script.sh)`. The script is expected to output the argument(s) wrapped with \@(). If the script e.g. creates a project file it should print out as `@(--project=path/to/projectfile.json)`. This output will be spliced into the argument string as such: `--suppress=memleak:src/file1.cpp --project=path/to/projectfile.json`. -- **Warning suppression**: Warnings of a specific type can be supressed with the --supress flag in the argument field in the extension settings. The extension also supports inline suppression for specific lines of code, simply write `// cppcheck-supress >warning id<` (see image below). +- **Warning suppression**: Warnings of a specific type can be supressed with the --suppress flag in the argument field in the extension settings. The extension also supports inline suppression for specific lines of code, simply write `// cppcheck-suppress >warning id<` (see image below). Suppressions can also be added through code actions, either as automatically generated inline suppressions or, if you are using a project file, as universal suppressions specified in your project file. ![Image showing how to suppress warnings](./images/suppression.png) ## Requirements diff --git a/src/extension.ts b/src/extension.ts index bb8967e..a83b74b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,8 @@ import { documentationLinkMap, getPremiumCertLink } from './util/documentation'; import { runCommand } from './util/scripts'; import { looksLikePath, resolvePath, findWorkspaceRoot } from './util/path'; import { diagnosticsUnion } from './util/diagnostics'; +import { CodeActionProvider } from './util/codeActions'; +import { writeSuppressionToProjectFile } from './util/files'; // To keep track of document changes we save hashed versions of their content to this record let documentHashMemory : Record = {}; @@ -17,6 +19,7 @@ let previewAnalysisTimer: NodeJS.Timeout | undefined; let previewedDocument: vscode.TextDocument | undefined; let cppcheckProgressIndicator: vscode.StatusBarItem; let checksRunning = false; +let cppcheckProjectFileUri: vscode.Uri | undefined; enum SeverityNumber { Info = 0, @@ -94,7 +97,23 @@ function getDocumentSha1(document: vscode.TextDocument): string { // This method is called when your extension is activated. // Your extension is activated the very first time the command is executed. -export async function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { + // Create a diagnostic collection. + const diagnosticCollection = vscode.languages.createDiagnosticCollection("Cppcheck"); + context.subscriptions.push(diagnosticCollection); + + // Set up code actions provider + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + { pattern: "**/*" }, + new CodeActionProvider(), + { + providedCodeActionKinds: [ + vscode.CodeActionKind.QuickFix + ] + } + ) + ); // Register a command to push user to workspace settings from walkthrough context.subscriptions.push( @@ -108,15 +127,74 @@ export async function activate(context: vscode.ExtensionContext) { } ) ); + + // Register a command for suppressing a warning type + context.subscriptions.push( + vscode.commands.registerCommand( + "cppcheck-official.suppressWarningAll", + async (diagnosticCode : string) => { + if (cppcheckProjectFileUri) { + const success = await writeSuppressionToProjectFile(cppcheckProjectFileUri, diagnosticCode); + if (success) { + vscode.window.showInformationMessage(`Suppression of ${diagnosticCode} added to project file ${cppcheckProjectFileUri.toString()}`); + await vscode.commands.executeCommand('cppcheck-official.hideWarningType', diagnosticCode); + } else { + vscode.window.showErrorMessage(`Failed to add suppression of ${diagnosticCode} to project file ${cppcheckProjectFileUri.toString()}`); + } + } else { + vscode.window.showInformationMessage(`Adding suppression is currently only supported for .cppcheck project files`); + } + } + ) + ); + + // Register a command for hiding a warning + context.subscriptions.push( + vscode.commands.registerCommand( + "cppcheck-official.hideWarning", + async (uri : vscode.Uri, diagnosticCode : string, range : vscode.Range) => { + const diagnostics = diagnosticCollection.get(uri); + const filteredDiagnostics = diagnostics?.filter((diagnostic : vscode.Diagnostic) => { + var code = diagnostic.code; + if (typeof(code) === "object" && typeof(code) !== null) { + code = code.value; + } + if (code === diagnosticCode && diagnostic.range.isEqual(range)) { + return false; + } + return true; + }); + diagnosticCollection.set(uri, filteredDiagnostics); + } + ) + ); + + // Register a command for hiding all warnings of a given type + context.subscriptions.push( + vscode.commands.registerCommand( + "cppcheck-official.hideWarningType", + async (diagnosticCode : string) => { + diagnosticCollection.forEach((uri : vscode.Uri, diagnostics : readonly vscode.Diagnostic[], collection : vscode.DiagnosticCollection) => { + const filteredDiagnostics = diagnostics?.filter((diagnostic : vscode.Diagnostic) => { + var code = diagnostic.code; + if (typeof(code) === "object" && typeof(code) !== null) { + code = code.value; + } + if (code === diagnosticCode) { + return false; + } + return true; + }); + collection.set(uri, filteredDiagnostics); + }); + } + ) + ); // ProgressIndicator status bar item to show when checks are running cppcheckProgressIndicator = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); context.subscriptions.push(cppcheckProgressIndicator); - // Create a diagnostic collection. - const diagnosticCollection = vscode.languages.createDiagnosticCollection("Cppcheck"); - context.subscriptions.push(diagnosticCollection); - function clearDiagnosticForDoc(doc: vscode.TextDocument): void { // Any file who was warnings generated from (and only from) the closed doc have their diagnostics cleared // NOTE: This includes the closed doc - its diagnostics will only be cleared if its warnings only come from analysis of it itself @@ -299,19 +377,29 @@ async function runCppcheckOnFileXML( }); let usingProjectFile = false; + cppcheckProjectFileUri = undefined; + const args = [ '--enable=all', '--inline-suppr', '--xml', - '--suppress=unusedFunction', - '--suppress=missingInclude', - '--suppress=missingIncludeSystem', ...argsParsed, ].filter(Boolean); - if (processedArgs.includes("--project")) { + + if (processedArgs.includes("--project=")) { usingProjectFile = true; args.push(`--file-filter=${filePath}`); + // If project file is of type .cppcheck we keep track of it + var projectFilePath = processedArgs.split('--project=')[1].split(' ')[0]; + var projectFileType = projectFilePath.split('.')[1]; + if (projectFileType.toLowerCase() === 'cppcheck') { + cppcheckProjectFileUri = vscode.Uri.file(projectFilePath); + } } else { + args.push( + '--suppress=unusedFunction', + '--suppress=missingInclude', + '--suppress=missingIncludeSystem'); args.push(filePath); } diff --git a/src/util/codeActions.ts b/src/util/codeActions.ts new file mode 100644 index 0000000..be6870b --- /dev/null +++ b/src/util/codeActions.ts @@ -0,0 +1,98 @@ +import * as vscode from 'vscode'; + +export class CodeActionProvider implements vscode.CodeActionProvider { + provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): vscode.CodeAction[] { + + const actions: vscode.CodeAction[] = []; + + for (const diagnostic of context.diagnostics) { + var diagnosticCode = diagnostic.code; + if (typeof(diagnosticCode) === "object" && typeof(diagnosticCode) !== null) { + diagnosticCode = diagnosticCode.value; + } + + // Set up one action for suppressing the specific warning on the line targeted by the diagnostic + const suppressAction = new vscode.CodeAction( + `Add comment that suppresses this ${diagnosticCode} warning`, + vscode.CodeActionKind.QuickFix + ); + + // Copy indentation from line affected by diagnostic + const lineText = document.lineAt(diagnostic.range.start.line).text; + const indent = lineText.match(/^\s*/)?.[0] ?? ""; + + // Insert suppression comment above affected line + const suppressLineEdit = new vscode.WorkspaceEdit(); + suppressLineEdit.insert( + document.uri, + new vscode.Position( + diagnostic.range.start.line, + 0, + ), + `${indent}// cppcheck-suppress ${diagnosticCode}\n` + ); + suppressAction.edit = suppressLineEdit; + + // For inline suppression we also hide the warning so user does not have to rerun analysis for it to disappear + suppressAction.command = { + command: "cppcheck-official.hideWarning", + title: "Hide warning", + arguments: [document.uri, diagnosticCode, diagnostic.range] + }; + suppressAction.diagnostics = [diagnostic]; + actions.push(suppressAction); + + // Set up an action for suppressing warning of a given type universally + const suppressTypeAction = new vscode.CodeAction( + `Suppress warning type ${diagnosticCode} universally (in project file)`, + vscode.CodeActionKind.QuickFix + ); + + suppressTypeAction.command = { + command: "cppcheck-official.suppressWarningAll", + title: "Suppress warning universally", + arguments: [diagnosticCode] + }; + + suppressTypeAction.diagnostics = [diagnostic]; + actions.push(suppressTypeAction); + + // Set up an action for hiding a warning + const hideAction = new vscode.CodeAction( + `Hide this ${diagnosticCode} warning`, + vscode.CodeActionKind.QuickFix + ); + + hideAction.command = { + command: "cppcheck-official.hideWarning", + title: "Hide warning", + arguments: [document.uri, diagnosticCode, diagnostic.range] + }; + + hideAction.diagnostics = [diagnostic]; + actions.push(hideAction); + + // Set up an action for hiding all warnings of a given type + const hideTypeAction = new vscode.CodeAction( + `Hide all ${diagnosticCode} warnings`, + vscode.CodeActionKind.QuickFix + ); + + hideTypeAction.command = { + command: "cppcheck-official.hideWarningType", + title: "Hide warning type", + arguments: [diagnosticCode] + }; + + hideTypeAction.diagnostics = [diagnostic]; + actions.push(hideTypeAction); + } + + return actions; + } +} \ No newline at end of file diff --git a/src/util/files.ts b/src/util/files.ts new file mode 100644 index 0000000..00e0e2d --- /dev/null +++ b/src/util/files.ts @@ -0,0 +1,55 @@ +import * as vscode from 'vscode'; + +export async function writeSuppressionToProjectFile(projectFileUri : vscode.Uri, warningType : String) : Promise { + const fileType = projectFileUri.toString().split('.')[1]; + if (fileType !== 'cppcheck') { + throw new Error(`Function writeSuppressionToProjectFile only supports writing to .cppcheck project files! Recieved file is of type .${fileType}`); + } + + // Open project file with vscode workspace API + const document = await vscode.workspace.openTextDocument(projectFileUri); + const text = document.getText(); + const edit = new vscode.WorkspaceEdit(); + var textToInsert = ''; + var positionToInsertAt = 0; + + // Search for suppressions section + const match = /]*>([\s\S]*?)<\/suppressions>/m.exec(text); + if (match !== null) { + // match !== null -> Suppressions section exists + const closeIndex = text.indexOf(""); + + // Determine indentation and construct the new suppression line + const endOfSuppressionsBlockLine = document.lineAt(document.positionAt(closeIndex).line); + const indentation = endOfSuppressionsBlockLine.text.match(/^\s*/)?.[0] ?? " "; + const newSuppressionLine = `${indentation}${warningType}\n${indentation}`; + + // We splice in the new line just before the end of the suppressions block + textToInsert = newSuppressionLine; + positionToInsertAt = closeIndex; + } else { + // match === null -> Suppressions section does not exist + const closeIndex = text.indexOf(""); + + // Determine indentation and construct the new suppressions block + const line = document.lineAt(document.positionAt(closeIndex).line - 1); + const indentation = line.text.match(/^\s*/)?.[0] ?? " "; + const suppressionsBlock = `${indentation}\n${indentation} ${warningType}\n${indentation}\n`; + + // Splice in the new suppressions block just before the end of the project-file + textToInsert = suppressionsBlock; + positionToInsertAt = closeIndex; + } + + // Apply edit to document + edit.insert( + document.uri, + document.positionAt(positionToInsertAt), + textToInsert + ); + const success = await vscode.workspace.applyEdit(edit); + + // Write file + await document.save(); + return success; +} \ No newline at end of file