Skip to content

Commit ab933fc

Browse files
committed
Add 'show next/previous alert' commands
1 parent 9002313 commit ab933fc

6 files changed

Lines changed: 130 additions & 30 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,14 @@
601601
"command": "codeQLQueryResults.previousPathStep",
602602
"title": "CodeQL: Show Previous Step on Path"
603603
},
604+
{
605+
"command": "codeQLQueryResults.nextAlert",
606+
"title": "CodeQL: Show Next Alert"
607+
},
608+
{
609+
"command": "codeQLQueryResults.previousAlert",
610+
"title": "CodeQL: Show Previous Alert"
611+
},
604612
{
605613
"command": "codeQL.restartQueryServer",
606614
"title": "CodeQL: Restart Query Server"

extensions/ql-vscode/src/interface.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
154154
this.navigatePathStep.bind(this, -1)
155155
)
156156
);
157+
this.push(
158+
commandRunner(
159+
'codeQLQueryResults.nextAlert',
160+
this.navigateAlert.bind(this, 1)
161+
)
162+
);
163+
this.push(
164+
commandRunner(
165+
'codeQLQueryResults.previousAlert',
166+
this.navigateAlert.bind(this, -1)
167+
)
168+
);
157169

158170
this.push(
159171
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
@@ -173,6 +185,10 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
173185
await this.postMessage({ t: 'navigatePath', direction });
174186
}
175187

188+
async navigateAlert(direction: number): Promise<void> {
189+
await this.postMessage({ t: 'navigateAlert', direction });
190+
}
191+
176192
protected getPanelConfig(): WebviewPanelConfig {
177193
return {
178194
viewId: 'resultsView',

extensions/ql-vscode/src/pure/interface-types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,22 @@ export interface ShowInterpretedPageMsg {
141141
queryPath: string;
142142
}
143143

144-
/** Advance to the next or previous path no in the path viewer */
144+
/** Advance to the next or previous path step in the path viewer */
145145
export interface NavigatePathMsg {
146146
t: 'navigatePath';
147147

148148
/** 1 for next, -1 for previous */
149149
direction: number;
150150
}
151151

152+
/** Advance to the next or previous alert in the path viewer */
153+
export interface NavigateAlertMsg {
154+
t: 'navigateAlert';
155+
156+
/** 1 for next, -1 for previous */
157+
direction: number;
158+
}
159+
152160
/**
153161
* A message indicating that the results view should untoggle the
154162
* "Show results in Problems view" checkbox.
@@ -165,6 +173,7 @@ export type IntoResultsViewMsg =
165173
| SetStateMsg
166174
| ShowInterpretedPageMsg
167175
| NavigatePathMsg
176+
| NavigateAlertMsg
168177
| UntoggleShowProblemsMsg;
169178

170179
/**

extensions/ql-vscode/src/pure/result-keys.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,48 @@
11
import * as sarif from 'sarif';
22

3+
/**
4+
* Identifies a result, a path, or one of the nodes on a path.
5+
*/
6+
interface ResultKeyBase {
7+
resultIndex: number;
8+
pathIndex?: number;
9+
pathNodeIndex?: number;
10+
}
11+
312
/**
413
* Identifies one of the results in a result set by its index in the result list.
514
*/
6-
export interface Result {
15+
export interface Result extends ResultKeyBase {
716
resultIndex: number;
17+
pathIndex?: undefined;
18+
pathNodeIndex?: undefined;
819
}
920

1021
/**
1122
* Identifies one of the paths associated with a result.
1223
*/
13-
export interface Path extends Result {
24+
export interface Path extends ResultKeyBase {
1425
pathIndex: number;
26+
pathNodeIndex?: undefined;
1527
}
1628

1729
/**
1830
* Identifies one of the nodes in a path.
1931
*/
20-
export interface PathNode extends Path {
32+
export interface PathNode extends ResultKeyBase {
33+
pathIndex: number;
2134
pathNodeIndex: number;
2235
}
2336

37+
export type ResultKey = Result | Path | PathNode;
38+
2439
/** Alias for `undefined` but more readable in some cases */
2540
export const none: PathNode | undefined = undefined;
2641

2742
/**
2843
* Looks up a specific result in a result set.
2944
*/
30-
export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefined {
45+
export function getResult(sarif: sarif.Log, key: Result | Path | PathNode): sarif.Result | undefined {
3146
if (sarif.runs.length === 0) return undefined;
3247
if (sarif.runs[0].results === undefined) return undefined;
3348
const results = sarif.runs[0].results;
@@ -37,7 +52,7 @@ export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefin
3752
/**
3853
* Looks up a specific path in a result set.
3954
*/
40-
export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefined {
55+
export function getPath(sarif: sarif.Log, key: Path | PathNode): sarif.ThreadFlow | undefined {
4156
const result = getResult(sarif, key);
4257
if (result === undefined) return undefined;
4358
let index = -1;
@@ -64,7 +79,7 @@ export function getPathNode(sarif: sarif.Log, key: PathNode): sarif.Location | u
6479
/**
6580
* Returns true if the two keys are both `undefined` or contain the same set of indices.
6681
*/
67-
export function equals(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
82+
export function equals(key1: Partial<PathNode> | undefined, key2: Partial<PathNode> | undefined): boolean {
6883
if (key1 === key2) return true;
6984
if (key1 === undefined || key2 === undefined) return false;
7085
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
@@ -73,7 +88,7 @@ export function equals(key1: PathNode | undefined, key2: PathNode | undefined):
7388
/**
7489
* Returns true if the two keys contain the same set of indices and neither are `undefined`.
7590
*/
76-
export function equalsNotUndefined(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
91+
export function equalsNotUndefined(key1: Partial<PathNode> | undefined, key2: Partial<PathNode> | undefined): boolean {
7792
if (key1 === undefined || key2 === undefined) return false;
7893
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
7994
}

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

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as Keys from '../../pure/result-keys';
55
import * as octicons from './octicons';
66
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection, emptyQueryResultsMessage } from './result-table-utils';
77
import { onNavigation, NavigationEvent } from './results';
8-
import { InterpretedResultSet, SarifInterpretationData } from '../../pure/interface-types';
8+
import { InterpretedResultSet, NavigateAlertMsg, NavigatePathMsg, SarifInterpretationData } from '../../pure/interface-types';
99
import {
1010
parseSarifPlainTextMessage,
1111
parseSarifLocation,
@@ -18,13 +18,13 @@ import { isWholeFileLoc, isLineColumnLoc } from '../../pure/bqrs-utils';
1818
export type PathTableProps = ResultTableProps & { resultSet: InterpretedResultSet<SarifInterpretationData> };
1919
export interface PathTableState {
2020
expanded: { [k: string]: boolean };
21-
selectedPathNode: undefined | Keys.PathNode;
21+
selectedItem: undefined | Keys.PathNode | Keys.Result;
2222
}
2323

2424
export class PathTable extends React.Component<PathTableProps, PathTableState> {
2525
constructor(props: PathTableProps) {
2626
super(props);
27-
this.state = { expanded: {}, selectedPathNode: undefined };
27+
this.state = { expanded: {}, selectedItem: undefined };
2828
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
2929
}
3030

@@ -96,7 +96,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
9696
const rows: JSX.Element[] = [];
9797
const { numTruncatedResults, sourceLocationPrefix } = resultSet.interpretation;
9898

99-
function renderRelatedLocations(msg: string, relatedLocations: Sarif.Location[]): JSX.Element[] {
99+
function renderRelatedLocations(msg: string, relatedLocations: Sarif.Location[], resultKey: Keys.PathNode | Keys.Result | undefined): JSX.Element[] {
100100
const relatedLocationsById: { [k: string]: Sarif.Location } = {};
101101
for (const loc of relatedLocations) {
102102
relatedLocationsById[loc.id!] = loc;
@@ -110,7 +110,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
110110
return <span key={i}>{part}</span>;
111111
} else {
112112
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
113-
undefined);
113+
resultKey);
114114
return <span key={i}>{renderedLocation}</span>;
115115
}
116116
});
@@ -122,16 +122,16 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
122122
return <span title={locationHint}>{msg}</span>;
123123
}
124124

125-
const updateSelectionCallback = (pathNodeKey: Keys.PathNode | undefined) => {
125+
const updateSelectionCallback = (resultKey: Keys.PathNode | Keys.Result | undefined) => {
126126
return () => {
127127
this.setState(previousState => ({
128128
...previousState,
129-
selectedPathNode: pathNodeKey
129+
selectedItem: resultKey
130130
}));
131131
};
132132
};
133133

134-
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
134+
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location, resultKey: Keys.PathNode | Keys.Result | undefined): JSX.Element | undefined {
135135
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
136136
if ('hint' in parsedLoc) {
137137
return renderNonLocation(text, parsedLoc.hint);
@@ -141,7 +141,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
141141
text,
142142
databaseUri,
143143
undefined,
144-
updateSelectionCallback(pathNodeKey)
144+
updateSelectionCallback(resultKey)
145145
);
146146
} else {
147147
return undefined;
@@ -154,7 +154,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
154154
*/
155155
function renderSarifLocation(
156156
loc: Sarif.Location,
157-
pathNodeKey: Keys.PathNode | undefined
157+
pathNodeKey: Keys.PathNode | Keys.Result | undefined
158158
): JSX.Element | undefined {
159159
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
160160
if ('hint' in parsedLoc) {
@@ -195,21 +195,25 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
195195
let expansionIndex = 0;
196196

197197
resultSet.interpretation.data.runs[0].results.forEach((result, resultIndex) => {
198+
const resultKey: Keys.Result = { resultIndex };
198199
const text = result.message.text || '[no text]';
199200
const msg: JSX.Element[] =
200201
result.relatedLocations === undefined ?
201202
[<span key="0">{text}</span>] :
202-
renderRelatedLocations(text, result.relatedLocations);
203+
renderRelatedLocations(text, result.relatedLocations, resultKey);
203204

204205
const currentResultExpanded = this.state.expanded[expansionIndex];
205206
const indicator = currentResultExpanded ? octicons.chevronDown : octicons.chevronRight;
206207
const location = result.locations !== undefined && result.locations.length > 0 &&
207-
renderSarifLocation(result.locations[0], Keys.none);
208+
renderSarifLocation(result.locations[0], resultKey);
208209
const locationCells = <td className="vscode-codeql__location-cell">{location}</td>;
209210

211+
const selectedItem = this.state.selectedItem;
212+
const resultRowIsSelected = selectedItem?.resultIndex === resultIndex && selectedItem.pathIndex === undefined;
213+
210214
if (result.codeFlows === undefined) {
211215
rows.push(
212-
<tr key={resultIndex} {...zebraStripe(resultIndex)}>
216+
<tr key={resultIndex} {...selectableZebraStripe(resultRowIsSelected, resultIndex)}>
213217
<td className="vscode-codeql__icon-cell">{octicons.info}</td>
214218
<td colSpan={3}>{msg}</td>
215219
{locationCells}
@@ -225,7 +229,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
225229
[expansionIndex];
226230

227231
rows.push(
228-
<tr {...zebraStripe(resultIndex)} key={resultIndex}>
232+
<tr {...selectableZebraStripe(resultRowIsSelected, resultIndex)} key={resultIndex}>
229233
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
230234
{indicator}
231235
</td>
@@ -268,7 +272,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
268272
const additionalMsg = step.location !== undefined ?
269273
renderSarifLocation(step.location, pathNodeKey) :
270274
'';
271-
const isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
275+
const isSelected = Keys.equalsNotUndefined(this.state.selectedItem, pathNodeKey);
272276
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
273277
const zebraIndex = resultIndex + stepIndex;
274278
rows.push(
@@ -303,14 +307,33 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
303307
}
304308

305309
private handleNavigationEvent(event: NavigationEvent) {
310+
switch (event.t) {
311+
case 'navigatePath': {
312+
this.handleNavigatePathStepEvent(event);
313+
break;
314+
}
315+
case 'navigateAlert': {
316+
this.handleNavigateAlertEvent(event);
317+
break;
318+
}
319+
}
320+
}
321+
322+
private handleNavigatePathStepEvent(event: NavigatePathMsg) {
306323
this.setState(prevState => {
307-
const { selectedPathNode } = prevState;
308-
if (selectedPathNode === undefined) return prevState;
324+
const { selectedItem } = prevState;
325+
if (selectedItem === undefined) return prevState;
309326

310-
const path = Keys.getPath(this.props.resultSet.interpretation.data, selectedPathNode);
327+
const selectedPath = selectedItem.pathIndex !== undefined
328+
? selectedItem
329+
: { ...selectedItem, pathIndex: 0 };
330+
331+
const path = Keys.getPath(this.props.resultSet.interpretation.data, selectedPath);
311332
if (path === undefined) return prevState;
312333

313-
const nextIndex = selectedPathNode.pathNodeIndex + event.direction;
334+
const nextIndex = selectedItem.pathNodeIndex !== undefined
335+
? (selectedItem.pathNodeIndex + event.direction)
336+
: 0;
314337
if (nextIndex < 0 || nextIndex >= path.locations.length) return prevState;
315338

316339
const sarifLoc = path.locations[nextIndex].location;
@@ -324,8 +347,35 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
324347
}
325348

326349
jumpToLocation(loc, this.props.databaseUri);
327-
const newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
328-
return { ...prevState, selectedPathNode: newSelection };
350+
const newSelection = { ...selectedPath, pathNodeIndex: nextIndex };
351+
return { ...prevState, selectedItem: newSelection };
352+
});
353+
}
354+
355+
private handleNavigateAlertEvent(event: NavigateAlertMsg) {
356+
this.setState(prevState => {
357+
const { selectedItem } = prevState;
358+
if (selectedItem === undefined) return prevState;
359+
360+
const nextIndex = selectedItem.resultIndex + event.direction;
361+
const nextSelection = { resultIndex: nextIndex };
362+
const result = Keys.getResult(this.props.resultSet.interpretation.data, nextSelection);
363+
if (result === undefined) {
364+
return prevState;
365+
}
366+
367+
const sarifLoc = result.locations?.[0];
368+
if (sarifLoc === undefined) {
369+
return prevState;
370+
}
371+
372+
const loc = parseSarifLocation(sarifLoc, this.props.resultSet.interpretation.sourceLocationPrefix);
373+
if (isNoLocation(loc)) {
374+
return prevState;
375+
}
376+
377+
jumpToLocation(loc, this.props.databaseUri);
378+
return { ...prevState, selectedItem: nextSelection };
329379
});
330380
}
331381

extensions/ql-vscode/src/view/results/results.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SortedResultSetInfo,
88
RawResultsSortState,
99
NavigatePathMsg,
10+
NavigateAlertMsg,
1011
QueryMetadata,
1112
ResultsPaths,
1213
ALERTS_TABLE_NAME,
@@ -62,7 +63,7 @@ interface ResultsViewState {
6263
isExpectingResultsUpdate: boolean;
6364
}
6465

65-
export type NavigationEvent = NavigatePathMsg;
66+
export type NavigationEvent = NavigatePathMsg | NavigateAlertMsg;
6667

6768
/**
6869
* Event handlers to be notified of navigation events coming from outside the webview.
@@ -146,6 +147,7 @@ export class ResultsApp extends React.Component<Record<string, never>, ResultsVi
146147
});
147148
break;
148149
case 'navigatePath':
150+
case 'navigateAlert':
149151
onNavigation.fire(msg);
150152
break;
151153

0 commit comments

Comments
 (0)