mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-27 00:36:39 +00:00
A C# enum's members got no graph node, so an enum case was invisible. Emit a member node per enum_member_declaration with a case_of edge to the enum (mirroring the Java/Kotlin/Swift enum handlers), deduped against a case-only sibling collision; explicit and implicit values both work and no builtin types are fabricated.
164 lines
5.6 KiB
Python
164 lines
5.6 KiB
Python
"""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. The first declaration keeps the
|
|
# node rather than a second edge hanging off it.
|
|
case_of, r = _extract(tmp_path, {"S.cs": (
|
|
"public enum Odd {\n"
|
|
" Value,\n"
|
|
" value\n"
|
|
"}\n"
|
|
)})
|
|
assert len(case_of) == 1
|
|
|
|
|
|
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
|