mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
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:
1 parent
4000de1546
commit
87edb5a278
2 files changed
+104
-1
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user