Skip to content

Commit 60d777a

Browse files
authored
Merge pull request #3113 from github/koesie10/compare-interpreted
Add SARIF result comparison to compare view
2 parents 34bd7c2 + cb2e502 commit 60d777a

File tree

6 files changed

+271
-60
lines changed

6 files changed

+271
-60
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 a prompt for downloading a GitHub database when opening a GitHub repository. [#3138](https://github.com/github/vscode-codeql/pull/3138)
66
- Avoid showing a popup when hovering over source elements in database source files. [#3125](https://github.com/github/vscode-codeql/pull/3125)
7+
- Add comparison of alerts when comparing query results. This allows viewing path explanations for differences in alerts. [#3113](https://github.com/github/vscode-codeql/pull/3113)
78

89
## 1.11.0 - 13 December 2023
910

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: 108 additions & 44 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,15 +28,18 @@ 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,
34+
getResultSetNames,
3035
} from "./result-set-names";
36+
import { compareInterpretedResults } from "./interpreted-results";
3137

3238
interface ComparePair {
3339
from: CompletedLocalQueryInfo;
34-
fromSchemas: BqrsInfo;
40+
fromInfo: CompareQueryInfo;
3541
to: CompletedLocalQueryInfo;
36-
toSchemas: BqrsInfo;
42+
toInfo: CompareQueryInfo;
3743

3844
commonResultSetNames: readonly string[];
3945
}
@@ -62,23 +68,48 @@ export class CompareView extends AbstractWebview<
6268
to: CompletedLocalQueryInfo,
6369
selectedResultSetName?: string,
6470
) {
65-
const fromSchemas = await this.cliServer.bqrsInfo(
66-
from.completedQuery.query.resultsPaths.resultsPath,
67-
);
68-
const toSchemas = await this.cliServer.bqrsInfo(
69-
to.completedQuery.query.resultsPaths.resultsPath,
70-
);
71+
const [fromSchemas, toSchemas] = await Promise.all([
72+
this.cliServer.bqrsInfo(
73+
from.completedQuery.query.resultsPaths.resultsPath,
74+
),
75+
this.cliServer.bqrsInfo(to.completedQuery.query.resultsPaths.resultsPath),
76+
]);
7177

72-
const commonResultSetNames = await findCommonResultSetNames(
73-
fromSchemas,
74-
toSchemas,
78+
const [fromSchemaNames, toSchemaNames] = await Promise.all([
79+
getResultSetNames(
80+
fromSchemas,
81+
from.completedQuery.query.metadata,
82+
from.completedQuery.query.resultsPaths.interpretedResultsPath,
83+
),
84+
getResultSetNames(
85+
toSchemas,
86+
to.completedQuery.query.metadata,
87+
to.completedQuery.query.resultsPaths.interpretedResultsPath,
88+
),
89+
]);
90+
91+
const commonResultSetNames = findCommonResultSetNames(
92+
fromSchemaNames,
93+
toSchemaNames,
7594
);
7695

7796
this.comparePair = {
7897
from,
79-
fromSchemas,
98+
fromInfo: {
99+
schemas: fromSchemas,
100+
schemaNames: fromSchemaNames,
101+
metadata: from.completedQuery.query.metadata,
102+
interpretedResultsPath:
103+
from.completedQuery.query.resultsPaths.interpretedResultsPath,
104+
},
80105
to,
81-
toSchemas,
106+
toInfo: {
107+
schemas: toSchemas,
108+
schemaNames: toSchemaNames,
109+
metadata: to.completedQuery.query.metadata,
110+
interpretedResultsPath:
111+
to.completedQuery.query.resultsPaths.interpretedResultsPath,
112+
},
82113
commonResultSetNames,
83114
};
84115

@@ -119,16 +150,28 @@ export class CompareView extends AbstractWebview<
119150
panel.reveal(undefined, true);
120151

121152
await this.waitForPanelLoaded();
122-
const { currentResultSetDisplayName, fromResultSet, toResultSet } =
123-
await this.findResultSetsToCompare(
124-
this.comparePair,
125-
selectedResultSetName,
126-
);
153+
const {
154+
currentResultSetName,
155+
currentResultSetDisplayName,
156+
fromResultSetName,
157+
toResultSetName,
158+
} = await this.findResultSetsToCompare(
159+
this.comparePair,
160+
selectedResultSetName,
161+
);
127162
if (currentResultSetDisplayName) {
128-
let result: RawQueryCompareResult | undefined;
163+
let result: QueryCompareResult | undefined;
129164
let message: string | undefined;
130165
try {
131-
result = this.compareResults(fromResultSet, toResultSet);
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+
}
132175
} catch (e) {
133176
message = getErrorMessage(e);
134177
}
@@ -205,31 +248,27 @@ export class CompareView extends AbstractWebview<
205248
}
206249

207250
private async findResultSetsToCompare(
208-
{ from, fromSchemas, to, toSchemas, commonResultSetNames }: ComparePair,
251+
{ fromInfo, toInfo, commonResultSetNames }: ComparePair,
209252
selectedResultSetName: string | undefined,
210253
) {
211-
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
212-
await findResultSetNames(
213-
fromSchemas,
214-
toSchemas,
215-
commonResultSetNames,
216-
selectedResultSetName,
217-
);
218-
219-
const fromResultSet = await this.getResultSet(
220-
fromSchemas,
254+
const {
255+
currentResultSetName,
256+
currentResultSetDisplayName,
221257
fromResultSetName,
222-
from.completedQuery.query.resultsPaths.resultsPath,
223-
);
224-
const toResultSet = await this.getResultSet(
225-
toSchemas,
226258
toResultSetName,
227-
to.completedQuery.query.resultsPaths.resultsPath,
259+
} = await findResultSetNames(
260+
fromInfo,
261+
toInfo,
262+
commonResultSetNames,
263+
selectedResultSetName,
228264
);
265+
229266
return {
267+
commonResultSetNames,
268+
currentResultSetName,
230269
currentResultSetDisplayName,
231-
fromResultSet,
232-
toResultSet,
270+
fromResultSetName,
271+
toResultSetName,
233272
};
234273
}
235274

@@ -252,12 +291,37 @@ export class CompareView extends AbstractWebview<
252291
return bqrsToResultSet(schema, chunk);
253292
}
254293

255-
private compareResults(
256-
fromResults: RawResultSet,
257-
toResults: RawResultSet,
258-
): RawQueryCompareResult {
259-
// Only compare columns that have the same name
260-
return resultsDiff(fromResults, toResults);
294+
private async compareResults(
295+
{ from, fromInfo, to, toInfo }: ComparePair,
296+
fromResultSetName: string,
297+
toResultSetName: string,
298+
): Promise<RawQueryCompareResult> {
299+
const [fromResultSet, toResultSet] = await Promise.all([
300+
this.getResultSet(
301+
fromInfo.schemas,
302+
fromResultSetName,
303+
from.completedQuery.query.resultsPaths.resultsPath,
304+
),
305+
this.getResultSet(
306+
toInfo.schemas,
307+
toResultSetName,
308+
to.completedQuery.query.resultsPaths.resultsPath,
309+
),
310+
]);
311+
312+
return resultsDiff(fromResultSet, toResultSet);
313+
}
314+
315+
private async compareInterpretedResults({
316+
from,
317+
to,
318+
}: ComparePair): Promise<InterpretedQueryCompareResult> {
319+
return compareInterpretedResults(
320+
this.databaseManager,
321+
this.cliServer,
322+
from,
323+
to,
324+
);
261325
}
262326

263327
private async openQuery(kind: "from" | "to") {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 database = databaseManager.findDatabaseItem(
29+
Uri.parse(toQuery.initialInfo.databaseInfo.databaseUri),
30+
);
31+
if (!database) {
32+
throw new Error(
33+
"Could not find database the queries. Please check that the database still exists.",
34+
);
35+
}
36+
37+
const [fromResultSet, toResultSet, sourceLocationPrefix] = await Promise.all([
38+
getInterpretedResults(
39+
fromQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
40+
),
41+
getInterpretedResults(
42+
toQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
43+
),
44+
database.getSourceLocationPrefix(cliServer),
45+
]);
46+
47+
if (!fromResultSet || !toResultSet) {
48+
throw new Error(
49+
"Could not find interpreted results for one or both queries.",
50+
);
51+
}
52+
53+
const fromResults = fromResultSet.runs[0].results;
54+
const toResults = toResultSet.runs[0].results;
55+
56+
if (!fromResults) {
57+
throw new Error("No results found in the 'from' query.");
58+
}
59+
60+
if (!toResults) {
61+
throw new Error("No results found in the 'to' query.");
62+
}
63+
64+
const { from, to } = sarifDiff(fromResults, toResults);
65+
66+
return {
67+
kind: "interpreted",
68+
sourceLocationPrefix,
69+
from,
70+
to,
71+
};
72+
}

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

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,49 @@
1+
import { pathExists } from "fs-extra";
12
import { BqrsInfo } from "../common/bqrs-cli-types";
2-
import { getDefaultResultSetName } from "../common/interface-types";
3+
import {
4+
ALERTS_TABLE_NAME,
5+
getDefaultResultSetName,
6+
QueryMetadata,
7+
} from "../common/interface-types";
38

4-
export async function findCommonResultSetNames(
5-
fromSchemas: BqrsInfo,
6-
toSchemas: BqrsInfo,
9+
export async function getResultSetNames(
10+
schemas: BqrsInfo,
11+
metadata: QueryMetadata | undefined,
12+
interpretedResultsPath: string | undefined,
713
): Promise<string[]> {
8-
const fromSchemaNames = fromSchemas["result-sets"].map(
9-
(schema) => schema.name,
10-
);
11-
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
14+
const schemaNames = schemas["result-sets"].map((schema) => schema.name);
15+
16+
if (metadata?.kind !== "graph" && interpretedResultsPath) {
17+
if (await pathExists(interpretedResultsPath)) {
18+
schemaNames.push(ALERTS_TABLE_NAME);
19+
}
20+
}
1221

22+
return schemaNames;
23+
}
24+
25+
export function findCommonResultSetNames(
26+
fromSchemaNames: string[],
27+
toSchemaNames: string[],
28+
): string[] {
1329
return fromSchemaNames.filter((name) => toSchemaNames.includes(name));
1430
}
1531

32+
export type CompareQueryInfo = {
33+
schemas: BqrsInfo;
34+
schemaNames: string[];
35+
metadata: QueryMetadata | undefined;
36+
interpretedResultsPath: string;
37+
};
38+
1639
export async function findResultSetNames(
17-
fromSchemas: BqrsInfo,
18-
toSchemas: BqrsInfo,
40+
from: CompareQueryInfo,
41+
to: CompareQueryInfo,
1942
commonResultSetNames: readonly string[],
2043
selectedResultSetName: string | undefined,
2144
) {
22-
const fromSchemaNames = fromSchemas["result-sets"].map(
23-
(schema) => schema.name,
24-
);
25-
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
45+
const fromSchemaNames = from.schemaNames;
46+
const toSchemaNames = to.schemaNames;
2647

2748
// Fall back on the default result set names if there are no common ones.
2849
const defaultFromResultSetName = fromSchemaNames.find((name) =>
@@ -47,6 +68,7 @@ export async function findResultSetNames(
4768
const toResultSetName = currentResultSetName || defaultToResultSetName!;
4869

4970
return {
71+
currentResultSetName,
5072
currentResultSetDisplayName:
5173
currentResultSetName ||
5274
`${defaultFromResultSetName} <-> ${defaultToResultSetName}`,

0 commit comments

Comments
 (0)