From 6ee9624c22b0eecaac48c8d1d1296350b1f9dae5 Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Sun, 4 Oct 2026 13:28:23 +0530 Subject: [PATCH] fix(vbnet): link enum members with case_of instead of contains --- graphify/extractors/vbnet.py | 15 ++++++++++++--- tests/test_vbnet_extractor.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/graphify/extractors/vbnet.py b/graphify/extractors/vbnet.py index 2b182a4e9d..3649a32944 100644 --- a/graphify/extractors/vbnet.py +++ b/graphify/extractors/vbnet.py @@ -185,14 +185,16 @@ def type_reference(name: str, node: Node) -> str: source_backed=False, ) - def add_data_member(type_id: str, member: Node, name: str, kind: str) -> str: + def add_data_member( + type_id: str, member: Node, name: str, kind: str, relation: str = "contains" + ) -> str: member_id = add_node( _make_id(type_id, kind, name.casefold(), str(member.start_point[0])), name, member, kind=kind, ) - add_edge(type_id, member_id, "contains", member) + add_edge(type_id, member_id, relation, member) return member_id def process_type(block: Node, parent_id: str, namespace: str) -> None: @@ -227,8 +229,15 @@ def process_type(block: Node, parent_id: str, namespace: str) -> None: if member.type == "enum_member": member_name = member.child_by_field_name("name") if member_name is not None: + # An enum member is a discriminant case, not a contained + # declaration: it gets a `case_of` edge like every other + # language with enums (Java #1719, C#, Swift, Scala, ...), + # not the `contains` edge used for real fields. The relation + # also matters to resolution — case_of targets are excluded + # from `New X()` constructor binding (see _member_nids). add_data_member( - type_id, member, _read_text(member_name, source), "enum_member" + type_id, member, _read_text(member_name, source), + "enum_member", relation="case_of", ) continue if member.type == "field_declaration": diff --git a/tests/test_vbnet_extractor.py b/tests/test_vbnet_extractor.py index 6e5204cfad..cd45f913e1 100644 --- a/tests/test_vbnet_extractor.py +++ b/tests/test_vbnet_extractor.py @@ -188,3 +188,39 @@ def test_vbnet_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys assert result["nodes"] == [] assert 'pip install "graphifyy[vbnet]"' in capsys.readouterr().err + + +def test_vbnet_enum_members_emit_case_of_not_contains(tmp_path): + """A VB.NET enum member is a discriminant case, so it must get a `case_of` + edge like every other language with enums (Java #1719, C#, Swift, Scala), + not the `contains` edge used for real fields. VB.NET was the lone outlier, + routing enum members through the same `contains` path as class fields. The + relation also matters to resolution: `case_of` targets are excluded from + `New X()` constructor binding, so an enum member named like a type can no + longer be mistaken for one. + """ + source = tmp_path / "Enums.vb" + source.write_text( + "Public Enum Color\n" + " Red\n" + " Green\n" + " Blue\n" + "End Enum\n" + "Public Class Widget\n" + " Private count As Integer\n" + "End Class\n", + encoding="utf-8", + ) + + result = extract([source], cache_root=tmp_path) + + case_of = _edge_labels(result, "case_of") + contains = _edge_labels(result, "contains") + # Each enum member hangs off its enum via case_of, not contains. + assert ("Color", "Red") in case_of + assert ("Color", "Green") in case_of + assert ("Color", "Blue") in case_of + assert ("Color", "Red") not in contains + # A real class field is a declaration, not a case: it keeps `contains`. + assert ("Widget", "count") in contains + assert ("Widget", "count") not in case_of