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.
This commit is contained in:
Koen Vlaswinkel 2023-12-06 14:04:47 +01:00
Родитель 6de954143c
Коммит ab1966abf1
5 изменённых файлов: 178 добавлений и 21 удалений

Просмотреть файл

@ -371,7 +371,9 @@ export interface SetComparisonsMessage {
readonly message: string | undefined;
}
type QueryCompareResult = RawQueryCompareResult | InterpretedQueryCompareResult;
export type QueryCompareResult =
| RawQueryCompareResult
| InterpretedQueryCompareResult;
/**
* from is the set of rows that have changes in the "from" query.
@ -388,7 +390,7 @@ export type RawQueryCompareResult = {
* from is the set of results that have changes in the "from" query.
* to is the set of results that have changes in the "to" query.
*/
type InterpretedQueryCompareResult = {
export type InterpretedQueryCompareResult = {
kind: "interpreted";
sourceLocationPrefix: string;
from: sarif.Result[];

Просмотреть файл

@ -1,7 +1,10 @@
import { ViewColumn } from "vscode";
import {
ALERTS_TABLE_NAME,
FromCompareViewMessage,
InterpretedQueryCompareResult,
QueryCompareResult,
RawQueryCompareResult,
ToCompareViewMessage,
} from "../common/interface-types";
@ -25,11 +28,12 @@ import { App } from "../common/app";
import { bqrsToResultSet } from "../common/bqrs-raw-results-mapper";
import { RawResultSet } from "../common/raw-result-types";
import {
CompareQueryInfo,
findCommonResultSetNames,
findResultSetNames,
CompareQueryInfo,
getResultSetNames,
} from "./result-set-names";
import { compareInterpretedResults } from "./interpreted-results";
interface ComparePair {
from: CompletedLocalQueryInfo;
@ -146,20 +150,28 @@ export class CompareView extends AbstractWebview<
panel.reveal(undefined, true);
await this.waitForPanelLoaded();
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
await this.findResultSetsToCompare(
this.comparePair,
selectedResultSetName,
);
const {
currentResultSetName,
currentResultSetDisplayName,
fromResultSetName,
toResultSetName,
} = await this.findResultSetsToCompare(
this.comparePair,
selectedResultSetName,
);
if (currentResultSetDisplayName) {
let result: RawQueryCompareResult | undefined;
let result: QueryCompareResult | undefined;
let message: string | undefined;
try {
result = await this.compareResults(
this.comparePair,
fromResultSetName,
toResultSetName,
);
if (currentResultSetName === ALERTS_TABLE_NAME) {
result = await this.compareInterpretedResults(this.comparePair);
} else {
result = await this.compareResults(
this.comparePair,
fromResultSetName,
toResultSetName,
);
}
} catch (e) {
message = getErrorMessage(e);
}
@ -239,15 +251,21 @@ export class CompareView extends AbstractWebview<
{ fromInfo, toInfo, commonResultSetNames }: ComparePair,
selectedResultSetName: string | undefined,
) {
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
await findResultSetNames(
fromInfo,
toInfo,
commonResultSetNames,
selectedResultSetName,
);
const {
currentResultSetName,
currentResultSetDisplayName,
fromResultSetName,
toResultSetName,
} = await findResultSetNames(
fromInfo,
toInfo,
commonResultSetNames,
selectedResultSetName,
);
return {
commonResultSetNames,
currentResultSetName,
currentResultSetDisplayName,
fromResultSetName,
toResultSetName,
@ -292,6 +310,18 @@ export class CompareView extends AbstractWebview<
return resultsDiff(fromResultSet, toResultSet);
}
private async compareInterpretedResults({
from,
to,
}: ComparePair): Promise<InterpretedQueryCompareResult> {
return compareInterpretedResults(
this.databaseManager,
this.cliServer,
from,
to,
);
}
private async openQuery(kind: "from" | "to") {
const toOpen =
kind === "from" ? this.comparePair?.from : this.comparePair?.to;

Просмотреть файл

@ -0,0 +1,74 @@
import { Uri } from "vscode";
import * as sarif from "sarif";
import { pathExists } from "fs-extra";
import { sarifParser } from "../common/sarif-parser";
import { CompletedLocalQueryInfo } from "../query-results";
import { DatabaseManager } from "../databases/local-databases";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { InterpretedQueryCompareResult } from "../common/interface-types";
import { sarifDiff } from "./sarif-diff";
async function getInterpretedResults(
interpretedResultsPath: string,
): Promise<sarif.Log | undefined> {
if (!(await pathExists(interpretedResultsPath))) {
return undefined;
}
return await sarifParser(interpretedResultsPath);
}
export async function compareInterpretedResults(
databaseManager: DatabaseManager,
cliServer: CodeQLCliServer,
fromQuery: CompletedLocalQueryInfo,
toQuery: CompletedLocalQueryInfo,
): Promise<InterpretedQueryCompareResult> {
const fromResultSet = await getInterpretedResults(
fromQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
);
const toResultSet = await getInterpretedResults(
toQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
);
if (!fromResultSet || !toResultSet) {
throw new Error(
"Could not find interpreted results for one or both queries.",
);
}
const database = databaseManager.findDatabaseItem(
Uri.parse(toQuery.initialInfo.databaseInfo.databaseUri),
);
if (!database) {
throw new Error(
"Could not find database the queries. Please check that the database still exists.",
);
}
const sourceLocationPrefix = await database.getSourceLocationPrefix(
cliServer,
);
const fromResults = fromResultSet.runs[0].results;
const toResults = toResultSet.runs[0].results;
if (!fromResults) {
throw new Error("No results found in the 'from' query.");
}
if (!toResults) {
throw new Error("No results found in the 'to' query.");
}
const { from, to } = sarifDiff(fromResults, toResults);
return {
kind: "interpreted",
sourceLocationPrefix,
from,
to,
};
}

Просмотреть файл

@ -68,6 +68,7 @@ export async function findResultSetNames(
const toResultSetName = currentResultSetName || defaultToResultSetName!;
return {
currentResultSetName,
currentResultSetDisplayName:
currentResultSetName ||
`${defaultFromResultSetName} <-> ${defaultToResultSetName}`,

Просмотреть файл

@ -0,0 +1,50 @@
import * as sarif from "sarif";
/**
* Compare the alerts of two queries. Use deep equality to determine if
* results have been added or removed across two invocations of a query.
*
* Assumptions:
*
* 1. Queries have the same sort order
* 2. Results are not changed or re-ordered, they are only added or removed
*
* @param fromResults the source query
* @param toResults the target query
*
* @throws Error when:
* 1. If either query is empty
* 2. If the queries are 100% disjoint
*/
export function sarifDiff(
fromResults: sarif.Result[],
toResults: sarif.Result[],
) {
if (!fromResults.length) {
throw new Error("CodeQL Compare: Source query has no results.");
}
if (!toResults.length) {
throw new Error("CodeQL Compare: Target query has no results.");
}
const results = {
from: arrayDiff(fromResults, toResults),
to: arrayDiff(toResults, fromResults),
};
if (
fromResults.length === results.from.length &&
toResults.length === results.to.length
) {
throw new Error("CodeQL Compare: No overlap between the selected queries.");
}
return results;
}
function arrayDiff<T>(source: readonly T[], toRemove: readonly T[]): T[] {
// Stringify the object so that we can compare hashes in the set
const rest = new Set(toRemove.map((item) => JSON.stringify(item)));
return source.filter((element) => !rest.has(JSON.stringify(element)));
}