tests/test_cpp_inherited_member_resolution.py
Rama v8 · réplica de lectura del repositorio original.
"""Inherited receiver members resolve only through unambiguous static base paths."""
import importlib.util
import json
import pytest
from graphify.extract import extract
pytestmark = pytest.mark.skipif(
importlib.util.find_spec("tree_sitter_cpp") is None,
reason="tree_sitter_cpp not installed",
)
def _extract(tmp_path, source, header="", extra=None):
files = {"use.cpp": source}
if header:
files["types.hpp"] = header
files["use.cpp"] = '#include "types.hpp"\n' + source
files.update(extra or {})
paths = []
for name, text in files.items():
path = tmp_path / name
path.write_text(text, encoding="utf-8")
paths.append(path)
return paths, extract(paths, root=tmp_path, cache_root=tmp_path / "cache", parallel=False)
def _calls(result, caller="Run()"):
nodes = {n["id"]: n for n in result["nodes"]}
owners = {e["target"]: nodes[e["source"]].get("_cpp_qualified")
for e in result["edges"] if e["relation"] in {"method", "defines"}}
return {(owners.get(e["target"]), nodes[e["target"]]["label"], e["confidence"])
for e in result["edges"] if e["relation"] == "calls"
and nodes[e["source"]]["label"] == caller}
@pytest.mark.parametrize("expression", ["x.Work()", "x.actions.Work()"])
def test_inherited_methods_and_fields(tmp_path, expression):
_, result = _extract(tmp_path, f"void Run(app::Derived& x) {{ {expression}; }}", """
namespace app {
struct Actions { void Work(); };
struct Base { Actions actions; void Work(); };
struct Mid : Base {};
struct Derived : Mid {};
}
""")
owner = "app::Actions" if "actions" in expression else "app::Base"
assert _calls(result) == {(owner, ".Work()", "INFERRED")}
@pytest.mark.parametrize("expression", ["this->actions.Work()", "actions.Work()", "this->Work()"])
def test_inherited_this_and_implicit_fields(tmp_path, expression):
_, result = _extract(tmp_path, f"""
struct Actions {{ void Work(); }};
struct Base {{ Actions actions; void Work(); }};
struct Derived : Base {{ void Run() {{ {expression}; }} }};
""")
owner = "Actions" if "actions" in expression else "Base"
confidence = "INFERRED" if "actions" in expression else "EXTRACTED"
assert _calls(result, ".Run()") == {(owner, ".Work()", confidence)}
def test_nearest_member_hides_a_base_member(tmp_path):
_, result = _extract(tmp_path, """
struct First { void Work(); }; struct Second { void Work(); };
struct Base { First actions; void Work(); };
struct Derived : Base { Second actions; void Work(); };
void Run(Derived& x) { x.actions.Work(); x.Work(); }
""")
assert _calls(result) == {("Second", ".Work()", "INFERRED"),
("Derived", ".Work()", "INFERRED")}
@pytest.mark.parametrize("hidden", ["int Work;", "using Work = int;", "struct Work {};",
"enum { Work };", "union Work { int x; };", "union { int Work; };",
"void Work(int); void Work(double);"])
def test_a_derived_declaration_blocks_a_base_method(tmp_path, hidden):
_, result = _extract(tmp_path, f"""
struct Base {{ void Work(); }};
struct Derived : Base {{ {hidden} }};
void Run(Derived& x) {{ x.Work(); }}
""")
assert not any(owner == "Base" for owner, _, _ in _calls(result))
def test_a_hidden_this_member_is_not_rebound_to_a_base_method(tmp_path):
_, result = _extract(tmp_path, """
struct Base { void Work(); };
struct Derived : Base { using Work = int; void Run() { this->Work(); } };
""")
assert _calls(result, ".Run()") == set()
def test_unknown_derived_field_hides_the_base_field(tmp_path):
_, result = _extract(tmp_path, """
struct Actions { void Work(); }; struct Base { Actions actions; };
struct Derived : Base { Missing actions; };
void Run(Derived& x) { x.actions.Work(); }
""")
assert _calls(result) == set()
@pytest.mark.parametrize("bases", ["Left, Right", "Left, Missing", "Missing, Left"])
def test_multiple_or_unknown_base_candidates_are_not_guessed(tmp_path, bases):
_, result = _extract(tmp_path, f"""
struct Left {{ void Work(); }}; struct Right {{ void Work(); }};
struct Derived : {bases} {{}};
void Run(Derived& x) {{ x.Work(); }}
""")
assert _calls(result) == set()
def test_multiple_inheritance_with_one_member_candidate_is_supported(tmp_path):
_, result = _extract(tmp_path, """
struct Left { void Work(); }; struct Right { void Other(); };
struct Derived : Left, Right {};
void Run(Derived& x) { x.Work(); }
""")
assert _calls(result) == {("Left", ".Work()", "INFERRED")}
def test_nonvirtual_diamond_is_ambiguous_even_with_one_method_node(tmp_path):
_, result = _extract(tmp_path, """
struct Base { void Work(); }; struct Left : Base {}; struct Right : Base {};
struct Derived : Left, Right {};
void Run(Derived& x) { x.Work(); }
""")
assert _calls(result) == set()
@pytest.mark.parametrize("base", ["Missing", "Unknown<Actions>", "virtual Actions"])
def test_unsupported_bases_are_not_guessed(tmp_path, base):
_, result = _extract(tmp_path, f"""
struct Actions {{ void Work(); }}; struct Derived : {base} {{}};
void Run(Derived& x) {{ x.Work(); }}
""")
assert _calls(result) == set()
def test_inheritance_cycle_stops(tmp_path):
_, result = _extract(tmp_path, """
struct First; struct Second : First {}; struct First : Second {};
void Run(First& x) { x.Work(); }
""")
assert _calls(result) == set()
def test_inherited_overload_family_is_not_a_unique_target(tmp_path):
_, result = _extract(tmp_path, """
struct Base { void Work(int); void Work(double); }; struct Derived : Base {};
void Run(Derived& x) { x.Work(1); }
""")
assert _calls(result) == set()
def test_virtual_call_stays_on_its_declared_static_base(tmp_path):
_, result = _extract(tmp_path, """
struct Base { virtual void Work(); }; struct Derived : Base { void Work() override; };
void Run(Base& x) { x.Work(); }
""")
assert _calls(result) == {("Base", ".Work()", "INFERRED")}
def test_aliased_base_preserves_namespace_and_case(tmp_path):
_, result = _extract(tmp_path, "void Run(app::Derived& x) { x.Work(); x.work(); }",
"namespace app { struct Base { void Work(); }; using Parent = Base; struct Derived : Parent {}; }",
{"other.hpp": "namespace other { struct Base { void Work(); }; }"})
assert _calls(result) == {("app::Base", ".Work()", "INFERRED")}
def test_a_scope_qualified_inherited_static_call(tmp_path):
_, result = _extract(tmp_path, """
struct Base { static void Work(); }; struct Derived : Base {};
void Run() { Derived::Work(); }
""")
assert _calls(result) == {("Base", ".Work()", "EXTRACTED")}
def test_a_scope_qualified_call_preserves_receiver_case(tmp_path):
_, result = _extract(tmp_path, """
struct Base { static void Work(); }; struct Derived : Base {};
void Run() { DERIVED::Work(); }
""")
assert _calls(result) == set()
def test_an_inherited_field_uses_its_declaring_class_alias(tmp_path):
_, result = _extract(tmp_path, """
struct Actions { void Work(); };
struct Base { using Alias = Actions; Alias actions; }; struct Derived : Base {};
void Run(Derived& x) { x.actions.Work(); }
""")
assert _calls(result) == {("Actions", ".Work()", "INFERRED")}
def test_unincluded_base_is_not_supplied_by_the_corpus(tmp_path):
_, result = _extract(tmp_path, "struct Derived : Base {}; void Run(Derived& x) { x.Work(); }",
extra={"other.hpp": "struct Base { void Work(); };"})
assert _calls(result) == set()
def test_deep_inheritance_stops_without_recursion_failure(tmp_path):
types = "struct Base0 { void Work(); };\n" + "\n".join(
f"struct Base{i} : Base{i - 1} {{}};" for i in range(1, 128))
_, result = _extract(tmp_path, types + "\nvoid Run(Base127& x) { x.Work(); }")
assert _calls(result) == set()
def test_incremental_caller_reuses_persisted_base_and_field_facts(tmp_path):
paths, original = _extract(tmp_path, "void Run(Derived& x) { x.actions.Work(); }", """
struct Actions { void Work(); }; struct Base { Actions actions; }; struct Derived : Base {};
""")
original = json.loads(json.dumps(original))
fresh = extract([paths[0]], root=tmp_path, cache_root=tmp_path / "cache", parallel=False,
resolution_context_nodes=[n for n in original["nodes"] if n.get("source_file") != "use.cpp"],
resolution_context_edges=[e for e in original["edges"] if e.get("source_file") != "use.cpp"])
assert _calls({"nodes": original["nodes"] + fresh["nodes"],
"edges": [e for e in original["edges"] if e["relation"] in {"method", "defines"}]
+ fresh["edges"]}) == {("Actions", ".Work()", "INFERRED")}
def test_old_ast_cache_does_not_hide_inherited_alias_fields(tmp_path, monkeypatch):
import graphify.cache as cache
assert cache._AST_CACHE_SCHEMA >= 43
with monkeypatch.context() as old:
old.setattr(cache, "_AST_CACHE_SCHEMA", 42)
paths, _ = _extract(tmp_path, "void Run(Derived& x) { x.actions.Work(); }", """
struct Actions { void Work(); };
struct Base { using Alias = Actions; Alias actions; }; struct Derived : Base {};
""")
for path in (tmp_path / "cache").rglob("*.json"):
value = path.read_text(encoding="utf-8")
path.write_text(value.replace('"_cpp_type_bindings"', '"old_bindings"')
.replace('"_cpp_lookup"', '"old_lookup"'), encoding="utf-8")
fresh = extract(paths, root=tmp_path, cache_root=tmp_path / "cache", parallel=False)
assert _calls(fresh) == {("Actions", ".Work()", "INFERRED")}
def test_new_member_edges_preserve_raw_call_metadata(tmp_path):
from graphify.extract import _resolve_cpp_member_calls, extract_cpp
path = tmp_path / "use.cpp"
path.write_text("struct Base { void Work(); }; struct Derived : Base {}; "
"void Run(Derived& x) { x.Work(); }", encoding="utf-8")
result = extract_cpp(path)
raw = next(rc for rc in result["raw_calls"] if rc.get("callee") == "Work")
raw["metadata"] = {"recovered_by": "cpp_conditional_initializers", "branch": "unknown"}
_resolve_cpp_member_calls([result], result["nodes"], result["edges"])
edge = next(e for e in result["edges"] if e["relation"] == "calls")
assert edge["metadata"] == raw["metadata"]
def test_include_traversal_is_cached_between_include_directives(tmp_path):
from graphify.cpp_resolution import CppTypeLookup
_, result = _extract(tmp_path, "void Run(Derived& x) { x.Work(); }", '#include "base.hpp"\nstruct Derived : Base {};',
{"base.hpp": "struct Base { void Work(); };"})
lookup = CppTypeLookup(result["nodes"], result["edges"])
visits = []
class CountedImports(list):
def __iter__(self):
visits.append(1)
return super().__iter__()
lookup.imports = {source: CountedImports(imports) for source, imports in lookup.imports.items()}
assert lookup.reachable("use.cpp", 100) == {"use.cpp", "types.hpp", "base.hpp"}
traversals = len(visits)
assert lookup.reachable("use.cpp", 200) == {"use.cpp", "types.hpp", "base.hpp"}
assert len(visits) == traversals
assert lookup.reachable("use.cpp", 0) == {"use.cpp"}
def test_absent_members_do_not_rewalk_shared_inheritance_subtrees(tmp_path, monkeypatch):
from graphify.cpp_resolution import CppTypeLookup
source = "struct A0 {}; struct B0 {};\n" + "\n".join(
f"struct A{i} : A{i - 1}, B{i - 1} {{}}; struct B{i} : A{i - 1}, B{i - 1} {{}};"
for i in range(1, 13))
_, result = _extract(tmp_path, source)
lookup = CppTypeLookup(result["nodes"], result["edges"])
original = lookup.resolve
visits = []
def counted(*args):
visits.append(1)
return original(*args)
monkeypatch.setattr(lookup, "resolve", counted)
[owner] = lookup.types["A12"]
assert lookup.member_owner(owner, "Missing", "method") is None
assert len(visits) <= 50