diff --git a/media/common/dep.svg b/media/common/dep.svg
new file mode 100644
index 0000000..53e5be5
--- /dev/null
+++ b/media/common/dep.svg
@@ -0,0 +1,14 @@
+
+
+
+ Slice
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 17fadfb..11f4b7a 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,31 @@
},
"icon": "icon.png",
"contributes": {
+ "viewsWelcome": [
+ {
+ "view": "aderyn-panel-diagnostics-provider",
+ "contents": "Visit the [Welcome Page](command:aderyn.commands.showOnboardPanel) to get started",
+ "when": "!aderyn-panel-diagnostics-provider.hasItems"
+ }
+ ],
+ "viewsContainers": {
+ "activitybar": [
+ {
+ "id": "aderyn-panel-diagnostics",
+ "title": "Aderyn Diagnostics",
+ "icon": "media/common/dep.svg"
+ }
+ ]
+ },
"views": {
+ "aderyn-panel-diagnostics": [
+ {
+ "id": "aderyn-panel-diagnostics-provider",
+ "name": "Aderyn Diagnostics",
+ "icon": "media/common/dep.svg",
+ "contextualTitle": "Aderyn Diagnostics"
+ }
+ ],
"explorer": [
{
"type": "webview",
diff --git a/src/extension.ts b/src/extension.ts
index 449192a..d5fbe2c 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,4 +1,12 @@
import * as vscode from 'vscode';
+import {
+ hasRecognizedProjectStructureAtWorkspaceRoot,
+ isKeyUsed,
+ isWindowsNotWSL,
+ Keys,
+ Logger,
+} from './utils';
+
import {
createOrInitLspClient,
startServing,
@@ -6,10 +14,11 @@ import {
createOrInitOnboardProvider,
createAderynStatusItem,
} from './state';
+
import { registerEditorCommands } from './commands';
import { registerWebviewPanels } from './webview-providers';
-import { isKeyUsed, isWindowsNotWSL, Keys, Logger } from './utils';
import { registerStatusBarItems } from './state/statusbar/index';
+import { registerDataProviders } from './panel-providers';
export function activate(context: vscode.ExtensionContext) {
if (isWindowsNotWSL()) {
@@ -25,13 +34,14 @@ export function activate(context: vscode.ExtensionContext) {
.then(() => registerWebviewPanels(context))
.then(() => registerEditorCommands(context))
.then(() => registerStatusBarItems(context))
+ .then(() => registerDataProviders(context))
.then(autoStartLspClientIfRequested);
}
async function autoStartLspClientIfRequested() {
const config = vscode.workspace.getConfiguration('aderyn.config');
const userPrefersAutoStart = config.get('autoStart');
- if (userPrefersAutoStart) {
+ if (userPrefersAutoStart && hasRecognizedProjectStructureAtWorkspaceRoot()) {
try {
startServing();
} catch (_ex) {
diff --git a/src/panel-providers/diagnostics-panel.ts b/src/panel-providers/diagnostics-panel.ts
new file mode 100644
index 0000000..c24eaf7
--- /dev/null
+++ b/src/panel-providers/diagnostics-panel.ts
@@ -0,0 +1,153 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import {
+ ensureWorkspacePreconditionsMetAndReturnProjectURI,
+ findProjectRoot,
+} from '../utils/index';
+import {
+ createAderynReportAndDeserialize,
+ isAderynAvailableOnPath,
+} from '../utils/install/aderyn';
+import { Report, IssueInstance, Issue } from '../utils/install/issues';
+import { Logger } from '../utils/logger';
+
+class AderynDiagnosticsProvider implements vscode.TreeDataProvider {
+ private _onDidChangeTreeData: vscode.EventEmitter =
+ new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event =
+ this._onDidChangeTreeData.event;
+
+ projectRootUri: string | null = null;
+
+ refresh(): void {
+ this._onDidChangeTreeData.fire();
+ }
+
+ getTreeItem(element: DiagnosticItem): vscode.TreeItem {
+ return element;
+ }
+
+ async getChildren(element?: DiagnosticItem): Promise {
+ if (!element) {
+ return this.prepareResults().then(this.getTopLevelItems);
+ }
+ if (element.itemKind == ItemKind.Category) {
+ return this.getIssueItems(element as CategoryItem);
+ }
+ if (element.itemKind == ItemKind.Issue) {
+ return this.getInstances(element as IssueItem);
+ }
+ return Promise.resolve([]);
+ }
+
+ async prepareResults(): Promise {
+ const logger = new Logger();
+ const aderynIsOnPath = await isAderynAvailableOnPath(logger);
+ if (aderynIsOnPath) {
+ const workspaceRoot =
+ ensureWorkspacePreconditionsMetAndReturnProjectURI(false);
+ if (!workspaceRoot) {
+ return Promise.reject('workspace pre-conditions unmet');
+ }
+ this.projectRootUri = findProjectRoot(workspaceRoot);
+ return await createAderynReportAndDeserialize(this.projectRootUri).catch(
+ (err) => {
+ logger.err(err);
+ vscode.window.showErrorMessage('Error fetching results from aderyn');
+ return null;
+ },
+ );
+ }
+ return null;
+ }
+
+ getTopLevelItems(report: Report | null): DiagnosticItem[] {
+ if (!report) {
+ return [];
+ }
+ const highIssues = report.highIssues.issues;
+ const lowIssues = report.lowIssues.issues;
+ return [new CategoryItem('High', highIssues), new CategoryItem('Low', lowIssues)];
+ }
+
+ getIssueItems(category: CategoryItem): DiagnosticItem[] {
+ return category.issues.map((issue) => new IssueItem(issue));
+ }
+
+ getInstances(issueItem: IssueItem): DiagnosticItem[] {
+ return issueItem.issue.instances.map(
+ (instance) => new InstanceItem(instance, this.projectRootUri ?? '.'),
+ );
+ }
+}
+
+const enum ItemKind {
+ Category,
+ Issue,
+ Instance,
+}
+
+export class DiagnosticItem extends vscode.TreeItem {
+ constructor(
+ public readonly label: string,
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
+ public readonly itemKind: ItemKind,
+ ) {
+ super(label, collapsibleState);
+ this.tooltip = `${this.label}`;
+ }
+}
+
+class CategoryItem extends DiagnosticItem {
+ constructor(
+ public readonly label: string,
+ public readonly issues: Issue[],
+ ) {
+ super(
+ label,
+ issues.length > 0
+ ? vscode.TreeItemCollapsibleState.Collapsed
+ : vscode.TreeItemCollapsibleState.Expanded,
+ ItemKind.Category,
+ );
+ this.tooltip = `${this.label} issues`;
+ }
+}
+
+class IssueItem extends DiagnosticItem {
+ constructor(public readonly issue: Issue) {
+ super(issue.title, vscode.TreeItemCollapsibleState.Collapsed, ItemKind.Issue);
+ this.tooltip = issue.description;
+ }
+}
+
+class InstanceItem extends DiagnosticItem {
+ constructor(
+ public readonly instance: IssueInstance,
+ projectRootUri: string,
+ ) {
+ super(
+ `${instance.contractPath} Line: ${instance.lineNo} ${instance.srcChar}`,
+ vscode.TreeItemCollapsibleState.None,
+ ItemKind.Instance,
+ );
+ this.tooltip = instance.contractPath;
+ this.command = {
+ title: 'Go to file',
+ command: 'vscode.open',
+ arguments: [
+ vscode.Uri.file(path.join(projectRootUri, instance.contractPath)),
+ {
+ selection: new vscode.Range(
+ instance.lineNo - 1,
+ 0,
+ instance.lineNo - 1,
+ 1000,
+ ),
+ },
+ ],
+ };
+ }
+}
+
+export { AderynDiagnosticsProvider };
diff --git a/src/panel-providers/index.ts b/src/panel-providers/index.ts
new file mode 100644
index 0000000..937aeb6
--- /dev/null
+++ b/src/panel-providers/index.ts
@@ -0,0 +1,4 @@
+import { AderynDiagnosticsProvider } from './diagnostics-panel';
+import { registerDataProviders } from './registrations';
+
+export { AderynDiagnosticsProvider, registerDataProviders };
diff --git a/src/panel-providers/registrations.ts b/src/panel-providers/registrations.ts
new file mode 100644
index 0000000..cf7db41
--- /dev/null
+++ b/src/panel-providers/registrations.ts
@@ -0,0 +1,19 @@
+import * as vscode from 'vscode';
+
+import { PanelProviders } from './variants';
+import { AderynDiagnosticsProvider as D } from './diagnostics-panel';
+
+function registerDataProviders(context: vscode.ExtensionContext) {
+ const diagnosticsDataProvider = new D();
+ const diagnosticsTreeView = vscode.window.createTreeView(PanelProviders.Diagnostics, {
+ treeDataProvider: diagnosticsDataProvider,
+ });
+ diagnosticsTreeView.onDidChangeVisibility((e) => {
+ if (e.visible) {
+ diagnosticsDataProvider.refresh();
+ }
+ });
+ context.subscriptions.push(diagnosticsTreeView);
+}
+
+export { registerDataProviders };
diff --git a/src/panel-providers/variants.ts b/src/panel-providers/variants.ts
new file mode 100644
index 0000000..50edbfa
--- /dev/null
+++ b/src/panel-providers/variants.ts
@@ -0,0 +1,5 @@
+const enum PanelProviders {
+ Diagnostics = 'aderyn-panel-diagnostics-provider',
+}
+
+export { PanelProviders };
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 932c4d9..2443011 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -4,6 +4,7 @@ import {
executeCommand,
isWindowsNotWSL,
ensureWorkspacePreconditionsMetAndReturnProjectURI,
+ hasRecognizedProjectStructureAtWorkspaceRoot,
} from './runtime';
import { ensureAderynIsInstalled } from './install';
import { isKeyUsed, Keys } from './keys';
@@ -12,6 +13,7 @@ import { readAderynConfigTemplate } from './metadata';
export {
findProjectRoot,
+ hasRecognizedProjectStructureAtWorkspaceRoot,
ensureWorkspacePreconditionsMetAndReturnProjectURI,
readAderynConfigTemplate,
isWindowsNotWSL,
diff --git a/src/utils/install/aderyn.ts b/src/utils/install/aderyn.ts
index 5ebd0bb..4510394 100644
--- a/src/utils/install/aderyn.ts
+++ b/src/utils/install/aderyn.ts
@@ -1,5 +1,10 @@
-import { executeCommand } from '../runtime';
+import {
+ ensureWorkspacePreconditionsMetAndReturnProjectURI,
+ executeCommand,
+ findProjectRoot,
+} from '../runtime';
import { Logger } from '../logger';
+import { parseAderynReportFromJsonString, Report } from './issues';
/**
* Checks if the command "aderyn" is available on path in the shell
@@ -19,4 +24,17 @@ async function isAderynAvailableOnPath(logger: Logger): Promise {
});
}
-export { isAderynAvailableOnPath };
+async function createAderynReportAndDeserialize(projectRootUri: string): Promise {
+ const cmd = `aderyn ${projectRootUri} -o report.json --stdout --skip-cloc`;
+ return executeCommand(cmd)
+ .then((text) => {
+ const match = text.match(/STDOUT START([\s\S]*?)STDOUT END/);
+ if (!match) {
+ throw new Error('corrupted json');
+ }
+ return match[1].trim();
+ })
+ .then((reportJson) => parseAderynReportFromJsonString(reportJson));
+}
+
+export { isAderynAvailableOnPath, createAderynReportAndDeserialize };
diff --git a/src/utils/install/index.ts b/src/utils/install/index.ts
index 332e033..f8506d8 100644
--- a/src/utils/install/index.ts
+++ b/src/utils/install/index.ts
@@ -1,7 +1,7 @@
import { Logger } from '../logger';
import { readPackageJson } from '../metadata';
import { hasReliableInternet } from '../runtime';
-import { isAderynAvailableOnPath } from './aderyn';
+import { isAderynAvailableOnPath, createAderynReportAndDeserialize } from './aderyn';
import {
whichAderyn,
installAderynWithAppropriateCmd,
@@ -155,4 +155,4 @@ async function ensureAderynIsInstalled(): Promise {
}
}
-export { ensureAderynIsInstalled };
+export { ensureAderynIsInstalled, createAderynReportAndDeserialize };
diff --git a/src/utils/install/issues.ts b/src/utils/install/issues.ts
new file mode 100644
index 0000000..17bcb3e
--- /dev/null
+++ b/src/utils/install/issues.ts
@@ -0,0 +1,98 @@
+// Run aderyn and deserialize the issues for upstream consumption
+// Ex - Used by Diagnostics Panel
+
+interface FilesSummary {
+ totalSourceUnits: number;
+ totalSloc: number;
+}
+
+interface FileDetail {
+ filePath: string;
+ nSloc: number;
+}
+
+interface FilesDetails {
+ filesDetails: FileDetail[];
+}
+
+interface IssueInstance {
+ contractPath: string;
+ lineNo: number;
+ src: string;
+ srcChar: string;
+}
+
+interface Issue {
+ title: string;
+ description: string;
+ detectorName: string;
+ instances: IssueInstance[];
+}
+
+interface Issues {
+ issues: Issue[];
+}
+
+interface IssueCount {
+ high: number;
+ low: number;
+}
+
+interface Report {
+ filesSummary: FilesSummary;
+ filesDetails: FilesDetails;
+ issueCount: IssueCount;
+ highIssues: Issues;
+ lowIssues: Issues;
+ detectorsUsed: string[];
+}
+
+function parseAderynReportFromJsonString(json: string): Report {
+ const obj = JSON.parse(json);
+
+ return {
+ filesSummary: {
+ totalSourceUnits: obj.files_summary.total_source_units,
+ totalSloc: obj.files_summary.total_sloc,
+ },
+ filesDetails: {
+ filesDetails: obj.files_details.files_details.map((file: any) => ({
+ filePath: file.file_path,
+ nSloc: file.n_sloc,
+ })),
+ },
+ issueCount: {
+ high: obj.issue_count.high,
+ low: obj.issue_count.low,
+ },
+ highIssues: {
+ issues: obj.high_issues.issues.map((issue: any) => ({
+ title: issue.title,
+ description: issue.description,
+ detectorName: issue.detector_name,
+ instances: issue.instances.map((instance: any) => ({
+ contractPath: instance.contract_path,
+ lineNo: instance.line_no,
+ src: instance.src,
+ srcChar: instance.src_char,
+ })),
+ })),
+ },
+ lowIssues: {
+ issues: obj.low_issues.issues.map((issue: any) => ({
+ title: issue.title,
+ description: issue.description,
+ detectorName: issue.detector_name,
+ instances: issue.instances.map((instance: any) => ({
+ contractPath: instance.contract_path,
+ lineNo: instance.line_no,
+ src: instance.src,
+ srcChar: instance.src_char,
+ })),
+ })),
+ },
+ detectorsUsed: obj.detectors_used,
+ };
+}
+
+export { parseAderynReportFromJsonString, Report, Issue, IssueInstance };
diff --git a/src/utils/runtime/index.ts b/src/utils/runtime/index.ts
index 5c1e121..61d67c8 100644
--- a/src/utils/runtime/index.ts
+++ b/src/utils/runtime/index.ts
@@ -4,7 +4,11 @@ import * as project from './project';
import { ExecuteCommandErrorType, ExecuteCommandError, SystemInfo } from './system';
const { getSystemInfo, executeCommand, hasReliableInternet, isWindowsNotWSL } = system;
-const { findProjectRoot, ensureWorkspacePreconditionsMetAndReturnProjectURI } = project;
+const {
+ findProjectRoot,
+ hasRecognizedProjectStructureAtWorkspaceRoot,
+ ensureWorkspacePreconditionsMetAndReturnProjectURI,
+} = project;
export {
findProjectRoot,
@@ -12,6 +16,7 @@ export {
executeCommand,
hasReliableInternet,
isWindowsNotWSL,
+ hasRecognizedProjectStructureAtWorkspaceRoot,
ensureWorkspacePreconditionsMetAndReturnProjectURI,
SystemInfo,
ExecuteCommandError,
diff --git a/src/utils/runtime/project.ts b/src/utils/runtime/project.ts
index ba2b64d..a8591a0 100644
--- a/src/utils/runtime/project.ts
+++ b/src/utils/runtime/project.ts
@@ -18,7 +18,20 @@ function findProjectRoot(projectRootUri: string): string {
}
currentDir = path.dirname(currentDir);
}
- return projectRootUri;
+ return currentDir;
+}
+
+function hasRecognizedProjectStructureAtWorkspaceRoot(): boolean {
+ const workspaceRoot = ensureWorkspacePreconditionsMetAndReturnProjectURI(false);
+ if (!workspaceRoot) return false;
+ const root = findProjectRoot(workspaceRoot);
+ return (
+ root != null &&
+ (fs.existsSync(path.join(root, 'aderyn.toml')) ||
+ fs.existsSync(path.join(root, 'foundry.toml')) ||
+ fs.existsSync(path.join(root, 'hardhat.config.ts')) ||
+ fs.existsSync(path.join(root, 'hardhat.config.js')))
+ );
}
function ensureWorkspacePreconditionsMetAndReturnProjectURI(
@@ -40,4 +53,8 @@ function ensureWorkspacePreconditionsMetAndReturnProjectURI(
return projectRootUri.toString().substring('file://'.length);
}
-export { findProjectRoot, ensureWorkspacePreconditionsMetAndReturnProjectURI };
+export {
+ findProjectRoot,
+ hasRecognizedProjectStructureAtWorkspaceRoot,
+ ensureWorkspacePreconditionsMetAndReturnProjectURI,
+};
diff --git a/src/webview-providers/onboard-panel/HowToUse.svelte b/src/webview-providers/onboard-panel/HowToUse.svelte
index ab99809..f477fd1 100644
--- a/src/webview-providers/onboard-panel/HowToUse.svelte
+++ b/src/webview-providers/onboard-panel/HowToUse.svelte
@@ -6,8 +6,8 @@
Aderyn will start automatically on commonly recognized project structures
- i.e when a foundry.toml or hardhat.config.ts is found in the
- root of the workspace
+ i.e when a foundry.toml or hardhat.config.ts or
+ aderyn.toml is found in the root of the workspace
If the project's framework config file is nested in some directory or is