Skip to content

Commit 9eec6d0

Browse files
committed
Compute file-filtered results
1 parent 679b2c6 commit 9eec6d0

File tree

3 files changed

+163
-3
lines changed

3 files changed

+163
-3
lines changed

extensions/ql-vscode/src/common/sarif-utils.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Location, Region } from "sarif";
1+
import type { Location, Region, Result } from "sarif";
22
import type { HighlightedRegion } from "../variant-analysis/shared/analysis-result";
33
import type { UrlValueResolvable } from "./raw-result-types";
44
import { isEmptyPath } from "./bqrs-utils";
@@ -252,3 +252,52 @@ export function parseHighlightedLine(
252252

253253
return { plainSection1, highlightedSection, plainSection2 };
254254
}
255+
256+
/**
257+
* Normalizes a file URI to a plain path for comparison purposes.
258+
* Strips the `file:` scheme prefix and decodes URI components.
259+
*/
260+
export function normalizeFileUri(uri: string): string {
261+
try {
262+
const path = uri.replace(/^file:\/*/, "/");
263+
return decodeURIComponent(path);
264+
} catch {
265+
return uri.replace(/^file:\/*/, "/");
266+
}
267+
}
268+
269+
interface ParsedResultLocation {
270+
uri: string;
271+
startLine?: number;
272+
endLine?: number;
273+
}
274+
275+
/**
276+
* Extracts all locations from a SARIF result, including relatedLocations.
277+
*/
278+
export function getLocationsFromSarifResult(
279+
result: Result,
280+
sourceLocationPrefix: string,
281+
): ParsedResultLocation[] {
282+
const sarifLocations: Location[] = [
283+
...(result.locations ?? []),
284+
...(result.relatedLocations ?? []),
285+
];
286+
const parsed: ParsedResultLocation[] = [];
287+
for (const loc of sarifLocations) {
288+
const p = parseSarifLocation(loc, sourceLocationPrefix);
289+
if ("hint" in p) {
290+
continue;
291+
}
292+
if (p.type === "wholeFileLocation") {
293+
parsed.push({ uri: p.uri });
294+
} else if (p.type === "lineColumnLocation") {
295+
parsed.push({
296+
uri: p.uri,
297+
startLine: p.startLine,
298+
endLine: p.endLine,
299+
});
300+
}
301+
}
302+
return parsed;
303+
}

extensions/ql-vscode/src/local-queries/results-view.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
GRAPH_TABLE_NAME,
5252
NavigationDirection,
5353
getDefaultResultSetName,
54+
RAW_RESULTS_LIMIT,
5455
} from "../common/interface-types";
5556
import { extLogger } from "../common/logging/vscode";
5657
import type { Logger } from "../common/logging";
@@ -62,6 +63,8 @@ import type {
6263
import { interpretResultsSarif, interpretGraphResults } from "../query-results";
6364
import type { QueryEvaluationInfo } from "../run-queries-shared";
6465
import {
66+
getLocationsFromSarifResult,
67+
normalizeFileUri,
6568
parseSarifLocation,
6669
parseSarifPlainTextMessage,
6770
} from "../common/sarif-utils";
@@ -82,7 +85,7 @@ import { redactableError } from "../common/errors";
8285
import type { ResultsViewCommands } from "../common/commands";
8386
import type { App } from "../common/app";
8487
import type { Disposable } from "../common/disposable-object";
85-
import type { RawResultSet } from "../common/raw-result-types";
88+
import type { RawResultSet, Row } from "../common/raw-result-types";
8689
import type { BqrsResultSetSchema } from "../common/bqrs-cli-types";
8790
import { CachedOperation } from "../language-support/contextual/cached-operation";
8891

@@ -349,6 +352,7 @@ export class ResultsView extends AbstractWebview<
349352
await this.openFile(msg.filePath);
350353
break;
351354
case "requestFileFilteredResults":
355+
void this.loadFileFilteredResults(msg.fileUri, msg.selectedTable);
352356
break;
353357
case "telemetry":
354358
telemetryListener?.sendUIInteraction(msg.action);
@@ -1125,6 +1129,83 @@ export class ResultsView extends AbstractWebview<
11251129
return undefined;
11261130
}
11271131

1132+
/**
1133+
* Loads all results from the given table that reference the given file URI,
1134+
* and sends them to the webview. Called on demand when the webview requests
1135+
* pre-filtered results for a specific (file, table) pair.
1136+
*/
1137+
private async loadFileFilteredResults(
1138+
fileUri: string,
1139+
selectedTable: string,
1140+
): Promise<void> {
1141+
const query = this._displayedQuery;
1142+
if (!query) {
1143+
void this.postMessage({
1144+
t: "setFileFilteredResults",
1145+
results: { fileUri, selectedTable },
1146+
});
1147+
return;
1148+
}
1149+
1150+
const normalizedFilterUri = normalizeFileUri(fileUri);
1151+
1152+
let rawRows: Row[] | undefined;
1153+
let sarifResults: Result[] | undefined;
1154+
1155+
// Load and filter raw BQRS results
1156+
try {
1157+
const resultSetSchemas = await this.getResultSetSchemas(
1158+
query.completedQuery,
1159+
);
1160+
const schema = resultSetSchemas.find((s) => s.name === selectedTable);
1161+
1162+
if (schema && schema.rows > 0) {
1163+
const resultsPath = query.completedQuery.getResultsPath(selectedTable);
1164+
const chunk = await this.cliServer.bqrsDecode(
1165+
resultsPath,
1166+
schema.name,
1167+
{
1168+
offset: schema.pagination?.offsets[0],
1169+
pageSize: schema.rows,
1170+
},
1171+
);
1172+
const resultSet = bqrsToResultSet(schema, chunk);
1173+
rawRows = filterRowsByFileUri(resultSet.rows, normalizedFilterUri);
1174+
if (rawRows.length > RAW_RESULTS_LIMIT) {
1175+
rawRows = rawRows.slice(0, RAW_RESULTS_LIMIT);
1176+
}
1177+
}
1178+
} catch (e) {
1179+
void this.logger.log(
1180+
`Error loading file-filtered raw results: ${getErrorMessage(e)}`,
1181+
);
1182+
}
1183+
1184+
// Filter SARIF results (already in memory)
1185+
if (this._interpretation?.data.t === "SarifInterpretationData") {
1186+
const allResults = this._interpretation.data.runs[0]?.results ?? [];
1187+
sarifResults = allResults.filter((result) => {
1188+
const locations = getLocationsFromSarifResult(
1189+
result,
1190+
this._interpretation!.sourceLocationPrefix,
1191+
);
1192+
return locations.some(
1193+
(loc) => normalizeFileUri(loc.uri) === normalizedFilterUri,
1194+
);
1195+
});
1196+
}
1197+
1198+
void this.postMessage({
1199+
t: "setFileFilteredResults",
1200+
results: {
1201+
fileUri,
1202+
selectedTable,
1203+
rawRows,
1204+
sarifResults,
1205+
},
1206+
});
1207+
}
1208+
11281209
dispose() {
11291210
super.dispose();
11301211

@@ -1133,3 +1214,32 @@ export class ResultsView extends AbstractWebview<
11331214
this.disposableEventListeners = [];
11341215
}
11351216
}
1217+
1218+
/**
1219+
* Filters raw result rows to those that have at least one location
1220+
* referencing the given file (compared by normalized URI).
1221+
*/
1222+
function filterRowsByFileUri(rows: Row[], normalizedFileUri: string): Row[] {
1223+
return rows.filter((row) => {
1224+
for (const cell of row) {
1225+
if (cell.type !== "entity") {
1226+
continue;
1227+
}
1228+
const url = cell.value.url;
1229+
if (!url) {
1230+
continue;
1231+
}
1232+
let uri: string | undefined;
1233+
if (
1234+
url.type === "wholeFileLocation" ||
1235+
url.type === "lineColumnLocation"
1236+
) {
1237+
uri = url.uri;
1238+
}
1239+
if (uri !== undefined && normalizeFileUri(uri) === normalizedFileUri) {
1240+
return true;
1241+
}
1242+
}
1243+
return false;
1244+
});
1245+
}

extensions/ql-vscode/src/view/results/ResultsApp.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import { ResultTables } from "./ResultTables";
2424
import { onNavigation } from "./navigation";
2525

2626
import "./resultsView.css";
27-
import { useCallback, useState } from "react";
27+
import { useCallback, useEffect, useState } from "react";
28+
import { vscode } from "../vscode-api";
2829

2930
/**
3031
* ResultsApp.tsx

0 commit comments

Comments
 (0)