Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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