diff --git a/src/extension.ts b/src/extension.ts index aefec69..483d8ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -75,24 +75,10 @@ function severityToNumber(sev: vscode.DiagnosticSeverity): SeverityNumber { } } -function parseMinSeverity(str: string): SeverityNumber { - switch (str.toLowerCase()) { - case "error": return SeverityNumber.Error; - case "warning": return SeverityNumber.Warning; - default: return SeverityNumber.Info; - } -} - -function filterOutDiagnosticsBelowSeverityLevel(diagnosticCollection : vscode.DiagnosticCollection, severity : vscode.DiagnosticSeverity) { - diagnosticCollection.forEach((uri : vscode.Uri, diagnostics : readonly vscode.Diagnostic[], collection: vscode.DiagnosticCollection) => { - const filteredDiagnostics = diagnostics?.filter((diagnostic : vscode.Diagnostic) => { - if (severityToNumber(diagnostic.severity) < severityToNumber(severity)) { - return false; - } - return true; - }); - collection.set(uri, filteredDiagnostics); - }); +function setDiagnosticHiddenStatus(diagnostic : vscode.Diagnostic, hiddenStatus : boolean) { + var metadata = diagnosticMetadataStore.get(diagnostic); + const newMetaData = { ...metadata, hidden: hiddenStatus }; + diagnosticMetadataStore.set(diagnostic, newMetaData); } function updateProgressIndicator(): void { @@ -127,6 +113,35 @@ export async function activate(context: vscode.ExtensionContext) { // Create a diagnostic collection. const diagnosticCollection = vscode.languages.createDiagnosticCollection("Cppcheck"); context.subscriptions.push(diagnosticCollection); + + // Create a map for storing all diagnostics, including hidden / filtered diagnostics. Key is file uri as a string + const uriDiagnosticsMap = new Map(); + + function filterDisplayedDiagnosticsBasedOnHiddenStatus() { + uriDiagnosticsMap.forEach((diagnostics : vscode.Diagnostic[], uri : string) => { + const filteredDiagnostics = diagnostics?.filter((diagnostic : vscode.Diagnostic) => { + var metadata = diagnosticMetadataStore.get(diagnostic); + if (metadata?.hidden) { + return false; + } + return true; + }); + diagnosticCollection.set(vscode.Uri.parse(uri), filteredDiagnostics); + }); + } + + function hideDiagnosticsBasedOnSeverityLevel(severity : vscode.DiagnosticSeverity) { + uriDiagnosticsMap.forEach((diagnostics : vscode.Diagnostic[]) => { + diagnostics?.forEach((diagnostic : vscode.Diagnostic) => { + if (severityToNumber(diagnostic.severity) < severityToNumber(severity)) { + setDiagnosticHiddenStatus(diagnostic, true); + } else { + setDiagnosticHiddenStatus(diagnostic, false); + } + }); + }); + filterDisplayedDiagnosticsBasedOnHiddenStatus(); + } // Set up code actions provider context.subscriptions.push( @@ -189,18 +204,17 @@ export async function activate(context: vscode.ExtensionContext) { 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) => { + const diagnostics = uriDiagnosticsMap.get(uri.toString()); + diagnostics?.forEach((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; + setDiagnosticHiddenStatus(diagnostic, true); } - return true; }); - diagnosticCollection.set(uri, filteredDiagnostics); + filterDisplayedDiagnosticsBasedOnHiddenStatus(); } ) ); @@ -210,19 +224,18 @@ export async function activate(context: vscode.ExtensionContext) { 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) => { + uriDiagnosticsMap.forEach((diagnostics : readonly vscode.Diagnostic[]) => { + diagnostics?.forEach((diagnostic : vscode.Diagnostic) => { var code = diagnostic.code; if (typeof(code) === "object" && typeof(code) !== null) { code = code.value; } if (code === diagnosticCode) { - return false; + setDiagnosticHiddenStatus(diagnostic, true); } - return true; }); - collection.set(uri, filteredDiagnostics); }); + filterDisplayedDiagnosticsBasedOnHiddenStatus(); } ) ); @@ -315,7 +328,7 @@ export async function activate(context: vscode.ExtensionContext) { ); // Clear diagnostics below severity level selected from the problems tab - filterOutDiagnosticsBelowSeverityLevel(diagnosticCollection, parseSeverity(selection.value)); + hideDiagnosticsBasedOnSeverityLevel(parseSeverity(selection.value)); updateMinSeverityOption(); } @@ -346,8 +359,10 @@ export async function activate(context: vscode.ExtensionContext) { for (const fileUri of Object.keys(fileRelationMap)) { if (fileRelationMap[fileUri].has(doc.uri.toString())) { if (fileRelationMap[fileUri].size <= 1) { + uriDiagnosticsMap.delete(fileUri); diagnosticCollection.delete(vscode.Uri.parse(fileUri)); fileRelationMap[fileUri].clear(); + filterDisplayedDiagnosticsBasedOnHiddenStatus(); } else { fileRelationMap[fileUri].delete(doc.uri.toString()); } @@ -382,7 +397,6 @@ export async function activate(context: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration(); const isEnabled = config.get("cppcheck-official.enable", true); - const minSevString = config.get("cppcheck-official.minSeverity", "info"); const userPath = config.get("cppcheck-official.path")?.trim() || ""; const commandPath = userPath ? resolvePath(userPath) : "cppcheck"; @@ -425,9 +439,13 @@ export async function activate(context: vscode.ExtensionContext) { document, commandPath, processedArgs, - minSevString, - diagnosticCollection + uriDiagnosticsMap, ); + + // Analysis in runCppcheckOnFileXML populates uriDiagnosticsMap with all warnings, regardless of min severity filter. + // Thus after running analysis we have to apply the severity filter (this also populates DiagnosticCollection, making the diagnostics visible) + const minSevString = config.get("cppcheck-official.minSeverity", "info"); + hideDiagnosticsBasedOnSeverityLevel(parseSeverity(minSevString)); } // Listen for file saves. @@ -496,18 +514,19 @@ async function runCppcheckOnFileXML( document: vscode.TextDocument, commandPath: string, processedArgs: string, - minSevString: string, - diagnosticCollection: vscode.DiagnosticCollection + uriDiagnosticsMap: Map, ): Promise { checksRunning = true; updateProgressIndicator(); // Clear existing diagnostics for this file - diagnosticCollection.delete(document.uri); + uriDiagnosticsMap.delete(document.uri.toString()); // Replace backslashes (used in paths in Windows environment) const filePath = document.fileName.replaceAll('\\', '/'); - const minSevNum = parseMinSeverity(minSevString); + + // We always call cppcheck with severity level info, and then filter warnings when displaying them + const minSevNum = SeverityNumber.Info; // Resolve paths for arguments where applicable const argsParsed = processedArgs.split(" ").map((arg) => { @@ -550,182 +569,186 @@ async function runCppcheckOnFileXML( cwd, }); - // if spawn fails (e.g. ENOENT or permission denied) - proc.on("error", (err) => { - console.error("Failed to start cppcheck:", err); - vscode.window.showErrorMessage(`Cppcheck failed to start: ${err.message}`); - }); - - let xmlOutput = ""; - let out = ""; - proc.stderr.on("data", d => xmlOutput += d.toString()); - proc.stdout.on("data", d => out += d.toString()); - proc.on("close", code => { - if (code && code > 0) { - // Non-zero code means an error has occured - let errorMessage = `Cppcheck failed with code ${code} (unknown error)`; - if (out.trim().length > 0) { - errorMessage = out.trim(); - } - errorMessage = `${errorMessage}, Command: ${commandPath} ${args.join(' ')}`; - vscode.window.showErrorMessage(errorMessage); - } - const parser = new xml2js.Parser({ explicitArray: true }); - parser.parseString(xmlOutput, async (err, result) => { - if (err) { - console.error("XML parse error:", err); - return; - } - - const errors = result.results?.errors?.[0]?.error || []; - const diagnostics: Record = {}; - for (const e of errors) { - const isCriticalError = criticalWarningTypes.includes(e.$.id); - const locations = e.location || []; - if (!locations.length) { - continue; - } + await new Promise((resolve, reject) => { + // if spawn fails (e.g. ENOENT or permission denied) + proc.on("error", (err) => { + console.error("Failed to start cppcheck:", err); + vscode.window.showErrorMessage(`Cppcheck failed to start: ${err.message}`); + reject(err); + }); - const mainLoc = locations[locations.length - 1].$; - // If main location is not current file, we are not using a project file and warning is not critical then skip displaying warning - if (!isCriticalError && usingProjectFile && !filePath.endsWith(mainLoc.file)) { - continue; + let xmlOutput = ""; + let out = ""; + proc.stderr.on("data", d => xmlOutput += d.toString()); + proc.stdout.on("data", d => out += d.toString()); + proc.on("close", code => { + if (code && code > 0) { + // Non-zero code means an error has occured + let errorMessage = `Cppcheck failed with code ${code} (unknown error)`; + if (out.trim().length > 0) { + errorMessage = out.trim(); } - - let mainLocDocument : vscode.TextDocument | undefined; - try { - mainLocDocument = await vscode.workspace.openTextDocument(mainLoc.file); - } catch { - // do nothing + errorMessage = `${errorMessage}, Command: ${commandPath} ${args.join(' ')}`; + vscode.window.showErrorMessage(errorMessage); + } + const parser = new xml2js.Parser({ explicitArray: true }); + parser.parseString(xmlOutput, async (err, result) => { + if (err) { + console.error("XML parse error:", err); + return; } - // Cppcheck line number is 1-indexed, while VS Code uses 0-indexing - let line = Number(mainLoc.line) - 1; - // Invalid line number usually means non-analysis output - if (isNaN(line) || line < 0 || line >= document.lineCount) { - if (isCriticalError) { - line = 0; - } else { + const errors = result.results?.errors?.[0]?.error || []; + const diagnostics: Record = {}; + for (const e of errors) { + const isCriticalError = criticalWarningTypes.includes(e.$.id); + const locations = e.location || []; + if (!locations.length) { continue; } - } - - // Cppcheck col number is 1-indexed, while VS Code uses 0-indexing - let col = Number(mainLoc.column) - 1; - if (isNaN(col) || col < 0 || !mainLocDocument || col > mainLocDocument.lineAt(line).text.length) { - col = 0; - } - - const severity = parseSeverity(e.$.severity); - if (!isCriticalError && severityToNumber(severity) < minSevNum) { - continue; - } - const range = new vscode.Range(line, col, line, mainLocDocument ? mainLocDocument.lineAt(line).text.length : col); - const diagnostic = new vscode.Diagnostic(range, e.$.msg, severity); - diagnostic.source = "cppcheck"; - // If we have a link to documentation, include it - diagnostic.code = documentationLinkMap[e.$.id] ? { - value: e.$.id, - target: vscode.Uri.parse(documentationLinkMap[e.$.id]) - } : getPremiumCertLink(e.$.id) ? { - value: e.$.id, - target: vscode.Uri.parse(getPremiumCertLink(e.$.id)) - } : e.$.id; - - // If warning has a symbol we keep track of it - const symbolName = e.symbol?.[0] ?? ''; - // Save line of code at main location if we can access it - const mainLocLine = mainLocDocument?.lineAt(line)?.text ?? ''; - - diagnosticMetadataStore.set(diagnostic, {symbolName, mainLocLine}); - - // Related Information - const relatedInfos: vscode.DiagnosticRelatedInformation[] = []; - for (let i = 1; i <= locations.length; i++) { - // Related information is ordered in reverse in XML object - const loc = locations[locations.length - i].$; - const msg = loc.info; - const lLine = Number(loc.line) - 1; - const lCol = Number(loc.col) - 1; - - if (msg === null || msg === undefined || isNaN(lLine) || lLine < 0 || lLine >= document.lineCount) { + const mainLoc = locations[locations.length - 1].$; + // If main location is not current file, we are not using a project file and warning is not critical then skip displaying warning + if (!isCriticalError && usingProjectFile && !filePath.endsWith(mainLoc.file)) { continue; } - var relatedDocument : vscode.TextDocument | undefined; + let mainLocDocument : vscode.TextDocument | undefined; try { - relatedDocument = await vscode.workspace.openTextDocument(loc.file); + mainLocDocument = await vscode.workspace.openTextDocument(mainLoc.file); } catch { - // Do nothing + // do nothing } - const relatedRange = new vscode.Range( - lLine, lCol, - lLine, relatedDocument ? relatedDocument.lineAt(lLine).text.length : lCol - ); - relatedInfos.push( - new vscode.DiagnosticRelatedInformation( - new vscode.Location(relatedDocument ? relatedDocument.uri : vscode.Uri.file(''), relatedRange), - msg - ) - ); - } - if (relatedInfos.length > 0) { - diagnostic.relatedInformation = relatedInfos; - } - const diagnosticFile = mainLoc.file; - var diagnosticFileIsOpenDocument = diagnosticFile === document.fileName; - if (!diagnosticFile.includes('/')) { - // If we do not have file path but only name we asume diagnosed file is open document if they share name - if (document.fileName.endsWith(diagnosticFile)) { - diagnosticFileIsOpenDocument = true; + + // Cppcheck line number is 1-indexed, while VS Code uses 0-indexing + let line = Number(mainLoc.line) - 1; + // Invalid line number usually means non-analysis output + if (isNaN(line) || line < 0 || line >= document.lineCount) { + if (isCriticalError) { + line = 0; + } else { + continue; + } } - } - if (diagnosticFileIsOpenDocument) { - const uri = document.uri.toString(); - if (diagnostics[uri] === null || diagnostics[uri] === undefined) { - diagnostics[uri] = []; + + // Cppcheck col number is 1-indexed, while VS Code uses 0-indexing + let col = Number(mainLoc.column) - 1; + if (isNaN(col) || col < 0 || !mainLocDocument || col > mainLocDocument.lineAt(line).text.length) { + col = 0; } - diagnostics[uri].push(diagnostic); - } else { - var relatedDocument : vscode.TextDocument | undefined; - try { - relatedDocument = await vscode.workspace.openTextDocument(mainLoc.file); - } catch { - // Do nothing + + const severity = parseSeverity(e.$.severity); + if (!isCriticalError && severityToNumber(severity) < minSevNum) { + continue; } - if (relatedDocument) { - // Proceed if we are able to open the document - const uri = relatedDocument.uri.toString(); + + const range = new vscode.Range(line, col, line, mainLocDocument ? mainLocDocument.lineAt(line).text.length : col); + const diagnostic = new vscode.Diagnostic(range, e.$.msg, severity); + diagnostic.source = "cppcheck"; + // If we have a link to documentation, include it + diagnostic.code = documentationLinkMap[e.$.id] ? { + value: e.$.id, + target: vscode.Uri.parse(documentationLinkMap[e.$.id]) + } : getPremiumCertLink(e.$.id) ? { + value: e.$.id, + target: vscode.Uri.parse(getPremiumCertLink(e.$.id)) + } : e.$.id; + + // If warning has a symbol we keep track of it + const symbolName = e.symbol?.[0] ?? ''; + // Save line of code at main location if we can access it + const mainLocLine = mainLocDocument?.lineAt(line)?.text ?? ''; + + diagnosticMetadataStore.set(diagnostic, { symbolName, mainLocLine, hidden: false }); + + // Related Information + const relatedInfos: vscode.DiagnosticRelatedInformation[] = []; + for (let i = 1; i <= locations.length; i++) { + // Related information is ordered in reverse in XML object + const loc = locations[locations.length - i].$; + const msg = loc.info; + const lLine = Number(loc.line) - 1; + const lCol = Number(loc.col) - 1; + + if (msg === null || msg === undefined || isNaN(lLine) || lLine < 0 || lLine >= document.lineCount) { + continue; + } + + var relatedDocument : vscode.TextDocument | undefined; + try { + relatedDocument = await vscode.workspace.openTextDocument(loc.file); + } catch { + // Do nothing + } + const relatedRange = new vscode.Range( + lLine, lCol, + lLine, relatedDocument ? relatedDocument.lineAt(lLine).text.length : lCol + ); + relatedInfos.push( + new vscode.DiagnosticRelatedInformation( + new vscode.Location(relatedDocument ? relatedDocument.uri : vscode.Uri.file(''), relatedRange), + msg + ) + ); + } + if (relatedInfos.length > 0) { + diagnostic.relatedInformation = relatedInfos; + } + const diagnosticFile = mainLoc.file; + var diagnosticFileIsOpenDocument = diagnosticFile === document.fileName; + if (!diagnosticFile.includes('/')) { + // If we do not have file path but only name we asume diagnosed file is open document if they share name + if (document.fileName.endsWith(diagnosticFile)) { + diagnosticFileIsOpenDocument = true; + } + } + if (diagnosticFileIsOpenDocument) { + const uri = document.uri.toString(); if (diagnostics[uri] === null || diagnostics[uri] === undefined) { diagnostics[uri] = []; } diagnostics[uri].push(diagnostic); + } else { + var relatedDocument : vscode.TextDocument | undefined; + try { + relatedDocument = await vscode.workspace.openTextDocument(mainLoc.file); + } catch { + // Do nothing + } + if (relatedDocument) { + // Proceed if we are able to open the document + const uri = relatedDocument.uri.toString(); + if (diagnostics[uri] === null || diagnostics[uri] === undefined) { + diagnostics[uri] = []; + } + diagnostics[uri].push(diagnostic); + } } } - } - const sourceDocumentUri = document.uri.toString(); - for (const uri of Object.keys(diagnostics)) { - var newDiagnostics = diagnostics[uri]; - // If file has existing diagnostics from analyzing other files we do not want to overwrite those - const existingDiagnostics = diagnosticCollection.get(vscode.Uri.parse(uri)); - if (existingDiagnostics) { - newDiagnostics = diagnosticsUnion(newDiagnostics, existingDiagnostics.flat()); - } - diagnosticCollection.set(vscode.Uri.parse(uri), newDiagnostics); - if (fileRelationMap[uri] === null ||fileRelationMap[uri] === undefined) { - fileRelationMap[uri] = new Set; + const sourceDocumentUri = document.uri.toString(); + for (const uri of Object.keys(diagnostics)) { + var newDiagnostics = diagnostics[uri]; + // If file has existing diagnostics from analyzing other files we do not want to overwrite those + const existingDiagnostics = uriDiagnosticsMap.get(uri); + if (existingDiagnostics) { + newDiagnostics = diagnosticsUnion(newDiagnostics, existingDiagnostics.flat()); + } + uriDiagnosticsMap.set(uri, newDiagnostics); + if (fileRelationMap[uri] === null ||fileRelationMap[uri] === undefined) { + fileRelationMap[uri] = new Set; + } + // NOTE: uri can be the same as sourceDocumentUri + fileRelationMap[uri].add(sourceDocumentUri); } - // NOTE: uri can be the same as sourceDocumentUri - fileRelationMap[uri].add(sourceDocumentUri); + resolve(); + }); + + // If checks have run without error, save hashed document content to memory + if (!code) { + const hashedContentOfFile = getDocumentSha1(document); + documentHashMemory[document.fileName] = hashedContentOfFile; } }); - - // If checks have run without error, save hashed document content to memory - if (!code) { - const hashedContentOfFile = getDocumentSha1(document); - documentHashMemory[document.fileName] = hashedContentOfFile; - } }); checksRunning = false; diff --git a/src/util/diagnostics.ts b/src/util/diagnostics.ts index 1fd21fb..e5ac63a 100644 --- a/src/util/diagnostics.ts +++ b/src/util/diagnostics.ts @@ -3,6 +3,7 @@ import * as vscode from 'vscode'; interface DiagnosticMetadata { symbolName?: string; mainLocLine?: string; + hidden: boolean; } export class DiagnosticMetadataStore {