tests/test_cpp_split_header_owner.py
Rama v8 · réplica de lectura del repositorio original.
"""Bind qualified C++ definitions through explicit included declarations."""
import pytest
from graphify.extract import extract
@pytest.mark.parametrize("reverse", [False, True])
def test_split_header_member_call_never_binds_unrelated_free_function(tmp_path, reverse):
(tmp_path / "include").mkdir()
(tmp_path / "src").mkdir()
h = tmp_path / "include/foo.h"
h.write_text("class Foo {public: void run(); void say();};", encoding="utf-8")
impl = tmp_path / "src/foo.cpp"
impl.write_text('#include "foo.h"\nvoid Foo::say() {}\nvoid Foo::run() {say(); utility();}', encoding="utf-8")
other = tmp_path / "src/bar.cpp"
other.write_text('namespace { void say() {} }\nvoid utility() {}', encoding="utf-8")
paths = [h, impl, other]
r = extract(list(reversed(paths)) if reverse else paths, root=tmp_path, parallel=False, cache_root=tmp_path)
nodes = {n["id"]: n for n in r["nodes"]}
caller = next(n["id"] for n in r["nodes"] if n["label"] == "Foo::run()")
calls = [e for e in r["edges"] if e["source"] == caller and e["relation"] == "calls"]
assert any(nodes[e["target"]]["label"] in (".say()", "Foo::say()") for e in calls)
assert not any(nodes[e["target"]]["label"] == "say()" for e in calls)
assert any(nodes[e["target"]]["label"] == "utility()" for e in calls)
owner = next(n["id"] for n in r["nodes"] if n["label"] == "Foo")
assert any(e["source"] == owner and e["target"] == caller and e["relation"] == "method" for e in r["edges"])
# An incremental caller must see the same included owner without copying
# the unchanged header's nodes into the fresh extraction.
impl.write_text(impl.read_text(encoding="utf-8") + "\n// edit\n", encoding="utf-8")
fresh = extract([impl], root=tmp_path, parallel=False, cache_root=tmp_path,
resolution_context_nodes=[n for n in r["nodes"] if n.get("source_file") != "src/foo.cpp"],
resolution_context_edges=[e for e in r["edges"] if e.get("source_file") != "src/foo.cpp"])
assert any(e["source"] == caller and e["target"] in nodes and nodes[e["target"]]["label"] == ".say()"
and e["relation"] == "calls" for e in fresh["edges"])
assert not any(n["label"] == "Foo" for n in fresh["nodes"])
def test_namespace_qualified_owner_is_not_a_same_named_included_type(tmp_path):
a = tmp_path / "left.h"
a.write_text("namespace Left {class Foo {public: void run(); void say();};}", encoding="utf-8")
b = tmp_path / "right.h"
b.write_text("namespace Right {class Foo {public: void run(); void say();};}", encoding="utf-8")
cpp = tmp_path / "main.cpp"
cpp.write_text('#include "left.h"\n#include "right.h"\nvoid Left::Foo::run(){say();}\nvoid Left::Foo::say(){}', encoding="utf-8")
r = extract([a, b, cpp], root=tmp_path, parallel=False, cache_root=tmp_path)
nodes = {n["id"]: n for n in r["nodes"]}
caller = next(n["id"] for n in r["nodes"] if n.get("_cpp_qualified") == "Left::Foo::run")
calls = [e for e in r["edges"] if e["source"] == caller and e["relation"] == "calls"]
assert calls
assert all(nodes[e["target"]]["source_file"] == "left.h" for e in calls)
def test_ambiguous_included_owner_fails_closed(tmp_path):
paths = []
for name in ["a.h", "b.h"]:
p = tmp_path / name
p.write_text("class Foo {public: void run(); void say();};", encoding="utf-8")
paths.append(p)
cpp = tmp_path / "main.cpp"
cpp.write_text('#include "a.h"\n#include "b.h"\nvoid Foo::run(){say();}', encoding="utf-8")
other = tmp_path / "other.cpp"
other.write_text("void say(){}", encoding="utf-8")
r = extract([*paths, cpp, other], root=tmp_path, parallel=False, cache_root=tmp_path)
caller = next(n["id"] for n in r["nodes"] if n["label"] == "Foo::run()")
assert not any(e["source"] == caller and e["relation"] == "calls" for e in r["edges"])
def test_colliding_namespace_types_do_not_manufacture_an_owner(tmp_path):
header = tmp_path / "foo.h"
header.write_text("namespace Left {class Foo {public: void run(); void say();};}\n"
"namespace Right {class Foo {public: void run(); void say();};}", encoding="utf-8")
cpp = tmp_path / "main.cpp"
cpp.write_text('#include "foo.h"\nvoid Right::Foo::run(){say();}', encoding="utf-8")
r = extract([header, cpp], root=tmp_path, parallel=False, cache_root=tmp_path)
caller = next(n["id"] for n in r["nodes"] if n["label"] == "Right::Foo::run()")
# The existing declaration-id collision is outside this fix. Incomplete
# class identity must not turn into positive evidence of an owning type.
assert not any(e["relation"] == "method" and e["target"] == caller for e in r["edges"])