Files
graphify/tests/test_language_resolvers.py
vamsi pavan mahesh gunturu 86ecb769b6 feat(ruby): type-aware member-call resolution via a resolver framework (#1499)
Resolve Ruby `obj.method()` calls by the inferred type of the receiver
instead of by globally-unique method name. `p = Processor.new; p.run`
now emits a `calls` edge to `Processor#run` and survives name collisions
with unrelated `Worker#run` definitions, where the old name-based match
either resolved by luck or dropped the edge as ambiguous.

Introduces graphify/resolver_registry.py, a behavior-identical
formalization of the existing tail-of-extract() language resolution
passes (Swift #1356, Python #1446 become registered entries), and
graphify/ruby_resolution.py, its first new consumer. Receiver type is
inferred only from unambiguous local `var = ClassName.new` bindings;
ambiguous or unknown receivers resolve to nothing (no false positives).

Note: Ruby member calls are now excluded from name-based cross-file
resolution and resolved by inferred type only. This is an intentional
precision-over-recall change scoped to Ruby: a cross-file `var.method`
whose receiver type cannot be proven from a local `X.new` binding no
longer resolves by name-luck (it produces no edge rather than a possibly
wrong one), matching the project's confidence model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:40:22 +01:00

75 lines
2.8 KiB
Python

"""Tests for the language resolver registry (graphify.resolver_registry).
The registry formalizes the previously hand-wired, suffix-gated cross-file
resolution passes. These tests pin its contract so future languages can be
registered with confidence: gating by suffix, in-order execution, in-place
mutation, and fault isolation (a failing pass logs and is skipped, never
aborting the build or blocking later passes).
"""
from __future__ import annotations
from pathlib import Path
from graphify.resolver_registry import (
LanguageResolver,
registered_resolvers,
run_language_resolvers,
)
def _make_resolver(name: str, suffix: str, log: list[str]) -> LanguageResolver:
def _resolve(per_file, all_nodes, all_edges):
log.append(name)
return LanguageResolver(name, frozenset({suffix}), _resolve)
def test_default_registry_contains_swift_then_python() -> None:
# Importing extract registers its resolvers into the shared registry. Order
# matters: it preserves the prior inlined wiring (Swift before Python).
import graphify.extract # noqa: F401 (registers resolvers on import)
names = [r.name for r in registered_resolvers()]
assert "swift_member_calls" in names
assert "python_member_calls" in names
assert names.index("swift_member_calls") < names.index("python_member_calls")
def test_resolver_runs_only_when_suffix_present() -> None:
log: list[str] = []
resolvers = [_make_resolver("ruby", ".rb", log), _make_resolver("go", ".go", log)]
run_language_resolvers([Path("a.rb")], [], [], [], resolvers=resolvers)
assert log == ["ruby"] # go skipped: no .go file present
def test_resolvers_run_in_given_order() -> None:
log: list[str] = []
resolvers = [_make_resolver("first", ".rb", log), _make_resolver("second", ".rb", log)]
run_language_resolvers([Path("a.rb")], [], [], [], resolvers=resolvers)
assert log == ["first", "second"]
def test_failing_resolver_is_isolated() -> None:
log: list[str] = []
def _boom(per_file, all_nodes, all_edges):
raise RuntimeError("resolver blew up")
resolvers = [
LanguageResolver("boom", frozenset({".rb"}), _boom),
_make_resolver("after", ".rb", log),
]
# Must not raise, and the later resolver still runs.
run_language_resolvers([Path("a.rb")], [], [], [], resolvers=resolvers)
assert log == ["after"]
def test_resolver_mutates_edges_in_place() -> None:
def _add_edge(per_file, all_nodes, all_edges):
all_edges.append({"source": "x", "target": "y", "relation": "calls"})
resolvers = [LanguageResolver("adder", frozenset({".rb"}), _add_edge)]
edges: list[dict] = []
run_language_resolvers([Path("a.rb")], [], [], edges, resolvers=resolvers)
assert edges == [{"source": "x", "target": "y", "relation": "calls"}]