Skip to content

fix(health): stop flagging reachable code as unreachable in the dataflow CFG#760

Merged
swati510 merged 2 commits into
mainfrom
fix/dataflow-cfg-reachability
Jul 10, 2026
Merged

fix(health): stop flagging reachable code as unreachable in the dataflow CFG#760
swati510 merged 2 commits into
mainfrom
fix/dataflow-cfg-reachability

Conversation

@RaghavChamadiya

Copy link
Copy Markdown
Member

Problem

CFG.reachable_ids() flagged plainly reachable lines as unreachable. A tree-wide sweep over packages/ reported 106 functions with unreachable lines; a hand-checked stratified sample was 0 for 5 real. Repro examples: the trailing return False in tool_search_symbols.py::_symbol_kind_for_request_kind, the guarded bare return in webhooks.py::_verify_gitlab_token, and the mid-chain return in graph-toolbar.tsx::scopeOverlaysToViewMode (the defect is language-independent).

No shipped consumer reads this method yet, but statement-level dead code is planned to build directly on it, so it needs to be right first.

Root causes (two)

  1. Comment nodes entered the CFG as statements. Tree-sitter emits a trailing comment as a named sibling after its statement, so return x # note puts a comment node after the terminator. _process_seq treated it as a statement and spawned an unreachable block whose start line is the return line itself. Every one of the five sampled false positives was this shape. Fixed by dropping comment nodes in _body_stmts, the single choke point that produces statement sequences (same substring idiom nloc counting uses; covers comment / line_comment / block_comment / doc_comment across grammars).

  2. finally bodies were unreachable when the try body and all handlers terminate. The finally entry was only wired from the normal fall-through paths, so a try: ... return / except: return / finally: cleanup() had its cleanup flagged, even though finally runs on every path. Hand-checking the 10 functions that survived fix 1 surfaced this: all 10 were finally bodies. Fixed with an edge from the protected-region entry to the finally entry, mirroring the existing body-to-handler approximation.

Also renames the misleading local in derive_facts that held the reachable set under the name unreachable.

Tests

New fixtures in tests/unit/health/test_dataflow_cfg.py: the five reported shapes (guarded bare return, trailing return after an if-chain, post-guard body, if-guarded return mid-function, TS mid-return chain, each with trailing comments), a standalone comment after a return, the two finally shapes (with and without handlers), and a guard that genuinely dead code after a comment still gets flagged.

Health suite: 927 passed. Full unit suite: 6806 passed, 1 skipped (the pre-existing test_bundled_changelog_in_sync failure on main was deselected; it is unrelated changelog drift).

Rerunning the tree-wide sweep: unreachable false positives drop from 106 functions to zero, and the true-unreachable fixtures stay green.

Perf-neutral: a list-comprehension filter where a list copy already happened, plus one extra edge per try-with-finally.

…low CFG

Two defects made CFG.reachable_ids() flag plainly reachable lines, tripping
a tree-wide sweep on 106 functions with zero real positives.

1. Comment nodes entered the CFG as statements. Tree-sitter emits a trailing
comment as a named sibling after its statement, so a return followed by a
same-line comment put a comment node after the terminator and _process_seq
spawned a bogus unreachable block at the return line. _body_stmts now drops
comment nodes, the same idiom nloc counting uses.

2. A finally body was only reachable through the normal join, so a try whose
body and handlers all terminate (return / raise) had its finally flagged
unreachable even though it runs on every path. The builder now adds an edge
from the protected-region entry to the finally entry, mirroring the existing
handler approximation.

Also renames the misleading local in derive_facts that held the reachable
set under the name unreachable.

No shipped consumer reads reachable_ids() yet; statement-level dead code
will build on it, so it gets fixture coverage for both shapes first.
@repowise-bot

repowise-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

✅ Health: 7.7 (unchanged)

📋 At a glance
3 hotspots touched · 2 co-change pairs left out · 1 file with recent fix history.

Files & modules (2)
  • packages (2 files)
    • .../dataflow/cfg.py
    • .../dataflow/facts.py
  • tests (1 file)
    • .../health/test_dataflow_cfg.py

⚠️ Change risk: moderate (riskier than 38% of this repo's commits · raw 8.3/10)
This change's risk is driven by:

  • more lines added than baseline

🩹 Review priority (files here with the most recent bug-fix history — defects cluster, so review these first)

🔎 More signals (2)

🔥 Hotspots touched (3)

  • .../health/test_dataflow_cfg.py — 2 commits/90d, 0 dependents · primary owner: Raghav Chamadiya (98%)
  • .../dataflow/cfg.py — 4 commits/90d, 7 dependents · primary owner: Raghav Chamadiya (100%)
  • .../dataflow/facts.py — 1 commits/90d, 3 dependents · primary owner: Raghav Chamadiya (100%)

🔗 Hidden coupling (1 file)

  • .../dataflow/cfg.py co-changes with these files (not in this PR):
    • .../complexity/languages.py (2× — 🟢 routine)
    • .../dialects/__init__.py (2× — 🟢 routine)

📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-10 15:29 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@swati510 swati510 merged commit f859c2e into main Jul 10, 2026
7 checks passed
@swati510 swati510 deleted the fix/dataflow-cfg-reachability branch July 10, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants