Ctrl K
ComunidadGuías…

tests/test_cpp_alias_member_resolution.py

Rama v8 · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Alias receiver calls use lexical declarations and explicit include reachability."""
import importlib.util
import json

import pytest

from graphify.extract import extract, extract_cpp

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.parent.mkdir(parents=True, exist_ok=True)
        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("declaration,type_name,expression", [
    ("using Alias = Worker;", "Alias&", "x.Work()"),
    ("typedef Worker Alias;", "Alias&", "x.Work()"),
    ("using Alias = Worker*;", "Alias", "x->Work()"),
    ("typedef Worker* Alias;", "Alias", "x->Work()"),
    ("using First = Worker; using Alias = First;", "Alias&", "x.Work()"),
    ("using Alias = std::unique_ptr<Worker>;", "Alias&", "x->Work()"),
])
def test_namespace_alias_receiver(tmp_path, declaration, type_name, expression):
    _, result = _extract(tmp_path, f"void Run(app::{type_name} x) {{ {expression}; }}",
                         f"namespace app {{ struct Worker {{ void Work(); }}; {declaration} }}")
    assert _calls(result) == {("app::Worker", ".Work()", "INFERRED")}


def test_local_aliases_use_the_receiver_declaration_scope(tmp_path):
    _, result = _extract(tmp_path, """
struct First { void Work(); }; struct Second { void Work(); };
using Alias = First;
void Run() {
    Alias outer;
    { using Alias = Second; Alias inner; inner.Work(); }
    { outer.Work(); using Alias = Second; }
}
""")
    assert _calls(result) == {("First", ".Work()", "INFERRED"),
                              ("Second", ".Work()", "INFERRED")}


@pytest.mark.parametrize("mixed", [False, True], ids=["crlf", "mixed"])
@pytest.mark.parametrize("final_newline", [False, True])
def test_line_endings_preserve_lookup_facts_and_resolution(tmp_path, mixed, final_newline):
    files = {
        "types.hpp": """// café: UTF-8 bytes before every stored position
struct Worker { void Work(); };
struct Other { void Work(); };
using Alias = Worker;
struct Base { Alias item; };
struct Derived : Base {};
""",
        "use.cpp": """// café: includes must not become retroactive
void Before(Alias& x) { x.Work(); }
#include "types.hpp"
void Run(Derived& x) { x.item.Work(); }
void Local() {
    Alias outer;
    { using Alias = Other; Alias inner; inner.Work(); }
    outer.Work();
}
void Hidden() { using Alias = Missing; Alias x; x.Work(); }
""",
    }
    if not final_newline:
        files = {name: source.rstrip("\n") for name, source in files.items()}
    paths = [tmp_path / name for name in files]
    for path in paths:
        path.write_bytes(files[path.name].encode("utf-8"))
    raw = [extract_cpp(path) for path in paths]
    cache_root = tmp_path / "cache"
    expected = extract(paths, root=tmp_path, cache_root=cache_root, parallel=False)
    assert _calls(expected) == {("Worker", ".Work()", "INFERRED")}
    assert _calls(expected, "Local()") == {("Worker", ".Work()", "INFERRED"),
                                          ("Other", ".Work()", "INFERRED")}
    assert _calls(expected, "Before()") == _calls(expected, "Hidden()") == set()

    for path in paths:
        source = files[path.name].replace("\n", "\r\n", 2 if mixed else -1).encode("utf-8")
        path.write_bytes(source)
    assert [extract_cpp(path) for path in paths] == raw
    for _ in range(2):  # Re-extraction, then the warm AST-cache path.
        actual = extract(paths, root=tmp_path, cache_root=cache_root, parallel=False)
        for key in ("nodes", "edges"):
            assert sorted(json.dumps(item, sort_keys=True) for item in actual[key]) == sorted(
                json.dumps(item, sort_keys=True) for item in expected[key])
    for path in paths:
        assert path.read_bytes() == files[path.name].replace(
            "\n", "\r\n", 2 if mixed else -1).encode("utf-8")


def test_class_alias_types_a_field(tmp_path):
    _, result = _extract(tmp_path, """
struct Worker { void Work(); };
struct Owner { using Alias = Worker; Alias item; void Run() { item.Work(); } };
""")
    assert _calls(result, ".Run()") == {("Worker", ".Work()", "INFERRED")}


def test_scope_qualified_alias_call(tmp_path):
    _, result = _extract(tmp_path, """
struct Worker { static void Work(); }; using Alias = Worker;
void Run() { Alias::Work(); }
""")
    assert _calls(result) == {("Worker", ".Work()", "EXTRACTED")}


def test_alias_target_cannot_be_supplied_by_an_unincluded_header(tmp_path):
    _, result = _extract(tmp_path, "using Alias = Worker; void Run(Alias& x) { x.Work(); }",
                         extra={"other.hpp": "struct Worker { void Work(); };"})
    assert _calls(result) == set()


def test_include_after_a_receiver_declaration_is_not_retroactive(tmp_path):
    _, result = _extract(tmp_path, 'void Run(Alias& x) { x.Work(); }\n#include "other.hpp"',
                         extra={"other.hpp": "struct Worker { void Work(); }; using Alias = Worker;"})
    assert _calls(result) == set()


def test_transitive_include_alias(tmp_path):
    _, result = _extract(tmp_path, "void Run(Alias& x) { x.Work(); }", '#include "other.hpp"',
                         {"other.hpp": "struct Worker { void Work(); }; using Alias = Worker;"})
    assert _calls(result) == {("Worker", ".Work()", "INFERRED")}


@pytest.mark.parametrize("declaration", [
    "using Alias = Missing;", "using Alias = Alias;", "using Alias = Other; using Other = Alias;",
    "using Alias = std::vector<Worker>;", "template<class T> using Alias = T;",
])
def test_unknown_or_unsupported_alias_does_not_guess(tmp_path, declaration):
    _, result = _extract(tmp_path, f"""
struct Worker {{ void Work(); }}; {declaration}
void Run(Alias& x) {{ x.Work(); }}
""")
    assert _calls(result) == set()


def test_later_alias_and_wrong_case_are_not_visible(tmp_path):
    _, result = _extract(tmp_path, """
struct Worker { void Work(); }; using alias = Worker;
void Run(Alias& x) { x.Work(); }
using Alias = Worker;
""")
    assert _calls(result) == set()


def test_unincluded_alias_and_target_are_not_visible(tmp_path):
    _, result = _extract(tmp_path, "void Run(Alias& x) { x.Work(); }", extra={
        "other.hpp": "struct Worker { void Work(); }; using Alias = Worker;",
    })
    assert _calls(result) == set()


def test_unknown_local_alias_hides_an_outer_alias(tmp_path):
    _, result = _extract(tmp_path, """
struct Worker { void Work(); }; using Alias = Worker;
void Run() { using Alias = Missing; Alias x; x.Work(); }
""")
    assert _calls(result) == set()


def test_namespace_directives_do_not_choose_between_ambiguous_aliases(tmp_path):
    _, result = _extract(tmp_path, """
namespace a { struct Worker { void Work(); }; using Alias = Worker; }
namespace b { struct Worker { void Work(); }; using Alias = Worker; }
using namespace a; using namespace b;
void Run(Alias& x) { x.Work(); }
""")
    assert _calls(result) == set()


def test_a_named_struct_typedef_is_still_a_receiver_type(tmp_path):
    _, result = _extract(tmp_path, """
typedef struct Worker { void Work(); } Worker;
void Run(Worker& x) { x.Work(); }
""")
    assert _calls(result) == {("Worker", ".Work()", "INFERRED")}


def test_unimported_namespace_type_is_not_visible_by_simple_name(tmp_path):
    _, result = _extract(tmp_path, """
namespace app { struct Worker { void Work(); }; }
void Run(Worker& x) { x.Work(); }
""")
    assert _calls(result) == set()


def test_deep_alias_chains_stop_without_recursion_failure(tmp_path):
    aliases = "using Alias0 = Worker;\n" + "\n".join(
        f"using Alias{i} = Alias{i - 1};" for i in range(1, 128))
    _, result = _extract(tmp_path, "struct Worker { void Work(); };\n" + aliases
                         + "\nvoid Run(Alias127& x) { x.Work(); }")
    assert _calls(result) == set()


def test_conditional_alias_alternatives_are_not_configured(tmp_path):
    _, result = _extract(tmp_path, """
struct First { void Work(); }; struct Second { void Work(); };
#if PICK_FIRST
using Alias = First;
#else
using Alias = Second;
#endif
void Run(Alias& x) { x.Work(); }
""")
    assert _calls(result) == set()


def test_a_conditional_alias_hides_an_outer_alias_without_becoming_configured(tmp_path):
    _, result = _extract(tmp_path, """
struct First { void Work(); }; struct Second { void Work(); }; using Alias = First;
void Run() {
#ifdef PICK_SECOND
using Alias = Second;
#endif
Alias x; x.Work();
}
""")
    assert _calls(result) == set()


def test_a_conventional_header_guard_allows_an_alias(tmp_path):
    _, result = _extract(tmp_path, "void Run(Alias& x) { x.Work(); }", """
#ifndef TYPES_HPP
#define TYPES_HPP
struct Worker { void Work(); }; using Alias = Worker;
#endif
""")
    assert _calls(result) == {("Worker", ".Work()", "INFERRED")}


def test_a_later_class_alias_blocks_an_outer_alias_in_an_inline_body(tmp_path):
    _, result = _extract(tmp_path, """
struct Good { void Work(); }; struct Bad { void Work(); }; using Alias = Good;
struct Holder { void Run() { Alias x; x.Work(); } using Alias = Bad; };
""")
    assert not any(owner == "Good" for owner, _, _ in _calls(result, ".Run()"))


@pytest.mark.parametrize("shadow", ["namespace Good = Other;", "using Good = Other;",
                                     "namespace Good {}", "struct Good {};", "union Good {};"])
def test_a_qualified_prefix_blocks_fallback_to_an_outer_namespace(tmp_path, shadow):
    other = "namespace Other {}" if shadow.startswith("namespace Good =") else "struct Other {};"
    _, result = _extract(tmp_path, f"""
namespace Good {{ struct Type {{ void Work(); }}; }}
{other}
namespace app {{ {shadow} void Run(Good::Type& x) {{ x.Work(); }} }}
""")
    assert _calls(result) == set()


def test_incremental_alias_receiver_uses_persisted_header_facts(tmp_path):
    paths, original = _extract(tmp_path, "void Run(app::Alias& x) { x.Work(); }",
                              "namespace app { struct Worker { void Work(); }; using Alias = Worker; }")
    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"])
    combined = {"nodes": original["nodes"] + fresh["nodes"],
                "edges": [e for e in original["edges"] if e["relation"] in {"method", "defines"}]
                + fresh["edges"]}
    assert _calls(combined) == {("app::Worker", ".Work()", "INFERRED")}


@pytest.mark.parametrize("source", [
    "using Alias = Foreign; void Run(Alias& x) { x.Work(); }",
    "void Run() { using Alias = Foreign; Alias x; x.Work(); }",
    "void Run() { using Alias = Foreign; Alias::Work(); }",
])
def test_an_unknown_alias_does_not_park_an_unrelated_class_name(tmp_path, source):
    _, result = _extract(tmp_path, source)
    assert not any(entry.get("receiver_type") == "Alias"
                   for node in result["nodes"]
                   for entry in node.get("metadata", {}).get("unresolved_calls", []))


def test_root_relative_literal_include_keeps_the_existing_import_proof(tmp_path):
    _, result = _extract(tmp_path, "", extra={
        "Client/Runner.cpp": '#include "Engine/Worker.hpp"\nvoid Run(Alias& x) { x.Work(); }',
        "Engine/Worker.hpp": "struct Worker { void Work(); }; using Alias = Worker;",
    })
    assert _calls(result) == {("Worker", ".Work()", "INFERRED")}


@pytest.mark.parametrize("header", [
    "struct Worker { void Work(); };\n#ifndef FEATURE\n#define FEATURE\nusing Alias = Worker;\n#endif",
    "#ifndef TYPES_HPP\n#define TYPES_HPP\nstruct Worker { void Work(); }; using Alias = Worker;\n#else\nint version;\n#endif",
    "struct Worker { void Work(); };\n#ifdef FEATURE\nusing Alias = Worker;\n#endif",
])
def test_feature_blocks_and_partial_guards_do_not_configure_aliases(tmp_path, header):
    _, result = _extract(tmp_path, "void Run(Alias& x) { x.Work(); }", header)
    assert _calls(result) == set()


def test_anonymous_nested_namespace_alias_does_not_become_a_global_alias(tmp_path):
    _, result = _extract(tmp_path, """
struct Worker { void Work(); };
namespace app { namespace { using Alias = Worker; } }
void Run(Alias& x) { x.Work(); }
""")
    assert _calls(result) == set()


@pytest.mark.parametrize("target", ["std::unique_ptr<Worker&>", "std::shared_ptr<Worker&&>",
                                     "std::unique_ptr<Ref>"])
def test_smart_pointer_pointees_do_not_erase_reference_aliases(tmp_path, target):
    _, result = _extract(tmp_path, f"""
struct Worker {{ void Work(); }}; using Ref = Worker&; using Alias = {target};
void Run(Alias& x) {{ x->Work(); }}
""")
    assert _calls(result) == set()


def test_reference_aliases_still_type_an_ordinary_receiver(tmp_path):
    _, result = _extract(tmp_path, """
struct Worker { void Work(); }; using Alias = Worker&;
void Run(Alias x) { x.Work(); }
""")
    assert _calls(result) == {("Worker", ".Work()", "INFERRED")}


@pytest.mark.parametrize("prefix,expected", [("std", set()), ("::std", {("Wrong", ".Work()", "INFERRED")})])
def test_a_shadowed_std_namespace_is_not_a_standard_pointer_wrapper(tmp_path, prefix, expected):
    _, result = _extract(tmp_path, f"""
struct Wrong {{ void Work(); }}; struct Actual {{ void Work(); }};
namespace app {{
namespace std {{ template<class T> struct unique_ptr {{ Actual* operator->(); }}; }}
using Alias = {prefix}::unique_ptr<Wrong>;
void Run(Alias& x) {{ x->Work(); }}
}}
""")
    assert _calls(result) == expected