Skip to content

Commit 538515e

Browse files
author
jgstern-agent
committed
fix: dead-code-maybe BFS follows dispatches_to, routes_to, wraps edges
The reachability BFS only followed 'calls' edges, so interface dispatch targets (Go Notifier.Notify implementations), HTTP route handlers, and middleware-wrapped functions were all falsely flagged as dead code. On alertmanager: false positives reduced from 781 to 634 (147 functions correctly reclassified as reachable). All 16 Notifier.Notify false positives eliminated. The BFS now uses _REACHABILITY_EDGE_TYPES = {calls, dispatches_to, routes_to, wraps}, matching the call-flow edge types the slice BFS uses for forward reachability. Signed-off-by: jgstern-agent <josh-agent@iterabloom.com>
1 parent b25b612 commit 538515e

4 files changed

Lines changed: 172 additions & 4 deletions

File tree

.ci/affected-tests.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Test selection manifest
2-
# Generated by smart-test at 2026-04-10T10:23:49-04:00
2+
# Generated by smart-test at 2026-04-10T11:12:24-04:00
33
# Mode: targeted
44
# Baseline: e2fb9e02102c793608778dce538cc121418600fc
5-
# Changed files: 6
5+
# Changed files: 7
66
# Changed source files: 2
77
# Selected tests: 50
88
#

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ This changelog tracks the **tool version** (package releases). The **schema vers
6767

6868
### Fixed
6969

70+
#### Dead code analysis
71+
72+
- **`dead-code-maybe` BFS follows `dispatches_to`, `routes_to`, and `wraps` edges**: the reachability BFS only followed `calls` edges, so interface dispatch targets (Go `Notifier.Notify` implementations), HTTP route handlers, and middleware-wrapped functions were all flagged as dead code. On alertmanager, this fix reduced false positives from 781 to 634 (147 functions correctly reclassified as reachable), and eliminated all 16 `Notifier.Notify` false positives. The BFS now follows the same call-flow edge types that the slice BFS uses.
73+
7074
#### Go analyzer
7175

7276
- **Cross-file struct method aggregation** (WI-hobuk): the structural interface matcher iterated per-file, so methods defined in a sibling file of the same package were dropped from the struct's effective method set. Fix: aggregate `struct_method_sets` per package directory before iterating struct candidates. Two structs of the same name in *different* directories still keep disjoint method sets (preserving the INV-zomuk fix). Scope expansion from INV-zomuk.

packages/hypergumbo-core/src/hypergumbo_core/cli.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3616,10 +3616,15 @@ def cmd_dead_code_maybe(args: argparse.Namespace) -> int:
36163616
if seeds_mode in ("tests", "all"):
36173617
seed_ids.update(test_symbols)
36183618

3619-
# BFS from seeds through call edges
3619+
# BFS from seeds through call-flow edges.
3620+
# calls: direct function/method calls
3621+
# dispatches_to: interface/abstract method → concrete implementation
3622+
# routes_to: HTTP route registration → handler function
3623+
# wraps: middleware wrapper → inner handler
3624+
_REACHABILITY_EDGE_TYPES = {"calls", "dispatches_to", "routes_to", "wraps"}
36203625
call_graph: dict[str, list[str]] = {}
36213626
for edge in edges:
3622-
if edge.get("type") == "calls":
3627+
if edge.get("type") in _REACHABILITY_EDGE_TYPES:
36233628
src = edge.get("src", "")
36243629
dst = edge.get("dst", "")
36253630
if src and dst:

packages/hypergumbo-core/tests/test_cli_dead_code.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,165 @@ def test_text_no_dead_code(self, tmp_path: Path) -> None:
242242

243243
assert "No potentially dead functions" in captured.getvalue()
244244

245+
def test_dispatches_to_makes_impl_reachable(self, tmp_path: Path) -> None:
246+
"""Interface dispatch edges make concrete implementations reachable.
247+
248+
In Go/Java, calling an interface method dispatches to concrete
249+
implementations via dispatches_to edges. The BFS must follow these
250+
edges so that implementations like Notifier.Notify in alertmanager
251+
are NOT flagged as dead code.
252+
"""
253+
import argparse
254+
255+
nodes = [
256+
# Entrypoint: main
257+
{"id": "go:main.go:1-10:main:function", "name": "main",
258+
"kind": "function", "language": "go", "path": "main.go",
259+
"span": {"start_line": 1, "end_line": 10},
260+
"meta": {"is_main": True}},
261+
# Interface method
262+
{"id": "go:notify.go:5-5:Notifier.Notify:method", "name": "Notifier.Notify",
263+
"kind": "method", "language": "go", "path": "notify.go",
264+
"span": {"start_line": 5, "end_line": 5}},
265+
# Concrete implementation
266+
{"id": "go:slack.go:10-50:Notifier.Notify:method", "name": "Notifier.Notify",
267+
"kind": "method", "language": "go", "path": "slack.go",
268+
"span": {"start_line": 10, "end_line": 50}},
269+
# Unreachable function (truly dead)
270+
{"id": "go:orphan.go:1-20:orphan:function", "name": "orphan",
271+
"kind": "function", "language": "go", "path": "orphan.go",
272+
"span": {"start_line": 1, "end_line": 20}},
273+
]
274+
edges = [
275+
# main calls interface method
276+
{"type": "calls", "src": "go:main.go:1-10:main:function",
277+
"dst": "go:notify.go:5-5:Notifier.Notify:method"},
278+
# Interface dispatches to concrete impl
279+
{"type": "dispatches_to",
280+
"src": "go:notify.go:5-5:Notifier.Notify:method",
281+
"dst": "go:slack.go:10-50:Notifier.Notify:method"},
282+
]
283+
bm_path = _make_behavior_map(tmp_path, nodes, edges)
284+
285+
args = argparse.Namespace(
286+
path=str(tmp_path), input=str(bm_path), format="json",
287+
seeds="entrypoints", min_confidence=0.0,
288+
)
289+
import io
290+
import sys
291+
captured = io.StringIO()
292+
old_stdout = sys.stdout
293+
sys.stdout = captured
294+
try:
295+
cmd_dead_code_maybe(args)
296+
finally:
297+
sys.stdout = old_stdout
298+
299+
output = json.loads(captured.getvalue())
300+
dead_names = {d["name"] for d in output.get("dead_candidates", [])}
301+
# Slack Notifier.Notify IS reachable via dispatches_to → NOT dead
302+
dead_ids = {d["id"] for d in output.get("dead_candidates", [])}
303+
assert "go:slack.go:10-50:Notifier.Notify:method" not in dead_ids
304+
# orphan is truly unreachable → dead
305+
assert "orphan" in dead_names
306+
307+
def test_routes_to_makes_handler_reachable(self, tmp_path: Path) -> None:
308+
"""Route registration edges make handlers reachable.
309+
310+
HTTP route registrations emit routes_to edges. The BFS must follow
311+
them so that route handlers are not flagged as dead.
312+
"""
313+
import argparse
314+
315+
nodes = [
316+
{"id": "go:main.go:1-10:main:function", "name": "main",
317+
"kind": "function", "language": "go", "path": "main.go",
318+
"span": {"start_line": 1, "end_line": 10},
319+
"meta": {"is_main": True}},
320+
# Route registration node
321+
{"id": "go:routes.go:5-5:GET /api:route", "name": "GET /api",
322+
"kind": "route", "language": "go", "path": "routes.go",
323+
"span": {"start_line": 5, "end_line": 5},
324+
"meta": {"route_path": "/api", "http_method": "GET"}},
325+
# Handler function
326+
{"id": "go:handler.go:10-40:handleAPI:function", "name": "handleAPI",
327+
"kind": "function", "language": "go", "path": "handler.go",
328+
"span": {"start_line": 10, "end_line": 40}},
329+
]
330+
edges = [
331+
{"type": "calls", "src": "go:main.go:1-10:main:function",
332+
"dst": "go:routes.go:5-5:GET /api:route"},
333+
{"type": "routes_to", "src": "go:routes.go:5-5:GET /api:route",
334+
"dst": "go:handler.go:10-40:handleAPI:function"},
335+
]
336+
bm_path = _make_behavior_map(tmp_path, nodes, edges)
337+
338+
args = argparse.Namespace(
339+
path=str(tmp_path), input=str(bm_path), format="json",
340+
seeds="entrypoints", min_confidence=0.0,
341+
)
342+
import io
343+
import sys
344+
captured = io.StringIO()
345+
old_stdout = sys.stdout
346+
sys.stdout = captured
347+
try:
348+
cmd_dead_code_maybe(args)
349+
finally:
350+
sys.stdout = old_stdout
351+
352+
output = json.loads(captured.getvalue())
353+
dead_ids = {d["id"] for d in output.get("dead_candidates", [])}
354+
assert "go:handler.go:10-40:handleAPI:function" not in dead_ids
355+
356+
def test_wraps_makes_inner_reachable(self, tmp_path: Path) -> None:
357+
"""Middleware wrapper edges make inner handlers reachable.
358+
359+
Go route registrations with middleware wrappers emit wraps edges.
360+
The BFS must follow them.
361+
"""
362+
import argparse
363+
364+
nodes = [
365+
{"id": "go:main.go:1-10:main:function", "name": "main",
366+
"kind": "function", "language": "go", "path": "main.go",
367+
"span": {"start_line": 1, "end_line": 10},
368+
"meta": {"is_main": True}},
369+
# Wrapper function
370+
{"id": "go:middleware.go:5-15:authWrap:function", "name": "authWrap",
371+
"kind": "function", "language": "go", "path": "middleware.go",
372+
"span": {"start_line": 5, "end_line": 15}},
373+
# Inner handler
374+
{"id": "go:api.go:20-50:query:function", "name": "query",
375+
"kind": "function", "language": "go", "path": "api.go",
376+
"span": {"start_line": 20, "end_line": 50}},
377+
]
378+
edges = [
379+
{"type": "calls", "src": "go:main.go:1-10:main:function",
380+
"dst": "go:middleware.go:5-15:authWrap:function"},
381+
{"type": "wraps", "src": "go:middleware.go:5-15:authWrap:function",
382+
"dst": "go:api.go:20-50:query:function"},
383+
]
384+
bm_path = _make_behavior_map(tmp_path, nodes, edges)
385+
386+
args = argparse.Namespace(
387+
path=str(tmp_path), input=str(bm_path), format="json",
388+
seeds="entrypoints", min_confidence=0.0,
389+
)
390+
import io
391+
import sys
392+
captured = io.StringIO()
393+
old_stdout = sys.stdout
394+
sys.stdout = captured
395+
try:
396+
cmd_dead_code_maybe(args)
397+
finally:
398+
sys.stdout = old_stdout
399+
400+
output = json.loads(captured.getvalue())
401+
dead_ids = {d["id"] for d in output.get("dead_candidates", [])}
402+
assert "go:api.go:20-50:query:function" not in dead_ids
403+
245404
def test_seeds_all_includes_tests(self, tmp_path: Path) -> None:
246405
"""--seeds all uses both entrypoints AND test functions as seeds."""
247406
import argparse

0 commit comments

Comments
 (0)