Files
graphify/tests/test_affected_member_seed.py
safishamsi 6631af7936 feat(ruby/affected): mixes_in edges for include/extend/prepend + method-bound callers in affected (#1668, #1669)
#1668: Ruby `include`/`extend`/`prepend <Const>` in a class/module body now emits
a `mixes_in` edge to the module. The mixin is captured during the node walk and
resolved cross-file by resolve_ruby_member_calls (single-owner guard, reusing
the #1640 module nodes as targets). The shared call pass skips these markers so
they are not mislabeled as `calls`. `extend self` and non-constant args are
skipped; ambiguous/undefined modules produce no edge. Rails concern composition
is now visible to affected/explain.

#1669: affected <Class> seeds the reverse walk with the root's own member nodes
(one method/contains hop) so callers that bind at method granularity (e.g.
Service.call -> the def self.call node, #1634) are reachable from the class.
method/contains stay out of the general relation-filtered walk (no forward
noise), and the seeded member nodes are not reported as hits.

Full suite: 2924 passed, 3 skipped. Verified end-to-end (Rails-shaped repros)
plus edge cases: extend self / undefined / ambiguous mixins emit nothing, mixins
are not emitted as calls, member methods aren't reported, class-level callers
still resolve, and one-hop seeding does not pull in downstream classes' methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 11:41:18 +01:00

62 lines
2.5 KiB
Python

"""#1669 — affected <Class> must reach callers that bind to the class's method
nodes (post-#1634 method-granularity resolution), by seeding the reverse walk
with the root's member nodes (one method/contains hop). method/contains stay out
of the general relation-filtered walk, so no forward noise is added elsewhere.
"""
from __future__ import annotations
import networkx as nx
from graphify.affected import affected_nodes
def _g():
g = nx.DiGraph()
for nid, label in [
("proc", "Processor"), ("proc_call", ".call()"),
("runner", "Runner"), ("runner_run", ".run()"),
]:
g.add_node(nid, label=label)
g.add_edge("proc", "proc_call", relation="method") # class owns method
g.add_edge("runner", "runner_run", relation="method")
g.add_edge("runner_run", "proc_call", relation="calls") # caller binds to method node (#1634)
return g
def test_class_affected_reaches_method_bound_caller():
g = _g()
hits = {h.node_id for h in affected_nodes(g, "proc", depth=2)}
assert "runner_run" in hits, "caller of Processor.call must be reachable from Processor"
def test_member_method_node_not_reported_as_hit():
g = _g()
hits = {h.node_id for h in affected_nodes(g, "proc", depth=2)}
# the class's own method node is a seed, not an affected node
assert "proc_call" not in hits
def test_method_contains_still_excluded_from_general_walk():
# A node two method-hops away (method of a DIFFERENT class discovered during
# the walk) must NOT be pulled in: only the root's own members are seeded.
g = nx.DiGraph()
for nid, label in [("a", "A"), ("a_m", ".m()"), ("b", "B"), ("b_m", ".n()")]:
g.add_node(nid, label=label)
g.add_edge("a", "a_m", relation="method")
g.add_edge("a_m", "b", relation="calls") # A.m calls class B
g.add_edge("b", "b_m", relation="method") # B's own method
hits = {h.node_id for h in affected_nodes(g, "a", depth=3)}
# We seeded A's members and walk reverse; B and B's method are downstream of A
# (A.m -> B), not reverse-callers of A, so they must not appear.
assert hits == set() or "b_m" not in hits
def test_class_level_caller_still_works():
# A caller bound to the class node itself (not a method) is unaffected.
g = nx.DiGraph()
g.add_node("svc", label="Svc")
g.add_node("caller", label=".use()")
g.add_edge("caller", "svc", relation="references")
hits = {h.node_id for h in affected_nodes(g, "svc", depth=2)}
assert "caller" in hits