diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index d638979fb..49a374eee 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -2,6 +2,8 @@ ## [UNRELEASED] +- Update results view to display the length of the shortest path for path queries. [#3687](https://github.com/github/vscode-codeql/pull/3687) + ## 1.14.0 - 7 August 2024 - Add Python support to the CodeQL Model Editor. [#3676](https://github.com/github/vscode-codeql/pull/3676) diff --git a/extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx b/extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx index ab4faff74..db8af0efd 100644 --- a/extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx +++ b/extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx @@ -11,8 +11,9 @@ import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCe import { useCallback, useMemo } from "react"; import { VerticalRule } from "../common/VerticalRule"; import type { UserSettings } from "../../common/interface-types"; +import { pluralize } from "../../common/word"; -interface Props { +export interface Props { path: ThreadFlow; pathIndex: number; resultIndex: number; @@ -65,7 +66,7 @@ export function AlertTablePathRow(props: Props) { onClick={handleDropdownClick} /> - Path + {`Path (${pluralize(path.locations.length, "step", "steps")})`} {currentPathExpanded && diff --git a/extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx b/extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx index c405f3200..3ae0e5bad 100644 --- a/extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx +++ b/extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx @@ -13,8 +13,9 @@ import { SarifLocation } from "./locations/SarifLocation"; import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations"; import { AlertTablePathRow } from "./AlertTablePathRow"; import type { UserSettings } from "../../common/interface-types"; +import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"; -interface Props { +export interface Props { result: Result; resultIndex: number; expanded: Set; @@ -83,6 +84,11 @@ export function AlertTableResultRow(props: Props) { /> ); + const allPaths = getAllPaths(result); + const shortestPath = Math.min( + ...allPaths.map((path) => path.locations.length), + ); + const currentResultExpanded = expanded.has(keyToString(resultKey)); return ( <> @@ -102,6 +108,9 @@ export function AlertTableResultRow(props: Props) { onClick={handleDropdownClick} /> {listUnordered} + + {shortestPath} + {msg} )} @@ -118,7 +127,7 @@ export function AlertTableResultRow(props: Props) { {currentResultExpanded && result.codeFlows && - getAllPaths(result).map((path, pathIndex) => ( + allPaths.map((path, pathIndex) => ( { + const render = (props?: Props) => { + const mockRef = { current: null } as React.RefObject; + const results = createMockResults(); + const threadFlow = results[0]?.codeFlows?.[0]?.threadFlows?.[0]; + + if (!threadFlow) { + throw new Error("ThreadFlow is undefined"); + } + reactRender( + , + ); + }; + + it("renders number of steps", () => { + render(); + + expect(screen.getByText("Path (3 steps)")).toBeInTheDocument(); + }); +}); diff --git a/extensions/ql-vscode/src/view/results/__tests__/AlertTableResultRow.spec.tsx b/extensions/ql-vscode/src/view/results/__tests__/AlertTableResultRow.spec.tsx new file mode 100644 index 000000000..0aa7279ae --- /dev/null +++ b/extensions/ql-vscode/src/view/results/__tests__/AlertTableResultRow.spec.tsx @@ -0,0 +1,33 @@ +import { render as reactRender, screen } from "@testing-library/react"; +import { AlertTableResultRow } from "../AlertTableResultRow"; +import type { Props } from "../AlertTableResultRow"; +import { createMockResults } from "../../../../test/factories/results/mockresults"; + +describe(AlertTableResultRow.name, () => { + const render = (props?: Props) => { + const mockRef = { current: null } as React.RefObject; + const results = createMockResults(); + + reactRender( + , + ); + }; + + it("renders shortest path badge", () => { + render(); + + expect(screen.getByTitle("Shortest path")).toHaveTextContent("3"); + }); +}); diff --git a/extensions/ql-vscode/test/factories/results/mockresults.ts b/extensions/ql-vscode/test/factories/results/mockresults.ts new file mode 100644 index 000000000..368c05bb3 --- /dev/null +++ b/extensions/ql-vscode/test/factories/results/mockresults.ts @@ -0,0 +1,104 @@ +import type { Result } from "sarif"; + +export function createMockResults(): Result[] { + return [ + { + ruleId: "java/sql-injection", + ruleIndex: 0, + rule: { id: "java/sql-injection", index: 0 }, + message: { + text: "This query depends on a [user-provided value](1).", + }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { startLine: 15, startColumn: 29, endColumn: 56 }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "87e2d3cc5b365094:1", + primaryLocationStartColumnFingerprint: "16", + }, + codeFlows: [ + { + threadFlows: [ + { + locations: [ + { + location: { + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { + startLine: 13, + startColumn: 25, + endColumn: 54, + }, + }, + message: { text: "id : String" }, + }, + }, + { + location: { + physicalLocation: { + artifactLocation: { + uri: "file:/", + index: 5, + }, + region: { + startLine: 13, + startColumn: 25, + endColumn: 54, + }, + }, + message: { text: "id : String" }, + }, + }, + { + location: { + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { + startLine: 15, + startColumn: 29, + endColumn: 56, + }, + }, + message: { text: "... + ..." }, + }, + }, + ], + }, + ], + }, + ], + relatedLocations: [ + { + id: 1, + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { startLine: 13, startColumn: 25, endColumn: 54 }, + }, + message: { text: "user-provided value" }, + }, + ], + }, + ]; +}