Skip to content

Commit 1db4e8b

Browse files
committed
Add ability to filter results to the current file or selection
A new checkbox appears above the result viewer table. When checked, only the results from the currently-viewed file are shown. Additionally, if the selection range is non-empty, only results whose first line overlaps within the selection range are shown.
1 parent 0f7a747 commit 1db4e8b

File tree

12 files changed

+904
-106
lines changed

12 files changed

+904
-106
lines changed

extensions/ql-vscode/src/common/interface-types.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,60 @@ interface UntoggleShowProblemsMsg {
220220
t: "untoggleShowProblems";
221221
}
222222

223+
export const enum SourceArchiveRelationship {
224+
/** The file is in the source archive of the database the query was run on. */
225+
CorrectArchive = "correct-archive",
226+
/** The file is in a source archive, but for a different database. */
227+
WrongArchive = "wrong-archive",
228+
/** The file is not in any source archive. */
229+
NotInArchive = "not-in-archive",
230+
}
231+
232+
/**
233+
* Information about the current editor selection, sent to the results view
234+
* so it can filter results to only those overlapping the selection.
235+
*/
236+
export interface EditorSelection {
237+
/** The file URI in result-compatible format. */
238+
fileUri: string;
239+
startLine: number;
240+
endLine: number;
241+
startColumn: number;
242+
endColumn: number;
243+
/** True if the selection is empty (just a cursor), in which case we match the whole file. */
244+
isEmpty: boolean;
245+
/** Describes the relationship between the current file and the query's database source archive. */
246+
sourceArchiveRelationship: SourceArchiveRelationship;
247+
}
248+
249+
interface SetEditorSelectionMsg {
250+
t: "setEditorSelection";
251+
selection: EditorSelection | undefined;
252+
wasFromUserInteraction?: boolean;
253+
}
254+
255+
/**
256+
* Results pre-filtered by file URI, sent from the extension when the
257+
* selection filter is active and the editor's file changes.
258+
* This bypasses pagination so the webview can apply line-range filtering
259+
* on the complete set of results for the file.
260+
*/
261+
export interface FileFilteredResults {
262+
/** The file URI these results were filtered for. */
263+
fileUri: string;
264+
/** The result set table these results were filtered for. */
265+
selectedTable: string;
266+
/** Raw result rows from the current result set that reference this file. */
267+
rawRows?: Row[];
268+
/** SARIF results that reference this file. */
269+
sarifResults?: Result[];
270+
}
271+
272+
interface SetFileFilteredResultsMsg {
273+
t: "setFileFilteredResults";
274+
results: FileFilteredResults | undefined;
275+
}
276+
223277
/**
224278
* A message sent into the results view.
225279
*/
@@ -229,7 +283,9 @@ export type IntoResultsViewMsg =
229283
| SetUserSettingsMsg
230284
| ShowInterpretedPageMsg
231285
| NavigateMsg
232-
| UntoggleShowProblemsMsg;
286+
| UntoggleShowProblemsMsg
287+
| SetEditorSelectionMsg
288+
| SetFileFilteredResultsMsg;
233289

234290
/**
235291
* A message sent from the results view.
@@ -241,7 +297,20 @@ export type FromResultsViewMsg =
241297
| ChangeRawResultsSortMsg
242298
| ChangeInterpretedResultsSortMsg
243299
| ChangePage
244-
| OpenFileMsg;
300+
| OpenFileMsg
301+
| RequestFileFilteredResultsMsg;
302+
303+
/**
304+
* Message from the results view to request pre-filtered results for
305+
* a specific (file, table) pair. The extension loads all results from
306+
* the given table that reference the given file and sends them back
307+
* via setFileFilteredResults.
308+
*/
309+
interface RequestFileFilteredResultsMsg {
310+
t: "requestFileFilteredResults";
311+
fileUri: string;
312+
selectedTable: string;
313+
}
245314

246315
/**
247316
* Message from the results view to open a source

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+
export 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/databases/local-databases/locations.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
window as Window,
1010
workspace,
1111
} from "vscode";
12+
import type { TextEditor } from "vscode";
1213
import { assertNever, getErrorMessage } from "../../common/helpers-pure";
1314
import type { Logger } from "../../common/logging";
1415
import type { DatabaseItem } from "./database-item";
@@ -76,6 +77,12 @@ function resolveWholeFileLocation(
7677
);
7778
}
7879

80+
/** Returned from `showLocation` and related functions, to indicate which editor and location was ultimately highlighted. */
81+
interface RevealedLocation {
82+
editor: TextEditor;
83+
location: Location;
84+
}
85+
7986
/**
8087
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
8188
* can be resolved, returns `undefined`.
@@ -105,9 +112,9 @@ export async function showResolvableLocation(
105112
loc: UrlValueResolvable,
106113
databaseItem: DatabaseItem | undefined,
107114
logger: Logger,
108-
): Promise<void> {
115+
): Promise<RevealedLocation | null> {
109116
try {
110-
await showLocation(tryResolveLocation(loc, databaseItem));
117+
return showLocation(tryResolveLocation(loc, databaseItem));
111118
} catch (e) {
112119
if (e instanceof Error && e.message.match(/File not found/)) {
113120
void Window.showErrorMessage(
@@ -116,12 +123,15 @@ export async function showResolvableLocation(
116123
} else {
117124
void logger.log(`Unable to jump to location: ${getErrorMessage(e)}`);
118125
}
126+
return null;
119127
}
120128
}
121129

122-
export async function showLocation(location?: Location) {
130+
export async function showLocation(
131+
location?: Location,
132+
): Promise<RevealedLocation | null> {
123133
if (!location) {
124-
return;
134+
return null;
125135
}
126136

127137
const doc = await workspace.openTextDocument(location.uri);
@@ -156,17 +166,19 @@ export async function showLocation(location?: Location) {
156166
editor.revealRange(range, TextEditorRevealType.InCenter);
157167
editor.setDecorations(shownLocationDecoration, [range]);
158168
editor.setDecorations(shownLocationLineDecoration, [range]);
169+
170+
return { editor, location };
159171
}
160172

161173
export async function jumpToLocation(
162174
databaseUri: string | undefined,
163175
loc: UrlValueResolvable,
164176
databaseManager: DatabaseManager,
165177
logger: Logger,
166-
) {
178+
): Promise<RevealedLocation | null> {
167179
const databaseItem =
168180
databaseUri !== undefined
169181
? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
170182
: undefined;
171-
await showResolvableLocation(loc, databaseItem, logger);
183+
return showResolvableLocation(loc, databaseItem, logger);
172184
}

0 commit comments

Comments
 (0)