Skip to content

Commit ab1966a

Browse files
committed
Add comparison of SARIF results in compare view
This wires up the comparison of SARIF results in the compare view. It uses the same diffing algorithm as the raw results, but it uses the SARIF results instead of the raw results.
1 parent 6de9541 commit ab1966a

File tree

5 files changed

+178
-21
lines changed

5 files changed

+178
-21
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,9 @@ export interface SetComparisonsMessage {
371371
readonly message: string | undefined;
372372
}
373373

374-
type QueryCompareResult = RawQueryCompareResult | InterpretedQueryCompareResult;
374+
export type QueryCompareResult =
375+
| RawQueryCompareResult
376+
| InterpretedQueryCompareResult;
375377

376378
/**
377379
* from is the set of rows that have changes in the "from" query.
@@ -388,7 +390,7 @@ export type RawQueryCompareResult = {
388390
* from is the set of results that have changes in the "from" query.
389391
* to is the set of results that have changes in the "to" query.
390392
*/
391-
type InterpretedQueryCompareResult = {
393+
export type InterpretedQueryCompareResult = {
392394
kind: "interpreted";
393395
sourceLocationPrefix: string;
394396
from: sarif.Result[];

extensions/ql-vscode/src/compare/compare-view.ts

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { ViewColumn } from "vscode";
22

33
import {
4+
ALERTS_TABLE_NAME,
45
FromCompareViewMessage,
6+
InterpretedQueryCompareResult,
7+
QueryCompareResult,
58
RawQueryCompareResult,
69
ToCompareViewMessage,
710
} from "../common/interface-types";
@@ -25,11 +28,12 @@ import { App } from "../common/app";
2528
import { bqrsToResultSet } from "../common/bqrs-raw-results-mapper";
2629
import { RawResultSet } from "../common/raw-result-types";
2730
import {
31+
CompareQueryInfo,
2832
findCommonResultSetNames,
2933
findResultSetNames,
30-
CompareQueryInfo,
3134
getResultSetNames,
3235
} from "./result-set-names";
36+
import { compareInterpretedResults } from "./interpreted-results";
3337

3438
interface ComparePair {
3539
from: CompletedLocalQueryInfo;
@@ -146,20 +150,28 @@ export class CompareView extends AbstractWebview<
146150
panel.reveal(undefined, true);
147151

148152
await this.waitForPanelLoaded();
149-
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
150-
await this.findResultSetsToCompare(
151-
this.comparePair,
152-
selectedResultSetName,
153-
);
153+
const {
154+
currentResultSetName,
155+
currentResultSetDisplayName,
156+
fromResultSetName,
157+
toResultSetName,
158+
} = await this.findResultSetsToCompare(
159+
this.comparePair,
160+
selectedResultSetName,
161+
);
154162
if (currentResultSetDisplayName) {
155-
let result: RawQueryCompareResult | undefined;
163+
let result: QueryCompareResult | undefined;
156164
let message: string | undefined;
157165
try {
158-
result = await this.compareResults(
159-
this.comparePair,
160-
fromResultSetName,
161-
toResultSetName,
162-
);
166+
if (currentResultSetName === ALERTS_TABLE_NAME) {
167+
result = await this.compareInterpretedResults(this.comparePair);
168+
} else {
169+
result = await this.compareResults(
170+
this.comparePair,
171+
fromResultSetName,
172+
toResultSetName,
173+
);
174+
}
163175
} catch (e) {
164176
message = getErrorMessage(e);
165177
}
@@ -239,15 +251,21 @@ export class CompareView extends AbstractWebview<
239251
{ fromInfo, toInfo, commonResultSetNames }: ComparePair,
240252
selectedResultSetName: string | undefined,
241253
) {
242-
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
243-
await findResultSetNames(
244-
fromInfo,
245-
toInfo,
246-
commonResultSetNames,
247-
selectedResultSetName,
248-
);
254+
const {
255+
currentResultSetName,
256+
currentResultSetDisplayName,
257+
fromResultSetName,
258+
toResultSetName,
259+
} = await findResultSetNames(
260+
fromInfo,
261+
toInfo,
262+
commonResultSetNames,
263+
selectedResultSetName,
264+
);
249265

250266
return {
267+
commonResultSetNames,
268+
currentResultSetName,
251269
currentResultSetDisplayName,
252270
fromResultSetName,
253271
toResultSetName,
@@ -292,6 +310,18 @@ export class CompareView extends AbstractWebview<
292310
return resultsDiff(fromResultSet, toResultSet);
293311
}
294312

313+
private async compareInterpretedResults({
314+
from,
315+
to,
316+
}: ComparePair): Promise<InterpretedQueryCompareResult> {
317+
return compareInterpretedResults(
318+
this.databaseManager,
319+
this.cliServer,
320+
from,
321+
to,
322+
);
323+
}
324+
295325
private async openQuery(kind: "from" | "to") {
296326
const toOpen =
297327
kind === "from" ? this.comparePair?.from : this.comparePair?.to;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Uri } from "vscode";
2+
import * as sarif from "sarif";
3+
import { pathExists } from "fs-extra";
4+
import { sarifParser } from "../common/sarif-parser";
5+
import { CompletedLocalQueryInfo } from "../query-results";
6+
import { DatabaseManager } from "../databases/local-databases";
7+
import { CodeQLCliServer } from "../codeql-cli/cli";
8+
import { InterpretedQueryCompareResult } from "../common/interface-types";
9+
10+
import { sarifDiff } from "./sarif-diff";
11+
12+
async function getInterpretedResults(
13+
interpretedResultsPath: string,
14+
): Promise<sarif.Log | undefined> {
15+
if (!(await pathExists(interpretedResultsPath))) {
16+
return undefined;
17+
}
18+
19+
return await sarifParser(interpretedResultsPath);
20+
}
21+
22+
export async function compareInterpretedResults(
23+
databaseManager: DatabaseManager,
24+
cliServer: CodeQLCliServer,
25+
fromQuery: CompletedLocalQueryInfo,
26+
toQuery: CompletedLocalQueryInfo,
27+
): Promise<InterpretedQueryCompareResult> {
28+
const fromResultSet = await getInterpretedResults(
29+
fromQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
30+
);
31+
32+
const toResultSet = await getInterpretedResults(
33+
toQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
34+
);
35+
36+
if (!fromResultSet || !toResultSet) {
37+
throw new Error(
38+
"Could not find interpreted results for one or both queries.",
39+
);
40+
}
41+
42+
const database = databaseManager.findDatabaseItem(
43+
Uri.parse(toQuery.initialInfo.databaseInfo.databaseUri),
44+
);
45+
if (!database) {
46+
throw new Error(
47+
"Could not find database the queries. Please check that the database still exists.",
48+
);
49+
}
50+
51+
const sourceLocationPrefix = await database.getSourceLocationPrefix(
52+
cliServer,
53+
);
54+
55+
const fromResults = fromResultSet.runs[0].results;
56+
const toResults = toResultSet.runs[0].results;
57+
58+
if (!fromResults) {
59+
throw new Error("No results found in the 'from' query.");
60+
}
61+
62+
if (!toResults) {
63+
throw new Error("No results found in the 'to' query.");
64+
}
65+
66+
const { from, to } = sarifDiff(fromResults, toResults);
67+
68+
return {
69+
kind: "interpreted",
70+
sourceLocationPrefix,
71+
from,
72+
to,
73+
};
74+
}

extensions/ql-vscode/src/compare/result-set-names.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export async function findResultSetNames(
6868
const toResultSetName = currentResultSetName || defaultToResultSetName!;
6969

7070
return {
71+
currentResultSetName,
7172
currentResultSetDisplayName:
7273
currentResultSetName ||
7374
`${defaultFromResultSetName} <-> ${defaultToResultSetName}`,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as sarif from "sarif";
2+
3+
/**
4+
* Compare the alerts of two queries. Use deep equality to determine if
5+
* results have been added or removed across two invocations of a query.
6+
*
7+
* Assumptions:
8+
*
9+
* 1. Queries have the same sort order
10+
* 2. Results are not changed or re-ordered, they are only added or removed
11+
*
12+
* @param fromResults the source query
13+
* @param toResults the target query
14+
*
15+
* @throws Error when:
16+
* 1. If either query is empty
17+
* 2. If the queries are 100% disjoint
18+
*/
19+
export function sarifDiff(
20+
fromResults: sarif.Result[],
21+
toResults: sarif.Result[],
22+
) {
23+
if (!fromResults.length) {
24+
throw new Error("CodeQL Compare: Source query has no results.");
25+
}
26+
27+
if (!toResults.length) {
28+
throw new Error("CodeQL Compare: Target query has no results.");
29+
}
30+
31+
const results = {
32+
from: arrayDiff(fromResults, toResults),
33+
to: arrayDiff(toResults, fromResults),
34+
};
35+
36+
if (
37+
fromResults.length === results.from.length &&
38+
toResults.length === results.to.length
39+
) {
40+
throw new Error("CodeQL Compare: No overlap between the selected queries.");
41+
}
42+
43+
return results;
44+
}
45+
46+
function arrayDiff<T>(source: readonly T[], toRemove: readonly T[]): T[] {
47+
// Stringify the object so that we can compare hashes in the set
48+
const rest = new Set(toRemove.map((item) => JSON.stringify(item)));
49+
return source.filter((element) => !rest.has(JSON.stringify(element)));
50+
}

0 commit comments

Comments
 (0)