Skip to content

Commit 199232a

Browse files
authored
Merge pull request #4362 from asgerf/asgerf/filter-results-to-selection
Add support for selection-based result filtering in result viewer
2 parents 2d70e02 + babc2b8 commit 199232a

File tree

13 files changed

+862
-104
lines changed

13 files changed

+862
-104
lines changed

extensions/ql-vscode/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## [UNRELEASED]
44

55
- Remove support for CodeQL CLI versions older than 2.22.4. [#4344](https://github.com/github/vscode-codeql/pull/4344)
6+
- Added support for selection-based result filtering via a checkbox in the result viewer. When enabled, only results from the currently-viewed file are shown. Additionally, if the editor selection is non-empty, only results within the selection range are shown. [#4362](https://github.com/github/vscode-codeql/pull/4362)
67

78
## 1.17.7 - 5 December 2025
89

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;
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+
| SetFileFilteredResultsMsg
288+
| SetEditorSelectionMsg;
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+
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: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ export async function showResolvableLocation(
105105
loc: UrlValueResolvable,
106106
databaseItem: DatabaseItem | undefined,
107107
logger: Logger,
108-
): Promise<void> {
108+
): Promise<Location | null> {
109109
try {
110-
await showLocation(tryResolveLocation(loc, databaseItem));
110+
return showLocation(tryResolveLocation(loc, databaseItem));
111111
} catch (e) {
112112
if (e instanceof Error && e.message.match(/File not found/)) {
113113
void Window.showErrorMessage(
@@ -116,12 +116,15 @@ export async function showResolvableLocation(
116116
} else {
117117
void logger.log(`Unable to jump to location: ${getErrorMessage(e)}`);
118118
}
119+
return null;
119120
}
120121
}
121122

122-
export async function showLocation(location?: Location) {
123+
export async function showLocation(
124+
location?: Location,
125+
): Promise<Location | null> {
123126
if (!location) {
124-
return;
127+
return null;
125128
}
126129

127130
const doc = await workspace.openTextDocument(location.uri);
@@ -156,17 +159,19 @@ export async function showLocation(location?: Location) {
156159
editor.revealRange(range, TextEditorRevealType.InCenter);
157160
editor.setDecorations(shownLocationDecoration, [range]);
158161
editor.setDecorations(shownLocationLineDecoration, [range]);
162+
163+
return location;
159164
}
160165

161166
export async function jumpToLocation(
162167
databaseUri: string | undefined,
163168
loc: UrlValueResolvable,
164169
databaseManager: DatabaseManager,
165170
logger: Logger,
166-
) {
171+
): Promise<Location | null> {
167172
const databaseItem =
168173
databaseUri !== undefined
169174
? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
170175
: undefined;
171-
await showResolvableLocation(loc, databaseItem, logger);
176+
return showResolvableLocation(loc, databaseItem, logger);
172177
}

0 commit comments

Comments
 (0)