Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SARIF result comparison to compare view #3113

Merged
merged 5 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [UNRELEASED]

- 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
Loading