Skip to content

Commit 814c2e4

Browse files
authored
Merge pull request #163 from Magyarimiki/feature/status-info
Add analysis status info
2 parents 134cf5e + 95953f2 commit 814c2e4

5 files changed

Lines changed: 221 additions & 9 deletions

File tree

src/backend/executor/bridge.ts

Lines changed: 126 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", ...}
@@ -425,6 +430,127 @@ export class ExecutorBridge implements Disposable {
425430
ExtensionApi.executorManager.addToQueue(process, 'replace');
426431
}
427432

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export class DiagnosticRenderer {
8585
ExtensionApi.diagnostics.diagnosticsUpdated(this.onDiagnosticUpdated, this, ctx.subscriptions);
8686

8787
workspace.onDidChangeTextDocument(({ document }) => {
88+
if (document.uri.scheme !== 'file') {
89+
return;
90+
}
8891
// Clear bug step decorations when there are unpersisted changes in the current active text editor.
8992
const editor = window.activeTextEditor;
9093
if (document.uri.fsPath === editor?.document.uri.fsPath) {

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: 90 additions & 9 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,22 +105,28 @@ 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();
93116

94117
constructor(ctx: ExtensionContext) {
95118
ctx.subscriptions.push(this._onDidChangeTreeData = new EventEmitter());
96119
window.onDidChangeActiveTextEditor(editor => {
97-
// this event is called twice by vscode. Ignore deactivation of the old editor.
98-
if (!editor) {
120+
// event is called twice. Ignore deactivation of the previous editor.
121+
if (editor === undefined) {
99122
return;
100123
}
101124

102-
this.refreshBugList();
125+
if (editor.document.uri.scheme !== 'file') {
126+
return;
127+
}
128+
129+
this.updateStatus();
103130
}, this, ctx.subscriptions);
104131

105132
ExtensionApi.diagnostics.diagnosticsUpdated(() => {
@@ -120,6 +147,23 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
120147
));
121148

122149
this.init();
150+
this.updateStatus();
151+
}
152+
153+
public addDynamicNode(id: string, node: ReportTreeItem) {
154+
this.dynamicTreeItems.set(id, node);
155+
}
156+
157+
public getNodeById(id: string): ReportTreeItem | undefined {
158+
return this.dynamicTreeItems.get(id);
159+
}
160+
161+
public getAllNodes(): Map<string, ReportTreeItem> {
162+
return this.treeItems;
163+
}
164+
165+
public refreshNode() {
166+
this._onDidChangeTreeData.fire();
123167
}
124168

125169
protected init() {
@@ -128,6 +172,15 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
128172
this.tree?.onDidChangeSelection((item: TreeViewSelectionChangeEvent<ReportTreeItem>) => {
129173
this.selectedTreeItems = item.selection;
130174
});
175+
176+
workspace.onDidChangeTextDocument(event => {
177+
if (event.document.uri.scheme !== 'file') {
178+
return;
179+
}
180+
if (event?.document === window.activeTextEditor?.document) {
181+
this.updateStatus();
182+
}
183+
});
131184
}
132185

133186
private _onDidChangeTreeData: EventEmitter<void>;
@@ -166,6 +219,24 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
166219
this._onDidChangeTreeData.fire();
167220
}
168221

222+
updateStatus() {
223+
if (window?.activeTextEditor?.document?.isDirty) {
224+
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
225+
if (statusNode) {
226+
statusNode?.setLabelAndIcon('Outdated (file is modified in the editor)', new ThemeIcon('edit'));
227+
if (statusNode.children) {
228+
statusNode.children.forEach(child => {
229+
child.setLabelAndIcon(undefined, new ThemeIcon('edit'));
230+
});
231+
}
232+
}
233+
} else {
234+
const executorBridge = ExtensionApi.executorBridge;
235+
executorBridge.getFileAnalysisStatus();
236+
}
237+
this._onDidChangeTreeData.fire();
238+
}
239+
169240
revealSelectedItems() {
170241
const selectedIds = new Set(this.selectedTreeItems.map(item => item.id));
171242
this.treeItems.forEach(root => root.traverse(item => {
@@ -308,7 +379,10 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
308379
// Get root level items.
309380
getRootItems(): ReportTreeItem[] | undefined {
310381
if (!this.currentEntryList?.length) {
311-
return [new ReportTreeItem('noReportsFound', 'No reports found', new ThemeIcon('pass'))];
382+
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
383+
statusNode?.setLabelAndIcon('Not in compilation database',
384+
new ThemeIcon('question', new ThemeColor('charts.orange')));
385+
return statusNode ? [ statusNode ] : undefined;
312386
}
313387

314388
const severityItems: { [key: string]: TreeDiagnosticReport[] } = {};
@@ -322,6 +396,13 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
322396

323397
const rootItems: ReportTreeItem[] = [];
324398

399+
let status = SidebarContainer.reportsView.getNodeById('statusItem');
400+
if (!status) {
401+
status = new ReportTreeItem('statusItem', 'Status', new ThemeIcon('warning'));
402+
SidebarContainer.reportsView.addDynamicNode('statusItem', status);
403+
}
404+
rootItems.push(status);
405+
325406
rootItems.push(...Object.entries(severityItems)
326407
.sort(([severityA]: [string, TreeDiagnosticReport[]], [severityB]: [string, TreeDiagnosticReport[]]) =>
327408
severityOrder[severityA] - severityOrder[severityB])

0 commit comments

Comments
 (0)