Skip to content

Commit a8abcea

Browse files
committed
feat: Add flow step navigation and diagnostics clearing commands in CodeQL Scanner
1 parent 3d0c307 commit a8abcea

File tree

3 files changed

+329
-23
lines changed

3 files changed

+329
-23
lines changed

src/extension.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,77 @@ export async function activate(context: vscode.ExtensionContext) {
5656
}),
5757
vscode.commands.registerCommand('codeql-scanner.reloadSARIF', async () => {
5858
await autoLoadExistingSARIFFiles(codeqlService, resultsProvider, uiProvider);
59+
}),
60+
vscode.commands.registerCommand('codeql-scanner.clearDiagnostics', () => {
61+
resultsProvider.clearResults();
62+
vscode.window.showInformationMessage('CodeQL diagnostics cleared.');
63+
}),
64+
vscode.commands.registerCommand('codeql-scanner.copyFlowPath', async (item) => {
65+
if (item && item.result && item.result.flowSteps) {
66+
const flowPath = item.result.flowSteps.map((step: any, index: number) => {
67+
const stepType = index === 0 ? 'Source' :
68+
index === item.result.flowSteps.length - 1 ? 'Sink' : 'Step';
69+
return `${stepType} ${index + 1}: ${step.file}:${step.startLine}${step.message ? ` - ${step.message}` : ''}`;
70+
}).join('\n');
71+
72+
await vscode.env.clipboard.writeText(flowPath);
73+
vscode.window.showInformationMessage('Flow path copied to clipboard!');
74+
} else {
75+
vscode.window.showWarningMessage('No flow path available for this item.');
76+
}
77+
}),
78+
vscode.commands.registerCommand('codeql-scanner.navigateFlowSteps', async (item) => {
79+
if (item && item.result && item.result.flowSteps && item.result.flowSteps.length > 0) {
80+
const flowSteps = item.result.flowSteps;
81+
82+
// Create quick pick items for each flow step
83+
interface FlowStepQuickPickItem extends vscode.QuickPickItem {
84+
stepData: any;
85+
}
86+
87+
const quickPickItems: FlowStepQuickPickItem[] = flowSteps.map((step: any, index: number) => {
88+
const stepType = index === 0 ? 'Source' :
89+
index === flowSteps.length - 1 ? 'Sink' : 'Step';
90+
const fileName = step.file.split('/').pop() || 'unknown';
91+
92+
return {
93+
label: `${stepType} ${index + 1}: ${fileName}:${step.startLine}`,
94+
description: step.message || '',
95+
detail: step.file,
96+
stepData: step
97+
};
98+
});
99+
100+
const selected = await vscode.window.showQuickPick(quickPickItems, {
101+
placeHolder: 'Select a flow step to navigate to'
102+
});
103+
104+
if (selected && selected.stepData) {
105+
const step = selected.stepData;
106+
const document = await vscode.workspace.openTextDocument(step.file);
107+
const editor = await vscode.window.showTextDocument(document);
108+
109+
const range = new vscode.Range(
110+
step.startLine - 1,
111+
step.startColumn - 1,
112+
step.endLine - 1,
113+
step.endColumn - 1
114+
);
115+
116+
editor.selection = new vscode.Selection(range.start, range.end);
117+
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
118+
}
119+
} else {
120+
vscode.window.showWarningMessage('No flow steps available for this item.');
121+
}
59122
})
60123
];
61124

62125
context.subscriptions.push(...commands);
63126

127+
// Register providers for disposal
128+
context.subscriptions.push(resultsProvider);
129+
64130
// Register logger disposal
65131
context.subscriptions.push({
66132
dispose: () => {
@@ -121,9 +187,13 @@ async function handleCommand(
121187
uiProvider.updateScanResults(results);
122188
}
123189
vscode.commands.executeCommand('setContext', 'codeql-scanner.hasResults', true);
124-
} else if (uiProvider) {
125-
// Update UI provider with empty results
126-
uiProvider.updateScanResults([]);
190+
} else {
191+
// Clear existing results and diagnostics
192+
resultsProvider.clearResults();
193+
if (uiProvider) {
194+
uiProvider.updateScanResults([]);
195+
}
196+
vscode.commands.executeCommand('setContext', 'codeql-scanner.hasResults', false);
127197
}
128198
break;
129199
case 'init':

src/providers/resultsProvider.ts

Lines changed: 177 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
11
import * as vscode from 'vscode';
2-
import { ScanResult } from '../services/codeqlService';
2+
import { ScanResult, FlowStep } from '../services/codeqlService';
33

44
export class ResultsProvider implements vscode.TreeDataProvider<ResultItem> {
55
private _onDidChangeTreeData: vscode.EventEmitter<ResultItem | undefined | null | void> = new vscode.EventEmitter<ResultItem | undefined | null | void>();
66
readonly onDidChangeTreeData: vscode.Event<ResultItem | undefined | null | void> = this._onDidChangeTreeData.event;
77

88
private results: ScanResult[] = [];
9+
private diagnosticCollection: vscode.DiagnosticCollection;
910

10-
constructor() {}
11+
constructor() {
12+
// Create a diagnostic collection for CodeQL security issues
13+
this.diagnosticCollection = vscode.languages.createDiagnosticCollection('codeql-security');
14+
}
1115

1216
refresh(): void {
1317
this._onDidChangeTreeData.fire();
1418
}
1519

1620
setResults(results: ScanResult[]): void {
1721
this.results = results;
22+
this.updateDiagnostics(results);
1823
this.refresh();
1924
}
2025

2126
getResults(): ScanResult[] {
2227
return this.results;
2328
}
2429

30+
clearResults(): void {
31+
this.results = [];
32+
this.diagnosticCollection.clear();
33+
this.refresh();
34+
}
35+
2536
getTreeItem(element: ResultItem): vscode.TreeItem {
2637
return element;
2738
}
@@ -64,14 +75,43 @@ export class ResultsProvider implements vscode.TreeDataProvider<ResultItem> {
6475
element.results!.map(result =>
6576
new ResultItem(
6677
`${result.ruleId}: ${result.message}`,
67-
vscode.TreeItemCollapsibleState.None,
78+
result.flowSteps && result.flowSteps.length > 0
79+
? vscode.TreeItemCollapsibleState.Collapsed
80+
: vscode.TreeItemCollapsibleState.None,
6881
'result',
6982
element.language,
7083
undefined,
7184
result
7285
)
7386
)
7487
);
88+
} else if (element.type === 'result' && element.result?.flowSteps) {
89+
// Fourth level - flow steps (hidden by default)
90+
const flowSteps = element.result.flowSteps;
91+
return Promise.resolve(
92+
flowSteps.map((step, index) => {
93+
const isSource = index === 0;
94+
const isSink = index === flowSteps.length - 1;
95+
const stepType = isSource ? 'Source' : isSink ? 'Sink' : 'Step';
96+
const fileName = step.file.split('/').pop() || 'unknown';
97+
98+
let label = `${stepType} ${index + 1}: ${fileName}:${step.startLine}`;
99+
if (step.message) {
100+
label += ` - ${step.message}`;
101+
}
102+
103+
return new ResultItem(
104+
label,
105+
vscode.TreeItemCollapsibleState.None,
106+
'flowStep',
107+
element.language,
108+
undefined,
109+
element.result,
110+
undefined,
111+
step
112+
);
113+
})
114+
);
75115
}
76116

77117
return Promise.resolve([]);
@@ -128,17 +168,112 @@ export class ResultsProvider implements vscode.TreeDataProvider<ResultItem> {
128168
return aIndex - bIndex;
129169
});
130170
}
171+
172+
private updateDiagnostics(results: ScanResult[]): void {
173+
// Clear existing diagnostics
174+
this.diagnosticCollection.clear();
175+
176+
if (!results || results.length === 0) {
177+
return;
178+
}
179+
180+
// Group diagnostics by file URI
181+
const diagnosticsMap = new Map<string, vscode.Diagnostic[]>();
182+
183+
results.forEach(result => {
184+
if (!result.location || !result.location.file) {
185+
return;
186+
}
187+
188+
const fileUri = vscode.Uri.file(result.location.file);
189+
const uriString = fileUri.toString();
190+
191+
// Create a range for the diagnostic with bounds checking
192+
const startLine = Math.max(0, (result.location.startLine || 1) - 1);
193+
const startColumn = Math.max(0, (result.location.startColumn || 1) - 1);
194+
const endLine = Math.max(startLine, (result.location.endLine || result.location.startLine || 1) - 1);
195+
const endColumn = Math.max(startColumn + 1, (result.location.endColumn || result.location.startColumn || 1) - 1);
196+
197+
const range = new vscode.Range(startLine, startColumn, endLine, endColumn);
198+
199+
// Map severity to VS Code diagnostic severity
200+
const severity = this.mapToVSCodeSeverity(result.severity);
201+
202+
// Create diagnostic with detailed message
203+
const flowInfo = result.flowSteps && result.flowSteps.length > 0
204+
? ` (${result.flowSteps.length} flow steps)`
205+
: '';
206+
const message = `[${result.severity?.toUpperCase()}] ${result.ruleId}: ${result.message}${flowInfo}`;
207+
const diagnostic = new vscode.Diagnostic(range, message, severity);
208+
209+
// Add additional information to the diagnostic
210+
diagnostic.source = 'CodeQL Security Scanner';
211+
diagnostic.code = result.ruleId;
212+
213+
// Add related information for flow steps
214+
if (result.flowSteps && result.flowSteps.length > 0) {
215+
diagnostic.relatedInformation = result.flowSteps.map((step, index) => {
216+
const stepRange = new vscode.Range(
217+
Math.max(0, step.startLine - 1),
218+
Math.max(0, step.startColumn - 1),
219+
Math.max(0, step.endLine - 1),
220+
Math.max(0, step.endColumn - 1)
221+
);
222+
return new vscode.DiagnosticRelatedInformation(
223+
new vscode.Location(vscode.Uri.file(step.file), stepRange),
224+
`Flow step ${index + 1}${step.message ? `: ${step.message}` : ''}`
225+
);
226+
});
227+
}
228+
229+
// Get or create diagnostics array for this file
230+
let fileDiagnostics = diagnosticsMap.get(uriString);
231+
if (!fileDiagnostics) {
232+
fileDiagnostics = [];
233+
diagnosticsMap.set(uriString, fileDiagnostics);
234+
}
235+
236+
fileDiagnostics.push(diagnostic);
237+
});
238+
239+
// Set diagnostics for each file
240+
diagnosticsMap.forEach((diagnostics, uriString) => {
241+
this.diagnosticCollection.set(vscode.Uri.parse(uriString), diagnostics);
242+
});
243+
}
244+
245+
private mapToVSCodeSeverity(severity: string): vscode.DiagnosticSeverity {
246+
switch (severity?.toLowerCase()) {
247+
case 'critical':
248+
case 'high':
249+
case 'error':
250+
return vscode.DiagnosticSeverity.Error;
251+
case 'medium':
252+
case 'warning':
253+
return vscode.DiagnosticSeverity.Warning;
254+
case 'low':
255+
case 'info':
256+
return vscode.DiagnosticSeverity.Information;
257+
default:
258+
return vscode.DiagnosticSeverity.Warning;
259+
}
260+
}
261+
262+
dispose(): void {
263+
this.diagnosticCollection.dispose();
264+
}
131265
}
132266

133267
export class ResultItem extends vscode.TreeItem {
134268
constructor(
135269
public readonly label: string,
136270
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
137-
public readonly type: 'language' | 'severity' | 'result',
271+
public readonly type: 'language' | 'severity' | 'result' | 'flowStep',
138272
public readonly language?: string,
139273
public readonly results?: ScanResult[],
140274
public readonly result?: ScanResult,
141-
public readonly severity?: string
275+
public readonly severity?: string,
276+
public readonly flowStep?: FlowStep
142277
) {
143278
super(label, collapsibleState);
144279

@@ -154,15 +289,24 @@ export class ResultItem extends vscode.TreeItem {
154289
return `${this.results?.length || 0} ${this.language} language issues`;
155290
} else if (this.type === 'severity') {
156291
return `${this.results?.length || 0} ${this.severity} severity issues in ${this.language}`;
157-
} else if (this.result) {
158-
return `${this.result.ruleId}: ${this.result.message}\\nFile: ${this.result.location.file}\\nLine: ${this.result.location.startLine}`;
292+
} else if (this.type === 'result' && this.result) {
293+
const flowInfo = this.result.flowSteps && this.result.flowSteps.length > 0
294+
? `\\nFlow steps: ${this.result.flowSteps.length}`
295+
: '';
296+
return `${this.result.ruleId}: ${this.result.message}\\nFile: ${this.result.location.file}\\nLine: ${this.result.location.startLine}${flowInfo}`;
297+
} else if (this.type === 'flowStep' && this.flowStep) {
298+
return `Flow step ${this.flowStep.stepIndex + 1}\\nFile: ${this.flowStep.file}\\nLine: ${this.flowStep.startLine}${this.flowStep.message ? `\\nMessage: ${this.flowStep.message}` : ''}`;
159299
}
160300
return this.label;
161301
}
162302

163303
private getDescription(): string {
164304
if (this.type === 'result' && this.result) {
165-
return `${this.result.location.file}:${this.result.location.startLine}`;
305+
const flowCount = this.result.flowSteps?.length || 0;
306+
const baseDesc = `${this.result.location.file}:${this.result.location.startLine}`;
307+
return flowCount > 0 ? `${baseDesc} (${flowCount} steps)` : baseDesc;
308+
} else if (this.type === 'flowStep' && this.flowStep) {
309+
return `${this.flowStep.file}:${this.flowStep.startLine}`;
166310
}
167311
return '';
168312
}
@@ -215,6 +359,15 @@ export class ResultItem extends vscode.TreeItem {
215359
default:
216360
return new vscode.ThemeIcon('circle-filled');
217361
}
362+
} else if (this.type === 'flowStep') {
363+
// Use different icons based on step index to show flow progression
364+
if (this.flowStep?.stepIndex === 0) {
365+
return new vscode.ThemeIcon('play', new vscode.ThemeColor('charts.green')); // Source
366+
} else if (this.flowStep && this.result?.flowSteps && this.flowStep.stepIndex === this.result.flowSteps.length - 1) {
367+
return new vscode.ThemeIcon('target', new vscode.ThemeColor('charts.red')); // Sink
368+
} else {
369+
return new vscode.ThemeIcon('arrow-right', new vscode.ThemeColor('charts.blue')); // Intermediate step
370+
}
218371
}
219372
return new vscode.ThemeIcon('circle-outline');
220373
}
@@ -236,6 +389,22 @@ export class ResultItem extends vscode.TreeItem {
236389
}
237390
]
238391
};
392+
} else if (this.type === 'flowStep' && this.flowStep) {
393+
return {
394+
command: 'vscode.open',
395+
title: 'Open Flow Step',
396+
arguments: [
397+
vscode.Uri.file(this.flowStep.file),
398+
{
399+
selection: new vscode.Range(
400+
this.flowStep.startLine - 1,
401+
this.flowStep.startColumn - 1,
402+
this.flowStep.endLine - 1,
403+
this.flowStep.endColumn - 1
404+
)
405+
}
406+
]
407+
};
239408
}
240409
return undefined;
241410
}

0 commit comments

Comments
 (0)