From 983431205729f99385b6b5959f47f73e7361d9d1 Mon Sep 17 00:00:00 2001 From: Minxi Hou Date: Thu, 25 Jun 2026 19:46:13 +0800 Subject: [PATCH 1/2] parser: tag CALLS edges inside dead guards with reachable flag Calls inside `if False:` and `if TYPE_CHECKING:` blocks are statically unreachable but the parser still emits CALLS edges for them, causing false positives in dead-code detection and impact analysis. Add a lightweight ancestor-walk helper that detects these two common Python idioms and sets extra["reachable"] = False on the affected CALLS edges. Live edges omit the key entirely so existing graph.db files remain forward-compatible (absent key means live). The detection covers all Tree-sitter languages routed through _extract_calls. Bespoke handlers (ReScript, Dart, Elixir) and C preprocessor guards (#if 0) are left for follow-up work. Closes #576 Signed-off-by: Minxi Hou --- code_review_graph/parser.py | 47 +++++++++++++++++++++++++++++ tests/fixtures/sample_dead_guard.py | 27 +++++++++++++++++ tests/test_parser.py | 24 +++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 tests/fixtures/sample_dead_guard.py diff --git a/code_review_graph/parser.py b/code_review_graph/parser.py index c55b2e8f..6b8ed6a4 100644 --- a/code_review_graph/parser.py +++ b/code_review_graph/parser.py @@ -4574,6 +4574,47 @@ def _extract_imports( line=child.start_point[0] + 1, )) + @staticmethod + def _is_in_static_dead_guard(node) -> bool: + """Walk ancestors to detect statically-dead guards. + + Returns True when ``node`` sits inside an ``if_statement`` whose + condition is ``False`` (the literal) or ``TYPE_CHECKING`` (the + ``typing`` sentinel). This covers the two most common Python + idioms for compile-time-only code: + + if False: + ... + if TYPE_CHECKING: + ... + + The walk stops at function/class/module boundaries so a dead + guard in an outer scope does not leak into nested definitions. + """ + cursor = node.parent + while cursor is not None: + ntype = cursor.type + # Stop at scope boundaries -- a dead guard in an outer + # function does not make an inner function's calls dead. + if ntype in ( + "function_definition", "class_definition", "module", + ): + break + if ntype == "if_statement": + cond = cursor.child_by_field_name("condition") + if cond is not None: + # ``if False:`` + if cond.type == "false": + return True + # ``if TYPE_CHECKING:`` + if ( + cond.type == "identifier" + and cond.text == b"TYPE_CHECKING" + ): + return True + cursor = cursor.parent + return False + def _extract_calls( self, child, @@ -4706,6 +4747,12 @@ def _extract_calls( call_name, file_path, language, import_map or {}, defined_names or set(), ) + # Tag calls inside statically-dead guards (if False: / + # if TYPE_CHECKING:) so downstream consumers can filter + # them out. Live edges omit the key (absent = live). + if self._is_in_static_dead_guard(child): + call_extra["reachable"] = False + edges.append(EdgeInfo( kind="CALLS", source=caller, diff --git a/tests/fixtures/sample_dead_guard.py b/tests/fixtures/sample_dead_guard.py new file mode 100644 index 00000000..82523970 --- /dev/null +++ b/tests/fixtures/sample_dead_guard.py @@ -0,0 +1,27 @@ +"""Fixture for testing dead-guard detection on CALLS edges. + +Contains calls under ``if False:``, ``if TYPE_CHECKING:``, and a live +call as a control. The parser should tag the first two with +``extra["reachable"] == False`` and leave the live call without the key. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from os.path import join as pjoin # noqa: F401 + + +def live_helper(): + pass + + +def caller(): + live_helper() + + if False: + dead_false_call() # noqa: F821 + + if TYPE_CHECKING: + dead_tc_call() # noqa: F821 diff --git a/tests/test_parser.py b/tests/test_parser.py index d1d96411..d08c26cb 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1116,6 +1116,30 @@ def test_detects_test_functions(self): assert "test_something" in test_names assert "helper" not in test_names + def test_dead_guard_reachable_flag(self): + """CALLS edges inside ``if False:`` / ``if TYPE_CHECKING:`` are + tagged with ``extra["reachable"] == False``; live calls omit the + key entirely. See: #576.""" + nodes, edges = self.parser.parse_file( + FIXTURES / "sample_dead_guard.py", + ) + calls = [e for e in edges if e.kind == "CALLS"] + + live = [e for e in calls if "live_helper" in e.target] + dead_false = [e for e in calls if "dead_false_call" in e.target] + dead_tc = [e for e in calls if "dead_tc_call" in e.target] + + # Control: live call has no reachable key + assert len(live) == 1 + assert "reachable" not in live[0].extra + + # Dead calls carry the flag + assert len(dead_false) == 1 + assert dead_false[0].extra.get("reachable") is False + + assert len(dead_tc) == 1 + assert dead_tc[0].extra.get("reachable") is False + class TestValueReferences: """Tests for REFERENCES edge extraction from function-as-value patterns.""" From ded4913f812aee9ae980db8a46bc0037e5e67d4d Mon Sep 17 00:00:00 2001 From: Minxi Hou Date: Fri, 26 Jun 2026 12:57:56 +0800 Subject: [PATCH 2/2] parser: also detect if 0: as a dead guard Extend _is_in_static_dead_guard to recognize the integer literal 0 as a dead condition, alongside False and TYPE_CHECKING. The if 0: pattern appears in C-extension bindings and some legacy codebases. In tree-sitter-python, 0 parses as an "integer" node with text b"0", so the check mirrors the existing "false" node check. Suggested-by: tirth8205 (PR #580 review) Signed-off-by: Minxi Hou --- code_review_graph/parser.py | 14 +++++++++++--- tests/fixtures/sample_dead_guard.py | 7 +++++-- tests/test_parser.py | 11 ++++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/code_review_graph/parser.py b/code_review_graph/parser.py index 6b8ed6a4..bf8fe163 100644 --- a/code_review_graph/parser.py +++ b/code_review_graph/parser.py @@ -4579,12 +4579,14 @@ def _is_in_static_dead_guard(node) -> bool: """Walk ancestors to detect statically-dead guards. Returns True when ``node`` sits inside an ``if_statement`` whose - condition is ``False`` (the literal) or ``TYPE_CHECKING`` (the - ``typing`` sentinel). This covers the two most common Python - idioms for compile-time-only code: + condition is ``False`` (the literal), the integer ``0``, or + ``TYPE_CHECKING`` (the ``typing`` sentinel). This covers the + most common Python idioms for compile-time-only code: if False: ... + if 0: + ... if TYPE_CHECKING: ... @@ -4606,6 +4608,12 @@ def _is_in_static_dead_guard(node) -> bool: # ``if False:`` if cond.type == "false": return True + # ``if 0:`` + if ( + cond.type == "integer" + and cond.text == b"0" + ): + return True # ``if TYPE_CHECKING:`` if ( cond.type == "identifier" diff --git a/tests/fixtures/sample_dead_guard.py b/tests/fixtures/sample_dead_guard.py index 82523970..4c4456fe 100644 --- a/tests/fixtures/sample_dead_guard.py +++ b/tests/fixtures/sample_dead_guard.py @@ -1,7 +1,7 @@ """Fixture for testing dead-guard detection on CALLS edges. -Contains calls under ``if False:``, ``if TYPE_CHECKING:``, and a live -call as a control. The parser should tag the first two with +Contains calls under ``if False:``, ``if 0:``, ``if TYPE_CHECKING:``, +and a live call as a control. The parser should tag the dead ones with ``extra["reachable"] == False`` and leave the live call without the key. """ @@ -23,5 +23,8 @@ def caller(): if False: dead_false_call() # noqa: F821 + if 0: + dead_zero_call() # noqa: F821 + if TYPE_CHECKING: dead_tc_call() # noqa: F821 diff --git a/tests/test_parser.py b/tests/test_parser.py index d08c26cb..a7125593 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1117,9 +1117,10 @@ def test_detects_test_functions(self): assert "helper" not in test_names def test_dead_guard_reachable_flag(self): - """CALLS edges inside ``if False:`` / ``if TYPE_CHECKING:`` are - tagged with ``extra["reachable"] == False``; live calls omit the - key entirely. See: #576.""" + """CALLS edges inside ``if False:`` / ``if 0:`` / + ``if TYPE_CHECKING:`` are tagged with + ``extra["reachable"] == False``; live calls omit the key + entirely. See: #576.""" nodes, edges = self.parser.parse_file( FIXTURES / "sample_dead_guard.py", ) @@ -1127,6 +1128,7 @@ def test_dead_guard_reachable_flag(self): live = [e for e in calls if "live_helper" in e.target] dead_false = [e for e in calls if "dead_false_call" in e.target] + dead_zero = [e for e in calls if "dead_zero_call" in e.target] dead_tc = [e for e in calls if "dead_tc_call" in e.target] # Control: live call has no reachable key @@ -1137,6 +1139,9 @@ def test_dead_guard_reachable_flag(self): assert len(dead_false) == 1 assert dead_false[0].extra.get("reachable") is False + assert len(dead_zero) == 1 + assert dead_zero[0].extra.get("reachable") is False + assert len(dead_tc) == 1 assert dead_tc[0].extra.get("reachable") is False