feature: display length of shortest path in local results UI
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## [UNRELEASED]
|
## [UNRELEASED]
|
||||||
|
|
||||||
|
- Update results view to display the length of the shortest path for path queries.
|
||||||
|
|
||||||
## 1.14.0 - 7 August 2024
|
## 1.14.0 - 7 August 2024
|
||||||
|
|
||||||
- Add Python support to the CodeQL Model Editor. [#3676](https://github.com/github/vscode-codeql/pull/3676)
|
- Add Python support to the CodeQL Model Editor. [#3676](https://github.com/github/vscode-codeql/pull/3676)
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCe
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { VerticalRule } from "../common/VerticalRule";
|
import { VerticalRule } from "../common/VerticalRule";
|
||||||
import type { UserSettings } from "../../common/interface-types";
|
import type { UserSettings } from "../../common/interface-types";
|
||||||
|
import { pluralize } from "../../common/word";
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
path: ThreadFlow;
|
path: ThreadFlow;
|
||||||
pathIndex: number;
|
pathIndex: number;
|
||||||
resultIndex: number;
|
resultIndex: number;
|
||||||
@@ -65,7 +66,7 @@ export function AlertTablePathRow(props: Props) {
|
|||||||
onClick={handleDropdownClick}
|
onClick={handleDropdownClick}
|
||||||
/>
|
/>
|
||||||
<td className="vscode-codeql__text-center" colSpan={4}>
|
<td className="vscode-codeql__text-center" colSpan={4}>
|
||||||
Path
|
{`Path (${pluralize(path.locations.length, "step", "steps")})`}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{currentPathExpanded &&
|
{currentPathExpanded &&
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import { SarifLocation } from "./locations/SarifLocation";
|
|||||||
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
|
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
|
||||||
import { AlertTablePathRow } from "./AlertTablePathRow";
|
import { AlertTablePathRow } from "./AlertTablePathRow";
|
||||||
import type { UserSettings } from "../../common/interface-types";
|
import type { UserSettings } from "../../common/interface-types";
|
||||||
|
import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react";
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
result: Result;
|
result: Result;
|
||||||
resultIndex: number;
|
resultIndex: number;
|
||||||
expanded: Set<string>;
|
expanded: Set<string>;
|
||||||
@@ -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));
|
const currentResultExpanded = expanded.has(keyToString(resultKey));
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -102,6 +108,9 @@ export function AlertTableResultRow(props: Props) {
|
|||||||
onClick={handleDropdownClick}
|
onClick={handleDropdownClick}
|
||||||
/>
|
/>
|
||||||
<td className="vscode-codeql__icon-cell">{listUnordered}</td>
|
<td className="vscode-codeql__icon-cell">{listUnordered}</td>
|
||||||
|
<td className="vscode-codeql__icon-cell">
|
||||||
|
<VSCodeBadge title="Shortest path">{shortestPath}</VSCodeBadge>
|
||||||
|
</td>
|
||||||
<td colSpan={3}>{msg}</td>
|
<td colSpan={3}>{msg}</td>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -118,7 +127,7 @@ export function AlertTableResultRow(props: Props) {
|
|||||||
</tr>
|
</tr>
|
||||||
{currentResultExpanded &&
|
{currentResultExpanded &&
|
||||||
result.codeFlows &&
|
result.codeFlows &&
|
||||||
getAllPaths(result).map((path, pathIndex) => (
|
allPaths.map((path, pathIndex) => (
|
||||||
<AlertTablePathRow
|
<AlertTablePathRow
|
||||||
key={`${resultIndex}-${pathIndex}`}
|
key={`${resultIndex}-${pathIndex}`}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { render as reactRender, screen } from "@testing-library/react";
|
||||||
|
import type { Props } from "../AlertTablePathRow";
|
||||||
|
import { AlertTablePathRow } from "../AlertTablePathRow";
|
||||||
|
import { createMockResults } from "../../../../test/factories/results/mockresults";
|
||||||
|
|
||||||
|
describe(AlertTablePathRow.name, () => {
|
||||||
|
const render = (props?: Props) => {
|
||||||
|
const mockRef = { current: null } as React.RefObject<HTMLTableRowElement>;
|
||||||
|
const results = createMockResults();
|
||||||
|
const threadFlow = results[0]?.codeFlows?.[0]?.threadFlows?.[0];
|
||||||
|
|
||||||
|
if (!threadFlow) {
|
||||||
|
throw new Error("ThreadFlow is undefined");
|
||||||
|
}
|
||||||
|
reactRender(
|
||||||
|
<AlertTablePathRow
|
||||||
|
resultIndex={1}
|
||||||
|
selectedItem={undefined}
|
||||||
|
selectedItemRef={mockRef}
|
||||||
|
path={threadFlow}
|
||||||
|
pathIndex={0}
|
||||||
|
currentPathExpanded={true}
|
||||||
|
databaseUri={"dbUri"}
|
||||||
|
sourceLocationPrefix="src"
|
||||||
|
userSettings={{ shouldShowProvenance: false }}
|
||||||
|
updateSelectionCallback={jest.fn()}
|
||||||
|
toggleExpanded={jest.fn()}
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders number of steps", () => {
|
||||||
|
render();
|
||||||
|
|
||||||
|
expect(screen.getByText("Path (3 steps)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<HTMLTableRowElement>;
|
||||||
|
const results = createMockResults();
|
||||||
|
|
||||||
|
reactRender(
|
||||||
|
<AlertTableResultRow
|
||||||
|
result={results[0]}
|
||||||
|
expanded={new Set()}
|
||||||
|
resultIndex={1}
|
||||||
|
selectedItem={undefined}
|
||||||
|
selectedItemRef={mockRef}
|
||||||
|
databaseUri={"dbUri"}
|
||||||
|
sourceLocationPrefix="src"
|
||||||
|
userSettings={{ shouldShowProvenance: false }}
|
||||||
|
updateSelectionCallback={jest.fn()}
|
||||||
|
toggleExpanded={jest.fn()}
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders shortest path badge", () => {
|
||||||
|
render();
|
||||||
|
|
||||||
|
expect(screen.getByTitle("Shortest path")).toHaveTextContent("3");
|
||||||
|
});
|
||||||
|
});
|
||||||
104
extensions/ql-vscode/test/factories/results/mockresults.ts
Normal file
104
extensions/ql-vscode/test/factories/results/mockresults.ts
Normal file
@@ -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" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user