Skip to content

Commit 0ab4bff

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 13fc321 commit 0ab4bff

5 files changed

Lines changed: 159 additions & 9 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,9 @@ export interface SetComparisonsMessage {
360360
readonly databaseUri: string;
361361
}
362362

363-
type QueryCompareResult = RawQueryCompareResult | InterpretedQueryCompareResult;
363+
export type QueryCompareResult =
364+
| RawQueryCompareResult
365+
| InterpretedQueryCompareResult;
364366

365367
/**
366368
* from is the set of rows that have changes in the "from" query.
@@ -377,7 +379,7 @@ export type RawQueryCompareResult = {
377379
* from is the set of results that have changes in the "from" query.
378380
* to is the set of results that have changes in the "to" query.
379381
*/
380-
type InterpretedQueryCompareResult = {
382+
export type InterpretedQueryCompareResult = {
381383
type: "interpreted";
382384
sourceLocationPrefix: string;
383385
from: sarif.Result[];

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

Lines changed: 29 additions & 6 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";
@@ -23,6 +26,7 @@ import { telemetryListener } from "../common/vscode/telemetry";
2326
import { redactableError } from "../common/errors";
2427
import { App } from "../common/app";
2528
import { findResultSetNames, CompareQueryInfo } from "./result-set-names";
29+
import { compareInterpretedResults } from "./interpreted-results";
2630

2731
interface ComparePair {
2832
from: CompletedLocalQueryInfo;
@@ -95,6 +99,7 @@ export class CompareView extends AbstractWebview<
9599
await this.waitForPanelLoaded();
96100
const {
97101
commonResultSetNames,
102+
currentResultSetName,
98103
currentResultSetDisplayName,
99104
fromResultSetName,
100105
toResultSetName,
@@ -103,14 +108,18 @@ export class CompareView extends AbstractWebview<
103108
selectedResultSetName,
104109
);
105110
if (currentResultSetDisplayName) {
106-
let result: RawQueryCompareResult | undefined;
111+
let result: QueryCompareResult | undefined;
107112
let message: string | undefined;
108113
try {
109-
result = await this.compareResults(
110-
this.comparePair,
111-
fromResultSetName,
112-
toResultSetName,
113-
);
114+
if (currentResultSetName === ALERTS_TABLE_NAME) {
115+
result = await this.compareInterpretedResults(this.comparePair);
116+
} else {
117+
result = await this.compareResults(
118+
this.comparePair,
119+
fromResultSetName,
120+
toResultSetName,
121+
);
122+
}
114123
} catch (e) {
115124
message = getErrorMessage(e);
116125
}
@@ -209,13 +218,15 @@ export class CompareView extends AbstractWebview<
209218
) {
210219
const {
211220
commonResultSetNames,
221+
currentResultSetName,
212222
currentResultSetDisplayName,
213223
fromResultSetName,
214224
toResultSetName,
215225
} = await findResultSetNames(fromInfo, toInfo, selectedResultSetName);
216226

217227
return {
218228
commonResultSetNames,
229+
currentResultSetName,
219230
currentResultSetDisplayName,
220231
fromResultSetName,
221232
toResultSetName,
@@ -260,6 +271,18 @@ export class CompareView extends AbstractWebview<
260271
return resultsDiff(fromResultSet, toResultSet);
261272
}
262273

274+
private async compareInterpretedResults({
275+
from,
276+
to,
277+
}: ComparePair): Promise<InterpretedQueryCompareResult> {
278+
return compareInterpretedResults(
279+
this.databaseManager,
280+
this.cliServer,
281+
from,
282+
to,
283+
);
284+
}
285+
263286
private async openQuery(kind: "from" | "to") {
264287
const toOpen =
265288
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+
type: "interpreted",
70+
sourceLocationPrefix,
71+
from,
72+
to,
73+
};
74+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { pathExists } from "fs-extra";
55
export type CompareQueryInfo = {
66
schemas: BQRSInfo;
77
metadata: QueryMetadata | undefined;
8-
interpretedResultsPath: string | undefined;
8+
interpretedResultsPath: string;
99
};
1010

1111
async function getResultSetNames(info: CompareQueryInfo): Promise<string[]> {
@@ -54,6 +54,7 @@ export async function findResultSetNames(
5454

5555
return {
5656
commonResultSetNames,
57+
currentResultSetName,
5758
currentResultSetDisplayName:
5859
currentResultSetName ||
5960
`${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)