Skip to content

Commit 066ef56

Browse files
committed
Add analysis status info
Signed-off-by: Magyarimiki <249292183+Magyarimiki@users.noreply.github.com>
1 parent b4c595c commit 066ef56

5 files changed

Lines changed: 224 additions & 7 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
"command": "codechecker.executor.analyzeProject",
6767
"title": "CodeChecker: Analyze entire project"
6868
},
69+
{
70+
"command": "codechecker.executor.getFileAnalysisStatus",
71+
"title": "CodeChecker: Get file analysis status"
72+
},
6973
{
7074
"command": "codechecker.executor.showCommandLine",
7175
"title": "CodeChecker: Show full CodeChecker analyze command line"

src/backend/executor/bridge.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {
44
EventEmitter,
55
ExtensionContext,
66
FileSystemWatcher,
7+
ThemeColor,
8+
ThemeIcon,
9+
TreeItemCollapsibleState,
710
Uri,
811
commands,
912
window,
@@ -20,6 +23,8 @@ import {
2023
import { ProcessStatusType, ProcessType, ScheduledProcess } from '.';
2124
import { NotificationType } from '../../editor/notifications';
2225
import { Editor } from '../../editor';
26+
import { SidebarContainer } from '../../sidebar';
27+
import { ReportTreeItem } from '../../sidebar/views';
2328

2429
// Structure:
2530
// CodeChecker analyzer version: \n {"base_package_version": "M.m.p", ...}
@@ -88,6 +93,9 @@ export class ExecutorBridge implements Disposable {
8893
ctx.subscriptions.push(
8994
commands.registerCommand('codechecker.executor.analyzeProject', this.analyzeProject, this)
9095
);
96+
ctx.subscriptions.push(
97+
commands.registerCommand('codechecker.executor.getFileAnalysisStatus', this.getFileAnalysisStatus, this)
98+
);
9199
ctx.subscriptions.push(
92100
commands.registerCommand('codechecker.executor.runCodeCheckerLog', this.runLogDefaultCommand, this)
93101
);
@@ -425,6 +433,127 @@ export class ExecutorBridge implements Disposable {
425433
ExtensionApi.executorManager.addToQueue(process, 'replace');
426434
}
427435

436+
public async getFileAnalysisStatus() {
437+
if (!await this.checkVersion()) {
438+
return;
439+
}
440+
if (this.checkedVersion < [ 6, 27, 0 ]) {
441+
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
442+
statusNode?.setLabelAndIcon('Status report requires CodeChecker 6.27.0 or higher.');
443+
return;
444+
}
445+
446+
const ccPath = getConfigAndReplaceVariables('codechecker.executor', 'executablePath') || 'CodeChecker';
447+
const reportsFolder = this.getReportsFolder();
448+
const fileUri = window.activeTextEditor?.document.uri;
449+
const fsPath = fileUri?.fsPath;
450+
451+
const statusArgs = [
452+
'parse',
453+
'--status',
454+
'--detailed',
455+
'-e',
456+
'json',
457+
reportsFolder,
458+
'--file',
459+
fsPath ?? ''
460+
];
461+
462+
const process = new ScheduledProcess(ccPath, statusArgs, { processType: ProcessType.status });
463+
464+
let processOutput = '';
465+
process.processStdout((output) => processOutput += output);
466+
process.processStatusChange(async status => {
467+
switch (status.type) {
468+
case ProcessStatusType.errored:
469+
break;
470+
case ProcessStatusType.finished:
471+
interface Analyzer {
472+
summary: {
473+
// eslint-disable-next-line @typescript-eslint/naming-convention
474+
'up-to-date': number,
475+
failed: number,
476+
missing: number,
477+
outdated: number
478+
}
479+
// eslint-disable-next-line @typescript-eslint/naming-convention
480+
'up-to-date': [],
481+
failed: [],
482+
missing: [],
483+
outdated: [],
484+
}
485+
486+
const analyzers = JSON.parse(processOutput);
487+
let uptodate = 0;
488+
let outdated = 0;
489+
let missing = 0;
490+
let failed = 0;
491+
const analyzerStatuses: ReportTreeItem[] = [];
492+
if (analyzers.analyzers) {
493+
for (const [ analyzer, status ] of Object.entries(analyzers.analyzers)) {
494+
const s = status as Analyzer;
495+
let iconname = '';
496+
if (s.summary['up-to-date'] > 0) {
497+
uptodate++;
498+
iconname = 'check';
499+
}
500+
if (s.summary.outdated > 0) {
501+
outdated++;
502+
iconname = 'clock';
503+
}
504+
if (s.summary.failed > 0) {
505+
failed++;
506+
iconname = 'error';
507+
}
508+
if (s.summary.missing > 0) {
509+
missing++;
510+
iconname = 'question';
511+
}
512+
let existingStatusNode = SidebarContainer.reportsView.getNodeById(analyzer);
513+
if (!existingStatusNode) {
514+
existingStatusNode = new ReportTreeItem(analyzer, analyzer,
515+
new ThemeIcon(iconname), []);
516+
const report = SidebarContainer.reportsView.getNodeById('statusItem');
517+
existingStatusNode.parent = report;
518+
SidebarContainer.reportsView.addDynamicNode(analyzer, existingStatusNode);
519+
analyzerStatuses.push(existingStatusNode);
520+
} else {
521+
existingStatusNode.iconPath = new ThemeIcon(iconname);
522+
}
523+
}
524+
}
525+
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
526+
if (!statusNode) {
527+
return;
528+
}
529+
if (uptodate === 0 && outdated === 0 && missing === 0 && failed === 0) {
530+
statusNode?.setLabelAndIcon('Analysis info is unavailable',
531+
new ThemeIcon('question', new ThemeColor('charts.red')));
532+
} else if (failed > 0) {
533+
statusNode?.setLabelAndIcon('Analysis failed',
534+
new ThemeIcon('error', new ThemeColor('charts.red')));
535+
} else if (outdated === 0 && failed === 0) {
536+
statusNode?.setLabelAndIcon('Analysis is up-to-date',
537+
new ThemeIcon('check', new ThemeColor('charts.green')));
538+
} else {
539+
statusNode?.setLabelAndIcon('Analysis is outdated',
540+
new ThemeIcon('clock', new ThemeColor('charts.red')));
541+
}
542+
if (analyzerStatuses.length > 0) {
543+
statusNode.setChildren(analyzerStatuses);
544+
statusNode.collapsibleState = TreeItemCollapsibleState.Collapsed;
545+
}
546+
SidebarContainer.reportsView.refreshNode();
547+
statusNode.collapse();
548+
break;
549+
default:
550+
break;
551+
}
552+
});
553+
554+
ExtensionApi.executorManager.addToQueue(process, 'replace');
555+
}
556+
428557
public async runLogCustomCommand(buildCommand?: string) {
429558
if (buildCommand === undefined) {
430559
const executorConfig = workspace.getConfiguration('codechecker.executor');

src/backend/executor/process.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export enum ProcessType {
2626
checkers = 'CodeChecker checkers',
2727
log = 'CodeChecker log',
2828
parse = 'CodeChecker parse',
29+
status = 'CodeChecker parse --status',
2930
version = 'CodeChecker analyzer-version',
3031
other = 'Other process',
3132
}

src/editor/notifications.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export class NotificationHandler {
168168
});
169169
this.activeNotifications.delete(process.commandLine);
170170

171+
SidebarContainer.reportsView.updateStatus();
171172
break;
172173
}
173174
case ProcessStatusType.warning: {

src/sidebar/views/reports.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,41 @@ import {
1818
} from 'vscode';
1919
import { ExtensionApi } from '../../backend';
2020
import { DiagnosticReport } from '../../backend/types';
21+
import { SidebarContainer } from '../sidebar_container';
2122

2223
export class ReportTreeItem extends TreeItem {
2324
parent: ReportTreeItem | undefined;
2425

2526
constructor(
2627
public readonly _id: string,
27-
public readonly label: string | TreeItemLabel,
28-
public readonly iconPath: ThemeIcon,
29-
public readonly children?: ReportTreeItem[] | undefined
28+
label: string | TreeItemLabel,
29+
iconPath: ThemeIcon,
30+
public children?: ReportTreeItem[]
3031
) {
31-
super(label, children?.length ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None);
32+
super(label, (children?.length) ?
33+
TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None);
3234
this._id = _id;
3335
this.label = label;
34-
this.iconPath = this.iconPath;
36+
this.iconPath = iconPath;
3537
this.children = children;
3638

3739
// Set parent for children automatically.
3840
this.children?.forEach(c => c.parent = this);
3941
}
4042

43+
setLabelAndIcon(label?: string, iconPath?: ThemeIcon) {
44+
if (label) {
45+
this.label = label;
46+
}
47+
if (iconPath) {
48+
this.iconPath = iconPath;
49+
}
50+
}
51+
52+
setChildren(children: ReportTreeItem[] | undefined) {
53+
this.children = children;
54+
}
55+
4156
// This function can be used to set ID attribute of a tree item and all the children of it based on the parent id.
4257
setId() {
4358
this.id = `${this.parent?.id ?? 'root'}_${this._id}`;
@@ -55,6 +70,12 @@ export class ReportTreeItem extends TreeItem {
5570
}
5671
}
5772

73+
collapse() {
74+
if (this.collapsibleState === TreeItemCollapsibleState.Expanded) {
75+
this.collapsibleState = TreeItemCollapsibleState.Collapsed;
76+
}
77+
}
78+
5879
traverse(cb: (item: ReportTreeItem) => void) {
5980
cb(this);
6081
this.children?.forEach(c => c.traverse(cb));
@@ -84,16 +105,26 @@ const severityOrder: { [key: string]: number } = {
84105

85106
export class ReportsView implements TreeDataProvider<ReportTreeItem> {
86107
protected currentFile?: Uri;
108+
protected isDirty: boolean = false;
87109
protected currentEntryList?: DiagnosticReport[];
88110

89111
protected tree?: TreeView<ReportTreeItem>;
90112
// Contains [fullpath => item] entries
91113
private treeItems: Map<string, ReportTreeItem> = new Map();
92114
private selectedTreeItems: ReportTreeItem[] = [];
115+
private dynamicTreeItems: Map<string, ReportTreeItem> = new Map();
116+
private rootItems: ReportTreeItem[] = [];
93117

94118
constructor(ctx: ExtensionContext) {
95119
ctx.subscriptions.push(this._onDidChangeTreeData = new EventEmitter());
96-
window.onDidChangeActiveTextEditor(this.refreshBugList, this, ctx.subscriptions);
120+
window.onDidChangeActiveTextEditor(editor => {
121+
// event is called twice. Ignore deactivation of the previous editor.
122+
if (editor === undefined) {
123+
return;
124+
}
125+
// this.refreshBugList();
126+
this.updateStatus();
127+
}, this, ctx.subscriptions);
97128

98129
ExtensionApi.diagnostics.diagnosticsUpdated(() => {
99130
// FIXME: fired twice when a file is opened freshly.
@@ -113,6 +144,23 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
113144
));
114145

115146
this.init();
147+
this.updateStatus();
148+
}
149+
150+
public addDynamicNode(id: string, node: ReportTreeItem) {
151+
this.dynamicTreeItems.set(id, node);
152+
}
153+
154+
public getNodeById(id: string): ReportTreeItem | undefined {
155+
return this.dynamicTreeItems.get(id);
156+
}
157+
158+
public getAllNodes(): Map<string, ReportTreeItem> {
159+
return this.treeItems;
160+
}
161+
162+
public refreshNode() {
163+
this._onDidChangeTreeData.fire();
116164
}
117165

118166
protected init() {
@@ -121,6 +169,12 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
121169
this.tree?.onDidChangeSelection((item: TreeViewSelectionChangeEvent<ReportTreeItem>) => {
122170
this.selectedTreeItems = item.selection;
123171
});
172+
173+
workspace.onDidChangeTextDocument(event => {
174+
if (event?.document === window.activeTextEditor?.document) {
175+
this.updateStatus();
176+
}
177+
});
124178
}
125179

126180
private _onDidChangeTreeData: EventEmitter<void>;
@@ -159,6 +213,24 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
159213
this._onDidChangeTreeData.fire();
160214
}
161215

216+
updateStatus() {
217+
if (window?.activeTextEditor?.document?.isDirty) {
218+
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
219+
if (statusNode) {
220+
statusNode?.setLabelAndIcon('Outdated (file is modified in the editor)', new ThemeIcon('edit'));
221+
if (statusNode.children) {
222+
statusNode.children.forEach(child => {
223+
child.setLabelAndIcon(undefined, new ThemeIcon('edit'));
224+
});
225+
}
226+
}
227+
} else {
228+
const executorBridge = ExtensionApi.executorBridge;
229+
executorBridge.getFileAnalysisStatus();
230+
}
231+
this._onDidChangeTreeData.fire();
232+
}
233+
162234
revealSelectedItems() {
163235
const selectedIds = new Set(this.selectedTreeItems.map(item => item.id));
164236
this.treeItems.forEach(root => root.traverse(item => {
@@ -301,7 +373,10 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
301373
// Get root level items.
302374
getRootItems(): ReportTreeItem[] | undefined {
303375
if (!this.currentEntryList?.length) {
304-
return [new ReportTreeItem('noReportsFound', 'No reports found', new ThemeIcon('pass'))];
376+
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
377+
statusNode?.setLabelAndIcon('Not in compilation database',
378+
new ThemeIcon('question', new ThemeColor('charts.orange')));
379+
return statusNode ? [ statusNode ] : undefined;
305380
}
306381

307382
const severityItems: { [key: string]: TreeDiagnosticReport[] } = {};
@@ -315,6 +390,13 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
315390

316391
const rootItems: ReportTreeItem[] = [];
317392

393+
let status = SidebarContainer.reportsView.getNodeById('statusItem');
394+
if (!status) {
395+
status = new ReportTreeItem('statusItem', 'Status', new ThemeIcon('warning'));
396+
SidebarContainer.reportsView.addDynamicNode('statusItem', status);
397+
}
398+
rootItems.push(status);
399+
318400
rootItems.push(...Object.entries(severityItems)
319401
.sort(([severityA]: [string, TreeDiagnosticReport[]], [severityB]: [string, TreeDiagnosticReport[]]) =>
320402
severityOrder[severityA] - severityOrder[severityB])

0 commit comments

Comments
 (0)