mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-13 19:07:10 +00:00
6631af7936
#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>
62 lines
2.5 KiB
Python
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
|