diff --git a/code_review_graph/parser.py b/code_review_graph/parser.py index c55b2e8f..bf8fe163 100644 --- a/code_review_graph/parser.py +++ b/code_review_graph/parser.py @@ -4574,6 +4574,55 @@ 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), 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: + ... + + 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 0:`` + if ( + cond.type == "integer" + and cond.text == b"0" + ): + 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 +4755,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..4c4456fe --- /dev/null +++ b/tests/fixtures/sample_dead_guard.py @@ -0,0 +1,30 @@ +"""Fixture for testing dead-guard detection on CALLS edges. + +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. +""" + +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 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 d1d96411..a7125593 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1116,6 +1116,35 @@ 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 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", + ) + 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_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 + 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_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 + class TestValueReferences: """Tests for REFERENCES edge extraction from function-as-value patterns."""