Skip to content

Commit ae8cab3

Browse files
authored
Merge pull request #232 from jcreedcmu/jcreed/sort-interpreted
Support sorting for interpreted alerts
2 parents 42c8ff5 + d5b35a4 commit ae8cab3

File tree

14 files changed

+238
-93
lines changed

14 files changed

+238
-93
lines changed

extensions/ql-vscode/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Add an icon next to any failed query runs in the query history
66
view.
7+
- Add the ability to sort alerts by alert message.
78

89
## 1.0.4 - 24 January 2020
910

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

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ export interface DatabaseInfo {
1717
}
1818

1919
/** Arbitrary query metadata */
20-
export interface QueryMetadata {
21-
name?: string,
22-
description?: string,
23-
id?: string,
24-
kind?: string
20+
export interface QueryMetadata {
21+
name?: string,
22+
description?: string,
23+
id?: string,
24+
kind?: string
2525
}
2626

2727
export interface PreviousExecution {
@@ -34,6 +34,11 @@ export interface PreviousExecution {
3434
export interface Interpretation {
3535
sourceLocationPrefix: string;
3636
numTruncatedResults: number;
37+
/**
38+
* sortState being undefined means don't sort, just present results in the order
39+
* they appear in the sarif file.
40+
*/
41+
sortState?: InterpretedResultsSortState;
3742
sarif: sarif.Log;
3843
}
3944

@@ -44,7 +49,7 @@ export interface ResultsPaths {
4449

4550
export interface SortedResultSetInfo {
4651
resultsPath: string;
47-
sortState: SortState;
52+
sortState: RawResultsSortState;
4853
}
4954

5055
export type SortedResultsMap = { [resultSet: string]: SortedResultSetInfo };
@@ -84,7 +89,12 @@ export interface NavigatePathMsg {
8489

8590
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
8691

87-
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;
92+
export type FromResultsViewMsg =
93+
| ViewSourceFileMsg
94+
| ToggleDiagnostics
95+
| ChangeRawResultsSortMsg
96+
| ChangeInterpretedResultsSortMsg
97+
| ResultViewLoaded;
8898

8999
interface ViewSourceFileMsg {
90100
t: 'viewSourceFile';
@@ -109,13 +119,34 @@ export enum SortDirection {
109119
asc, desc
110120
}
111121

112-
export interface SortState {
122+
export interface RawResultsSortState {
113123
columnIndex: number;
114-
direction: SortDirection;
124+
sortDirection: SortDirection;
125+
}
126+
127+
export type InterpretedResultsSortColumn =
128+
'alert-message';
129+
130+
export interface InterpretedResultsSortState {
131+
sortBy: InterpretedResultsSortColumn;
132+
sortDirection: SortDirection;
115133
}
116134

117-
interface ChangeSortMsg {
135+
interface ChangeRawResultsSortMsg {
118136
t: 'changeSort';
119137
resultSetName: string;
120-
sortState?: SortState;
138+
/**
139+
* sortState being undefined means don't sort, just present results in the order
140+
* they appear in the sarif file.
141+
*/
142+
sortState?: RawResultsSortState;
143+
}
144+
145+
interface ChangeInterpretedResultsSortMsg {
146+
t: 'changeInterpretedSort';
147+
/**
148+
* sortState being undefined means don't sort, just present results in the order
149+
* they appear in the sarif file.
150+
*/
151+
sortState?: InterpretedResultsSortState;
121152
}

extensions/ql-vscode/src/interface.ts

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { CodeQLCliServer } from './cli';
1010
import { DatabaseItem, DatabaseManager } from './databases';
1111
import { showAndLogErrorMessage } from './helpers';
1212
import { assertNever } from './helpers-pure';
13-
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap } from './interface-types';
13+
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
1414
import { Logger } from './logging';
1515
import * as messages from './messages';
1616
import { CompletedQuery, interpretResults } from './query-results';
@@ -86,6 +86,29 @@ export function webviewUriToFileUri(webviewUri: string): Uri {
8686
return Uri.file(path);
8787
}
8888

89+
function sortMultiplier(sortDirection: SortDirection): number {
90+
switch (sortDirection) {
91+
case SortDirection.asc: return 1;
92+
case SortDirection.desc: return -1;
93+
}
94+
}
95+
96+
function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedResultsSortState | undefined): void {
97+
if (sortState !== undefined) {
98+
const multiplier = sortMultiplier(sortState.sortDirection);
99+
switch (sortState.sortBy) {
100+
case 'alert-message':
101+
results.sort((a, b) =>
102+
a.message.text === undefined ? 0 :
103+
b.message.text === undefined ? 0 :
104+
multiplier * (a.message.text?.localeCompare(b.message.text)));
105+
break;
106+
default:
107+
assertNever(sortState.sortBy);
108+
}
109+
}
110+
}
111+
89112
export class InterfaceManager extends DisposableObject {
90113
private _displayedQuery?: CompletedQuery;
91114
private _panel: vscode.WebviewPanel | undefined;
@@ -138,6 +161,17 @@ export class InterfaceManager extends DisposableObject {
138161
return this._panel;
139162
}
140163

164+
private async changeSortState(update: (query: CompletedQuery) => Promise<void>): Promise<void> {
165+
if (this._displayedQuery === undefined) {
166+
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
167+
return;
168+
}
169+
// Notify the webview that it should expect new results.
170+
await this.postMessage({ t: 'resultsUpdating' });
171+
await update(this._displayedQuery);
172+
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
173+
}
174+
141175
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
142176
switch (msg.t) {
143177
case 'viewSourceFile': {
@@ -179,17 +213,12 @@ export class InterfaceManager extends DisposableObject {
179213
this._panelLoadedCallBacks.forEach(cb => cb());
180214
this._panelLoadedCallBacks = [];
181215
break;
182-
case 'changeSort': {
183-
if (this._displayedQuery === undefined) {
184-
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
185-
break;
186-
}
187-
// Notify the webview that it should expect new results.
188-
await this.postMessage({ t: 'resultsUpdating' });
189-
await this._displayedQuery.updateSortState(this.cliServer, msg.resultSetName, msg.sortState);
190-
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
216+
case 'changeSort':
217+
await this.changeSortState((query) => query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState));
218+
break;
219+
case 'changeInterpretedSort':
220+
await this.changeSortState((query) => query.updateInterpretedSortState(this.cliServer, msg.sortState));
191221
break;
192-
}
193222
default:
194223
assertNever(msg);
195224
}
@@ -223,7 +252,7 @@ export class InterfaceManager extends DisposableObject {
223252
return;
224253
}
225254

226-
const interpretation = await this.interpretResultsInfo(results.query);
255+
const interpretation = await this.interpretResultsInfo(results.query, results.interpretedResultsSortState);
227256

228257
const sortedResultsMap: SortedResultsMap = {};
229258
results.sortedResultsInfo.forEach((v, k) =>
@@ -268,28 +297,29 @@ export class InterfaceManager extends DisposableObject {
268297
});
269298
}
270299

271-
private async getTruncatedResults(metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo: cli.SourceInfo | undefined, sourceLocationPrefix: string): Promise<Interpretation> {
272-
const sarif = await interpretResults(this.cliServer, metadata, resultsPaths.interpretedResultsPath, sourceInfo);
300+
private async getTruncatedResults(metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo: cli.SourceInfo | undefined, sourceLocationPrefix: string, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation> {
301+
const sarif = await interpretResults(this.cliServer, metadata, resultsPaths.resultsPath, sourceInfo);
273302
// For performance reasons, limit the number of results we try
274303
// to serialize and send to the webview. TODO: possibly also
275304
// limit number of paths per result, number of steps per path,
276305
// or throw an error if we are in aggregate trying to send
277306
// massively too much data, as it can make the extension
278307
// unresponsive.
308+
279309
let numTruncatedResults = 0;
280310
sarif.runs.forEach(run => {
281311
if (run.results !== undefined) {
312+
sortInterpretedResults(run.results, sortState);
282313
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
283314
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
284315
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
285316
}
286317
}
287318
});
288-
return { sarif, sourceLocationPrefix, numTruncatedResults };
289-
;
319+
return { sarif, sourceLocationPrefix, numTruncatedResults, sortState };
290320
}
291321

292-
private async interpretResultsInfo(query: QueryInfo): Promise<Interpretation | undefined> {
322+
private async interpretResultsInfo(query: QueryInfo, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation | undefined> {
293323
let interpretation: Interpretation | undefined = undefined;
294324
if (await query.hasInterpretedResults()
295325
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
@@ -300,7 +330,7 @@ export class InterfaceManager extends DisposableObject {
300330
const sourceInfo = sourceArchiveUri === undefined ?
301331
undefined :
302332
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
303-
interpretation = await this.getTruncatedResults(query.metadata, query.resultsPaths, sourceInfo, sourceLocationPrefix);
333+
interpretation = await this.getTruncatedResults(query.metadata, query.resultsPaths, sourceInfo, sourceLocationPrefix, sortState);
304334
}
305335
catch (e) {
306336
// If interpretation fails, accept the error and continue
@@ -318,7 +348,13 @@ export class InterfaceManager extends DisposableObject {
318348
const sourceInfo = sourceArchiveUri === undefined ?
319349
undefined :
320350
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
321-
const interpretation = await this.getTruncatedResults(metadata, resultsInfo, sourceInfo, sourceLocationPrefix);
351+
const interpretation = await this.getTruncatedResults(
352+
metadata,
353+
resultsInfo,
354+
sourceInfo,
355+
sourceLocationPrefix,
356+
undefined,
357+
);
322358

323359
try {
324360
await this.showProblemResultsAsDiagnostics(interpretation, database);

extensions/ql-vscode/src/query-results.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as cli from './cli';
55
import * as sarif from 'sarif';
66
import * as fs from 'fs-extra';
77
import * as path from 'path';
8-
import { SortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata } from "./interface-types";
8+
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState } from "./interface-types";
99
import { QueryHistoryConfig } from "./config";
1010
import { QueryHistoryItemOptions } from "./query-history";
1111

@@ -15,11 +15,20 @@ export class CompletedQuery implements QueryWithResults {
1515
readonly result: messages.EvaluationResult;
1616
readonly database: DatabaseInfo;
1717
options: QueryHistoryItemOptions;
18+
1819
/**
1920
* Map from result set name to SortedResultSetInfo.
2021
*/
2122
sortedResultsInfo: Map<string, SortedResultSetInfo>;
2223

24+
/**
25+
* How we're currently sorting alerts. This is not mere interface
26+
* state due to truncation; on re-sort, we want to read in the file
27+
* again, sort it, and only ship off a reasonable number of results
28+
* to the webview. Undefined means to use whatever order is in the
29+
* sarif file.
30+
*/
31+
interpretedResultsSortState: InterpretedResultsSortState | undefined;
2332

2433
constructor(
2534
evalaution: QueryWithResults,
@@ -92,7 +101,8 @@ export class CompletedQuery implements QueryWithResults {
92101
toString(): string {
93102
return this.interpolate(this.getLabel());
94103
}
95-
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
104+
105+
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: RawResultsSortState | undefined): Promise<void> {
96106
if (sortState === undefined) {
97107
this.sortedResultsInfo.delete(resultSetName);
98108
return;
@@ -103,10 +113,13 @@ export class CompletedQuery implements QueryWithResults {
103113
sortState
104114
};
105115

106-
await server.sortBqrs(this.query.resultsPaths.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.direction]);
116+
await server.sortBqrs(this.query.resultsPaths.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.sortDirection]);
107117
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
108118
}
109119

120+
async updateInterpretedSortState(_server: cli.CodeQLCliServer, sortState: InterpretedResultsSortState | undefined): Promise<void> {
121+
this.interpretedResultsSortState = sortState;
122+
}
110123
}
111124

112125
/**

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import * as Sarif from 'sarif';
44
import * as Keys from '../result-keys';
55
import { LocationStyle } from 'semmle-bqrs';
66
import * as octicons from './octicons';
7-
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation } from './result-table-utils';
8-
import { PathTableResultSet, onNavigation, NavigationEvent } from './results';
7+
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
8+
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
99
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
10+
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
1011

1112
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
1213
export interface PathTableState {
@@ -43,9 +44,41 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
4344
e.preventDefault();
4445
}
4546

47+
sortClass(column: InterpretedResultsSortColumn): string {
48+
const sortState = this.props.resultSet.sortState;
49+
if (sortState !== undefined && sortState.sortBy === column) {
50+
return sortState.sortDirection === SortDirection.asc ? 'sort-asc' : 'sort-desc';
51+
}
52+
else {
53+
return 'sort-none';
54+
}
55+
}
56+
57+
getNextSortState(column: InterpretedResultsSortColumn): InterpretedResultsSortState | undefined {
58+
const oldSortState = this.props.resultSet.sortState;
59+
const prevDirection = oldSortState && oldSortState.sortBy === column ? oldSortState.sortDirection : undefined;
60+
const nextDirection = nextSortDirection(prevDirection, true);
61+
return nextDirection === undefined ? undefined :
62+
{ sortBy: column, sortDirection: nextDirection };
63+
}
64+
65+
toggleSortStateForColumn(column: InterpretedResultsSortColumn): void {
66+
vscode.postMessage({
67+
t: 'changeInterpretedSort',
68+
sortState: this.getNextSortState(column),
69+
});
70+
}
71+
4672
render(): JSX.Element {
4773
const { databaseUri, resultSet } = this.props;
4874

75+
const header = <thead>
76+
<tr>
77+
<th colSpan={2}></th>
78+
<th className={this.sortClass('alert-message') + ' vscode-codeql__alert-message-cell'} colSpan={3} onClick={() => this.toggleSortStateForColumn('alert-message')}>Message</th>
79+
</tr>
80+
</thead>;
81+
4982
const rows: JSX.Element[] = [];
5083
const { numTruncatedResults, sourceLocationPrefix } = resultSet;
5184

@@ -65,7 +98,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
6598
result.push(<span>{part} </span>);
6699
} else {
67100
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
68-
undefined);
101+
undefined);
69102
result.push(<span>{renderedLocation} </span>);
70103
}
71104
} return result;
@@ -93,7 +126,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
93126
return renderNonLocation(text, parsedLoc.hint);
94127
case LocationStyle.FivePart:
95128
case LocationStyle.WholeFile:
96-
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
129+
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
97130
}
98131
return undefined;
99132
}
@@ -231,6 +264,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
231264
}
232265

233266
return <table className={className}>
267+
{header}
234268
<tbody>{rows}</tbody>
235269
</table>;
236270
}

0 commit comments

Comments
 (0)