Ctrl K
ComunidadGuías…

tests/test_cpp_type_alias.py

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

Archivo original ↗Preguntar a KiBot
"""C++ `using X = ...;` and `typedef ... X;` at namespace scope are declarations.

The C++ extractor emitted no node for an alias declaration or a typedef, so a
field, parameter or return typed with one, even in the declaring header,
resolved to a SOURCELESS stub that the corpus rewire can grow into a hub. A
namespace- or file-scope alias is now a type node contained by its file; every
declarator of a typedef counts (`typedef int Handle, *HandlePtr;`,
`typedef int (*Fn)(int);`, `typedef int Arr[4];`), and an alias template
(`template <class T> using Ptr = T*;`) too. An include guard or an
`#if`/`#ifdef` block is a conditional wrapper, not a scope: an alias inside
one at file scope is still file-scope.

Not declarations here: a member alias (`using value_type = T;` in a class: one
in every container, and C++ class-member ids are not class-qualified, so they
would fold into one hub); the struct a C-style `typedef struct Foo {...} Foo;`
names (the struct's own node is it); and a same-name re-export
(`using Bar = ns::Bar;`), which denotes the original type and declares no new
name.
"""
from __future__ import annotations

import importlib.util
import textwrap
from pathlib import Path

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",
)

_HEADER = textwrap.dedent("""\
    #include <vector>
    #include <functional>
    namespace app {
    using IntVec = std::vector<int>;
    typedef int Handle, *HandlePtr;
    typedef struct { int x; } Point;
    typedef int (*Fn)(int);
    template <typename T> using Ptr = T*;
    using Callback = std::function<void(int)>;
    struct Widget {
      using value_type = int;
      typedef int size_type;
      IntVec items;
      Handle h;
      Point p;
      Fn f;
      Ptr<int> raw;
      Callback cb;
      IntVec get(Handle h, Callback cb);
    };
    }
    """)


def _extract(tmp_path: Path, files: dict[str, str]) -> dict:
    for name, body in files.items():
        (tmp_path / name).parent.mkdir(parents=True, exist_ok=True)
        (tmp_path / name).write_text(body, encoding="utf-8")
    return extract([tmp_path / n for n in files], root=tmp_path,
                   cache_root=tmp_path / "graphify-out", parallel=False)


def _labels(result: dict) -> dict[str, str]:
    return {n["id"]: n["label"] for n in result["nodes"]}


def _pairs(result: dict, relation: str) -> set[tuple[str, str]]:
    label = _labels(result)
    return {(label.get(e["source"], str(e["source"])), label.get(e["target"], str(e["target"])))
            for e in result["edges"] if e["relation"] == relation}


def test_each_namespace_scope_alias_is_a_sourced_node_on_its_line(tmp_path: Path):
    result = _extract(tmp_path, {"widget.hpp": _HEADER})
    expected = {"IntVec": "L4", "Handle": "L5", "HandlePtr": "L5", "Point": "L6",
                "Fn": "L7", "Ptr": "L8", "Callback": "L9"}
    for label, line in expected.items():
        matches = [n for n in result["nodes"] if n["label"] == label]
        assert len(matches) == 1, label
        assert matches[0]["source_file"], label
        assert matches[0]["source_location"] == line, label
    assert [n["label"] for n in result["nodes"] if not n.get("source_file")] == []


def test_each_alias_is_contained_by_its_file(tmp_path: Path):
    contained = {tgt for _src, tgt in _pairs(_extract(tmp_path, {"widget.hpp": _HEADER}), "contains")}
    assert {"IntVec", "Handle", "HandlePtr", "Point", "Fn", "Ptr", "Callback"} <= contained


def test_fields_reference_the_local_aliases(tmp_path: Path):
    refs = _pairs(_extract(tmp_path, {"widget.hpp": _HEADER}), "references")
    assert {("Widget", "IntVec"), ("Widget", "Handle"), ("Widget", "Point"), ("Widget", "Fn"),
            ("Widget", "Ptr"), ("Widget", "Callback")} <= refs


def test_member_aliases_are_not_file_level_nodes(tmp_path: Path):
    result = _extract(tmp_path, {"widget.hpp": _HEADER})
    assert [n for n in result["nodes"] if n["label"] in ("value_type", "size_type")] == []


def test_a_c_style_typedef_of_a_named_struct_is_the_struct(tmp_path: Path):
    result = _extract(tmp_path, {"foo.hpp": "typedef struct Foo { int y; } Foo;\nstruct User { Foo f; };\n"})
    foos = [n for n in result["nodes"] if n["label"] == "Foo"]
    assert len(foos) == 1 and foos[0]["source_file"]
    assert ("User", "Foo") in _pairs(result, "references")


def test_a_same_name_re_export_emits_no_node(tmp_path: Path):
    result = _extract(tmp_path, {"api.hpp": "namespace ns { struct Bar {}; }\nusing Bar = ns::Bar;\n"})
    bars = [n for n in result["nodes"] if n["label"] == "Bar"]
    assert len(bars) == 1
    assert bars[0]["source_location"] == "L1"


def test_a_reference_from_another_file_lands_on_the_alias(tmp_path: Path):
    result = _extract(tmp_path, {
        "types.hpp": "#include <vector>\nusing IntVec = std::vector<int>;\n",
        "user.hpp": '#include "types.hpp"\nstruct Bag { IntVec v; };\n',
    })
    [alias] = [n for n in result["nodes"] if n["label"] == "IntVec"]
    assert alias["source_file"].endswith("types.hpp")
    label = _labels(result)
    targets = {e["target"] for e in result["edges"]
               if e["relation"] == "references" and label.get(e["source"]) == "Bag"}
    assert targets == {alias["id"]}


_GUARDED = textwrap.dedent("""\
    #ifndef CONFIG_HPP
    #define CONFIG_HPP
    using Guarded = int;
    typedef long GuardedT;
    #if defined(USE_WIDE)
    using Char = wchar_t;
    #elif defined(USE_U16)
    using Char16 = char16_t;
    #else
    using Narrow = char;
    #endif
    #ifdef HAVE_POOL
    template <typename T> using Pool = T*;
    #endif
    struct Config {
    #ifdef DEBUG
      using debug_type = int;
    #endif
      Guarded g;
      GuardedT t;
      Narrow n;
    };
    #endif
    """)


def test_aliases_inside_include_guards_and_conditionals_are_nodes(tmp_path: Path):
    result = _extract(tmp_path, {"config.hpp": _GUARDED})
    expected = {"Guarded": "L3", "GuardedT": "L4", "Char": "L6", "Char16": "L8",
                "Narrow": "L10", "Pool": "L13"}
    for label, line in expected.items():
        matches = [n for n in result["nodes"] if n["label"] == label]
        assert len(matches) == 1, label
        assert matches[0]["source_file"], label
        assert matches[0]["source_location"] == line, label
    assert {("Config", "Guarded"), ("Config", "GuardedT"), ("Config", "Narrow")} <= _pairs(result, "references")


def test_a_member_alias_inside_a_conditional_is_still_a_member(tmp_path: Path):
    result = _extract(tmp_path, {"config.hpp": _GUARDED})
    assert [n for n in result["nodes"] if n["label"] == "debug_type"] == []


@pytest.mark.parametrize("decl", ["using Session = int;", "typedef int Session;"])
def test_a_type_reference_lands_on_the_alias_not_a_function_differing_in_case(
        tmp_path: Path, decl: str):
    """`Session` and `session()` in one file are two declarations since #4904,
    and `Session s;` names the alias, not the function."""
    result = _extract(tmp_path, {"box.cpp": f"{decl}\nvoid session() {{}}\nstruct Holder {{ Session s; }};\n"})
    refs = _pairs(result, "references")
    assert ("Holder", "Session") in refs
    assert ("Holder", "session()") not in refs


def test_an_alias_named_after_the_file_extension_is_contained_by_its_file(tmp_path: Path):
    """`using cpp = int;` in `a.cpp` spells the file's own id, so the registry
    salts the alias's id (#4854); the file must still contain it."""
    result = _extract(tmp_path, {"a.cpp": "using cpp = int;\nstruct Box { cpp v; };\n"})
    assert len([n for n in result["nodes"] if n["label"] == "cpp" and n.get("source_file")]) == 1
    assert ("a.cpp", "cpp") in _pairs(result, "contains")
    assert ("Box", "cpp") in _pairs(result, "references")