Skip to content

Commit fceea64

Browse files
committed
More work on diffs
1 parent e9fbd6d commit fceea64

25 files changed

+892
-297
lines changed

extensions/ql-vscode/gulpfile.js/webpack.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const config: webpack.Configuration = {
55
mode: 'development',
66
entry: {
77
resultsView: './src/view/results.tsx',
8-
compareView: './src/compare/view/compare.tsx',
8+
compareView: './src/compare/view/Compare.tsx',
99
},
1010
output: {
1111
path: path.resolve(__dirname, '..', 'out'),
Lines changed: 175 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
import { DisposableObject } from "semmle-vscode-utils";
2-
import { WebviewPanel, ExtensionContext, window as Window, ViewColumn, Uri } from "vscode";
3-
import * as path from 'path';
2+
import {
3+
WebviewPanel,
4+
ExtensionContext,
5+
window as Window,
6+
ViewColumn,
7+
Uri,
8+
} from "vscode";
9+
import * as path from "path";
410

511
import { tmpDir } from "../run-queries";
612
import { CompletedQuery } from "../query-results";
7-
import { CompareViewMessage } from "../interface-types";
13+
import {
14+
FromCompareViewMessage,
15+
ToCompareViewMessage,
16+
QueryCompareResult,
17+
} from "../interface-types";
818
import { Logger } from "../logging";
919
import { CodeQLCliServer } from "../cli";
1020
import { DatabaseManager } from "../databases";
11-
import { getHtmlForWebview, WebviewReveal } from "../webview-utils";
12-
import { showAndLogErrorMessage } from "../helpers";
21+
import {
22+
getHtmlForWebview,
23+
jumpToLocation,
24+
} from "../webview-utils";
25+
import { adaptSchema, adaptBqrs, RawResultSet } from "../adapt";
26+
import { BQRSInfo } from "../bqrs-cli-types";
27+
import resultsDiff from "./resultsDiff";
1328

1429
interface ComparePair {
1530
from: CompletedQuery;
@@ -19,6 +34,8 @@ interface ComparePair {
1934
export class CompareInterfaceManager extends DisposableObject {
2035
private comparePair: ComparePair | undefined;
2136
private panel: WebviewPanel | undefined;
37+
private panelLoaded = false;
38+
private panelLoadedCallBacks: (() => void)[] = [];
2239

2340
constructor(
2441
public ctx: ExtensionContext,
@@ -29,20 +46,59 @@ export class CompareInterfaceManager extends DisposableObject {
2946
super();
3047
}
3148

32-
showResults(from: CompletedQuery, to: CompletedQuery, forceReveal = WebviewReveal.NotForced) {
49+
async showResults(
50+
from: CompletedQuery,
51+
to: CompletedQuery,
52+
selectedResultSetName?: string
53+
) {
3354
this.comparePair = { from, to };
34-
if (forceReveal === WebviewReveal.Forced) {
35-
this.getPanel().reveal(undefined, true);
55+
this.getPanel().reveal(undefined, true);
56+
57+
await this.waitForPanelLoaded();
58+
const [
59+
commonResultSetNames,
60+
currentResultSetName,
61+
fromResultSet,
62+
toResultSet,
63+
] = await this.findCommonResultSetNames(from, to, selectedResultSetName);
64+
if (currentResultSetName) {
65+
await this.postMessage({
66+
t: "setComparisons",
67+
stats: {
68+
fromQuery: {
69+
// since we split the description into several rows
70+
// only run interpolation if the label is user-defined
71+
// otherwise we will wind up with duplicated rows
72+
name: from.options.label
73+
? from.interpolate(from.getLabel())
74+
: from.queryName,
75+
status: from.statusString,
76+
time: from.time,
77+
},
78+
toQuery: {
79+
name: to.options.label
80+
? to.interpolate(from.getLabel())
81+
: to.queryName,
82+
status: to.statusString,
83+
time: to.time,
84+
},
85+
},
86+
columns: fromResultSet.schema.columns,
87+
commonResultSetNames,
88+
currentResultSetName: currentResultSetName,
89+
rows: this.compareResults(fromResultSet, toResultSet),
90+
datebaseUri: to.database.databaseUri,
91+
});
3692
}
3793
}
3894

3995
getPanel(): WebviewPanel {
4096
if (this.panel == undefined) {
4197
const { ctx } = this;
4298
const panel = (this.panel = Window.createWebviewPanel(
43-
"compareView", // internal name
44-
"Compare CodeQL Query Results", // user-visible name
45-
{ viewColumn: ViewColumn.Beside, preserveFocus: true },
99+
"compareView",
100+
"Compare CodeQL Query Results",
101+
{ viewColumn: ViewColumn.Active, preserveFocus: true },
46102
{
47103
enableScripts: true,
48104
enableFindWidget: true,
@@ -54,7 +110,10 @@ export class CompareInterfaceManager extends DisposableObject {
54110
}
55111
));
56112
this.panel.onDidDispose(
57-
() => this.panel = undefined,
113+
() => {
114+
this.panel = undefined;
115+
this.comparePair = undefined;
116+
},
58117
null,
59118
ctx.subscriptions
60119
);
@@ -64,10 +123,14 @@ export class CompareInterfaceManager extends DisposableObject {
64123
);
65124

66125
const stylesheetPathOnDisk = Uri.file(
67-
ctx.asAbsolutePath("out/compareView.css")
126+
ctx.asAbsolutePath("out/resultsView.css")
68127
);
69128

70-
panel.webview.html = getHtmlForWebview(panel.webview, scriptPathOnDisk, stylesheetPathOnDisk);
129+
panel.webview.html = getHtmlForWebview(
130+
panel.webview,
131+
scriptPathOnDisk,
132+
stylesheetPathOnDisk
133+
);
71134
panel.webview.onDidReceiveMessage(
72135
async (e) => this.handleMsgFromView(e),
73136
undefined,
@@ -77,9 +140,103 @@ export class CompareInterfaceManager extends DisposableObject {
77140
return this.panel;
78141
}
79142

80-
private async handleMsgFromView(msg: CompareViewMessage): Promise<void> {
81-
/** TODO */
82-
showAndLogErrorMessage(JSON.stringify(msg));
83-
showAndLogErrorMessage(JSON.stringify(this.comparePair));
143+
private waitForPanelLoaded(): Promise<void> {
144+
return new Promise((resolve) => {
145+
if (this.panelLoaded) {
146+
resolve();
147+
} else {
148+
this.panelLoadedCallBacks.push(resolve);
149+
}
150+
});
151+
}
152+
153+
private async handleMsgFromView(msg: FromCompareViewMessage): Promise<void> {
154+
switch (msg.t) {
155+
case "compareViewLoaded":
156+
this.panelLoaded = true;
157+
this.panelLoadedCallBacks.forEach((cb) => cb());
158+
this.panelLoadedCallBacks = [];
159+
break;
160+
161+
case "changeCompare":
162+
this.changeTable(msg.newResultSetName);
163+
break;
164+
165+
case "viewSourceFile":
166+
await jumpToLocation(msg, this.databaseManager, this.logger);
167+
break;
168+
}
169+
}
170+
171+
private postMessage(msg: ToCompareViewMessage): Thenable<boolean> {
172+
return this.getPanel().webview.postMessage(msg);
173+
}
174+
175+
private async findCommonResultSetNames(
176+
from: CompletedQuery,
177+
to: CompletedQuery,
178+
selectedResultSetName: string | undefined
179+
): Promise<[string[], string, RawResultSet, RawResultSet]> {
180+
const fromSchemas = await this.cliServer.bqrsInfo(
181+
from.query.resultsPaths.resultsPath
182+
);
183+
const toSchemas = await this.cliServer.bqrsInfo(
184+
to.query.resultsPaths.resultsPath
185+
);
186+
const fromSchemaNames = fromSchemas["result-sets"].map(
187+
(schema) => schema.name
188+
);
189+
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
190+
const commonResultSetNames = fromSchemaNames.filter((name) =>
191+
toSchemaNames.includes(name)
192+
);
193+
const currentResultSetName = selectedResultSetName || commonResultSetNames[0];
194+
const fromResultSet = await this.getResultSet(
195+
fromSchemas,
196+
currentResultSetName,
197+
from.query.resultsPaths.resultsPath
198+
);
199+
const toResultSet = await this.getResultSet(
200+
toSchemas,
201+
currentResultSetName,
202+
to.query.resultsPaths.resultsPath
203+
);
204+
return [
205+
commonResultSetNames,
206+
currentResultSetName,
207+
fromResultSet,
208+
toResultSet,
209+
];
210+
}
211+
212+
private async changeTable(newResultSetName: string) {
213+
if (!this.comparePair?.from || !this.comparePair.to) {
214+
return;
215+
}
216+
await this.showResults(this.comparePair.from, this.comparePair.to, newResultSetName);
217+
}
218+
219+
private async getResultSet(
220+
bqrsInfo: BQRSInfo,
221+
resultSetName: string,
222+
resultsPath: string
223+
): Promise<RawResultSet> {
224+
const schema = bqrsInfo["result-sets"].find(
225+
(schema) => schema.name === resultSetName
226+
);
227+
if (!schema) {
228+
throw new Error(`Schema ${resultSetName} not found.`);
229+
}
230+
const chunk = await this.cliServer.bqrsDecode(resultsPath, resultSetName);
231+
const adaptedSchema = adaptSchema(schema);
232+
return adaptBqrs(adaptedSchema, chunk);
233+
}
234+
235+
private compareResults(
236+
fromResults: RawResultSet,
237+
toResults: RawResultSet
238+
): QueryCompareResult {
239+
// Only compare columns that have the same name
240+
return resultsDiff(fromResults, toResults);
84241
}
85242
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { RawResultSet } from "../adapt";
2+
import { QueryCompareResult } from "../interface-types";
3+
4+
/**
5+
* Compare the rows of two queries. Use deep equality to determine if
6+
* rows have been added or removed across two invocations of a query.
7+
*
8+
* Assumptions:
9+
*
10+
* 1. Queries have the same sort order
11+
* 2. Queries have same number and order of columns
12+
* 3. Rows are not changed or re-ordered, they are only added or removed
13+
*
14+
* @param fromResults the source query
15+
* @param toResults the target query
16+
*
17+
* @throws Error when:
18+
* 1. number of columns do not match
19+
* 2. If either query is empty
20+
* 3. If the queries are 100% disjoint
21+
*/
22+
export default function resultsDiff(
23+
fromResults: RawResultSet,
24+
toResults: RawResultSet
25+
): QueryCompareResult {
26+
27+
if (fromResults.schema.columns.length !== toResults.schema.columns.length) {
28+
throw new Error("CodeQL Compare: Columns do not match.");
29+
}
30+
31+
if (!fromResults.rows.length) {
32+
throw new Error("CodeQL Compare: Source query has no results.");
33+
}
34+
35+
if (!toResults.rows.length) {
36+
throw new Error("CodeQL Compare: Target query has no results.");
37+
}
38+
39+
const results = {
40+
from: arrayDiff(fromResults.rows, toResults.rows),
41+
to: arrayDiff(toResults.rows, fromResults.rows),
42+
};
43+
44+
if (
45+
fromResults.rows.length === results.from.length &&
46+
toResults.rows.length === results.to.length
47+
) {
48+
throw new Error("CodeQL Compare: No overlap between the selected queries.");
49+
}
50+
51+
return results;
52+
}
53+
54+
function arrayDiff<T>(source: readonly T[], toRemove: readonly T[]): T[] {
55+
// Stringify the object so that we can compare hashes in the set
56+
const rest = new Set(toRemove.map((item) => JSON.stringify(item)));
57+
return source.filter((element) => !rest.has(JSON.stringify(element)));
58+
}

0 commit comments

Comments
 (0)