From f18834fae94205015ee6ba8400814433f1f5ae88 Mon Sep 17 00:00:00 2001 From: davidramnero Date: Fri, 11 Sep 2026 13:00:36 +0200 Subject: [PATCH 1/6] feature / full project analysis --- src/extension.ts | 321 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 305 insertions(+), 16 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index e86f8e3..f0e1004 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,6 +26,7 @@ let previewedDocument: vscode.TextDocument | undefined; let cppcheckProgressIndicator: vscode.StatusBarItem; let severityOption: vscode.StatusBarItem; let hiddenTypesOption: vscode.StatusBarItem; +let fullAnalysisStatusBarItem: vscode.StatusBarItem; let checksRunning = false; let usesPremiumCppcheck = false; @@ -60,6 +61,26 @@ const pathVariableArgs = [ '--rule-file', ]; +async function processArguments(args : string) { + // If user enter arguments as array we parse them into space separated string format + if (args.startsWith("[") && args.endsWith("]")) { + args = args.replaceAll("[", "").replaceAll("]", "").replaceAll(",", " "); + } + + var processedArgs = ''; + // If argument field contains command to run script we do so here + if (args.includes('@(')) { + const scriptCommand = args.split("@(")[1].split(")")[0]; + const scriptOutput = await runCommand(scriptCommand); + // We expect that the script output that is to be used as arguments will be wrapped with ${} + const scriptOutputTrimmed = scriptOutput.split("@(")[1].split(")")[0]; + processedArgs = args.split("@(")[0] + scriptOutputTrimmed + args.split(")")?.[1]; + } else { + processedArgs = args; + } + return processedArgs; +} + function parseSeverity(str: string): vscode.DiagnosticSeverity { const lower = str.toLowerCase(); if (lower.includes("error")) { @@ -111,9 +132,11 @@ function updateProgressIndicator(): void { cppcheckProgressIndicator.show(); // To avoid crowding status bar we alternate between progress indicator and severity option item severityOption.hide(); + fullAnalysisStatusBarItem.hide(); } else { cppcheckProgressIndicator.hide(); severityOption.show(); + fullAnalysisStatusBarItem.show(); } } @@ -361,6 +384,69 @@ export async function activate(context: vscode.ExtensionContext) { } ) ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "cppcheck-official.runFullAnalysis", + async () => { + + const selection = await vscode.window.showQuickPick( + [ + { + label: "1 thread", + description: "No parallel threads", + value: "-j1" + }, + { + label: "2 threads", + description: "2 parallel threads", + value: "-j2" + }, + { + label: "4 threads", + description: "4 parallel threads", + value: "-j4" + } + ], + { + title: "Select how many threads to run in parallel for full analysis" + } + ); + if (!selection) { + return; + } + + const config = vscode.workspace.getConfiguration(); + const userPath = config.get("cppcheck-official.path")?.trim() || ""; + const commandPath = userPath ? resolvePath(userPath) : "cppcheck"; + + var args = config.get("cppcheck-official.arguments", ""); + const processedArgs = await processArguments(args); + + // Check if cppcheck is available + cp.exec(`"${commandPath}" --version`, (error, stdout) => { + if (error) { + vscode.window.showErrorMessage( + `Cppcheck: Could not find or run '${commandPath}'. ` + + `Please install cppcheck or set 'cppcheck-official.path' correctly.` + ); + return; + } + usesPremiumCppcheck = stdout.toLowerCase().includes('premium'); + }); + + console.log('processedArgs', processedArgs); + + // Run + await runFullAnalysis( + commandPath, + processedArgs, + uriDiagnosticsMap, + selection.value, + ); + } + ) + ); context.subscriptions.push( vscode.commands.registerCommand( @@ -423,6 +509,12 @@ export async function activate(context: vscode.ExtensionContext) { // Call update function once at setup to set the UI text to the settings current value updateHiddenWarningTypesOption(); + // Full analysis status bar item + fullAnalysisStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 8); + fullAnalysisStatusBarItem.command = "cppcheck-official.runFullAnalysis"; + fullAnalysisStatusBarItem.text = `$(play) Full Analysis`; + fullAnalysisStatusBarItem.show(); + context.subscriptions.push(fullAnalysisStatusBarItem); function clearDiagnosticForDoc(doc: vscode.TextDocument): void { // Any file who was warnings generated from (and only from) the closed doc have their diagnostics cleared @@ -472,22 +564,7 @@ export async function activate(context: vscode.ExtensionContext) { const commandPath = userPath ? resolvePath(userPath) : "cppcheck"; var args = config.get("cppcheck-official.arguments", ""); - // If user enter arguments as array we parse them into space separated string format - if (args.startsWith("[") && args.endsWith("]")) { - args = args.replaceAll("[", "").replaceAll("]", "").replaceAll(",", " "); - } - - var processedArgs = ''; - // If argument field contains command to run script we do so here - if (args.includes('@(')) { - const scriptCommand = args.split("@(")[1].split(")")[0]; - const scriptOutput = await runCommand(scriptCommand); - // We expect that the script output that is to be used as arguments will be wrapped with ${} - const scriptOutputTrimmed = scriptOutput.split("@(")[1].split(")")[0]; - processedArgs = args.split("@(")[0] + scriptOutputTrimmed + args.split(")")?.[1]; - } else { - processedArgs = args; - } + const processedArgs = await processArguments(args); // If disabled, clear any existing diagnostics for this doc. if (!isEnabled) { @@ -832,5 +909,217 @@ async function runCppcheckOnFileXML( updateProgressIndicator(); } +async function runFullAnalysis( + commandPath: string, + processedArgs: string, + uriDiagnosticsMap: Map, + threadsOption: string, +): Promise { + if (!processedArgs.includes("--project=")) { + throw new Error("full analysis called without specified project file!"); + } + + checksRunning = true; + updateProgressIndicator(); + + // Clear existing diagnostics for all files + uriDiagnosticsMap.clear(); + + // 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) => { + let cleanedArg = arg.replaceAll("\"",""); + const isPathArgument = pathVariableArgs.some(a => cleanedArg.startsWith(a)); + // Some arguments such as addon may be either a path or the name of a built in addon + if (isPathArgument && looksLikePath(cleanedArg)) { + const splitArg = cleanedArg.split('='); + return `${splitArg[0]}=${resolvePath(splitArg[1])}`; + } + return arg; + }); + + let usingProjectFile = true; + var projectFilePath = processedArgs.split('--project=')[1].split(' ')[0]; + projectFileStore.clear(); + projectFileStore.setUri(vscode.Uri.file(projectFilePath)); + + const args = [ + '--enable=all', + '--inline-suppr', + '--xml', + threadsOption, + ...argsParsed, + ].filter(Boolean); + + if (usesPremiumCppcheck) { + args.push('--premium=safety-off'); + } + + let proc; + const cwd = findWorkspaceRoot(); + proc = cp.spawn(commandPath, args, { + cwd, + }); + + 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); + }); + + 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; + } + + 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) { + continue; + } + + let mainLocDocument : vscode.TextDocument | undefined; + try { + mainLocDocument = await vscode.workspace.openTextDocument(mainLoc.file); + } catch { + // do nothing + } + + // 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 || (mainLocDocument && line >= mainLocDocument.lineCount)) { + if (isCriticalError) { + line = 0; + } else { + 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, 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 || (mainLocDocument && lLine >= mainLocDocument.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; + } + 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); + } + } + 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); + } + resolve(); + }); + }); + }); + + checksRunning = false; + updateProgressIndicator(); +} + // This method is called when your extension is deactivated export function deactivate() {} From a058be1fd9e74f12998028905a180bc2cf86fd54 Mon Sep 17 00:00:00 2001 From: davidramnero Date: Sun, 13 Sep 2026 19:04:01 +0200 Subject: [PATCH 2/6] working full analysis --- src/extension.ts | 145 +++++++------------------------ src/helpers/xmlParsingHelpers.ts | 50 +++++++++++ src/util/path.ts | 22 +++++ 3 files changed, 101 insertions(+), 116 deletions(-) create mode 100644 src/helpers/xmlParsingHelpers.ts diff --git a/src/extension.ts b/src/extension.ts index f0e1004..9eba717 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,10 +5,11 @@ import * as crypto from 'crypto'; import { documentationLinkMap, getPremiumCertLink } from './util/documentation'; import { runCommand } from './util/scripts'; -import { looksLikePath, resolvePath, findWorkspaceRoot } from './util/path'; +import { looksLikePath, resolvePath, findWorkspaceRoot, splitArgsAndResolvePaths } from './util/path'; import { DiagnosticMetadataStore, diagnosticsUnion } from './util/diagnostics'; import { CodeActionProvider } from './util/codeActions'; import { ProjectFileStore, writeSuppressionToProjectFile } from './util/files'; +import { extractRelatedInformation } from './helpers/xmlParsingHelpers'; // To keep track of document changes we save hashed versions of their content to this record let documentHashMemory : Record = {}; @@ -53,14 +54,6 @@ const criticalWarningTypes = [ 'unknownMacro' ]; -const pathVariableArgs = [ - '--project', - '--addon', - '--suppressions-list', - '--include', - '--rule-file', -]; - async function processArguments(args : string) { // If user enter arguments as array we parse them into space separated string format if (args.startsWith("[") && args.endsWith("]")) { @@ -435,8 +428,6 @@ export async function activate(context: vscode.ExtensionContext) { usesPremiumCppcheck = stdout.toLowerCase().includes('premium'); }); - console.log('processedArgs', processedArgs); - // Run await runFullAnalysis( commandPath, @@ -444,6 +435,12 @@ export async function activate(context: vscode.ExtensionContext) { uriDiagnosticsMap, selection.value, ); + + const minSevString = config.get("cppcheck-official.minSeverity", "info"); + hideDiagnosticsBasedOnSeverityLevel(parseSeverity(minSevString)); + filterDisplayedDiagnosticsBasedOnHiddenStatus(); + // Analysis in runFullAnalysis 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) } ) ); @@ -679,16 +676,7 @@ async function runCppcheckOnFileXML( const minSevNum = SeverityNumber.Info; // Resolve paths for arguments where applicable - const argsParsed = processedArgs.split(" ").map((arg) => { - let cleanedArg = arg.replaceAll("\"",""); - const isPathArgument = pathVariableArgs.some(a => cleanedArg.startsWith(a)); - // Some arguments such as addon may be either a path or the name of a built in addon - if (isPathArgument && looksLikePath(cleanedArg)) { - const splitArg = cleanedArg.split('='); - return `${splitArg[0]}=${resolvePath(splitArg[1])}`; - } - return arg; - }); + const argsParsed = splitArgsAndResolvePaths(processedArgs); let usingProjectFile = false; projectFileStore.clear(); @@ -815,36 +803,9 @@ async function runCppcheckOnFileXML( 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 - ) - ); - } + // Parse Related Information + const relatedInfos: vscode.DiagnosticRelatedInformation[] = await extractRelatedInformation(locations); + if (relatedInfos.length > 0) { diagnostic.relatedInformation = relatedInfos; } @@ -929,18 +890,8 @@ async function runFullAnalysis( const minSevNum = SeverityNumber.Info; // Resolve paths for arguments where applicable - const argsParsed = processedArgs.split(" ").map((arg) => { - let cleanedArg = arg.replaceAll("\"",""); - const isPathArgument = pathVariableArgs.some(a => cleanedArg.startsWith(a)); - // Some arguments such as addon may be either a path or the name of a built in addon - if (isPathArgument && looksLikePath(cleanedArg)) { - const splitArg = cleanedArg.split('='); - return `${splitArg[0]}=${resolvePath(splitArg[1])}`; - } - return arg; - }); + const argsParsed = splitArgsAndResolvePaths(processedArgs); - let usingProjectFile = true; var projectFilePath = processedArgs.split('--project=')[1].split(' ')[0]; projectFileStore.clear(); projectFileStore.setUri(vscode.Uri.file(projectFilePath)); @@ -1002,22 +953,19 @@ async function runFullAnalysis( } 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) { - continue; - } - let mainLocDocument : vscode.TextDocument | undefined; try { mainLocDocument = await vscode.workspace.openTextDocument(mainLoc.file); } catch { - // do nothing + // If we can't open the file in the context of a full analysis we have no reference to where the error is occurring and are forced to skip it + vscode.window.showInformationMessage(`Unable to find location of error [${e.$.id}]: ${e.$.msg}`); + continue; } // 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 || (mainLocDocument && line >= mainLocDocument.lineCount)) { + if (isNaN(line) || line < 0 || line >= mainLocDocument.lineCount) { if (isCriticalError) { line = 0; } else { @@ -1027,7 +975,7 @@ async function runFullAnalysis( // 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) { + if (isNaN(col) || col < 0 || col > mainLocDocument.lineAt(line).text.length) { col = 0; } @@ -1036,7 +984,7 @@ async function runFullAnalysis( continue; } - const range = new vscode.Range(line, col, line, mainLocDocument ? mainLocDocument.lineAt(line).text.length : col); + const range = new vscode.Range(line, col, line, mainLocDocument.lineAt(line).text.length); const diagnostic = new vscode.Diagnostic(range, e.$.msg, severity); diagnostic.source = "cppcheck"; // If we have a link to documentation, include it @@ -1050,58 +998,23 @@ async function runFullAnalysis( // 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 ?? ''; + // Save line of code at main location + 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 || (mainLocDocument && lLine >= mainLocDocument.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 - ) - ); - } + // Parse Related Information + const relatedInfos: vscode.DiagnosticRelatedInformation[] = await extractRelatedInformation(locations); + if (relatedInfos.length > 0) { diagnostic.relatedInformation = relatedInfos; } - 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 uri = mainLoc.file; + if (diagnostics[uri] === null || diagnostics[uri] === undefined) { + diagnostics[uri] = []; } + diagnostics[uri].push(diagnostic); } for (const uri of Object.keys(diagnostics)) { var newDiagnostics = diagnostics[uri]; diff --git a/src/helpers/xmlParsingHelpers.ts b/src/helpers/xmlParsingHelpers.ts new file mode 100644 index 0000000..7c89950 --- /dev/null +++ b/src/helpers/xmlParsingHelpers.ts @@ -0,0 +1,50 @@ +import * as vscode from 'vscode'; + +interface locationObject { + info: string, + line: string, + col: string, + file: string, +} + +interface XmlAnalysisOutput { + $: locationObject; +} + +export async function extractRelatedInformation(locations : Array) { + 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) { + continue; + } + + var relatedDocument : vscode.TextDocument | undefined; + try { + relatedDocument = await vscode.workspace.openTextDocument(loc.file); + } catch { + // Do nothing + } + + if (relatedDocument && lLine > relatedDocument.lineAt(lLine).text.length) { + continue; + } + + 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 + ) + ); + } + return relatedInfos; +} \ No newline at end of file diff --git a/src/util/path.ts b/src/util/path.ts index 08602e9..068e047 100644 --- a/src/util/path.ts +++ b/src/util/path.ts @@ -2,6 +2,28 @@ import * as path from "path"; import * as os from "os"; import * as vscode from 'vscode'; +const pathVariableArgs = [ + '--project', + '--addon', + '--suppressions-list', + '--include', + '--rule-file', +]; + +export function splitArgsAndResolvePaths(args: string) : Array { + const result = args.split(" ").map((arg) => { + let cleanedArg = arg.replaceAll("\"",""); + const isPathArgument = pathVariableArgs.some(a => cleanedArg.startsWith(a)); + // Some arguments such as addon may be either a path or the name of a built in addon + if (isPathArgument && looksLikePath(cleanedArg)) { + const splitArg = cleanedArg.split('='); + return `${splitArg[0]}=${resolvePath(splitArg[1])}`; + } + return arg; + }); + return result; +} + export function looksLikePath(arg: string): boolean { if ( arg.includes('/') From 5227e0c6f452acd1ad4b777a3f9c95472716d3b6 Mon Sep 17 00:00:00 2001 From: davidramnero Date: Sun, 13 Sep 2026 19:48:34 +0200 Subject: [PATCH 3/6] finished extracting functions from analysis to keep things DRY --- src/extension.ts | 118 +++++++----------- ...ParsingHelpers.ts => diagnosticHelpers.ts} | 17 ++- 2 files changed, 64 insertions(+), 71 deletions(-) rename src/helpers/{xmlParsingHelpers.ts => diagnosticHelpers.ts} (64%) diff --git a/src/extension.ts b/src/extension.ts index 9eba717..731198a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,12 +9,14 @@ import { looksLikePath, resolvePath, findWorkspaceRoot, splitArgsAndResolvePaths import { DiagnosticMetadataStore, diagnosticsUnion } from './util/diagnostics'; import { CodeActionProvider } from './util/codeActions'; import { ProjectFileStore, writeSuppressionToProjectFile } from './util/files'; -import { extractRelatedInformation } from './helpers/xmlParsingHelpers'; +import { extractRelatedInformation, setUpDiagnostic } from './helpers/diagnosticHelpers'; // To keep track of document changes we save hashed versions of their content to this record 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> = {}; +// Create a map for storing all diagnostics, including hidden / filtered diagnostics. Key is file uri as a string +let uriDiagnosticsMap: Map = new Map; // To keep track of hidden warning types we save them to the hiddenTypes set let hiddenTypes: Set = new Set; // Some diagnostics have symbol names associated with them, which we keep track of in diagnosticMetadataStore @@ -54,6 +56,27 @@ const criticalWarningTypes = [ 'unknownMacro' ]; +function mapDiagnostics(diagnostics : Record, sourceDocumentUri? : string) { + 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 sourceDocumentUri is passed we keep track of file relations + if (sourceDocumentUri) { + if (fileRelationMap[uri] === null ||fileRelationMap[uri] === undefined) { + fileRelationMap[uri] = new Set; + } + // NOTE: uri can be the same as sourceDocumentUri + fileRelationMap[uri].add(sourceDocumentUri); + } + } +} + async function processArguments(args : string) { // If user enter arguments as array we parse them into space separated string format if (args.startsWith("[") && args.endsWith("]")) { @@ -99,13 +122,13 @@ function setDiagnosticHiddenStatus(diagnostic : vscode.Diagnostic, hiddenStatus diagnosticMetadataStore.set(diagnostic, newMetaData); } -function applyHiddenTypesFilter(uriDiagnosticsMap : Map) { +function applyHiddenTypesFilter() { hiddenTypes.forEach((warningType) => { - setHiddenStatusBasedOnType(uriDiagnosticsMap, warningType, true); + setHiddenStatusBasedOnType(warningType, true); }); } -function setHiddenStatusBasedOnType(uriDiagnosticsMap : Map, diagnosticCode : string, hidden : boolean) { +function setHiddenStatusBasedOnType(diagnosticCode : string, hidden : boolean) { uriDiagnosticsMap.forEach((diagnostics : readonly vscode.Diagnostic[]) => { diagnostics?.forEach((diagnostic : vscode.Diagnostic) => { var code = diagnostic.code; @@ -161,12 +184,9 @@ export async function activate(context: vscode.ExtensionContext) { 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() { // Make sure the hidden types filter has been applied - applyHiddenTypesFilter(uriDiagnosticsMap); + applyHiddenTypesFilter(); uriDiagnosticsMap.forEach((diagnostics : vscode.Diagnostic[], uri : string) => { const filteredDiagnostics = diagnostics?.filter((diagnostic : vscode.Diagnostic) => { var metadata = diagnosticMetadataStore.get(diagnostic); @@ -275,7 +295,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand( "cppcheck-official.hideWarningType", async (diagnosticCode : string) => { - setHiddenStatusBasedOnType(uriDiagnosticsMap, diagnosticCode, true); + setHiddenStatusBasedOnType(diagnosticCode, true); hiddenTypes.add(diagnosticCode); updateHiddenWarningTypesOption(); filterDisplayedDiagnosticsBasedOnHiddenStatus(); @@ -432,7 +452,6 @@ export async function activate(context: vscode.ExtensionContext) { await runFullAnalysis( commandPath, processedArgs, - uriDiagnosticsMap, selection.value, ); @@ -466,7 +485,7 @@ export async function activate(context: vscode.ExtensionContext) { } hiddenTypes.delete(selection.value); - setHiddenStatusBasedOnType(uriDiagnosticsMap, selection.value, false); + setHiddenStatusBasedOnType(selection.value, false); filterDisplayedDiagnosticsBasedOnHiddenStatus(); updateHiddenWarningTypesOption(); @@ -585,7 +604,6 @@ export async function activate(context: vscode.ExtensionContext) { document, commandPath, processedArgs, - uriDiagnosticsMap, ); // Analysis in runCppcheckOnFileXML populates uriDiagnosticsMap with all warnings, regardless of min severity filter. @@ -661,7 +679,6 @@ async function runCppcheckOnFileXML( document: vscode.TextDocument, commandPath: string, processedArgs: string, - uriDiagnosticsMap: Map, ): Promise { checksRunning = true; updateProgressIndicator(); @@ -784,23 +801,15 @@ async function runCppcheckOnFileXML( 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 ?? ''; + // Set up a vscode.diagnostic object + const colEnd = mainLocDocument ? mainLocDocument.lineAt(line).text.length : col; + const diagnostic = setUpDiagnostic(line, col, line, colEnd, e.$.id, e.$.msg, severity); + diagnosticMetadataStore.set(diagnostic, { symbolName, mainLocLine, hidden: false }); // Parse Related Information @@ -824,15 +833,9 @@ async function runCppcheckOnFileXML( } 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 (mainLocDocument) { + // Proceed if we have the document + const uri = mainLocDocument.uri.toString(); if (diagnostics[uri] === null || diagnostics[uri] === undefined) { diagnostics[uri] = []; } @@ -840,21 +843,10 @@ async function runCppcheckOnFileXML( } } } + // Map diagnostics to the uriDiagnosticsMap 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); - } + mapDiagnostics(diagnostics, sourceDocumentUri); + resolve(); }); @@ -873,7 +865,6 @@ async function runCppcheckOnFileXML( async function runFullAnalysis( commandPath: string, processedArgs: string, - uriDiagnosticsMap: Map, threadsOption: string, ): Promise { if (!processedArgs.includes("--project=")) { @@ -984,22 +975,14 @@ async function runFullAnalysis( continue; } - const range = new vscode.Range(line, col, line, mainLocDocument.lineAt(line).text.length); - 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 const mainLocLine = mainLocDocument.lineAt(line).text; + + // Set up a vscode.diagnostic object + const colEnd = mainLocDocument ? mainLocDocument.lineAt(line).text.length : col; + const diagnostic = setUpDiagnostic(line, col, line, colEnd, e.$.id, e.$.msg, severity); diagnosticMetadataStore.set(diagnostic, { symbolName, mainLocLine, hidden: false }); @@ -1016,15 +999,10 @@ async function runFullAnalysis( } diagnostics[uri].push(diagnostic); } - 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); - } + + // Map diagnostics to the uriDiagnosticsMap + mapDiagnostics(diagnostics); + resolve(); }); }); diff --git a/src/helpers/xmlParsingHelpers.ts b/src/helpers/diagnosticHelpers.ts similarity index 64% rename from src/helpers/xmlParsingHelpers.ts rename to src/helpers/diagnosticHelpers.ts index 7c89950..ef0425b 100644 --- a/src/helpers/xmlParsingHelpers.ts +++ b/src/helpers/diagnosticHelpers.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; - +import { documentationLinkMap, getPremiumCertLink } from '../util/documentation'; interface locationObject { info: string, line: string, @@ -11,6 +11,21 @@ interface XmlAnalysisOutput { $: locationObject; } +export function setUpDiagnostic(lineStart : number, colStart : number, lineEnd : number, colEnd : number, warningId : string, warningMessage : string, severity : vscode.DiagnosticSeverity) { + const range = new vscode.Range(lineStart, colStart, lineEnd, colEnd); + const diagnostic = new vscode.Diagnostic(range, warningMessage, severity); + diagnostic.source = "cppcheck"; + // If we have a link to documentation, include it + diagnostic.code = documentationLinkMap[warningId] ? { + value: warningId, + target: vscode.Uri.parse(documentationLinkMap[warningId]) + } : getPremiumCertLink(warningId) ? { + value: warningId, + target: vscode.Uri.parse(getPremiumCertLink(warningId)) + } : warningId; + return diagnostic; +} + export async function extractRelatedInformation(locations : Array) { const relatedInfos: vscode.DiagnosticRelatedInformation[] = []; for (let i = 1; i <= locations.length; i++) { From 4a32b5d2a4936522c104ca0f93f5aabd9fc10c42 Mon Sep 17 00:00:00 2001 From: davidramnero Date: Sun, 13 Sep 2026 20:13:45 +0200 Subject: [PATCH 4/6] fixed hide warning being broken --- src/extension.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 731198a..7349756 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,9 +3,8 @@ import * as cp from 'child_process'; import * as xml2js from 'xml2js'; import * as crypto from 'crypto'; -import { documentationLinkMap, getPremiumCertLink } from './util/documentation'; import { runCommand } from './util/scripts'; -import { looksLikePath, resolvePath, findWorkspaceRoot, splitArgsAndResolvePaths } from './util/path'; +import { resolvePath, findWorkspaceRoot, splitArgsAndResolvePaths } from './util/path'; import { DiagnosticMetadataStore, diagnosticsUnion } from './util/diagnostics'; import { CodeActionProvider } from './util/codeActions'; import { ProjectFileStore, writeSuppressionToProjectFile } from './util/files'; @@ -281,11 +280,9 @@ export async function activate(context: vscode.ExtensionContext) { } if (code === diagnosticCode && diagnostic.range.isEqual(range)) { setDiagnosticHiddenStatus(diagnostic, true); - setDiagnosticHiddenStatus(diagnostic, true); } }); filterDisplayedDiagnosticsBasedOnHiddenStatus(); - filterDisplayedDiagnosticsBasedOnHiddenStatus(); } ) ); @@ -993,7 +990,7 @@ async function runFullAnalysis( diagnostic.relatedInformation = relatedInfos; } - const uri = mainLoc.file; + const uri = mainLocDocument.uri.toString(); if (diagnostics[uri] === null || diagnostics[uri] === undefined) { diagnostics[uri] = []; } From 95bed40c4bdf79b7a3ce277f4f7a547db214b7e4 Mon Sep 17 00:00:00 2001 From: davidramnero Date: Sun, 13 Sep 2026 20:25:13 +0200 Subject: [PATCH 5/6] clear file relations when full analysis is ran --- src/extension.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 7349756..16292fd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -176,6 +176,12 @@ function getDocumentSha1(document: vscode.TextDocument): string { .digest('hex'); } +function clearFileRelationMap() { + for (const fileUri of Object.keys(fileRelationMap)) { + fileRelationMap[fileUri].clear; + } +} + // 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) { @@ -874,6 +880,9 @@ async function runFullAnalysis( // Clear existing diagnostics for all files uriDiagnosticsMap.clear(); + // Clear file relation map when we run full analysis + clearFileRelationMap(); + // We always call cppcheck with severity level info, and then filter warnings when displaying them const minSevNum = SeverityNumber.Info; From 1550b3de005b31be06398fcbb2e482b0ab9d519d Mon Sep 17 00:00:00 2001 From: davidramnero Date: Mon, 14 Sep 2026 16:05:28 +0200 Subject: [PATCH 6/6] hide full analysis option if project file not present --- src/extension.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 16292fd..09ff2ed 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -151,7 +151,12 @@ function updateProgressIndicator(): void { } else { cppcheckProgressIndicator.hide(); severityOption.show(); - fullAnalysisStatusBarItem.show(); + // If a project file exists, show full analysis status bar item + if (projectFileStore.getUri()) { + fullAnalysisStatusBarItem.show(); + } else { + fullAnalysisStatusBarItem.hide(); + } } } @@ -532,7 +537,10 @@ export async function activate(context: vscode.ExtensionContext) { fullAnalysisStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 8); fullAnalysisStatusBarItem.command = "cppcheck-official.runFullAnalysis"; fullAnalysisStatusBarItem.text = `$(play) Full Analysis`; - fullAnalysisStatusBarItem.show(); + // If a project file exists, show full analysis status bar item + if (projectFileStore.getUri()) { + fullAnalysisStatusBarItem.show(); + } context.subscriptions.push(fullAnalysisStatusBarItem); function clearDiagnosticForDoc(doc: vscode.TextDocument): void { @@ -760,7 +768,7 @@ async function runCppcheckOnFileXML( return; } - const errors = result.results?.errors?.[0]?.error || []; + const errors = result?.results?.errors?.[0]?.error || []; const diagnostics: Record = {}; for (const e of errors) { const isCriticalError = criticalWarningTypes.includes(e.$.id); @@ -817,7 +825,6 @@ async function runCppcheckOnFileXML( // Parse Related Information const relatedInfos: vscode.DiagnosticRelatedInformation[] = await extractRelatedInformation(locations); - if (relatedInfos.length > 0) { diagnostic.relatedInformation = relatedInfos; } @@ -871,7 +878,7 @@ async function runFullAnalysis( threadsOption: string, ): Promise { if (!processedArgs.includes("--project=")) { - throw new Error("full analysis called without specified project file!"); + throw new Error("Full analysis called without specified project file!"); } checksRunning = true; @@ -940,7 +947,7 @@ async function runFullAnalysis( return; } - const errors = result.results?.errors?.[0]?.error || []; + const errors = result?.results?.errors?.[0]?.error || []; const diagnostics: Record = {}; for (const e of errors) { const isCriticalError = criticalWarningTypes.includes(e.$.id);