Skip to content

Commit 08f5361

Browse files
committed
Actual show filtered results
1 parent a9fd7be commit 08f5361

File tree

3 files changed

+182
-8
lines changed

3 files changed

+182
-8
lines changed

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@ import { AlertTableNoResults } from "./AlertTableNoResults";
66
import { AlertTableHeader } from "./AlertTableHeader";
77

88
export function ResultTable(props: ResultTableProps) {
9-
const { resultSet, userSettings } = props;
9+
const { resultSet, userSettings, filteredRawRows, filteredSarifResults } =
10+
props;
1011
switch (resultSet.t) {
11-
case "RawResultSet":
12-
return <RawTable {...props} resultSet={resultSet.resultSet} />;
12+
case "RawResultSet": {
13+
const filteredResultSet = {
14+
...resultSet.resultSet,
15+
rows: filteredRawRows ?? resultSet.resultSet.rows,
16+
};
17+
return <RawTable {...props} resultSet={filteredResultSet} />;
18+
}
1319
case "InterpretedResultSet": {
1420
const data = resultSet.interpretation.data;
1521
switch (data.t) {
1622
case "SarifInterpretationData": {
23+
const results = filteredSarifResults ?? data.runs[0].results ?? [];
1724
return (
1825
<AlertTable
19-
results={data.runs[0].results ?? []}
26+
results={results}
2027
databaseUri={props.databaseUri}
2128
sourceLocationPrefix={
2229
resultSet.interpretation.sourceLocationPrefix

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ import {
1616
GRAPH_TABLE_NAME,
1717
SELECT_TABLE_NAME,
1818
} from "../../common/interface-types";
19-
import { tableHeaderClassName } from "./result-table-utils";
19+
import {
20+
filterRawRows,
21+
filterSarifResults,
22+
tableHeaderClassName,
23+
} from "./result-table-utils";
2024
import { vscode } from "../vscode-api";
2125
import { sendTelemetry } from "../common/telemetry";
2226
import { ResultTable } from "./ResultTable";
2327
import { ResultTablesHeader } from "./ResultTablesHeader";
24-
import { useCallback, useEffect, useMemo, useState } from "react";
28+
import { useCallback, useMemo } from "react";
2529
import { ResultCount } from "./ResultCount";
2630
import { ProblemsViewCheckbox } from "./ProblemsViewCheckbox";
2731
import { SelectionFilterCheckbox } from "./SelectionFilterCheckbox";
@@ -196,6 +200,44 @@ export function ResultTables(props: ResultTablesProps) {
196200
const isLoadingFilteredResults =
197201
selectionFilter != null && fileFilteredResults == null;
198202

203+
// Filter rows at line granularity (if filtering is enabled)
204+
const filteredRawRows = useMemo(() => {
205+
if (!selectionFilter || !resultSet || resultSet.t !== "RawResultSet") {
206+
return undefined;
207+
}
208+
const sourceRows = fileFilteredResults?.rawRows;
209+
if (sourceRows == null) {
210+
return undefined;
211+
}
212+
return filterRawRows(sourceRows, selectionFilter);
213+
}, [selectionFilter, fileFilteredResults, resultSet]);
214+
215+
// Filter SARIF results at line granularity (if filtering is enabled)
216+
const filteredSarifResults = useMemo(() => {
217+
if (
218+
!selectionFilter ||
219+
!resultSet ||
220+
resultSet.t !== "InterpretedResultSet"
221+
) {
222+
return undefined;
223+
}
224+
const data = resultSet.interpretation.data;
225+
if (data.t !== "SarifInterpretationData") {
226+
return undefined;
227+
}
228+
const sourceResults =
229+
fileFilteredResults?.sarifResults !== undefined
230+
? fileFilteredResults.sarifResults
231+
: (data.runs[0].results ?? []);
232+
return filterSarifResults(
233+
sourceResults,
234+
resultSet.interpretation.sourceLocationPrefix,
235+
selectionFilter,
236+
);
237+
}, [selectionFilter, fileFilteredResults, resultSet]);
238+
239+
const filteredCount = filteredRawRows?.length ?? filteredSarifResults?.length;
240+
199241
return (
200242
<div>
201243
<ResultTablesHeader {...props} selectedTable={selectedTable} />
@@ -224,8 +266,12 @@ export function ResultTables(props: ResultTablesProps) {
224266
Updating results…
225267
</span>
226268
) : null}
269+
{isLoadingFilteredResults && (
270+
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>
271+
Updating filtered results…
272+
</span>
273+
)}
227274
</div>
228-
{isLoadingFilteredResults && <span>Loading filtered results…</span>}
229275
{!isLoadingFilteredResults && resultSet && resultSetName && (
230276
<ResultTable
231277
key={resultSetName}
@@ -241,6 +287,8 @@ export function ResultTables(props: ResultTablesProps) {
241287
}}
242288
offset={offset}
243289
selectionFilter={selectionFilter}
290+
filteredRawRows={filteredRawRows}
291+
filteredSarifResults={filteredSarifResults}
244292
/>
245293
)}
246294
</div>

extensions/ql-vscode/src/view/results/result-table-utils.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
EditorSelection,
23
QueryMetadata,
34
RawResultsSortState,
45
ResultSet,
@@ -7,7 +8,16 @@ import type {
78
import { SortDirection } from "../../common/interface-types";
89
import { assertNever } from "../../common/helpers-pure";
910
import { vscode } from "../vscode-api";
10-
import type { UrlValueResolvable } from "../../common/raw-result-types";
11+
import type {
12+
CellValue,
13+
Row,
14+
UrlValueResolvable,
15+
} from "../../common/raw-result-types";
16+
import type { Result } from "sarif";
17+
import {
18+
getLocationsFromSarifResult,
19+
normalizeFileUri,
20+
} from "../../common/sarif-utils";
1121

1222
export interface ResultTableProps {
1323
resultSet: ResultSet;
@@ -30,6 +40,8 @@ export interface ResultTableProps {
3040
*/
3141
showRawResults: () => void;
3242

43+
filteredRawRows?: Row[];
44+
filteredSarifResults?: Result[];
3345
selectionFilter?: EditorSelection;
3446
}
3547

@@ -109,3 +121,110 @@ export function nextSortDirection(
109121
return assertNever(direction);
110122
}
111123
}
124+
125+
/**
126+
* Extracts all resolvable locations from a raw result row.
127+
*/
128+
function getLocationsFromRawRow(
129+
row: Row,
130+
): Array<{ uri: string; startLine?: number; endLine?: number }> {
131+
const locations: Array<{
132+
uri: string;
133+
startLine?: number;
134+
endLine?: number;
135+
}> = [];
136+
137+
for (const cell of row) {
138+
const loc = getLocationFromCell(cell);
139+
if (loc) {
140+
locations.push(loc);
141+
}
142+
}
143+
144+
return locations;
145+
}
146+
147+
function getLocationFromCell(
148+
cell: CellValue,
149+
): { uri: string; startLine?: number; endLine?: number } | undefined {
150+
if (cell.type !== "entity") {
151+
return undefined;
152+
}
153+
const url = cell.value.url;
154+
if (!url) {
155+
return undefined;
156+
}
157+
if (url.type === "wholeFileLocation") {
158+
return { uri: url.uri };
159+
}
160+
if (url.type === "lineColumnLocation") {
161+
return {
162+
uri: url.uri,
163+
startLine: url.startLine,
164+
endLine: url.endLine,
165+
};
166+
}
167+
return undefined;
168+
}
169+
170+
/**
171+
* Checks if a result location overlaps with the editor selection.
172+
* If the selection is empty (just a cursor), matches any result in the same file.
173+
*/
174+
function doesLocationOverlapSelection(
175+
loc: { uri: string; startLine?: number; endLine?: number },
176+
selection: EditorSelection,
177+
): boolean {
178+
const normalizedLocUri = normalizeFileUri(loc.uri);
179+
const normalizedSelUri = normalizeFileUri(selection.fileUri);
180+
181+
if (normalizedLocUri !== normalizedSelUri) {
182+
return false;
183+
}
184+
185+
// If selection is empty (just a cursor), match the whole file
186+
if (selection.isEmpty) {
187+
return true;
188+
}
189+
190+
// If the result location has no line info, it's a whole-file location — include it
191+
if (loc.startLine === undefined) {
192+
return true;
193+
}
194+
195+
// Only include results whose starting line falls within the selection range
196+
return (
197+
loc.startLine >= selection.startLine && loc.startLine <= selection.endLine
198+
);
199+
}
200+
201+
/**
202+
* Filters raw result rows to those with at least one location overlapping the selection.
203+
*/
204+
export function filterRawRows(
205+
rows: readonly Row[],
206+
selection: EditorSelection,
207+
): Row[] {
208+
return rows.filter((row) => {
209+
const locations = getLocationsFromRawRow(row);
210+
return locations.some((loc) =>
211+
doesLocationOverlapSelection(loc, selection),
212+
);
213+
});
214+
}
215+
216+
/**
217+
* Filters SARIF results to those with at least one location overlapping the selection.
218+
*/
219+
export function filterSarifResults(
220+
results: Result[],
221+
sourceLocationPrefix: string,
222+
selection: EditorSelection,
223+
): Result[] {
224+
return results.filter((result) => {
225+
const locations = getLocationsFromSarifResult(result, sourceLocationPrefix);
226+
return locations.some((loc) =>
227+
doesLocationOverlapSelection(loc, selection),
228+
);
229+
});
230+
}

0 commit comments

Comments
 (0)