Skip to content
Open
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]

- 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;
}

/**
* 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;
}
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/common/vscode/webview-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function getHtmlForWebview(
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'${
allowWasmEval ? " 'wasm-unsafe-eval'" : ""
allowWasmEval ? " 'wasm-unsafe-eval' 'unsafe-eval'" : ""
}; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${
webview.cspSource
};">
Expand Down
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