diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md
index d638979fb..001ad6c0a 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.
+
## 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..17f00e783
--- /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 "../AlertTablePathRow";
+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" },
+ },
+ ],
+ },
+ ];
+}