Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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]

- Remove support for CodeQL CLI versions older than 2.22.4. [#4344](https://github.com/github/vscode-codeql/pull/4344)
- 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)

## 1.17.7 - 5 December 2025

Expand Down
73 changes: 71 additions & 2 deletions extensions/ql-vscode/src/common/interface-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,60 @@ interface UntoggleShowProblemsMsg {
t: "untoggleShowProblems";
}

export const enum SourceArchiveRelationship {
/** The file is in the source archive of the database the query was run on. */
CorrectArchive = "correct-archive",
/** The file is in a source archive, but for a different database. */
WrongArchive = "wrong-archive",
/** The file is not in any source archive. */
NotInArchive = "not-in-archive",
}

/**
* Information about the current editor selection, sent to the results view
* so it can filter results to only those overlapping the selection.
*/
export interface EditorSelection {
/** The file URI in result-compatible format. */
fileUri: string;
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
/** True if the selection is empty (just a cursor), in which case we match the whole file. */
isEmpty: boolean;
/** Describes the relationship between the current file and the query's database source archive. */
sourceArchiveRelationship: SourceArchiveRelationship;
}

interface SetEditorSelectionMsg {
t: "setEditorSelection";
selection: EditorSelection | undefined;
wasFromUserInteraction?: boolean;
}

/**
* Results pre-filtered by file URI, sent from the extension when the
* selection filter is active and the editor's file changes.
* This bypasses pagination so the webview can apply line-range filtering
* on the complete set of results for the file.
*/
export interface FileFilteredResults {
/** The file URI these results were filtered for. */
fileUri: string;
/** The result set table these results were filtered for. */
selectedTable: string;
/** Raw result rows from the current result set that reference this file. */
rawRows?: Row[];
/** SARIF results that reference this file. */
sarifResults?: Result[];
}

interface SetFileFilteredResultsMsg {
t: "setFileFilteredResults";
results: FileFilteredResults | undefined;
}

/**
* A message sent into the results view.
*/
Expand All @@ -229,7 +283,9 @@ export type IntoResultsViewMsg =
| SetUserSettingsMsg
| ShowInterpretedPageMsg
| NavigateMsg
| UntoggleShowProblemsMsg;
| UntoggleShowProblemsMsg
| SetEditorSelectionMsg
| SetFileFilteredResultsMsg;

/**
* A message sent from the results view.
Expand All @@ -241,7 +297,20 @@ export type FromResultsViewMsg =
| ChangeRawResultsSortMsg
| ChangeInterpretedResultsSortMsg
| ChangePage
| OpenFileMsg;
| OpenFileMsg
| RequestFileFilteredResultsMsg;

/**
* Message from the results view to request pre-filtered results for
* a specific (file, table) pair. The extension loads all results from
* the given table that reference the given file and sends them back
* via setFileFilteredResults.
*/
interface RequestFileFilteredResultsMsg {
t: "requestFileFilteredResults";
fileUri: string;
selectedTable: string;
}

/**
* Message from the results view to open a source
Expand Down
51 changes: 50 additions & 1 deletion extensions/ql-vscode/src/common/sarif-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Location, Region } from "sarif";
import type { Location, Region, Result } from "sarif";
import type { HighlightedRegion } from "../variant-analysis/shared/analysis-result";
import type { UrlValueResolvable } from "./raw-result-types";
import { isEmptyPath } from "./bqrs-utils";
Expand Down Expand Up @@ -252,3 +252,52 @@ export function parseHighlightedLine(

return { plainSection1, highlightedSection, plainSection2 };
}

/**
* Normalizes a file URI to a plain path for comparison purposes.
* Strips the `file:` scheme prefix and decodes URI components.
*/
export function normalizeFileUri(uri: string): string {
try {
const path = uri.replace(/^file:\/*/, "/");
return decodeURIComponent(path);
} catch {
return uri.replace(/^file:\/*/, "/");
}
}

interface ParsedResultLocation {
uri: string;
startLine?: number;
endLine?: number;
}

/**
* Extracts all locations from a SARIF result, including relatedLocations.
*/
export function getLocationsFromSarifResult(
result: Result,
sourceLocationPrefix: string,
): ParsedResultLocation[] {
const sarifLocations: Location[] = [
...(result.locations ?? []),
...(result.relatedLocations ?? []),
];
const parsed: ParsedResultLocation[] = [];
for (const loc of sarifLocations) {
const p = parseSarifLocation(loc, sourceLocationPrefix);
if ("hint" in p) {
continue;
}
if (p.type === "wholeFileLocation") {
parsed.push({ uri: p.uri });
} else if (p.type === "lineColumnLocation") {
parsed.push({
uri: p.uri,
startLine: p.startLine,
endLine: p.endLine,
});
}
}
return parsed;
}
24 changes: 18 additions & 6 deletions extensions/ql-vscode/src/databases/local-databases/locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
window as Window,
workspace,
} from "vscode";
import type { TextEditor } from "vscode";
import { assertNever, getErrorMessage } from "../../common/helpers-pure";
import type { Logger } from "../../common/logging";
import type { DatabaseItem } from "./database-item";
Expand Down Expand Up @@ -76,6 +77,12 @@ function resolveWholeFileLocation(
);
}

/** Returned from `showLocation` and related functions, to indicate which editor and location was ultimately highlighted. */
interface RevealedLocation {
editor: TextEditor;
location: Location;
}

/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.
Expand Down Expand Up @@ -105,9 +112,9 @@ export async function showResolvableLocation(
loc: UrlValueResolvable,
databaseItem: DatabaseItem | undefined,
logger: Logger,
): Promise<void> {
): Promise<RevealedLocation | null> {
try {
await showLocation(tryResolveLocation(loc, databaseItem));
return showLocation(tryResolveLocation(loc, databaseItem));
} catch (e) {
if (e instanceof Error && e.message.match(/File not found/)) {
void Window.showErrorMessage(
Expand All @@ -116,12 +123,15 @@ export async function showResolvableLocation(
} else {
void logger.log(`Unable to jump to location: ${getErrorMessage(e)}`);
}
return null;
}
}

export async function showLocation(location?: Location) {
export async function showLocation(
location?: Location,
): Promise<RevealedLocation | null> {
if (!location) {
return;
return null;
}

const doc = await workspace.openTextDocument(location.uri);
Expand Down Expand Up @@ -156,17 +166,19 @@ export async function showLocation(location?: Location) {
editor.revealRange(range, TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
editor.setDecorations(shownLocationLineDecoration, [range]);

return { editor, location };
}

export async function jumpToLocation(
databaseUri: string | undefined,
loc: UrlValueResolvable,
databaseManager: DatabaseManager,
logger: Logger,
) {
): Promise<RevealedLocation | null> {
const databaseItem =
databaseUri !== undefined
? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
: undefined;
await showResolvableLocation(loc, databaseItem, logger);
return showResolvableLocation(loc, databaseItem, logger);
}
Loading
Loading