fix(julia): extract macro definitions and @enum types

Two core Julia constructs were dropped by the extractor:

- macro name(...) ... end produced no node, even though macros are first-class,
  heavily used definitions. The grammar gives a macro the same signature shape
  as a function, so it now mints a definition (labelled @name) and its body is
  walked for calls -- with the signature skipped so the macro does not call
  itself.
- @enum Name a b c (and the @enum Name begin ... end block form) is a
  macrocall_expression that was ignored entirely. The first identifier in the
  argument list is the enum type; the rest, including members inside a begin/end
  block, are contained by it. Other macro calls still fall through so
  definitions nested in e.g. an @testset block are walked.

Added tests covering macro extraction with body-call attribution (no self-loop)
and both @enum forms with members contained by their type.
This commit is contained in:
rajatnagda45 committed 2026-09-25 23:57:23 +05:30
1 parent 4000de1546
commit 87edb5a278
2 files changed
+104 -1

No files matched your search

+55 -1
View File
@@ -214,6 +214,60 @@ def extract_julia(path: Path) -> dict:
function_bodies.append((func_nid, node))
return
# Macro: macro foo(...) ... end. Same grammar shape as a function (a
# `signature` wrapping a call_expression). Without this branch every
# macro definition was dropped, even though macros are first-class,
# heavily used definitions in Julia.
if t == "macro_definition":
sig_node = next((c for c in node.children if c.type == "signature"), None)
if sig_node:
macro_name = _func_name_from_signature(sig_node)
if macro_name:
macro_nid = _make_id(stem, "@" + macro_name)
line = node.start_point[0] + 1
add_node(macro_nid, f"@{macro_name}", line)
add_edge(scope_nid, macro_nid, "defines", line)
function_bodies.append((macro_nid, node))
return
# @enum: `@enum Name a b c` or `@enum Name begin a; b end`. This lowers
# to a macrocall_expression; the first identifier in the argument list is
# the enum type, the rest (including those inside a begin/end block) are
# its members. Other macro calls fall through so definitions nested in
# e.g. an `@testset begin ... end` are still walked.
if t == "macrocall_expression":
macro_id = next((c for c in node.children if c.type == "macro_identifier"), None)
macro_name = None
if macro_id is not None:
id_node = next((c for c in macro_id.children if c.type == "identifier"), None)
if id_node is not None:
macro_name = _read_text(id_node, source)
if macro_name == "enum":
arglist = next((c for c in node.children if c.type == "macro_argument_list"), None)
names = []
if arglist is not None:
for c in arglist.children:
if c.type == "identifier":
names.append(c)
elif c.type == "compound_statement":
names.extend(cc for cc in c.children if cc.type == "identifier")
if names:
enum_name = _read_text(names[0], source)
line = node.start_point[0] + 1
enum_nid = _make_id(stem, enum_name)
add_node(enum_nid, enum_name, line)
add_edge(scope_nid, enum_nid, "defines", line)
for member in names[1:]:
m_name = _read_text(member, source)
m_line = member.start_point[0] + 1
m_nid = _make_id(stem, enum_name, m_name)
add_node(m_nid, m_name, m_line)
add_edge(enum_nid, m_nid, "contains", m_line)
return
for child in node.children:
walk(child, scope_nid)
return
# Short function: foo(x) = expr
if t == "assignment":
lhs = node.children[0] if node.children else None
@@ -279,7 +333,7 @@ def extract_julia(path: Path) -> dict:
# the boundary check returning early on the top-level node itself.
# Skip the "signature" child — it contains the function's own call_expression
# which would create a self-loop.
if body_node.type == "function_definition":
if body_node.type in ("function_definition", "macro_definition"):
for child in body_node.children:
if child.type != "signature":
walk_calls(child, func_nid)
+49
View File
@@ -2235,6 +2235,55 @@ def test_julia_abstract_type_with_supertype_is_extracted(tmp_path):
assert ("Dog", "Animal") in _edge_labels(r, "inherits"), "abstract inherits edge dropped"
def test_julia_macro_definition_is_extracted(tmp_path):
"""`macro name(...) ... end` must be a definition, with the calls in its
body attributed to it. Macros are first-class Julia definitions and were
dropped entirely."""
f = tmp_path / "macros.jl"
f.write_text(
"function helper(x)\n"
" return x + 1\n"
"end\n"
"macro sayhello(name)\n"
" return :( helper($name) )\n"
"end\n"
)
r = extract_julia(f)
assert "error" not in r
labels = [n["label"] for n in r["nodes"]]
assert "@sayhello" in labels, "macro definition dropped"
# the macro body's call to a helper is credited to the macro, not a self-loop
assert ("@sayhello", "helper") in _edge_labels(r, "calls")
assert ("@sayhello", "@sayhello") not in _edge_labels(r, "calls")
def test_julia_enum_and_members_are_extracted(tmp_path):
"""`@enum` defines a type and its members, in both the inline and the
begin/end block forms. The whole macrocall was previously ignored."""
f = tmp_path / "enums.jl"
f.write_text(
"@enum Fruit apple orange banana\n"
"@enum Color begin\n"
" red\n"
" green\n"
"end\n"
)
r = extract_julia(f)
assert "error" not in r
labels = {n["label"] for n in r["nodes"]}
assert {"Fruit", "apple", "orange", "banana", "Color", "red", "green"} <= labels
contains = _edge_labels(r, "contains")
assert {("Fruit", "apple"), ("Fruit", "banana"),
("Color", "red"), ("Color", "green")} <= contains
# the members hang off their enum, not the file
file_nid = next(n["id"] for n in r["nodes"] if n["label"] == "enums.jl")
lab = {n["id"]: n["label"] for n in r["nodes"]}
assert not [
e for e in r["edges"]
if e["source"] == file_nid and lab.get(e["target"]) in {"apple", "red"}
]
def test_julia_struct_field_type_context():
r = extract_julia(FIXTURES / "sample.jl")
assert ("Point", "Float64") in _edge_labels(r, "references", "field")