11import * as vscode from 'vscode' ;
2- import { ScanResult } from '../services/codeqlService' ;
2+ import { ScanResult , FlowStep } from '../services/codeqlService' ;
33
44export 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
133267export 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