Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions graphify/extractors/vbnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — process_type()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Expand Down Expand Up @@ -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":
Expand Down
36 changes: 36 additions & 0 deletions tests/test_vbnet_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading