Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
"command": "codechecker.executor.analyzeProject",
"title": "CodeChecker: Analyze entire project"
},
{
"command": "codechecker.executor.getFileAnalysisStatus",
"title": "CodeChecker: Get file analysis status"
},
{
"command": "codechecker.executor.showCommandLine",
"title": "CodeChecker: Show full CodeChecker analyze command line"
Expand Down
129 changes: 129 additions & 0 deletions src/backend/executor/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
EventEmitter,
ExtensionContext,
FileSystemWatcher,
ThemeColor,
ThemeIcon,
TreeItemCollapsibleState,
Uri,
commands,
window,
Expand All @@ -20,6 +23,8 @@ import {
import { ProcessStatusType, ProcessType, ScheduledProcess } from '.';
import { NotificationType } from '../../editor/notifications';
import { Editor } from '../../editor';
import { SidebarContainer } from '../../sidebar';
import { ReportTreeItem } from '../../sidebar/views';

// Structure:
// CodeChecker analyzer version: \n {"base_package_version": "M.m.p", ...}
Expand Down Expand Up @@ -88,6 +93,9 @@ export class ExecutorBridge implements Disposable {
ctx.subscriptions.push(
commands.registerCommand('codechecker.executor.analyzeProject', this.analyzeProject, this)
);
ctx.subscriptions.push(
commands.registerCommand('codechecker.executor.getFileAnalysisStatus', this.getFileAnalysisStatus, this)
);
ctx.subscriptions.push(
commands.registerCommand('codechecker.executor.runCodeCheckerLog', this.runLogDefaultCommand, this)
);
Expand Down Expand Up @@ -425,6 +433,127 @@ export class ExecutorBridge implements Disposable {
ExtensionApi.executorManager.addToQueue(process, 'replace');
}

public async getFileAnalysisStatus() {
if (!await this.checkVersion()) {
return;
}
if (this.checkedVersion < [ 6, 27, 0 ]) {
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
statusNode?.setLabelAndIcon('Status report requires CodeChecker 6.27.0 or higher.');
return;
}

const ccPath = getConfigAndReplaceVariables('codechecker.executor', 'executablePath') || 'CodeChecker';
const reportsFolder = this.getReportsFolder();
const fileUri = window.activeTextEditor?.document.uri;
const fsPath = fileUri?.fsPath;

const statusArgs = [
'parse',
'--status',
'--detailed',
'-e',
'json',
reportsFolder,
'--file',
fsPath ?? ''
];

const process = new ScheduledProcess(ccPath, statusArgs, { processType: ProcessType.status });

let processOutput = '';
process.processStdout((output) => processOutput += output);
process.processStatusChange(async status => {
switch (status.type) {
case ProcessStatusType.errored:
break;
case ProcessStatusType.finished:
interface Analyzer {
summary: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'up-to-date': number,
failed: number,
missing: number,
outdated: number
}
// eslint-disable-next-line @typescript-eslint/naming-convention
'up-to-date': [],
failed: [],
missing: [],
outdated: [],
}

const analyzers = JSON.parse(processOutput);
let uptodate = 0;
let outdated = 0;
let missing = 0;
let failed = 0;
const analyzerStatuses: ReportTreeItem[] = [];
if (analyzers.analyzers) {
for (const [ analyzer, status ] of Object.entries(analyzers.analyzers)) {
const s = status as Analyzer;
let iconname = '';
if (s.summary['up-to-date'] > 0) {
uptodate++;
iconname = 'check';
}
if (s.summary.outdated > 0) {
outdated++;
iconname = 'clock';
}
if (s.summary.failed > 0) {
failed++;
iconname = 'error';
}
if (s.summary.missing > 0) {
missing++;
iconname = 'question';
}
let existingStatusNode = SidebarContainer.reportsView.getNodeById(analyzer);
if (!existingStatusNode) {
existingStatusNode = new ReportTreeItem(analyzer, analyzer,
new ThemeIcon(iconname), []);
const report = SidebarContainer.reportsView.getNodeById('statusItem');
existingStatusNode.parent = report;
SidebarContainer.reportsView.addDynamicNode(analyzer, existingStatusNode);
analyzerStatuses.push(existingStatusNode);
} else {
existingStatusNode.iconPath = new ThemeIcon(iconname);
}
}
}
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
if (!statusNode) {
return;
}
if (uptodate === 0 && outdated === 0 && missing === 0 && failed === 0) {
statusNode?.setLabelAndIcon('Analysis info is unavailable',
new ThemeIcon('question', new ThemeColor('charts.red')));
} else if (failed > 0) {
statusNode?.setLabelAndIcon('Analysis failed',
new ThemeIcon('error', new ThemeColor('charts.red')));
} else if (outdated === 0 && failed === 0) {
statusNode?.setLabelAndIcon('Analysis is up-to-date',
new ThemeIcon('check', new ThemeColor('charts.green')));
} else {
statusNode?.setLabelAndIcon('Analysis is outdated',
new ThemeIcon('clock', new ThemeColor('charts.red')));
}
if (analyzerStatuses.length > 0) {
statusNode.setChildren(analyzerStatuses);
statusNode.collapsibleState = TreeItemCollapsibleState.Collapsed;
}
SidebarContainer.reportsView.refreshNode();
statusNode.collapse();
break;
default:
break;
}
});

ExtensionApi.executorManager.addToQueue(process, 'replace');
}

public async runLogCustomCommand(buildCommand?: string) {
if (buildCommand === undefined) {
const executorConfig = workspace.getConfiguration('codechecker.executor');
Expand Down
1 change: 1 addition & 0 deletions src/backend/executor/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum ProcessType {
checkers = 'CodeChecker checkers',
log = 'CodeChecker log',
parse = 'CodeChecker parse',
status = 'CodeChecker parse --status',
version = 'CodeChecker analyzer-version',
other = 'Other process',
}
Expand Down
1 change: 1 addition & 0 deletions src/editor/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export class NotificationHandler {
});
this.activeNotifications.delete(process.commandLine);

SidebarContainer.reportsView.updateStatus();
break;
}
case ProcessStatusType.warning: {
Expand Down
96 changes: 89 additions & 7 deletions src/sidebar/views/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,41 @@ import {
} from 'vscode';
import { ExtensionApi } from '../../backend';
import { DiagnosticReport } from '../../backend/types';
import { SidebarContainer } from '../sidebar_container';

export class ReportTreeItem extends TreeItem {
parent: ReportTreeItem | undefined;

constructor(
public readonly _id: string,
public readonly label: string | TreeItemLabel,
public readonly iconPath: ThemeIcon,
public readonly children?: ReportTreeItem[] | undefined
label: string | TreeItemLabel,
iconPath: ThemeIcon,
public children?: ReportTreeItem[]
) {
super(label, children?.length ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None);
super(label, (children?.length) ?
TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None);
this._id = _id;
this.label = label;
this.iconPath = this.iconPath;
this.iconPath = iconPath;
this.children = children;

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

setLabelAndIcon(label?: string, iconPath?: ThemeIcon) {
if (label) {
this.label = label;
}
if (iconPath) {
this.iconPath = iconPath;
}
}

setChildren(children: ReportTreeItem[] | undefined) {
this.children = children;
}

// This function can be used to set ID attribute of a tree item and all the children of it based on the parent id.
setId() {
this.id = `${this.parent?.id ?? 'root'}_${this._id}`;
Expand All @@ -55,6 +70,12 @@ export class ReportTreeItem extends TreeItem {
}
}

collapse() {
if (this.collapsibleState === TreeItemCollapsibleState.Expanded) {
this.collapsibleState = TreeItemCollapsibleState.Collapsed;
}
}

traverse(cb: (item: ReportTreeItem) => void) {
cb(this);
this.children?.forEach(c => c.traverse(cb));
Expand Down Expand Up @@ -84,16 +105,26 @@ const severityOrder: { [key: string]: number } = {

export class ReportsView implements TreeDataProvider<ReportTreeItem> {
protected currentFile?: Uri;
protected isDirty: boolean = false;
protected currentEntryList?: DiagnosticReport[];

protected tree?: TreeView<ReportTreeItem>;
// Contains [fullpath => item] entries
private treeItems: Map<string, ReportTreeItem> = new Map();
private selectedTreeItems: ReportTreeItem[] = [];
private dynamicTreeItems: Map<string, ReportTreeItem> = new Map();
private rootItems: ReportTreeItem[] = [];

constructor(ctx: ExtensionContext) {
ctx.subscriptions.push(this._onDidChangeTreeData = new EventEmitter());
window.onDidChangeActiveTextEditor(this.refreshBugList, this, ctx.subscriptions);
window.onDidChangeActiveTextEditor(editor => {
// event is called twice. Ignore deactivation of the previous editor.
if (editor === undefined) {
return;
}
// this.refreshBugList();
this.updateStatus();
}, this, ctx.subscriptions);

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

this.init();
this.updateStatus();
}

public addDynamicNode(id: string, node: ReportTreeItem) {
this.dynamicTreeItems.set(id, node);
}

public getNodeById(id: string): ReportTreeItem | undefined {
return this.dynamicTreeItems.get(id);
}

public getAllNodes(): Map<string, ReportTreeItem> {
return this.treeItems;
}

public refreshNode() {
this._onDidChangeTreeData.fire();
}

protected init() {
Expand All @@ -121,6 +169,12 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
this.tree?.onDidChangeSelection((item: TreeViewSelectionChangeEvent<ReportTreeItem>) => {
this.selectedTreeItems = item.selection;
});

workspace.onDidChangeTextDocument(event => {
if (event?.document === window.activeTextEditor?.document) {
this.updateStatus();
}
});
}

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

updateStatus() {
if (window?.activeTextEditor?.document?.isDirty) {
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
if (statusNode) {
statusNode?.setLabelAndIcon('Outdated (file is modified in the editor)', new ThemeIcon('edit'));
if (statusNode.children) {
statusNode.children.forEach(child => {
child.setLabelAndIcon(undefined, new ThemeIcon('edit'));
});
}
}
} else {
const executorBridge = ExtensionApi.executorBridge;
executorBridge.getFileAnalysisStatus();
}
this._onDidChangeTreeData.fire();
}

revealSelectedItems() {
const selectedIds = new Set(this.selectedTreeItems.map(item => item.id));
this.treeItems.forEach(root => root.traverse(item => {
Expand Down Expand Up @@ -301,7 +373,10 @@ export class ReportsView implements TreeDataProvider<ReportTreeItem> {
// Get root level items.
getRootItems(): ReportTreeItem[] | undefined {
if (!this.currentEntryList?.length) {
return [new ReportTreeItem('noReportsFound', 'No reports found', new ThemeIcon('pass'))];
const statusNode = SidebarContainer.reportsView.getNodeById('statusItem');
statusNode?.setLabelAndIcon('Not in compilation database',
new ThemeIcon('question', new ThemeColor('charts.orange')));
return statusNode ? [ statusNode ] : undefined;
}

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

const rootItems: ReportTreeItem[] = [];

let status = SidebarContainer.reportsView.getNodeById('statusItem');
if (!status) {
status = new ReportTreeItem('statusItem', 'Status', new ThemeIcon('warning'));
SidebarContainer.reportsView.addDynamicNode('statusItem', status);
}
rootItems.push(status);

rootItems.push(...Object.entries(severityItems)
.sort(([severityA]: [string, TreeDiagnosticReport[]], [severityB]: [string, TreeDiagnosticReport[]]) =>
severityOrder[severityA] - severityOrder[severityB])
Expand Down