Ctrl K
ComunidadGuías…

tests/test_csharp_enum_members.py

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

Archivo original ↗Preguntar a KiBot
"""C# enum members get a node and a `case_of` edge, like Java's (#1719).

`enum_declaration` is in `_CSHARP_CONFIG.class_types`, so the enum type was a
node while its members were not — the type sat in the graph as a leaf. Java has
emitted a node per `enum_constant` with a `case_of` edge since #1719 and Kotlin
since #1738 (both from #1700), and Swift does the same for `enum_entry`. The C#
walk already descends into `enum_member_declaration_list` with the enum as
`parent_class_nid`, so this is the Java shape applied where it was missing.
"""
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)
    case_of = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "case_of"}
    return case_of, 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_enum_member_becomes_a_node(tmp_path):
    case_of, r = _extract(tmp_path, {"S.cs": (
        "public enum ProcessingType {\n"
        "    Process\n"
        "}\n"
    )})
    assert "Process" in _labels(r)
    assert (_find(r, "ProcessingType"), _find(r, "Process")) in case_of


def test_every_member_gets_its_own_node(tmp_path):
    case_of, r = _extract(tmp_path, {"S.cs": (
        "public enum ProcessingType {\n"
        "    Process,\n"
        "    ProcessAndCompleteIndex\n"
        "}\n"
    )})
    enum_id = _find(r, "ProcessingType")
    assert (enum_id, _find(r, "Process")) in case_of
    assert (enum_id, _find(r, "ProcessAndCompleteIndex")) in case_of


def test_an_explicit_value_does_not_change_the_member(tmp_path):
    # `= 1` parses as a `value` field next to `name`; the member is read off
    # `name`, so an assigned constant is the same shape as a bare one.
    case_of, r = _extract(tmp_path, {"S.cs": (
        "public enum EntityType {\n"
        "    Variant = 1,\n"
        "    Product = 2\n"
        "}\n"
    )})
    enum_id = _find(r, "EntityType")
    assert (enum_id, _find(r, "Variant")) in case_of
    assert (enum_id, _find(r, "Product")) in case_of


def test_case_only_sibling_members_do_not_duplicate_an_edge(tmp_path):
    # Legal C#, and one id after normalization. Each member gets its own node
    # (#4415) with its own edge, rather than a second edge hanging off the
    # first member's node.
    case_of, r = _extract(tmp_path, {"S.cs": (
        "public enum Odd {\n"
        "    Value,\n"
        "    value\n"
        "}\n"
    )})
    odd = _find(r, "Odd")
    assert case_of == {(odd, _find(r, "Value")), (odd, _find(r, "value"))}
    assert _find(r, "Value") != _find(r, "value")


def test_a_namespaced_enum_still_emits_members(tmp_path):
    # File-scoped namespaces are handled in the same walk; the enum must not
    # lose its members to the namespace hop.
    case_of, r = _extract(tmp_path, {"S.cs": (
        "namespace Catalog.Events;\n"
        "public enum EntityType {\n"
        "    Brand\n"
        "}\n"
    )})
    assert (_find(r, "EntityType"), _find(r, "Brand")) in case_of


def test_an_enum_nested_in_a_class_emits_members(tmp_path):
    # The member's parent is the enum, not the enclosing class.
    case_of, r = _extract(tmp_path, {"S.cs": (
        "public class Holder {\n"
        "    public enum Mode {\n"
        "        Fast\n"
        "    }\n"
        "}\n"
    )})
    mode = _find(r, "Mode")
    assert (mode, _find(r, "Fast")) in case_of
    assert (_find(r, "Holder"), _find(r, "Fast")) not in case_of


def test_the_enum_type_node_is_kept(tmp_path):
    # Regression guard: the member nodes are additive. The enum type stays a
    # node and stays contained by its file.
    _, r = _extract(tmp_path, {"S.cs": (
        "public enum ProcessingType {\n"
        "    Process\n"
        "}\n"
    )})
    enum_id = _find(r, "ProcessingType")
    contains = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "contains"}
    assert any(target == enum_id for _, target in contains)


def test_a_member_is_not_a_method(tmp_path):
    # A member lands on `case_of`, never on `method` — nothing should read it
    # as a callable.
    case_of, r = _extract(tmp_path, {"S.cs": (
        "public enum ProcessingType {\n"
        "    Process\n"
        "}\n"
    )})
    member = _find(r, "Process")
    methods = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "method"}
    assert (_find(r, "ProcessingType"), member) in case_of
    assert (_find(r, "ProcessingType"), member) not in methods


def test_an_empty_enum_emits_no_members(tmp_path):
    case_of, r = _extract(tmp_path, {"S.cs": "public enum Empty {\n}\n"})
    assert "Empty" in _labels(r)
    assert case_of == set()


def test_a_property_and_an_enum_member_sharing_a_name_stay_separate(tmp_path):
    # Member ids are scoped to their declaring type, so a class property and an
    # enum member with the same name are two nodes, not one shared node.
    case_of, r = _extract(tmp_path, {"S.cs": (
        "public enum Mode {\n"
        "    Active\n"
        "}\n"
        "public class Widget {\n"
        "    public bool Active { get; set; }\n"
        "}\n"
    )})
    defines = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "defines"}
    enum_member = next(t for s, t in case_of if s == _find(r, "Mode"))
    class_member = next(t for s, t in defines if s == _find(r, "Widget"))
    assert enum_member != class_member


# ── #4245: a call must never bind to a same-named enum member ──────────────

def test_a_bare_call_to_a_delegate_field_does_not_bind_to_a_same_named_enum_case(tmp_path):
    """Invoking a delegate-typed field whose name collides with one of its own
    enum's cases must get no edge to the case — an enum member can never be a
    call target."""
    case_of, r = _extract(tmp_path, {"Eval.cs": (
        "namespace Demo {\n"
        "    public delegate float Curve(float t);\n"
        "    public class Eval {\n"
        "        public enum Kind { Linear, Delegate }\n"
        "        public Kind Mode;\n"
        "        public Curve Delegate;\n"
        "        public float Evaluate(float t) {\n"
        "            switch (Mode) {\n"
        "                case Kind.Delegate: return Delegate != null ? Delegate(t) : t;\n"
        "                default: return t;\n"
        "            }\n"
        "        }\n"
        "    }\n"
        "}\n"
    )})
    # Pin the member named "Delegate" specifically (Kind also has "Linear"); the
    # collision is only exercised when the asserted-against node is that member.
    kind_id = _find(r, "Kind")
    _label = {n["id"]: (n.get("label") or "").strip(".()") for n in r["nodes"]}
    delegate_member = next(
        t for s, t in case_of if s == kind_id and _label.get(t) == "Delegate"
    )
    calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
    assert not any(tgt == delegate_member for _src, tgt in calls), (
        f"a call must never land on the enum member, got: {calls}"
    )


def test_element_access_member_call_still_binds_to_the_real_method_not_an_enum_case(tmp_path):
    """Control: `Handles[i].Pause()` must still resolve to the real `Pause()`
    method on `Handle`, not to an unrelated same-named enum member elsewhere
    in the corpus — the fix must not make a genuine call unresolved."""
    case_of, r = _extract(tmp_path, {"Handles.cs": (
        "namespace Demo {\n"
        "    public enum State { Pause, Play }\n"
        "    public class Handle { public void Pause() { } }\n"
        "    public class Injector {\n"
        "        public Handle[] Handles;\n"
        "        public void StopAll() {\n"
        "            for (int i = 0; i < Handles.Length; i++) { Handles[i].Pause(); }\n"
        "        }\n"
        "    }\n"
        "}\n"
    )})
    pause_member = next(t for s, t in case_of if s == _find(r, "State"))
    pause_method = next(
        n["id"] for n in r["nodes"]
        if n["label"] == ".Pause()"
    )
    calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
    stopall = _find(r, ".StopAll()")
    assert (stopall, pause_method) in calls
    assert (stopall, pause_member) not in calls