Skip to content

Commit

Permalink
Merge pull request #3113 from github/koesie10/compare-interpreted
Browse files Browse the repository at this point in the history
Add SARIF result comparison to compare view
  • Loading branch information
koesie10 authored Dec 15, 2023
2 parents 34bd7c2 + cb2e502 commit 60d777a
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 60 deletions.
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Add a prompt for downloading a GitHub database when opening a GitHub repository. [#3138](https://github.com/github/vscode-codeql/pull/3138)
- Avoid showing a popup when hovering over source elements in database source files. [#3125](https://github.com/github/vscode-codeql/pull/3125)
- 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)

## 1.11.0 - 13 December 2023

Expand Down
6 changes: 4 additions & 2 deletions extensions/ql-vscode/src/common/interface-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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[];
Expand Down
152 changes: 108 additions & 44 deletions extensions/ql-vscode/src/compare/compare-view.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ViewColumn } from "vscode";

import {
ALERTS_TABLE_NAME,
FromCompareViewMessage,
InterpretedQueryCompareResult,
QueryCompareResult,
RawQueryCompareResult,
ToCompareViewMessage,
} from "../common/interface-types";
Expand All @@ -25,15 +28,18 @@ import { App } from "../common/app";
import { bqrsToResultSet } from "../common/bqrs-raw-results-mapper";
import { RawResultSet } from "../common/raw-result-types";
import {
CompareQueryInfo,
findCommonResultSetNames,
findResultSetNames,
getResultSetNames,
} from "./result-set-names";
import { compareInterpretedResults } from "./interpreted-results";

interface ComparePair {
from: CompletedLocalQueryInfo;
fromSchemas: BqrsInfo;
fromInfo: CompareQueryInfo;
to: CompletedLocalQueryInfo;
toSchemas: BqrsInfo;
toInfo: CompareQueryInfo;

commonResultSetNames: readonly string[];
}
Expand Down Expand Up @@ -62,23 +68,48 @@ export class CompareView extends AbstractWebview<
to: CompletedLocalQueryInfo,
selectedResultSetName?: string,
) {
const fromSchemas = await this.cliServer.bqrsInfo(
from.completedQuery.query.resultsPaths.resultsPath,
);
const toSchemas = await this.cliServer.bqrsInfo(
to.completedQuery.query.resultsPaths.resultsPath,
);
const [fromSchemas, toSchemas] = await Promise.all([
this.cliServer.bqrsInfo(
from.completedQuery.query.resultsPaths.resultsPath,
),
this.cliServer.bqrsInfo(to.completedQuery.query.resultsPaths.resultsPath),
]);

const commonResultSetNames = await findCommonResultSetNames(
fromSchemas,
toSchemas,
const [fromSchemaNames, toSchemaNames] = await Promise.all([
getResultSetNames(
fromSchemas,
from.completedQuery.query.metadata,
from.completedQuery.query.resultsPaths.interpretedResultsPath,
),
getResultSetNames(
toSchemas,
to.completedQuery.query.metadata,
to.completedQuery.query.resultsPaths.interpretedResultsPath,
),
]);

const commonResultSetNames = findCommonResultSetNames(
fromSchemaNames,
toSchemaNames,
);

this.comparePair = {
from,
fromSchemas,
fromInfo: {
schemas: fromSchemas,
schemaNames: fromSchemaNames,
metadata: from.completedQuery.query.metadata,
interpretedResultsPath:
from.completedQuery.query.resultsPaths.interpretedResultsPath,
},
to,
toSchemas,
toInfo: {
schemas: toSchemas,
schemaNames: toSchemaNames,
metadata: to.completedQuery.query.metadata,
interpretedResultsPath:
to.completedQuery.query.resultsPaths.interpretedResultsPath,
},
commonResultSetNames,
};

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

await this.waitForPanelLoaded();
const { currentResultSetDisplayName, fromResultSet, toResultSet } =
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 = this.compareResults(fromResultSet, toResultSet);
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);
}
Expand Down Expand Up @@ -205,31 +248,27 @@ export class CompareView extends AbstractWebview<
}

private async findResultSetsToCompare(
{ from, fromSchemas, to, toSchemas, commonResultSetNames }: ComparePair,
{ fromInfo, toInfo, commonResultSetNames }: ComparePair,
selectedResultSetName: string | undefined,
) {
const { currentResultSetDisplayName, fromResultSetName, toResultSetName } =
await findResultSetNames(
fromSchemas,
toSchemas,
commonResultSetNames,
selectedResultSetName,
);

const fromResultSet = await this.getResultSet(
fromSchemas,
const {
currentResultSetName,
currentResultSetDisplayName,
fromResultSetName,
from.completedQuery.query.resultsPaths.resultsPath,
);
const toResultSet = await this.getResultSet(
toSchemas,
toResultSetName,
to.completedQuery.query.resultsPaths.resultsPath,
} = await findResultSetNames(
fromInfo,
toInfo,
commonResultSetNames,
selectedResultSetName,
);

return {
commonResultSetNames,
currentResultSetName,
currentResultSetDisplayName,
fromResultSet,
toResultSet,
fromResultSetName,
toResultSetName,
};
}

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

private compareResults(
fromResults: RawResultSet,
toResults: RawResultSet,
): RawQueryCompareResult {
// Only compare columns that have the same name
return resultsDiff(fromResults, toResults);
private async compareResults(
{ from, fromInfo, to, toInfo }: ComparePair,
fromResultSetName: string,
toResultSetName: string,
): Promise<RawQueryCompareResult> {
const [fromResultSet, toResultSet] = await Promise.all([
this.getResultSet(
fromInfo.schemas,
fromResultSetName,
from.completedQuery.query.resultsPaths.resultsPath,
),
this.getResultSet(
toInfo.schemas,
toResultSetName,
to.completedQuery.query.resultsPaths.resultsPath,
),
]);

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") {
Expand Down
72 changes: 72 additions & 0 deletions extensions/ql-vscode/src/compare/interpreted-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 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 [fromResultSet, toResultSet, sourceLocationPrefix] = await Promise.all([
getInterpretedResults(
fromQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
),
getInterpretedResults(
toQuery.completedQuery.query.resultsPaths.interpretedResultsPath,
),
database.getSourceLocationPrefix(cliServer),
]);

if (!fromResultSet || !toResultSet) {
throw new Error(
"Could not find interpreted results for one or both queries.",
);
}

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,
};
}
50 changes: 36 additions & 14 deletions extensions/ql-vscode/src/compare/result-set-names.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
import { pathExists } from "fs-extra";
import { BqrsInfo } from "../common/bqrs-cli-types";
import { getDefaultResultSetName } from "../common/interface-types";
import {
ALERTS_TABLE_NAME,
getDefaultResultSetName,
QueryMetadata,
} from "../common/interface-types";

export async function findCommonResultSetNames(
fromSchemas: BqrsInfo,
toSchemas: BqrsInfo,
export async function getResultSetNames(
schemas: BqrsInfo,
metadata: QueryMetadata | undefined,
interpretedResultsPath: string | undefined,
): Promise<string[]> {
const fromSchemaNames = fromSchemas["result-sets"].map(
(schema) => schema.name,
);
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
const schemaNames = schemas["result-sets"].map((schema) => schema.name);

if (metadata?.kind !== "graph" && interpretedResultsPath) {
if (await pathExists(interpretedResultsPath)) {
schemaNames.push(ALERTS_TABLE_NAME);
}
}

return schemaNames;
}

export function findCommonResultSetNames(
fromSchemaNames: string[],
toSchemaNames: string[],
): string[] {
return fromSchemaNames.filter((name) => toSchemaNames.includes(name));
}

export type CompareQueryInfo = {
schemas: BqrsInfo;
schemaNames: string[];
metadata: QueryMetadata | undefined;
interpretedResultsPath: string;
};

export async function findResultSetNames(
fromSchemas: BqrsInfo,
toSchemas: BqrsInfo,
from: CompareQueryInfo,
to: CompareQueryInfo,
commonResultSetNames: readonly string[],
selectedResultSetName: string | undefined,
) {
const fromSchemaNames = fromSchemas["result-sets"].map(
(schema) => schema.name,
);
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
const fromSchemaNames = from.schemaNames;
const toSchemaNames = to.schemaNames;

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

return {
currentResultSetName,
currentResultSetDisplayName:
currentResultSetName ||
`${defaultFromResultSetName} <-> ${defaultToResultSetName}`,
Expand Down
Loading

0 comments on commit 60d777a

Please sign in to comment.