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
55 changes: 55 additions & 0 deletions code_review_graph/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions tests/fixtures/sample_dead_guard.py
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down