diff --git a/graphify/extractors/pascal.py b/graphify/extractors/pascal.py index 398edb22e..bdfcf6e23 100644 --- a/graphify/extractors/pascal.py +++ b/graphify/extractors/pascal.py @@ -545,11 +545,43 @@ def extract_pascal(path: Path) -> dict: if t == "declType": type_name = None kind_node = None + enum_node = None for child in node.children: if child.type == "identifier" and type_name is None: type_name = _read(child) elif child.type in ("declClass", "declIntf", "declHelper") and kind_node is None: kind_node = child + elif child.type == "type" and enum_node is None: + # An enumerated type (`TColor = (clRed, clGreen);`) nests its + # values under type -> declEnum -> declEnumValue. + enum_node = next( + (gc for gc in child.children if gc.type == "declEnum"), None + ) + if type_name and enum_node is not None: + # A Pascal enumerated type is a named type whose values are its + # cases. Without this branch declType matched only class/interface/ + # helper kinds, so the whole enum type AND its values fell through + # and were dropped. Emit the type node plus a `case_of` edge per + # value — the Pascal parity of Java #1719 / C# / Swift / Scala enums. + enum_nid = _make_id(stem, type_name) + add_node(enum_nid, type_name, line) + add_edge(parent_nid, enum_nid, "contains", line) + for value in enum_node.children: + if value.type != "declEnumValue": + continue + value_name_node = next( + (c for c in value.children if c.type == "identifier"), None + ) + if value_name_node is None: + continue + value_name = _read(value_name_node) + if not value_name: + continue + value_line = value.start_point[0] + 1 + value_nid = _make_id(enum_nid, value_name) + add_node(value_nid, value_name, value_line) + add_edge(enum_nid, value_nid, "case_of", value_line) + return if type_name and kind_node: cls_nid = _make_id(stem, type_name) add_node(cls_nid, type_name, line) diff --git a/tests/test_pascal.py b/tests/test_pascal.py index e54564bcf..609360990 100644 --- a/tests/test_pascal.py +++ b/tests/test_pascal.py @@ -1,9 +1,17 @@ """Tests for the Pascal/Delphi extractor.""" from __future__ import annotations +import importlib.util as _ilu from pathlib import Path +import pytest + FIXTURES = Path(__file__).parent / "fixtures" +_needs_pascal = pytest.mark.skipif( + _ilu.find_spec("tree_sitter_pascal") is None, + reason="tree-sitter-pascal not installed (enum types need the AST path)", +) + def _labels(r): return [n["label"] for n in r["nodes"]] @@ -85,6 +93,44 @@ def test_pascal_inherits_from_base(): assert found, "TDataProcessor should have at least one inherits edge" +@_needs_pascal +def test_pascal_enum_type_and_values_are_extracted(tmp_path): + """A Pascal enumerated type becomes a node with a `case_of` edge per value. + + `declType` matched only class/interface/helper kinds, so an enumerated type + (`TColor = (clRed, clGreen, clBlue);`) and all of its values fell through and + were dropped — the whole type vanished. An enum's values are its cases, so + each becomes a node with a `case_of` edge to the type, matching every other + language with enums (Java #1719 / C# / Swift / Scala). + """ + from graphify.extract import extract_pascal + p = tmp_path / "colors.pas" + p.write_text( + "unit Colors;\n" + "interface\n" + "type\n" + " TColor = (clRed, clGreen, clBlue);\n" + " TNum = Integer;\n" + "implementation\n" + "end.\n", + encoding="utf-8", + ) + r = extract_pascal(p) + assert "error" not in r + labels = {n["label"] for n in r["nodes"]} + # Pre-fix the whole `TColor = (...)` declaration vanished. + assert {"TColor", "clRed", "clGreen", "clBlue"} <= labels + ids = {n["id"]: n["label"] for n in r["nodes"]} + case_of = { + (ids.get(e["source"]), ids.get(e["target"])) + for e in r["edges"] + if e["relation"] == "case_of" + } + assert ("TColor", "clRed") in case_of + assert ("TColor", "clGreen") in case_of + assert ("TColor", "clBlue") in case_of + + def test_pascal_finds_calls(): from graphify.extract import extract_pascal r = extract_pascal(FIXTURES / "sample.pas")