diff --git a/src/extension.ts b/src/extension.ts index 5a88690..9ef0a46 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,7 +6,7 @@ import * as crypto from 'crypto'; import { documentationLinkMap, getPremiumCertLink } from './util/documentation'; import { runCommand } from './util/scripts'; import { looksLikePath, resolvePath, findWorkspaceRoot } from './util/path'; -import { diagnosticsUnion } from './util/diagnostics'; +import { DiagnosticMetadataStore, diagnosticsUnion } from './util/diagnostics'; import { CodeActionProvider } from './util/codeActions'; import { writeSuppressionToProjectFile } from './util/files'; @@ -14,6 +14,8 @@ import { writeSuppressionToProjectFile } from './util/files'; let documentHashMemory : Record = {}; // To keep track of warnings for files created from analysis of other files we save their relations to fileRelationMap let fileRelationMap: Record> = {}; +// Some diagnostics have symbol names associated with them, which we keep track of in diagnosticMetadataStore +const diagnosticMetadataStore = new DiagnosticMetadataStore(); let previewAnalysisTimer: NodeJS.Timeout | undefined; let previewedDocument: vscode.TextDocument | undefined; @@ -129,7 +131,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.languages.registerCodeActionsProvider( { pattern: "**/*" }, - new CodeActionProvider(), + new CodeActionProvider(diagnosticMetadataStore), { providedCodeActionKinds: [ vscode.CodeActionKind.QuickFix @@ -155,12 +157,21 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand( "cppcheck-official.suppressWarningAll", - async (diagnosticCode : string) => { + async (diagnosticCode : string, file? : string, symbolName? : string) => { if (cppcheckProjectFileUri) { - const success = await writeSuppressionToProjectFile(cppcheckProjectFileUri, diagnosticCode); + const success = await writeSuppressionToProjectFile(cppcheckProjectFileUri, diagnosticCode, file, symbolName); if (success) { - vscode.window.showInformationMessage(`Suppression of ${diagnosticCode} added to project file ${cppcheckProjectFileUri.toString()}`); - await vscode.commands.executeCommand('cppcheck-official.hideWarningType', diagnosticCode); + // Construct information message to display to the user + const fileMessagePart = file ? ` for file ${file}` : ''; + const symbolNameMessagePart = symbolName ? ` for symbol name ${symbolName}` : ''; + const fileOrSymbolExtraMessage = file || symbolName ? '. Note: you have to re-analyze the file for the suppression to take effect.' : ''; + const completeInformationMessageText = `Suppression of ${diagnosticCode} added to project file ${cppcheckProjectFileUri.toString()}${fileMessagePart}${symbolNameMessagePart}${fileOrSymbolExtraMessage}`; + vscode.window.showInformationMessage(completeInformationMessageText); + + // Only hide warnings if suppression is global, since hide command does not support file or symbol filter for now + if (!file && !symbolName) { + await vscode.commands.executeCommand('cppcheck-official.hideWarningType', diagnosticCode); + } } else { vscode.window.showErrorMessage(`Failed to add suppression of ${diagnosticCode} to project file ${cppcheckProjectFileUri.toString()}`); } @@ -214,6 +225,51 @@ export async function activate(context: vscode.ExtensionContext) { ) ); + context.subscriptions.push( + vscode.commands.registerCommand( + "cppcheck-official.suppressWarningAdvanced", + async (diagnosticCode : string, doc : vscode.TextDocument, diagnostic : vscode.Diagnostic) => { + const storedSymbolName = diagnosticMetadataStore.get(diagnostic)?.symbolName; + const symbolExistsForDiagnostic = !!storedSymbolName; + const file = await vscode.window.showInputBox( + { + value: doc.fileName, + title: symbolExistsForDiagnostic + ? "Create Cppcheck Suppression by file and / or symbol (leave blank to skip file filter) (1/2)" + : "Create Cppcheck Suppression by file" + } + ); + // User presses ESC -> file === undefined + if (file === undefined) { + return; + } + let symbolName = null; + if (symbolExistsForDiagnostic) { + symbolName = await vscode.window.showQuickPick( + [ + { + label: `Symbol: ${storedSymbolName}`, + value: storedSymbolName + }, + { + label: 'Not symbol specific', + value: null + }, + ], + { + title: "Create Cppcheck Suppression by file and / or symbol (2/2)" + } + ); + // User presses ESC -> symbolName === undefined + if (symbolName === undefined) { + return; + } + } + await vscode.commands.executeCommand('cppcheck-official.suppressWarningAll', diagnosticCode, file, symbolName?.value); + } + ) + ); + context.subscriptions.push( vscode.commands.registerCommand( "cppcheck-official.selectMinSeverity", @@ -579,6 +635,11 @@ async function runCppcheckOnFileXML( target: vscode.Uri.parse(getPremiumCertLink(e.$.id)) } : e.$.id; + // If warning has a symbol we keep track of it + if (e.symbol?.[0]) { + diagnosticMetadataStore.set(diagnostic, {symbolName: e.symbol?.[0]}); + } + // Related Information const relatedInfos: vscode.DiagnosticRelatedInformation[] = []; for (let i = 1; i <= locations.length; i++) { diff --git a/src/util/codeActions.ts b/src/util/codeActions.ts index be6870b..5aff427 100644 --- a/src/util/codeActions.ts +++ b/src/util/codeActions.ts @@ -1,6 +1,9 @@ import * as vscode from 'vscode'; - +import { DiagnosticMetadataStore } from './diagnostics'; export class CodeActionProvider implements vscode.CodeActionProvider { + constructor( + private readonly metadataStore: DiagnosticMetadataStore + ) {} provideCodeActions( document: vscode.TextDocument, range: vscode.Range, @@ -61,7 +64,25 @@ export class CodeActionProvider implements vscode.CodeActionProvider { suppressTypeAction.diagnostics = [diagnostic]; actions.push(suppressTypeAction); - + + // Set up an action for suppressing warning based on file or symbol name + const symbol = this.metadataStore.get(diagnostic)?.symbolName; + const suppressAdvancedAction = new vscode.CodeAction( + symbol + ? `Suppress warning ${diagnosticCode} based on file and / or symbol` + :`Suppress warning ${diagnosticCode} based on file`, + vscode.CodeActionKind.QuickFix + ); + + suppressAdvancedAction.command = { + command: "cppcheck-official.suppressWarningAdvanced", + title: "Suppress warning advanced", + arguments: [diagnosticCode, document, diagnostic] + }; + + suppressAdvancedAction.diagnostics = [diagnostic]; + actions.push(suppressAdvancedAction); + // Set up an action for hiding a warning const hideAction = new vscode.CodeAction( `Hide this ${diagnosticCode} warning`, diff --git a/src/util/diagnostics.ts b/src/util/diagnostics.ts index 0054b1a..f28fcb2 100644 --- a/src/util/diagnostics.ts +++ b/src/util/diagnostics.ts @@ -1,5 +1,21 @@ import * as vscode from 'vscode'; +interface DiagnosticMetadata { + symbolName?: string; +} + +export class DiagnosticMetadataStore { + private readonly map = new WeakMap(); + + set(diagnostic: vscode.Diagnostic, metadata: DiagnosticMetadata) { + this.map.set(diagnostic, metadata); + } + + get(diagnostic: vscode.Diagnostic) { + return this.map.get(diagnostic); + } +} + export function diagnosticsUnion(diagnosticsA : vscode.Diagnostic[], diagnosticB : vscode.Diagnostic[]) : vscode.Diagnostic[] { const diagnosticsUnion = new Array; // Add all elements from diagnosticsA to result array diff --git a/src/util/files.ts b/src/util/files.ts index 00e0e2d..6d280c6 100644 --- a/src/util/files.ts +++ b/src/util/files.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -export async function writeSuppressionToProjectFile(projectFileUri : vscode.Uri, warningType : String) : Promise { +export async function writeSuppressionToProjectFile(projectFileUri : vscode.Uri, warningType : String, file? : String, symbolName? : 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}`); @@ -13,6 +13,15 @@ export async function writeSuppressionToProjectFile(projectFileUri : vscode.Uri, var textToInsert = ''; var positionToInsertAt = 0; + // If file or symbolName is specified we set up that part of the suppression block + let options = ''; + if (file) { + options = ` file="${file}"`; + } + if (symbolName) { + options += ` symbolName="${symbolName}"`; + } + // Search for suppressions section const match = /]*>([\s\S]*?)<\/suppressions>/m.exec(text); if (match !== null) { @@ -22,7 +31,7 @@ export async function writeSuppressionToProjectFile(projectFileUri : vscode.Uri, // 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}`; + const newSuppressionLine = `${indentation}${warningType}\n${indentation}`; // We splice in the new line just before the end of the suppressions block textToInsert = newSuppressionLine; @@ -34,7 +43,7 @@ export async function writeSuppressionToProjectFile(projectFileUri : vscode.Uri, // 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`; + 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;