Ctrl K
ComunidadGuías…

tests/test_csharp_member_nodes.py

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

Archivo original ↗Preguntar a KiBot
"""C# properties get a node, like C++ data members (#3006).

`_CSHARP_CONFIG` emitted a `references` edge to a member's *type* and no node for
the member, so C# was the only language with a class-shaped type layer whose
state was absent from the graph. C++ emits a node per data member with `defines`,
#2971 is adding the same for C structs, and #2220 gave Swift computed properties
nodes. This brings C# to that line.

Properties only, not fields. `ids.normalize_id` casefolds and strips leading
underscores, so `_count` and `Count` are the same id: emitting both would hand
the node to whichever the parser reached first, which is the private backing
field, hiding the public member behind it. In C# the property is the state's
public identity, so it is the half worth having first.
"""
from __future__ import annotations

import os
import tempfile
from pathlib import Path

from graphify.extract import extract


def _extract(tmp_path, files: dict[str, str]):
    for name, body in files.items():
        p = tmp_path / name
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(body)
    old = os.getcwd()
    try:
        os.chdir(tmp_path)
        r = extract([Path(n) for n in files], cache_root=Path(tempfile.mkdtemp()))
    finally:
        os.chdir(old)
    defines = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "defines"}
    return defines, r


def _find(r, label):
    return next(n["id"] for n in r["nodes"] if n["label"] == label)


def _labels(r):
    return [n["label"] for n in r["nodes"]]


def test_auto_property_becomes_a_member_node(tmp_path):
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Variant {\n"
        "    public bool IsShowCategory { get; set; }\n"
        "}\n"
    )})
    assert (_find(r, "Variant"), _find(r, "IsShowCategory")) in defines


def test_every_property_on_a_class_gets_its_own_node(tmp_path):
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Variant {\n"
        "    public bool IsShowCategory { get; set; }\n"
        "    public bool ShowOnWeb { get; set; }\n"
        "}\n"
    )})
    variant = _find(r, "Variant")
    assert (variant, _find(r, "IsShowCategory")) in defines
    assert (variant, _find(r, "ShowOnWeb")) in defines


def test_a_primitive_property_still_gets_a_node(tmp_path):
    # `int` produces no member-worthy type reference, and the property is still
    # part of the class's state.
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Counter {\n"
        "    public int Count { get; set; }\n"
        "}\n"
    )})
    assert (_find(r, "Counter"), _find(r, "Count")) in defines


def test_a_generic_parameter_typed_property_still_gets_a_node(tmp_path):
    # The type-reference path returns early for a type parameter, which is right
    # for a reference and wrong for the member itself.
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Box<T> {\n"
        "    public T Value { get; set; }\n"
        "}\n"
    )})
    assert (_find(r, "Box"), _find(r, "Value")) in defines


def test_a_backing_field_does_not_take_the_property_node(tmp_path):
    # `_count` and `Count` normalize to one id. The public member is the one to
    # keep, and there is exactly one edge rather than two onto a shared node.
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Counter {\n"
        "    private int _count;\n"
        "    public int Count { get; set; }\n"
        "}\n"
    )})
    assert (_find(r, "Counter"), _find(r, "Count")) in defines
    assert len(defines) == 1
    assert "_count" not in _labels(r)


def test_a_field_alone_makes_no_member_node_but_keeps_its_type_reference(tmp_path):
    # Fields are out for now, and the reference to a field's type is untouched.
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Runner {\n"
        "    private readonly Worker _worker;\n"
        "}\n"
        "public class Worker { }\n"
    )})
    assert defines == set()
    references = {(e["source"], e["target"], e.get("context")) for e in r["edges"]
                  if e["relation"] == "references"}
    assert (_find(r, "Runner"), _find(r, "Worker"), "field") in references


def test_property_type_references_are_kept(tmp_path):
    # Regression guard for #1591: the member node is additive, the reference to
    # the property's type stays.
    _, r = _extract(tmp_path, {"S.cs": (
        "public class Holder {\n"
        "    public Widget Main { get; set; }\n"
        "}\n"
        "public class Widget { }\n"
    )})
    references = {(e["source"], e["target"], e.get("context")) for e in r["edges"]
                  if e["relation"] == "references"}
    assert (_find(r, "Holder"), _find(r, "Widget"), "field") in references
    assert "Main" in _labels(r)


def test_methods_are_still_methods(tmp_path):
    # A property is not a method: it lands on `defines`, the method on `method`.
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Variant {\n"
        "    public bool Flag { get; set; }\n"
        "    public void Touch() { }\n"
        "}\n"
    )})
    variant = _find(r, "Variant")
    methods = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "method"}
    assert (variant, _find(r, "Flag")) in defines
    assert (variant, _find(r, ".Touch()")) in methods
    assert (variant, _find(r, ".Touch()")) not in defines


def test_case_only_sibling_properties_do_not_duplicate_an_edge(tmp_path):
    # Legal C#, and one id after normalization. Each property gets its own node
    # (#4415) and one edge, rather than a second edge hung on the first
    # property's node.
    defines, r = _extract(tmp_path, {"S.cs": (
        "public class Odd {\n"
        "    public int Count { get; set; }\n"
        "    public int count { get; set; }\n"
        "}\n"
    )})
    odd = _find(r, "Odd")
    assert defines == {(odd, _find(r, "Count")), (odd, _find(r, "count"))}
    assert _find(r, "Count") != _find(r, "count")


# The calls a property, indexer or operator makes belong to that member: its
# accessor and operator bodies run code just like a method's.

def _calls(r):
    return {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}


def _kind(r, kind):
    return [n for n in r["nodes"] if (n.get("metadata") or {}).get("kind") == kind]


def test_property_accessor_bodies_call_from_the_property(tmp_path):
    _, r = _extract(tmp_path, {"S.cs": (
        "public class Svc {\n"
        "    int _v;\n"
        "    public int Block { get { return Load(); } set { Save(value); } }\n"
        "    public int Arrow { get => Load(); }\n"
        "    public int Body => Compute();\n"
        "    public int Auto { get; set; } = Seed();\n"
        "    int Load() { return _v; }\n"
        "    void Save(int v) { _v = v; }\n"
        "    int Compute() { return 1; }\n"
        "    static int Seed() { return 0; }\n"
        "}\n"
    )})
    calls = _calls(r)
    load, save = _find(r, ".Load()"), _find(r, ".Save()")
    assert {(_find(r, "Block"), load), (_find(r, "Block"), save)} <= calls
    assert (_find(r, "Arrow"), load) in calls
    assert (_find(r, "Body"), _find(r, ".Compute()")) in calls
    # An initializer runs in the constructor, not in the property.
    assert (_find(r, "Auto"), _find(r, ".Seed()")) not in calls


def test_accessor_call_prefers_its_own_type_like_a_method(tmp_path):
    # Other comes last, so a file-wide name lookup would pick its Load().
    _, r = _extract(tmp_path, {"S.cs": (
        "public class Svc {\n"
        "    public int Value => Load();\n"
        "    int Load() { return 1; }\n"
        "}\n"
        "public class Other { public int Load() { return 2; } }\n"
    )})
    own = next(n["id"] for n in r["nodes"] if n["label"] == ".Load()" and "svc" in n["id"])
    other = next(n["id"] for n in r["nodes"] if n["label"] == ".Load()" and "other" in n["id"])
    assert (_find(r, "Value"), own) in _calls(r)
    assert (_find(r, "Value"), other) not in _calls(r)


def test_accessor_call_through_a_typed_receiver_resolves_cross_file(tmp_path):
    _, r = _extract(tmp_path, {
        "Repo.cs": "namespace Demo { public class Repo { public int Count() { return 0; } } }\n",
        "Svc.cs": (
            "namespace Demo { public class Svc {\n"
            "    private Repo _repo;\n"
            "    public int Total => _repo.Count();\n"
            "} }\n"
        ),
    })
    assert (_find(r, "Total"), _find(r, ".Count()")) in _calls(r)


def test_indexer_is_a_node_and_owns_its_accessor_calls(tmp_path):
    _, r = _extract(tmp_path, {"S.cs": (
        "public class Bag {\n"
        "    public int This { get; set; }\n"
        "    public int this[int i] { get { return Get(i); } }\n"
        "    public int this[string k] => Find(k);\n"
        "    int Get(int i) { return i; }\n"
        "    int Find(string k) { return 0; }\n"
        "}\n"
    )})
    indexers = {n["metadata"]["signature"]: n["id"] for n in _kind(r, "indexer")}
    assert set(indexers) == {"int", "string"}
    assert indexers["int"] != indexers["string"]
    # The `This` property keeps the id it had before indexers were nodes.
    assert _find(r, "This") == _find(r, "Bag") + "_this"
    assert _find(r, "This") not in indexers.values()
    calls = _calls(r)
    assert (indexers["int"], _find(r, ".Get()")) in calls
    assert (indexers["string"], _find(r, ".Find()")) in calls


def test_operators_and_conversions_are_nodes_that_own_their_calls(tmp_path):
    _, r = _extract(tmp_path, {"S.cs": (
        "public class Money {\n"
        "    public static Money operator +(Money a, Money b) => Add(a, b);\n"
        "    public static explicit operator int(Money m) { return ToInt(m); }\n"
        "    public static explicit operator long(Money m) { return ToLong(m); }\n"
        "    static Money Add(Money a, Money b) { return a; }\n"
        "    static int ToInt(Money m) { return 0; }\n"
        "    static long ToLong(Money m) { return 0; }\n"
        "}\n"
    )})
    ops = {n["label"]: n["id"] for n in _kind(r, "operator")}
    assert set(ops) == {".operator +()", ".operator int()", ".operator long()"}
    calls = _calls(r)
    assert (ops[".operator +()"], _find(r, ".Add()")) in calls
    assert (ops[".operator int()"], _find(r, ".ToInt()")) in calls
    assert (ops[".operator long()"], _find(r, ".ToLong()")) in calls
    assert (ops[".operator int()"], _find(r, ".ToLong()")) not in calls


def test_accessor_base_call_resolves_to_the_base_in_another_file(tmp_path):
    _, r = _extract(tmp_path, {
        "Base.cs": "namespace Demo { public class Base { public int Compute() { return 0; } } }\n",
        "Svc.cs": (
            "namespace Demo { public class Svc : Base {\n"
            "    public int Total => base.Compute();\n"
            "} }\n"
        ),
    })
    assert (_find(r, "Total"), _find(r, ".Compute()")) in _calls(r)


def test_accessor_call_binds_the_other_partial_half_over_a_same_named_method(tmp_path):
    _, r = _extract(tmp_path, {
        "SvcA.cs": "namespace Demo { public partial class Svc { public int Value => Load(); } }\n",
        "SvcB.cs": "namespace Demo { public partial class Svc { int Load() { return 1; } } }\n",
        "Other.cs": "namespace Demo { public class Other { public int Load() { return 2; } } }\n",
    })
    own = next(n["id"] for n in r["nodes"] if n["label"] == ".Load()" and "svc" in n["id"])
    other = next(n["id"] for n in r["nodes"] if n["label"] == ".Load()" and "other" in n["id"])
    assert (_find(r, "Value"), own) in _calls(r)
    assert (_find(r, "Value"), other) not in _calls(r)


def test_explicit_interface_indexers_keep_their_own_calls(tmp_path):
    _, r = _extract(tmp_path, {"S.cs": (
        "public interface IA { int this[int i] { get; } }\n"
        "public interface IB { int this[int i] { get; } }\n"
        "public class C : IA, IB {\n"
        "    int IA.this[int i] => A();\n"
        "    int IB.this[int i] => B();\n"
        "    int A() { return 1; }\n"
        "    int B() { return 2; }\n"
        "}\n"
    )})
    by_line = {n["source_location"]: n["id"] for n in _kind(r, "indexer")
               if "_c_" in n["id"]}
    assert len(by_line) == 2
    calls = _calls(r)
    assert (by_line["L4"], _find(r, ".A()")) in calls
    assert (by_line["L5"], _find(r, ".B()")) in calls
    assert (by_line["L4"], _find(r, ".B()")) not in calls


def test_property_sharing_a_methods_id_does_not_lend_it_its_calls(tmp_path):
    # `IRun.Next` and `Next()` normalize to one id, which the method owns; the
    # property's call must not be credited to the method (a self-loop here).
    _, r = _extract(tmp_path, {"S.cs": (
        "public interface IRun { int Next { get; } }\n"
        "public class Run : IRun {\n"
        "    int Next() { return Other(); }\n"
        "    int IRun.Next => Next();\n"
        "    int Other() { return 0; }\n"
        "}\n"
    )})
    method = _find(r, ".Next()")
    assert (method, method) not in _calls(r)
    assert (method, _find(r, ".Other()")) in _calls(r)