"""Template syntax preserves known member ownership without guessing receivers.""" import json import pytest from graphify.cache import cache_dir from graphify.extract import extract def _extract(tmp_path, files): paths = [] for name, source in files.items(): path = tmp_path / name path.write_text(source, encoding="utf-8") paths.append(path) return extract(paths, root=tmp_path, parallel=False, cache_root=tmp_path) def _calls(result, caller): nodes = {n["id"]: n for n in result["nodes"]} caller_ids = {n["id"] for n in nodes.values() if n["label"] == caller} return [nodes[e["target"]] for e in result["edges"] if e["relation"] == "calls" and e["source"] in caller_ids] def test_template_declarations_and_definitions_keep_their_class(tmp_path): result = _extract(tmp_path, {"worker.cpp": """ struct Worker { template void Tick(); template T& Get(); template void Inline() { Tick(); } }; template void Worker::Tick() {} template T& Worker::Get() { static T value; return value; } void Run(Worker& w) { w.Tick(); } """}) nodes = {n["id"]: n for n in result["nodes"]} owner = next(n["id"] for n in nodes.values() if n["label"] == "Worker") members = [nodes[e["target"]] for e in result["edges"] if e["relation"] == "method" and e["source"] == owner] assert {n["label"] for n in members} == {".Tick()", ".Get()", ".Inline()"} assert len({n["id"] for n in members}) == 3 assert len([n for n in nodes.values() if "Tick" in n["label"]]) == 1 assert [n["label"] for n in _calls(result, "Run()")] == [".Tick()"] assert [n["label"] for n in _calls(result, ".Inline()")] == [".Tick()"] assert not any(not n.get("source_file") for n in nodes.values()) @pytest.mark.parametrize("expression", [ "w.Tick()", "w.template Tick()", "w.Tick>>()", "p->Tick()", "client.worker.Tick()", "Worker::Tick()", ]) def test_explicit_template_member_call_uses_the_known_owner(tmp_path, expression): result = _extract(tmp_path, { "worker.hpp": """ struct Worker { template static void Tick() {} }; struct Client { Worker worker; }; """, "use.cpp": '#include "worker.hpp"\n' f"void Run(Worker& w, Worker* p, Client& client) {{ {expression}; }}", }) assert [n["label"] for n in _calls(result, "Run()")] == [".Tick()"] @pytest.mark.parametrize("source", [ "void Run() { unknown.Tick(); }", "void Run(int& w) { w.Tick(); }", "struct Empty {}; void Run(Empty& w) { w.Tick(); }", "void Run() { auto w = GetWorker(); w.Tick(); }", ]) def test_unknown_template_receiver_never_binds_by_member_name(tmp_path, source): result = _extract(tmp_path, {"use.cpp": """ struct Worker { template void Tick() {} }; template void Tick() {} """ + source}) assert _calls(result, "Run()") == [] def test_colliding_template_member_owners_remain_ambiguous(tmp_path): result = _extract(tmp_path, { "a.hpp": "struct Worker { template void Tick() {} };", "b.hpp": "struct Worker { template void Tick() {} };", "use.cpp": '#include "a.hpp"\n#include "b.hpp"\n' "void Run(Worker& w) { w.Tick(); }", }) assert _calls(result, "Run()") == [] def test_world_get_component_overload_family_is_owned_and_callable(tmp_path): result = _extract(tmp_path, {"world.hpp": """ struct World { template T& GetComponent(int entity); template const T& GetComponent(int entity) const; }; template T& World::GetComponent(int entity) { static T t; return t; } template const T& World::GetComponent(int entity) const { static T t; return t; } struct Health {}; void Run(World& world) { world.GetComponent(1); } """}) targets = _calls(result, "Run()") assert [n["label"] for n in targets] == [".GetComponent()"] assert len([n for n in result["nodes"] if "GetComponent" in n["label"]]) == 1 def test_generic_template_owner_does_not_attach_to_an_unrelated_class(tmp_path): result = _extract(tmp_path, {"use.cpp": """ struct Worker { template void Tick(); }; template void Other::Tick() {} void Run(Worker& w) { w.Tick(); } """}) targets = _calls(result, "Run()") assert [n["label"] for n in targets] == [".Tick()"] assert not any(n["label"] == "Other::Tick()" for n in targets) @pytest.mark.parametrize("primary_parameters, parameters, arguments", [ ("class A", "class T", "T"), ("class A", "typename Value", "Value"), ("class A, class B", "class First, typename Second", "First, Second"), ]) def test_primary_template_definition_reuses_its_declared_member( tmp_path, primary_parameters, parameters, arguments, ): result = _extract(tmp_path, {"box.cpp": f""" template<{primary_parameters}> struct Box {{ void Run(); void Next(); }}; template<{parameters}> void Box<{arguments}>::Run() {{ Next(); }} """}) nodes = {n["id"]: n for n in result["nodes"]} owner = next(n["id"] for n in nodes.values() if n["label"] == "Box") run = [n for n in nodes.values() if "Run" in n["label"]] assert [n["label"] for n in run] == [".Run()"] assert any(e["source"] == owner and e["target"] == run[0]["id"] and e["relation"] == "method" for e in result["edges"]) assert [n["label"] for n in _calls(result, ".Run()")] == [".Next()"] assert all(n.get("source_file") for n in run) @pytest.mark.parametrize("declaration, definition", [ ("template struct Box { void Run(); };", "template<> void Box::Run() {}"), ("template struct Box { void Run(); };", "template void Box::Run() {}"), ("template struct Box { void Run(); };", "template void Box::Run() {}"), ("template struct Box { void Run(); void Run(int); };", "template void Box::Run() {}"), ("template struct Box { void _Run(); void Run(); };", "template void Box::Run() {}"), ("template struct Box { void Run(); void _Run(); };", "template void Box::Run() {}"), ("template struct Box { T Run(); };", "template T Box::Run() {}"), ("template struct Box { void Run(); };", "template int Box::Run() {}"), ("template struct Box { void Run() const; };", "template void Box::Run() {}"), ("template struct Box { template void Run(); };", "template template void Box::Run() {}"), ("struct Box { void Run(); };", "template void Box::Run() {}"), ("template struct Box { void Run(); }; struct Outer { struct Box { void Run(); }; };", "template void Box::Run() {}"), ("namespace Left { template struct Box { void Run(); }; }", "namespace Right { template void Box::Run() {} }"), ("template struct Box { void Run(); };", "template void Box::Run() {}"), ("template struct Box { void Run(); };", "template void Box::Run() {}"), ("template struct Box { void Run(); };", "template requires Ready void Box::Run() {}"), ]) def test_unproven_template_owner_keeps_its_separate_definition(tmp_path, declaration, definition): result = _extract(tmp_path, {"box.cpp": declaration + "\n" + definition}) nodes = {n["id"]: n for n in result["nodes"]} definitions = {n["id"] for n in nodes.values() if "::Run()" in n["label"]} assert len(definitions) == 1 assert not any(e["relation"] == "method" and e["target"] in definitions for e in result["edges"]) def test_primary_template_owner_preserves_qualifiers_namespace_and_guards(tmp_path): result = _extract(tmp_path, {"box.cpp": """namespace ns { template struct Box { bool Run() const & noexcept; void Next(); }; template bool Box::Run() const & noexcept { int values[] = { #if FEATURE Next(), #else 0, #endif }; return true; } } """}) nodes = {n["id"]: n for n in result["nodes"]} run = next(n for n in nodes.values() if n["label"] == ".Run()") assert run["_cpp_qualified"] == "ns::Box::Run" assert len([n for n in nodes.values() if "Run" in n["label"]]) == 1 calls = [e for e in result["edges"] if e["relation"] == "calls" and e["source"] == run["id"]] assert [nodes[e["target"]]["label"] for e in calls] == [".Next()"] assert calls[0]["metadata"]["cpp_conditions"] == [{"group_line": 5, "branch": 0}] assert calls[0]["source_file"] == "box.cpp" assert calls[0]["source_location"] == "L6" def test_primary_template_owner_records_are_order_and_cache_stable(tmp_path): files = { "box.cpp": "template struct Box { void Run(); void Next(); };\n" "template void Box::Run() { Next(); }", "other.cpp": "void Run() {}", } cold = _extract(tmp_path, files) warm = _extract(tmp_path, dict(reversed(list(files.items())))) entries = list(cache_dir(tmp_path).glob("*.json")) assert entries for entry in entries: entry.unlink() reverse_cold = _extract(tmp_path, dict(reversed(list(files.items())))) def records(result): return {key: sorted(json.dumps(record, sort_keys=True) for record in result[key]) for key in ("nodes", "edges")} assert records(cold) == records(warm) == records(reverse_cold) @pytest.mark.parametrize("source, caller", [ ("template void Run(T& x) { x.template Tick(); }", "Run()"), ("template void Run(T* x) { x->template Tick(); }", "Run()"), ("template void Run(std::unique_ptr& x) { x->template Tick(); }", "Run()"), ("template void Run(std::shared_ptr& x) { x->template Tick(); }", "Run()"), ("template void Run(T& x) { x.template Tick(); }", "Run()"), ("template void Run() { T::template Tick(); }", "Run()"), ("template struct Holder { T value; " "void Run() { value.template Tick(); } };", ".Run()"), ]) def test_dependent_receiver_does_not_bind_to_a_global_template_parameter_name(tmp_path, source, caller): result = _extract(tmp_path, {"use.cpp": "struct T { template void Tick() {} };\n" + source}) assert _calls(result, caller) == [] @pytest.mark.parametrize("parameters, dependent_type", [ ("class A", "typename A::Type"), ("template class A", "typename A::Type"), ("template class A = Default", "typename A::Type"), ]) @pytest.mark.parametrize("pointer", [False, True]) def test_dependent_nested_parameter_never_uses_a_same_named_concrete_type( tmp_path, parameters, dependent_type, pointer, ): result = _extract(tmp_path, {"use.cpp": "namespace A { struct Type { void Work(); }; }\n" f"template<{parameters}> void Run({dependent_type}{'*' if pointer else '&'} x) " f"{{ x{'->' if pointer else '.'}Work(); }}"}) assert _calls(result, "Run()") == [] assert not any(n.get("metadata", {}).get("unresolved_calls") for n in result["nodes"]) @pytest.mark.parametrize("parameters, dependent_type", [ ("class A", "typename A::Type"), ("template class A", "typename A::Type"), ]) def test_dependent_nested_field_keeps_its_unknown_type(tmp_path, parameters, dependent_type): result = _extract(tmp_path, {"use.cpp": "namespace A { struct Type { void Work(); }; }\n" f"template<{parameters}> struct Holder {{ {dependent_type} value; " "void Run() { value.Work(); } };"}) assert _calls(result, ".Run()") == [] def test_template_template_inner_parameter_does_not_shadow_a_corpus_type(tmp_path): result = _extract(tmp_path, {"use.cpp": """ struct Inner { void Work(); }; template class A> void Run(Inner& x) { x.Work(); } """}) assert [n["label"] for n in _calls(result, "Run()")] == [".Work()"] def test_template_template_parameter_stays_a_local_binding(tmp_path): result = _extract(tmp_path, {"use.cpp": "template class A> struct Holder { A value; };"}) assert not any(n["label"] == "A" for n in result["nodes"]) @pytest.mark.parametrize("declaration, expression", [ ("typename A::Type x, y;", "x.Work(); y.Work();"), ("t x;", "x.Work();"), ("Container x;", "x.Work();"), ("std::unique_ptr x;", "x->Work();"), ]) def test_dependent_local_shapes_never_use_the_in_file_member_fallback(tmp_path, declaration, expression): result = _extract(tmp_path, {"use.cpp": "namespace A { struct Type { void Work(); }; }\n" f"template class A, class t> void Run() {{ {declaration} {expression} }}"}) assert _calls(result, "Run()") == [] def test_dependent_binding_does_not_suppress_another_functions_auto_receiver(tmp_path): result = _extract(tmp_path, {"use.cpp": """ struct Known { void Paint(); }; Known Make(); template void Other(t& x) { x.Paint(); } void Run() { auto x = Make(); x.Paint(); } """}) assert _calls(result, "Other()") == [] assert [n["label"] for n in _calls(result, "Run()")] == [".Paint()"] def test_auto_local_shadows_an_unknown_dependent_field(tmp_path): result = _extract(tmp_path, {"use.cpp": """ struct Known { void Paint(); }; Known Make(); template struct Holder { t value; void Run() { auto value = Make(); value.Paint(); } }; """}) assert [n["label"] for n in _calls(result, ".Run()")] == [".Paint()"] def test_qualified_concrete_template_argument_stays_known_with_a_shadowing_parameter(tmp_path): result = _extract(tmp_path, { "global.hpp": "struct T { template void Tick() {} };", "types.hpp": "namespace ns { struct T { template void Tick() {} }; }", "use.cpp": '#include "global.hpp"\n#include "types.hpp"\n' "template void Run(std::unique_ptr& x) { x->template Tick(); }", }) targets = _calls(result, "Run()") assert [n["label"] for n in targets] == [".Tick()"] nodes = {n["id"]: n for n in result["nodes"]} owners = [nodes[e["source"]] for e in result["edges"] if e["relation"] == "method" and e["target"] == targets[0]["id"]] assert [n["_cpp_qualified"] for n in owners] == ["ns::T"] def test_local_callable_shadows_a_free_template_function(tmp_path): result = _extract(tmp_path, {"use.cpp": """ template void Tick() {} void Run() { auto Tick = [] {}; Tick(); } """}) assert _calls(result, "Run()") == [] def test_incremental_template_call_uses_serialized_header_context(tmp_path): result = _extract(tmp_path, { "worker.hpp": "struct Worker { template void Tick() {} };", "use.cpp": '#include "worker.hpp"\nvoid Run(Worker& w) { w.Tick(); }', }) result = json.loads(json.dumps(result)) caller = tmp_path / "use.cpp" caller.write_text(caller.read_text(encoding="utf-8") + "\n// edit\n", encoding="utf-8") updated = extract( [caller], root=tmp_path, cache_root=tmp_path, parallel=False, resolution_context_nodes=[n for n in result["nodes"] if n.get("source_file") != "use.cpp"], resolution_context_edges=[e for e in result["edges"] if e.get("source_file") != "use.cpp"], ) nodes = {n["id"]: n for n in [*result["nodes"], *updated["nodes"]]} targets = [nodes[e["target"]]["label"] for e in updated["edges"] if e["relation"] == "calls"] assert targets == [".Tick()"] assert not any(n.get("source_file") == "worker.hpp" for n in updated["nodes"])