Skip to content

Commit f18834f

Browse files
committed
feature / full project analysis
1 parent c14ef24 commit f18834f

1 file changed

Lines changed: 305 additions & 16 deletions

File tree

‎src/extension.ts‎

Lines changed: 305 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let previewedDocument: vscode.TextDocument | undefined;
2626
let cppcheckProgressIndicator: vscode.StatusBarItem;
2727
let severityOption: vscode.StatusBarItem;
2828
let hiddenTypesOption: vscode.StatusBarItem;
29+
let fullAnalysisStatusBarItem: vscode.StatusBarItem;
2930
let checksRunning = false;
3031
let usesPremiumCppcheck = false;
3132

@@ -60,6 +61,26 @@ const pathVariableArgs = [
6061
'--rule-file',
6162
];
6263

64+
async function processArguments(args : string) {
65+
// If user enter arguments as array we parse them into space separated string format
66+
if (args.startsWith("[") && args.endsWith("]")) {
67+
args = args.replaceAll("[", "").replaceAll("]", "").replaceAll(",", " ");
68+
}
69+
70+
var processedArgs = '';
71+
// If argument field contains command to run script we do so here
72+
if (args.includes('@(')) {
73+
const scriptCommand = args.split("@(")[1].split(")")[0];
74+
const scriptOutput = await runCommand(scriptCommand);
75+
// We expect that the script output that is to be used as arguments will be wrapped with ${}
76+
const scriptOutputTrimmed = scriptOutput.split("@(")[1].split(")")[0];
77+
processedArgs = args.split("@(")[0] + scriptOutputTrimmed + args.split(")")?.[1];
78+
} else {
79+
processedArgs = args;
80+
}
81+
return processedArgs;
82+
}
83+
6384
function parseSeverity(str: string): vscode.DiagnosticSeverity {
6485
const lower = str.toLowerCase();
6586
if (lower.includes("error")) {
@@ -111,9 +132,11 @@ function updateProgressIndicator(): void {
111132
cppcheckProgressIndicator.show();
112133
// To avoid crowding status bar we alternate between progress indicator and severity option item
113134
severityOption.hide();
135+
fullAnalysisStatusBarItem.hide();
114136
} else {
115137
cppcheckProgressIndicator.hide();
116138
severityOption.show();
139+
fullAnalysisStatusBarItem.show();
117140
}
118141
}
119142

@@ -361,6 +384,69 @@ export async function activate(context: vscode.ExtensionContext) {
361384
}
362385
)
363386
);
387+
388+
context.subscriptions.push(
389+
vscode.commands.registerCommand(
390+
"cppcheck-official.runFullAnalysis",
391+
async () => {
392+
393+
const selection = await vscode.window.showQuickPick(
394+
[
395+
{
396+
label: "1 thread",
397+
description: "No parallel threads",
398+
value: "-j1"
399+
},
400+
{
401+
label: "2 threads",
402+
description: "2 parallel threads",
403+
value: "-j2"
404+
},
405+
{
406+
label: "4 threads",
407+
description: "4 parallel threads",
408+
value: "-j4"
409+
}
410+
],
411+
{
412+
title: "Select how many threads to run in parallel for full analysis"
413+
}
414+
);
415+
if (!selection) {
416+
return;
417+
}
418+
419+
const config = vscode.workspace.getConfiguration();
420+
const userPath = config.get<string>("cppcheck-official.path")?.trim() || "";
421+
const commandPath = userPath ? resolvePath(userPath) : "cppcheck";
422+
423+
var args = config.get<string>("cppcheck-official.arguments", "");
424+
const processedArgs = await processArguments(args);
425+
426+
// Check if cppcheck is available
427+
cp.exec(`"${commandPath}" --version`, (error, stdout) => {
428+
if (error) {
429+
vscode.window.showErrorMessage(
430+
`Cppcheck: Could not find or run '${commandPath}'. ` +
431+
`Please install cppcheck or set 'cppcheck-official.path' correctly.`
432+
);
433+
return;
434+
}
435+
usesPremiumCppcheck = stdout.toLowerCase().includes('premium');
436+
});
437+
438+
console.log('processedArgs', processedArgs);
439+
440+
// Run
441+
await runFullAnalysis(
442+
commandPath,
443+
processedArgs,
444+
uriDiagnosticsMap,
445+
selection.value,
446+
);
447+
}
448+
)
449+
);
364450

365451
context.subscriptions.push(
366452
vscode.commands.registerCommand(
@@ -423,6 +509,12 @@ export async function activate(context: vscode.ExtensionContext) {
423509
// Call update function once at setup to set the UI text to the settings current value
424510
updateHiddenWarningTypesOption();
425511

512+
// Full analysis status bar item
513+
fullAnalysisStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 8);
514+
fullAnalysisStatusBarItem.command = "cppcheck-official.runFullAnalysis";
515+
fullAnalysisStatusBarItem.text = `$(play) Full Analysis`;
516+
fullAnalysisStatusBarItem.show();
517+
context.subscriptions.push(fullAnalysisStatusBarItem);
426518

427519
function clearDiagnosticForDoc(doc: vscode.TextDocument): void {
428520
// 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) {
472564
const commandPath = userPath ? resolvePath(userPath) : "cppcheck";
473565

474566
var args = config.get<string>("cppcheck-official.arguments", "");
475-
// If user enter arguments as array we parse them into space separated string format
476-
if (args.startsWith("[") && args.endsWith("]")) {
477-
args = args.replaceAll("[", "").replaceAll("]", "").replaceAll(",", " ");
478-
}
479-
480-
var processedArgs = '';
481-
// If argument field contains command to run script we do so here
482-
if (args.includes('@(')) {
483-
const scriptCommand = args.split("@(")[1].split(")")[0];
484-
const scriptOutput = await runCommand(scriptCommand);
485-
// We expect that the script output that is to be used as arguments will be wrapped with ${}
486-
const scriptOutputTrimmed = scriptOutput.split("@(")[1].split(")")[0];
487-
processedArgs = args.split("@(")[0] + scriptOutputTrimmed + args.split(")")?.[1];
488-
} else {
489-
processedArgs = args;
490-
}
567+
const processedArgs = await processArguments(args);
491568

492569
// If disabled, clear any existing diagnostics for this doc.
493570
if (!isEnabled) {
@@ -832,5 +909,217 @@ async function runCppcheckOnFileXML(
832909
updateProgressIndicator();
833910
}
834911

912+
async function runFullAnalysis(
913+
commandPath: string,
914+
processedArgs: string,
915+
uriDiagnosticsMap: Map<string, vscode.Diagnostic[]>,
916+
threadsOption: string,
917+
): Promise<void> {
918+
if (!processedArgs.includes("--project=")) {
919+
throw new Error("full analysis called without specified project file!");
920+
}
921+
922+
checksRunning = true;
923+
updateProgressIndicator();
924+
925+
// Clear existing diagnostics for all files
926+
uriDiagnosticsMap.clear();
927+
928+
// We always call cppcheck with severity level info, and then filter warnings when displaying them
929+
const minSevNum = SeverityNumber.Info;
930+
931+
// Resolve paths for arguments where applicable
932+
const argsParsed = processedArgs.split(" ").map((arg) => {
933+
let cleanedArg = arg.replaceAll("\"","");
934+
const isPathArgument = pathVariableArgs.some(a => cleanedArg.startsWith(a));
935+
// Some arguments such as addon may be either a path or the name of a built in addon
936+
if (isPathArgument && looksLikePath(cleanedArg)) {
937+
const splitArg = cleanedArg.split('=');
938+
return `${splitArg[0]}=${resolvePath(splitArg[1])}`;
939+
}
940+
return arg;
941+
});
942+
943+
let usingProjectFile = true;
944+
var projectFilePath = processedArgs.split('--project=')[1].split(' ')[0];
945+
projectFileStore.clear();
946+
projectFileStore.setUri(vscode.Uri.file(projectFilePath));
947+
948+
const args = [
949+
'--enable=all',
950+
'--inline-suppr',
951+
'--xml',
952+
threadsOption,
953+
...argsParsed,
954+
].filter(Boolean);
955+
956+
if (usesPremiumCppcheck) {
957+
args.push('--premium=safety-off');
958+
}
959+
960+
let proc;
961+
const cwd = findWorkspaceRoot();
962+
proc = cp.spawn(commandPath, args, {
963+
cwd,
964+
});
965+
966+
await new Promise<void>((resolve, reject) => {
967+
// if spawn fails (e.g. ENOENT or permission denied)
968+
proc.on("error", (err) => {
969+
console.error("Failed to start cppcheck:", err);
970+
vscode.window.showErrorMessage(`Cppcheck failed to start: ${err.message}`);
971+
reject(err);
972+
});
973+
974+
let xmlOutput = "";
975+
let out = "";
976+
proc.stderr.on("data", d => xmlOutput += d.toString());
977+
proc.stdout.on("data", d => out += d.toString());
978+
proc.on("close", code => {
979+
if (code && code > 0) {
980+
// Non-zero code means an error has occured
981+
let errorMessage = `Cppcheck failed with code ${code} (unknown error)`;
982+
if (out.trim().length > 0) {
983+
errorMessage = out.trim();
984+
}
985+
errorMessage = `${errorMessage}, Command: ${commandPath} ${args.join(' ')}`;
986+
vscode.window.showErrorMessage(errorMessage);
987+
}
988+
const parser = new xml2js.Parser({ explicitArray: true });
989+
parser.parseString(xmlOutput, async (err, result) => {
990+
if (err) {
991+
console.error("XML parse error:", err);
992+
return;
993+
}
994+
995+
const errors = result.results?.errors?.[0]?.error || [];
996+
const diagnostics: Record<string, vscode.Diagnostic[]> = {};
997+
for (const e of errors) {
998+
const isCriticalError = criticalWarningTypes.includes(e.$.id);
999+
const locations = e.location || [];
1000+
if (!locations.length) {
1001+
continue;
1002+
}
1003+
1004+
const mainLoc = locations[locations.length - 1].$;
1005+
// If main location is not current file, we are not using a project file and warning is not critical then skip displaying warning
1006+
if (!isCriticalError && usingProjectFile) {
1007+
continue;
1008+
}
1009+
1010+
let mainLocDocument : vscode.TextDocument | undefined;
1011+
try {
1012+
mainLocDocument = await vscode.workspace.openTextDocument(mainLoc.file);
1013+
} catch {
1014+
// do nothing
1015+
}
1016+
1017+
// Cppcheck line number is 1-indexed, while VS Code uses 0-indexing
1018+
let line = Number(mainLoc.line) - 1;
1019+
// Invalid line number usually means non-analysis output
1020+
if (isNaN(line) || line < 0 || (mainLocDocument && line >= mainLocDocument.lineCount)) {
1021+
if (isCriticalError) {
1022+
line = 0;
1023+
} else {
1024+
continue;
1025+
}
1026+
}
1027+
1028+
// Cppcheck col number is 1-indexed, while VS Code uses 0-indexing
1029+
let col = Number(mainLoc.column) - 1;
1030+
if (isNaN(col) || col < 0 || !mainLocDocument || col > mainLocDocument.lineAt(line).text.length) {
1031+
col = 0;
1032+
}
1033+
1034+
const severity = parseSeverity(e.$.severity);
1035+
if (!isCriticalError && severityToNumber(severity) < minSevNum) {
1036+
continue;
1037+
}
1038+
1039+
const range = new vscode.Range(line, col, line, mainLocDocument ? mainLocDocument.lineAt(line).text.length : col);
1040+
const diagnostic = new vscode.Diagnostic(range, e.$.msg, severity);
1041+
diagnostic.source = "cppcheck";
1042+
// If we have a link to documentation, include it
1043+
diagnostic.code = documentationLinkMap[e.$.id] ? {
1044+
value: e.$.id,
1045+
target: vscode.Uri.parse(documentationLinkMap[e.$.id])
1046+
} : getPremiumCertLink(e.$.id) ? {
1047+
value: e.$.id,
1048+
target: vscode.Uri.parse(getPremiumCertLink(e.$.id))
1049+
} : e.$.id;
1050+
1051+
// If warning has a symbol we keep track of it
1052+
const symbolName = e.symbol?.[0] ?? '';
1053+
// Save line of code at main location if we can access it
1054+
const mainLocLine = mainLocDocument?.lineAt(line)?.text ?? '';
1055+
1056+
diagnosticMetadataStore.set(diagnostic, { symbolName, mainLocLine, hidden: false });
1057+
1058+
// Related Information
1059+
const relatedInfos: vscode.DiagnosticRelatedInformation[] = [];
1060+
for (let i = 1; i <= locations.length; i++) {
1061+
// Related information is ordered in reverse in XML object
1062+
const loc = locations[locations.length - i].$;
1063+
const msg = loc.info;
1064+
const lLine = Number(loc.line) - 1;
1065+
const lCol = Number(loc.col) - 1;
1066+
1067+
if (msg === null || msg === undefined || isNaN(lLine) || lLine < 0 || (mainLocDocument && lLine >= mainLocDocument.lineCount)) {
1068+
continue;
1069+
}
1070+
1071+
var relatedDocument : vscode.TextDocument | undefined;
1072+
try {
1073+
relatedDocument = await vscode.workspace.openTextDocument(loc.file);
1074+
} catch {
1075+
// Do nothing
1076+
}
1077+
const relatedRange = new vscode.Range(
1078+
lLine, lCol,
1079+
lLine, relatedDocument ? relatedDocument.lineAt(lLine).text.length : lCol
1080+
);
1081+
relatedInfos.push(
1082+
new vscode.DiagnosticRelatedInformation(
1083+
new vscode.Location(relatedDocument ? relatedDocument.uri : vscode.Uri.file(''), relatedRange),
1084+
msg
1085+
)
1086+
);
1087+
}
1088+
if (relatedInfos.length > 0) {
1089+
diagnostic.relatedInformation = relatedInfos;
1090+
}
1091+
var relatedDocument : vscode.TextDocument | undefined;
1092+
try {
1093+
relatedDocument = await vscode.workspace.openTextDocument(mainLoc.file);
1094+
} catch {
1095+
// Do nothing
1096+
}
1097+
if (relatedDocument) {
1098+
// Proceed if we are able to open the document
1099+
const uri = relatedDocument.uri.toString();
1100+
if (diagnostics[uri] === null || diagnostics[uri] === undefined) {
1101+
diagnostics[uri] = [];
1102+
}
1103+
diagnostics[uri].push(diagnostic);
1104+
}
1105+
}
1106+
for (const uri of Object.keys(diagnostics)) {
1107+
var newDiagnostics = diagnostics[uri];
1108+
// If file has existing diagnostics from analyzing other files we do not want to overwrite those
1109+
const existingDiagnostics = uriDiagnosticsMap.get(uri);
1110+
if (existingDiagnostics) {
1111+
newDiagnostics = diagnosticsUnion(newDiagnostics, existingDiagnostics.flat());
1112+
}
1113+
uriDiagnosticsMap.set(uri, newDiagnostics);
1114+
}
1115+
resolve();
1116+
});
1117+
});
1118+
});
1119+
1120+
checksRunning = false;
1121+
updateProgressIndicator();
1122+
}
1123+
8351124
// This method is called when your extension is deactivated
8361125
export function deactivate() {}

0 commit comments

Comments
 (0)