Ctrl K
ComunidadGuías…

tests/test_cpp_receiver_parameters.py

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

Archivo original ↗Preguntar a KiBot
"""C++ member calls through parameter and field receivers resolve (#3215).

`_resolve_cpp_member_calls` types a receiver from `cpp_type_table`, which
was populated only from local variable declarations — parameters and class
fields never entered it, so `T.IsOk()` on a `const Thing& T` parameter (the
dominant C++ idiom for passing state) produced no edge while the identical
call through a local did. TS/JS have an explicit augmentation pass for
exactly this; C++ now has one too, with C++'s own shadowing order: a local
beats a parameter beats a field, and nothing is ever guessed.
"""
from __future__ import annotations

import io
import tempfile
from contextlib import redirect_stdout
from pathlib import Path

from graphify.extract import extract

THING_H = (
    "#pragma once\n"
    "namespace NS {\n"
    "class Thing {\n"
    "public:\n"
    "    bool IsOk() const { return true; }\n"
    "};\n"
    "}\n"
)

USE_CPP = (
    '#include "thing.h"\n'
    "using namespace NS;\n"
    "bool Local()                  { Thing t; return t.IsOk(); }\n"
    "bool ParamRef(const Thing& T) { return T.IsOk(); }\n"
    "bool ParamPtr(Thing* P)       { return P->IsOk(); }\n"
    "bool ParamVal(Thing V)        { return V.IsOk(); }\n"
)

HOLDER_CPP = (
    '#include "thing.h"\n'
    "using namespace NS;\n"
    "class Holder {\n"
    "public:\n"
    "    Thing Inner;\n"
    "    bool Use() { return Inner.IsOk(); }\n"
    "};\n"
)


def _calls(tmp_path, files: dict[str, str]):
    for name, body in files.items():
        (tmp_path / name).write_text(body, encoding="utf-8")
    with redirect_stdout(io.StringIO()):
        r = extract([tmp_path / n for n in files], cache_root=Path(tempfile.mkdtemp()),
                    root=tmp_path, parallel=False)
    def norm(label):
        return str(label).strip(".").removesuffix("()")
    labels = {n["id"]: norm(n["label"]) for n in r["nodes"]}
    return {(labels.get(e["source"]), labels.get(e["target"]))
            for e in r["edges"] if e.get("relation") == "calls"}, r


def test_every_parameter_form_resolves_like_the_local_does(tmp_path):
    calls, _ = _calls(tmp_path, {"thing.h": THING_H, "use.cpp": USE_CPP})
    for caller in ("Local", "ParamRef", "ParamPtr", "ParamVal"):
        assert (caller, "IsOk") in calls, f"{caller}: {sorted(calls)}"


def test_a_class_field_receiver_resolves(tmp_path):
    calls, _ = _calls(tmp_path, {"thing.h": THING_H, "holder.cpp": HOLDER_CPP})
    assert ("Use", "IsOk") in calls, sorted(calls)


def test_a_shadowing_local_beats_the_parameter(tmp_path):
    """C++ name lookup: the innermost declaration wins. A function whose
    parameter is `Thing T` but which redeclares a local `Other T` must type
    the receiver `T` as Other, not Thing.

    Thing and Other carry DIFFERENTLY-named methods so the outcome is
    decisive: `T.Ready()` resolves only if `T` is typed Other (the local),
    and yields nothing if the parameter type Thing had won — no empty-set
    escape hatch. Locals are folded into the file table before parameters, so
    the first-write-wins table must record `T -> Other`.
    """
    files = {
        "thing.h": THING_H,  # class Thing has IsOk(), not Ready()
        "other.h": ("#pragma once\nclass Other {\npublic:\n"
                    "    bool Ready() const { return true; }\n};\n"),
        "use.cpp": ('#include "thing.h"\n#include "other.h"\nusing namespace NS;\n'
                    "bool Shadow(Thing T) { { Other T; return T.Ready(); } }\n"),
    }
    calls, _ = _calls(tmp_path, files)
    shadow_edges = [(s, t) for s, t in calls if s == "Shadow"]
    # The local Other T shadows the parameter Thing T: the receiver types as
    # Other, so the call binds to Other::Ready.
    assert ("Shadow", "Ready") in calls, shadow_edges
    # And it must NOT have bound to anything on Thing (the shadowed parameter).
    assert all(t == "Ready" for _s, t in shadow_edges), shadow_edges


def test_builtin_typed_parameters_contribute_nothing(tmp_path):
    files = {
        "thing.h": THING_H,
        "use.cpp": ('#include "thing.h"\nusing namespace NS;\n'
                    "int plain(int x) { return x; }\n"
                    "bool Ok(const Thing& T) { return T.IsOk(); }\n"),
    }
    calls, _ = _calls(tmp_path, files)
    assert ("Ok", "IsOk") in calls
    assert not any(s == "plain" for s, _t in calls)


def test_chained_receivers_stay_deferred(tmp_path):
    """`B.Inner.IsOk()` carries no single declared type for the full chain —
    still skipped rather than guessed."""
    files = {
        "thing.h": THING_H,
        "use.cpp": ('#include "thing.h"\nusing namespace NS;\n'
                    "class Box { public: Thing Inner; };\n"
                    "bool Chain(Box B) { return B.Inner.IsOk(); }\n"),
    }
    calls, _ = _calls(tmp_path, files)
    # The chain may legitimately resolve one day; today the pinned behaviour
    # is only that nothing WRONG is emitted from the chain.
    assert all(t in ("IsOk",) or s != "Chain" for s, t in calls)


def test_the_existing_receiver_tiers_are_unchanged(tmp_path):
    files = {
        "thing.h": THING_H,
        "use.cpp": ('#include "thing.h"\nusing namespace NS;\n'
                    "bool Scoped() { return Thing().IsOk(); }\n"
                    "bool ViaLocal() { Thing t; return t.IsOk(); }\n"),
    }
    calls, _ = _calls(tmp_path, files)
    assert ("ViaLocal", "IsOk") in calls