Skip to content

Commit 3e8c53b

Browse files
committed
Highlight currently selected path node
1 parent a559404 commit 3e8c53b

File tree

4 files changed

+153
-31
lines changed

4 files changed

+153
-31
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as sarif from 'sarif';
2+
3+
/**
4+
* Identifies one of the results in a result set by its index in the result list.
5+
*/
6+
export interface Result {
7+
resultIndex: number;
8+
}
9+
10+
/**
11+
* Identifies one of the paths associated with a result.
12+
*/
13+
export interface Path extends Result {
14+
pathIndex: number;
15+
}
16+
17+
/**
18+
* Identifies one of the nodes in a path.
19+
*/
20+
export interface PathNode extends Path {
21+
pathNodeIndex: number;
22+
}
23+
24+
/** Alias for `undefined` but more readable in some cases */
25+
export const none: PathNode | undefined = undefined;
26+
27+
/**
28+
* Looks up a specific result in a result set.
29+
*/
30+
export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefined {
31+
if (sarif.runs.length === 0) return undefined;
32+
if (sarif.runs[0].results === undefined) return undefined;
33+
let results = sarif.runs[0].results;
34+
return results[key.resultIndex];
35+
}
36+
37+
/**
38+
* Looks up a specific path in a result set.
39+
*/
40+
export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefined {
41+
let result = getResult(sarif, key);
42+
if (result === undefined) return undefined;
43+
let index = -1;
44+
if (result.codeFlows === undefined) return undefined;
45+
for (let codeFlows of result.codeFlows) {
46+
for (let threadFlow of codeFlows.threadFlows) {
47+
++index;
48+
if (index == key.pathIndex)
49+
return threadFlow;
50+
}
51+
}
52+
return undefined;
53+
}
54+
55+
/**
56+
* Looks up a specific path node in a result set.
57+
*/
58+
export function getPathNode(sarif: sarif.Log, key: PathNode): sarif.Location | undefined {
59+
let path = getPath(sarif, key);
60+
if (path === undefined) return undefined;
61+
return path.locations[key.pathNodeIndex];
62+
}
63+
64+
/**
65+
* Returns true if the two keys are both `undefined` or contain the same set of indices.
66+
*/
67+
export function equals(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
68+
if (key1 === key2) return true;
69+
if (key1 === undefined || key2 === undefined) return false;
70+
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
71+
}
72+
73+
/**
74+
* Returns true if the two keys contain the same set of indices and neither are `undefined`.
75+
*/
76+
export function equalsNotUndefined(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
77+
if (key1 === undefined || key2 === undefined) return false;
78+
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
79+
}
80+
81+
/**
82+
* Returns the list of paths in the given SARIF result.
83+
*
84+
* Path nodes indices are relative to this flattened list.
85+
*/
86+
export function getAllPaths(result: sarif.Result): sarif.ThreadFlow[] {
87+
if (result.codeFlows === undefined) return [];
88+
let paths = [];
89+
for (const codeFlow of result.codeFlows) {
90+
for (const threadFlow of codeFlow.threadFlows) {
91+
paths.push(threadFlow);
92+
}
93+
}
94+
return paths;
95+
}

extensions/ql-vscode/src/view/alert-table.tsx

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as path from 'path';
22
import * as React from 'react';
33
import * as Sarif from 'sarif';
4+
import * as Keys from '../result-keys';
45
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
56
import * as octicons from './octicons';
6-
import { className, renderLocation, ResultTableProps, zebraStripe } from './result-table-utils';
7+
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe } from './result-table-utils';
78
import { PathTableResultSet } from './results';
89

910
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
1011
export interface PathTableState {
1112
expanded: { [k: string]: boolean };
13+
selectedPathNode: undefined | Keys.PathNode;
1214
}
1315

1416
interface SarifLink {
@@ -72,7 +74,7 @@ export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: stri
7274
export class PathTable extends React.Component<PathTableProps, PathTableState> {
7375
constructor(props: PathTableProps) {
7476
super(props);
75-
this.state = { expanded: {} };
77+
this.state = { expanded: {}, selectedPathNode: undefined };
7678
}
7779

7880
/**
@@ -118,7 +120,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
118120
if (typeof part === "string") {
119121
result.push(<span>{part} </span>);
120122
} else {
121-
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest]);
123+
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
124+
undefined);
122125
result.push(<span>{renderedLocation} </span>);
123126
}
124127
} return result;
@@ -191,14 +194,23 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
191194
}
192195
}
193196

194-
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location): JSX.Element | undefined {
197+
const updateSelectionCallback = (pathNodeKey: Keys.PathNode | undefined) => {
198+
return () => {
199+
this.setState(previousState => ({
200+
...previousState,
201+
selectedPathNode: pathNodeKey
202+
}));
203+
}
204+
};
205+
206+
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
195207
const parsedLoc = parseSarifLocation(loc);
196208
switch (parsedLoc.t) {
197209
case 'NoLocation':
198210
return renderNonLocation(text, parsedLoc.hint);
199211
case LocationStyle.FivePart:
200212
case LocationStyle.WholeFile:
201-
return renderLocation(parsedLoc, text, databaseUri);
213+
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
202214
}
203215
return undefined;
204216
}
@@ -207,7 +219,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
207219
* Render sarif location as a link with the text being simply a
208220
* human-readable form of the location itself.
209221
*/
210-
function renderSarifLocation(loc: Sarif.Location): JSX.Element | undefined {
222+
function renderSarifLocation(loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
211223
const parsedLoc = parseSarifLocation(loc);
212224
let shortLocation, longLocation: string;
213225
switch (parsedLoc.t) {
@@ -216,11 +228,11 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
216228
case LocationStyle.WholeFile:
217229
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
218230
longLocation = `${parsedLoc.userVisibleFile}`;
219-
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
231+
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
220232
case LocationStyle.FivePart:
221233
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.lineStart}:${parsedLoc.colStart}`;
222234
longLocation = `${parsedLoc.userVisibleFile}`;
223-
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
235+
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
224236
}
225237
}
226238

@@ -245,7 +257,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
245257
const currentResultExpanded = this.state.expanded[expansionIndex];
246258
const indicator = currentResultExpanded ? octicons.chevronDown : octicons.chevronRight;
247259
const location = result.locations !== undefined && result.locations.length > 0 &&
248-
renderSarifLocation(result.locations[0]);
260+
renderSarifLocation(result.locations[0], Keys.none);
249261
const locationCells = <td className="vscode-codeql__location-cell">{location}</td>;
250262

251263
if (result.codeFlows === undefined) {
@@ -260,12 +272,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
260272
);
261273
}
262274
else {
263-
const paths: Sarif.ThreadFlow[] = [];
264-
for (const codeFlow of result.codeFlows) {
265-
for (const threadFlow of codeFlow.threadFlows) {
266-
paths.push(threadFlow);
267-
}
268-
}
275+
const paths: Sarif.ThreadFlow[] = Keys.getAllPaths(result);
269276

270277
const indices = paths.length == 1 ?
271278
[expansionIndex, expansionIndex + 1] : /* if there's exactly one path, auto-expand
@@ -288,7 +295,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
288295
);
289296
expansionIndex++;
290297

291-
paths.forEach(path => {
298+
paths.forEach((path, pathIndex) => {
299+
const pathKey = { resultIndex, pathIndex };
292300
const currentPathExpanded = this.state.expanded[expansionIndex];
293301
if (currentResultExpanded) {
294302
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
@@ -305,25 +313,27 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
305313
expansionIndex++;
306314

307315
if (currentResultExpanded && currentPathExpanded) {
308-
let pathIndex = 1;
309-
for (const step of path.locations) {
316+
const pathNodes = path.locations;
317+
for (let pathNodeIndex = 0; pathNodeIndex < pathNodes.length; ++pathNodeIndex) {
318+
const pathNodeKey: Keys.PathNode = { ...pathKey, pathNodeIndex };
319+
const step = pathNodes[pathNodeIndex];
310320
const msg = step.location !== undefined && step.location.message !== undefined ?
311-
renderSarifLocationWithText(step.location.message.text, step.location) :
321+
renderSarifLocationWithText(step.location.message.text, step.location, pathNodeKey) :
312322
'[no location]';
313323
const additionalMsg = step.location !== undefined ?
314-
renderSarifLocation(step.location) :
324+
renderSarifLocation(step.location, pathNodeKey) :
315325
'';
316-
317-
const stepIndex = resultIndex + pathIndex;
326+
let isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
327+
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
328+
const zebraIndex = resultIndex + stepIndex;
318329
rows.push(
319-
<tr>
330+
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined}>
320331
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
321332
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
322-
<td {...zebraStripe(stepIndex, 'vscode-codeql__path-index-cell')}>{pathIndex}</td>
323-
<td {...zebraStripe(stepIndex)}>{msg} </td>
324-
<td {...zebraStripe(stepIndex, 'vscode-codeql__location-cell')}>{additionalMsg}</td>
333+
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
334+
<td {...selectableZebraStripe(isSelected, zebraIndex)}>{msg} </td>
335+
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__location-cell')}>{additionalMsg}</td>
325336
</tr>);
326-
pathIndex++;
327337
}
328338
}
329339
});

extensions/ql-vscode/src/view/result-table-utils.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ export const toggleDiagnosticsClassName = `${className}-toggle-diagnostics`;
1616
export const evenRowClassName = 'vscode-codeql__result-table-row--even';
1717
export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
1818
export const pathRowClassName = 'vscode-codeql__result-table-row--path';
19+
export const selectedRowClassName = 'vscode-codeql__result-table-row--selected';
1920

2021
export function jumpToLocationHandler(
2122
loc: ResolvableLocationValue,
22-
databaseUri: string
23+
databaseUri: string,
24+
callback?: () => void
2325
): (e: React.MouseEvent) => void {
2426
return (e) => {
2527
vscode.postMessage({
@@ -29,14 +31,15 @@ export function jumpToLocationHandler(
2931
});
3032
e.preventDefault();
3133
e.stopPropagation();
34+
if (callback) callback();
3235
};
3336
}
3437

3538
/**
3639
* Render a location as a link which when clicked displays the original location.
3740
*/
3841
export function renderLocation(loc: LocationValue | undefined, label: string | undefined,
39-
databaseUri: string, title?: string): JSX.Element {
42+
databaseUri: string, title?: string, callback?: () => void): JSX.Element {
4043

4144
// If the label was empty, use a placeholder instead, so the link is still clickable.
4245
let displayLabel = label;
@@ -51,7 +54,7 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
5154
return <a href="#"
5255
className="vscode-codeql__result-table-location-link"
5356
title={title}
54-
onClick={jumpToLocationHandler(resolvableLoc, databaseUri)}>{displayLabel}</a>;
57+
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>{displayLabel}</a>;
5558
} else {
5659
return <span title={title}>{displayLabel}</span>;
5760
}
@@ -63,5 +66,15 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
6366
* Returns the attributes for a zebra-striped table row at position `index`.
6467
*/
6568
export function zebraStripe(index: number, ...otherClasses: string[]): { className: string } {
66-
return { className: [(index % 2) ? oddRowClassName : evenRowClassName, otherClasses].join(' ') };
69+
return { className: [(index % 2) ? oddRowClassName : evenRowClassName, ...otherClasses].join(' ') };
70+
}
71+
72+
/**
73+
* Returns the attributes for a zebra-striped table row at position `index`,
74+
* with highlighting if `isSelected` is true.
75+
*/
76+
export function selectableZebraStripe(isSelected: boolean, index: number, ...otherClasses: string[]): { className: string } {
77+
return isSelected
78+
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
79+
: zebraStripe(index, ...otherClasses)
6780
}

extensions/ql-vscode/src/view/resultsView.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ select {
8787
background-color: var(--vscode-textBlockQuote-background);
8888
}
8989

90+
.vscode-codeql__result-table-row--selected {
91+
background-color: var(--vscode-editor-findMatchBackground);
92+
}
93+
9094
td.vscode-codeql__icon-cell {
9195
text-align: center;
9296
position: relative;

0 commit comments

Comments
 (0)