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
23 changes: 14 additions & 9 deletions graph-ui/src/components/FilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,18 @@ function CheckRow({
return (
<button
onClick={onToggle}
className={`flex items-center gap-1.5 text-[11px] font-medium transition-all ${
className={`flex items-start gap-1.5 w-full min-w-0 text-left text-[11px] font-medium leading-snug transition-all ${
checked ? "text-primary" : "text-foreground/40"
}`}
>
<span
className={`w-3.5 h-3.5 rounded border flex items-center justify-center transition-all ${
className={`w-3.5 h-3.5 mt-[1px] shrink-0 rounded border flex items-center justify-center transition-all ${
checked ? "border-primary bg-primary/20" : "border-foreground/15"
}`}
>
{checked && <span className="text-primary text-[9px]">✓</span>}
</span>
{label}
<span className="min-w-0 whitespace-normal break-words">{label}</span>
{count !== undefined && (
<span className="text-foreground/25 tabular-nums">{count.toLocaleString()}</span>
)}
Expand Down Expand Up @@ -196,14 +196,14 @@ export function FilterPanel({
</p>
</div>

{/* Dead-code view */}
{/* Reference coverage (backend status key remains "dead") */}
<div className="px-4 pt-2 border-t border-border/30 space-y-2 shrink-0">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
<span className="text-[10px] text-foreground/30 uppercase tracking-widest">
Dead code
Reference coverage
</span>
<span className="text-[10px] text-red-400/80 tabular-nums">
{deadCount.toLocaleString()} dead
<span className="text-[10px] text-red-400/80 tabular-nums leading-snug whitespace-normal break-words text-right">
{deadCount.toLocaleString()} with no known inbound references
</span>
</div>

Expand All @@ -215,7 +215,7 @@ export function FilterPanel({
<CheckRow
checked={showOnlyDead}
onToggle={onToggleShowOnlyDead}
label="Show only dead code"
label="Show only nodes with no known inbound references"
/>
<CheckRow
checked={hideEntryPoints}
Expand All @@ -224,6 +224,11 @@ export function FilterPanel({
/>
<CheckRow checked={hideTests} onToggle={onToggleHideTests} label="Hide tests" />

<p className="text-[9px] leading-snug text-foreground/30 whitespace-normal break-words">
No known inbound references does not prove code is unused. Static analysis
may miss constructors, implicit or framework calls, and callbacks.
</p>

{/* Legend (only meaningful while colored by status) */}
{deadCodeView && (
<div className="flex flex-wrap gap-x-2 gap-y-1 pt-1">
Expand Down
149 changes: 138 additions & 11 deletions graph-ui/src/components/GraphTab.deadcode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,69 @@ vi.mock("./GraphScene", () => ({
computeCameraTarget: () => null,
}));

/* Server-classified layout: three unreferenced-coverage candidates (constructor,
* framework method, callback) plus nodes that must not enter that set just
* because in_calls is zero or absent. Edges match the reported degrees. */
const SAMPLE: GraphData = {
nodes: [
{
id: 1, x: 0, y: 0, z: 0, label: "Function", name: "orphan",
file_path: "src/orphan.ts", size: 1, color: "#fff", status: "dead", in_calls: 0,
id: 1, x: 0, y: 0, z: 0, label: "Method", name: "constructor",
file_path: "src/widget.ts", size: 1, color: "#fff", status: "dead", in_calls: 0,
},
{
id: 2, x: 1, y: 0, z: 0, label: "Function", name: "used",
file_path: "src/used.ts", size: 1, color: "#fff", status: "normal", in_calls: 3,
id: 2, x: 1, y: 0, z: 0, label: "Method", name: "onCreate",
file_path: "src/activity.ts", size: 1, color: "#fff", status: "dead", in_calls: 0,
},
{
id: 3, x: 2, y: 0, z: 0, label: "Function", name: "handleClick",
file_path: "src/ui.ts", size: 1, color: "#fff", status: "dead", in_calls: 0,
},
{
id: 4, x: 3, y: 0, z: 0, label: "Function", name: "processRequest",
file_path: "src/used.ts", size: 1, color: "#fff", status: "normal", in_calls: 2,
},
{
id: 5, x: 4, y: 0, z: 0, label: "Function", name: "formatName",
file_path: "src/format.ts", size: 1, color: "#fff", status: "normal", in_calls: 0,
},
{
id: 6, x: 5, y: 0, z: 0, label: "Function", name: "main",
file_path: "src/main.ts", size: 1, color: "#fff", status: "entry", in_calls: 0,
},
{
id: 7, x: 6, y: 0, z: 0, label: "Function", name: "testProcess",
file_path: "src/used.test.ts", size: 1, color: "#fff", status: "test", in_calls: 0,
},
{
id: 8, x: 7, y: 0, z: 0, label: "Function", name: "publicApi",
file_path: "src/api.ts", size: 1, color: "#fff", status: "exported", in_calls: 0,
},
{
id: 9, x: 8, y: 0, z: 0, label: "Class", name: "App",
file_path: "src/app.ts", size: 1, color: "#fff", status: "structural",
},
{
id: 10, x: 9, y: 0, z: 0, label: "Function", name: "mystery",
file_path: "src/mystery.ts", size: 1, color: "#fff", in_calls: 0,
},
],
edges: [
{ source: 6, target: 4, type: "CALLS" },
{ source: 8, target: 4, type: "CALLS" },
{ source: 4, target: 5, type: "USAGE" },
],
total_nodes: 10,
};

const NO_CANDIDATES: GraphData = {
nodes: [
{
id: 1, x: 0, y: 0, z: 0, label: "Function", name: "used",
file_path: "src/used.ts", size: 1, color: "#fff", status: "single", in_calls: 1,
},
{
id: 2, x: 1, y: 0, z: 0, label: "Function", name: "main",
file_path: "src/main.ts", size: 1, color: "#fff", status: "entry", in_calls: 0,
},
],
edges: [{ source: 2, target: 1, type: "CALLS" }],
Expand All @@ -46,19 +100,92 @@ describe("GraphTab dead-code filters", () => {
vi.unstubAllGlobals();
});

it("shows the dead count and filters to only dead code on toggle", async () => {
it("reports no known inbound references without calling the code dead or live", async () => {
mockLayoutFetch(SAMPLE);
render(<GraphTab project="demo" />);

expect(await screen.findByText("Filters")).toBeInTheDocument();
expect(screen.getByText("Reference coverage")).toBeInTheDocument();
expect(screen.getByText("3 with no known inbound references")).toBeInTheDocument();
expect(
screen.getByText(
"No known inbound references does not prove code is unused. Static analysis may miss constructors, implicit or framework calls, and callbacks.",
),
).toBeInTheDocument();
expect(screen.queryByText("Dead code")).not.toBeInTheDocument();
expect(screen.queryByText(/^\d+ dead$/)).not.toBeInTheDocument();
expect(screen.queryByText("Show only dead code")).not.toBeInTheDocument();
expect(screen.queryByText("Dead (0 callers)")).not.toBeInTheDocument();
/* Caveat is visible before status coloring; legend is not. */
expect(screen.queryByText("No known inbound references")).not.toBeInTheDocument();
expect(screen.queryByText(/filtered from/)).not.toBeInTheDocument();
});

it("colors the legend as coverage, not as proven dead code", async () => {
mockLayoutFetch(SAMPLE);
render(<GraphTab project="demo" />);

expect(await screen.findByText("Filters")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Color by status" }));

expect(screen.getByText("No known inbound references")).toBeInTheDocument();
expect(screen.getByText("One caller")).toBeInTheDocument();
expect(screen.queryByText("Dead code")).not.toBeInTheDocument();
expect(screen.queryByText(/^\d+ dead$/)).not.toBeInTheDocument();
expect(screen.queryByText("Show only dead code")).not.toBeInTheDocument();
expect(screen.queryByText("Dead (0 callers)")).not.toBeInTheDocument();
});

it("filters to the backend dead category and restores the full layout", async () => {
mockLayoutFetch(SAMPLE);
render(<GraphTab project="demo" />);

/* Panel loaded; the dead-code section reports one dead node. */
expect(await screen.findByText("Filters")).toBeInTheDocument();
expect(screen.getByText("1 dead")).toBeInTheDocument();
expect(screen.getByText(/10 nodes/)).toBeInTheDocument();

fireEvent.click(
screen.getByRole("button", {
name: /Show only nodes with no known inbound references/,
}),
);
expect(await screen.findByText(/filtered from 10/)).toBeInTheDocument();
expect(screen.getByText(/3 nodes/)).toBeInTheDocument();

const search = screen.getByPlaceholderText("Search...");
for (const name of ["constructor", "onCreate", "handleClick"]) {
fireEvent.change(search, { target: { value: name } });
expect(screen.getByRole("button", { name: new RegExp(name) })).toBeInTheDocument();
}
/* Server-provided non-dead statuses stay out, even with in_calls of 0. */
for (const name of ["processRequest", "formatName", "main", "testProcess", "publicApi", "App", "mystery"]) {
fireEvent.change(search, { target: { value: name } });
expect(screen.queryByRole("button", { name: new RegExp(name) })).not.toBeInTheDocument();
}

/* Both nodes visible initially — no "filtered from" notice. */
fireEvent.click(
screen.getByRole("button", {
name: /Show only nodes with no known inbound references/,
}),
);
expect(screen.queryByText(/filtered from/)).not.toBeInTheDocument();
expect(screen.getByText(/10 nodes/)).toBeInTheDocument();
});

it("shows a zero count and empty candidate selection when none are classified dead", async () => {
mockLayoutFetch(NO_CANDIDATES);
render(<GraphTab project="demo" />);

expect(await screen.findByText("Filters")).toBeInTheDocument();
expect(screen.getByText("0 with no known inbound references")).toBeInTheDocument();
expect(screen.getByText(/2 nodes/)).toBeInTheDocument();

/* Toggling "Show only dead code" hides the non-dead node. */
fireEvent.click(screen.getByRole("button", { name: /Show only dead code/ }));
expect(await screen.findByText(/filtered from 2/)).toBeInTheDocument();
fireEvent.click(
screen.getByRole("button", {
name: /Show only nodes with no known inbound references/,
}),
);
expect(screen.getByText("All nodes filtered out")).toBeInTheDocument();
expect(screen.getByText("Filters")).toBeInTheDocument();
expect(screen.getByText("0 with no known inbound references")).toBeInTheDocument();
});
});
9 changes: 5 additions & 4 deletions graph-ui/src/lib/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ export function colorForLabel(label: string): string {
return LABEL_COLORS[label] ?? DEFAULT_COLOR;
}

/* Dead-code status → color (matches layout3d.c status strings).
* dead zero callers + zero usages, not entry/test/exported
/* Status → color (matches layout3d.c status strings).
* dead no known inbound CALLS/USAGE/CALL_REFERENCE; not proven unused
* (constructors, framework dispatch, and callbacks may be missed)
* single exactly one caller
* entry entry points / routes
* test test code
* normal healthy (>=2 callers)
* exported/structural → dimmed grey (not dead-code candidates) */
* exported/structural → dimmed grey (not unreferenced-coverage candidates) */
const STATUS_COLORS: Record<string, string> = {
dead: "#ef4444",
single: "#f97316",
Expand All @@ -44,7 +45,7 @@ export function colorForStatus(status?: string): string {
}

export const STATUS_LEGEND: { status: string; label: string; color: string }[] = [
{ status: "dead", label: "Dead (0 callers)", color: STATUS_COLORS.dead },
{ status: "dead", label: "No known inbound references", color: STATUS_COLORS.dead },
{ status: "single", label: "One caller", color: STATUS_COLORS.single },
{ status: "entry", label: "Entry / route", color: STATUS_COLORS.entry },
{ status: "test", label: "Test", color: STATUS_COLORS.test },
Expand Down
Loading