Skip to content

Commit e1c03b0

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 cdd15d8 commit e1c03b0

5 files changed

Lines changed: 179 additions & 23 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
@@ -369,7 +369,9 @@ export interface SetComparisonsMessage {
369369
readonly message: string | undefined;
370370
}
371371

372-
type QueryCompareResult = RawQueryCompareResult | InterpretedQueryCompareResult;
372+
export type QueryCompareResult =
373+
| RawQueryCompareResult
374+
| InterpretedQueryCompareResult;
373375

374376
/**
375377
* from is the set of rows that have changes in the "from" query.
@@ -386,7 +388,7 @@ export type RawQueryCompareResult = {
386388
* from is the set of results that have changes in the "from" query.
387389
* to is the set of results that have changes in the "to" query.
388390
*/
389-
type InterpretedQueryCompareResult = {
391+
export type InterpretedQueryCompareResult = {
390392
kind: "interpreted";
391393
sourceLocationPrefix: string;
392394
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";
@@ -23,11 +26,12 @@ import { telemetryListener } from "../common/vscode/telemetry";
2326
import { redactableError } from "../common/errors";
2427
import { App } from "../common/app";
2528
import {
29+
CompareQueryInfo,
2630
findCommonResultSetNames,
2731
findResultSetNames,
28-
CompareQueryInfo,
2932
getResultSetNames,
3033
} from "./result-set-names";
34+
import { compareInterpretedResults } from "./interpreted-results";
3135

3236
interface ComparePair {
3337
from: CompletedLocalQueryInfo;
@@ -144,20 +148,28 @@ export class CompareView extends AbstractWebview<
144148
panel.reveal(undefined, true);
145149

146150
await this.waitForPanelLoaded();
147-
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
148-
await this.findResultSetsToCompare(
149-
this.comparePair,
150-
selectedResultSetName,
151-
);
151+
const {
152+
currentResultSetName,
153+
currentResultSetDisplayName,
154+
fromResultSetName,
155+
toResultSetName,
156+
} = await this.findResultSetsToCompare(
157+
this.comparePair,
158+
selectedResultSetName,
159+
);
152160
if (currentResultSetDisplayName) {
153-
let result: RawQueryCompareResult | undefined;
161+
let result: QueryCompareResult | undefined;
154162
let message: string | undefined;
155163
try {
156-
result = await this.compareResults(
157-
this.comparePair,
158-
fromResultSetName,
159-
toResultSetName,
160-
);
164+
if (currentResultSetName === ALERTS_TABLE_NAME) {
165+
result = await this.compareInterpretedResults(this.comparePair);
166+
} else {
167+
result = await this.compareResults(
168+
this.comparePair,
169+
fromResultSetName,
170+
toResultSetName,
171+
);
172+
}
161173
} catch (e) {
162174
message = getErrorMessage(e);
163175
}
@@ -237,15 +249,21 @@ export class CompareView extends AbstractWebview<
237249
{ fromInfo, toInfo, commonResultSetNames }: ComparePair,
238250
selectedResultSetName: string | undefined,
239251
) {
240-
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
241-
await findResultSetNames(
242-
fromInfo,
243-
toInfo,
244-
commonResultSetNames,
245-
selectedResultSetName,
246-
);
252+
const {
253+
currentResultSetName,
254+
currentResultSetDisplayName,
255+
fromResultSetName,
256+
toResultSetName,
257+
} = await findResultSetNames(
258+
fromInfo,
259+
toInfo,
260+
commonResultSetNames,
261+
selectedResultSetName,
262+
);
247263

248264
return {
265+
commonResultSetNames,
266+
currentResultSetName,
249267
currentResultSetDisplayName,
250268
fromResultSetName,
251269
toResultSetName,
@@ -290,6 +308,18 @@ export class CompareView extends AbstractWebview<
290308
return resultsDiff(fromResultSet, toResultSet);
291309
}
292310

311+
private async compareInterpretedResults({
312+
from,
313+
to,
314+
}: ComparePair): Promise<InterpretedQueryCompareResult> {
315+
return compareInterpretedResults(
316+
this.databaseManager,
317+
this.cliServer,
318+
from,
319+
to,
320+
);
321+
}
322+
293323
private async openQuery(kind: "from" | "to") {
294324
const toOpen =
295325
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { pathExists } from "fs-extra";
2-
32
import { BQRSInfo } from "../common/bqrs-cli-types";
43
import {
54
getDefaultResultSetName,
@@ -39,7 +38,7 @@ export type CompareQueryInfo = {
3938
schemas: BQRSInfo;
4039
schemaNames: string[];
4140
metadata: QueryMetadata | undefined;
42-
interpretedResultsPath: string | undefined;
41+
interpretedResultsPath: string;
4342
};
4443

4544
export async function findResultSetNames(
@@ -74,6 +73,7 @@ export async function findResultSetNames(
7473
const toResultSetName = currentResultSetName || defaultToResultSetName!;
7574

7675
return {
76+
currentResultSetName,
7777
currentResultSetDisplayName:
7878
currentResultSetName ||
7979
`${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)