From 984a6a8f0ad0c8630d81e18babc75976b7da0387 Mon Sep 17 00:00:00 2001 From: Synvoya <16019863+Synvoya@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:00:37 +1000 Subject: [PATCH] fix(julia): emit imports for qualified, relative, and scoped-selected forms Only bare-identifier imports (`using Foo`) emitted edges. tree-sitter-julia wraps qualified paths in `scoped_identifier` (`using Base.Threads`), relative paths in `import_path` (`using ..Sibling`), and the package of a `selected_import` may itself be a `scoped_identifier` (`import Base.Threads: nthreads`). None of those were matched, so qualified and relative imports were silently dropped, and scoped selected-imports pointed at the selected symbol instead of the module. Resolve the module name from identifier / scoped_identifier / import_path in all three positions. Adds fixture lines + a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- graphify/extract.py | 42 +++++++++++++++++++++++++++++----------- tests/fixtures/sample.jl | 2 ++ tests/test_languages.py | 14 ++++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index 9941988..4ad8e01 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -7285,19 +7285,39 @@ def extract_julia(path: Path) -> dict: # Using / Import if t in ("using_statement", "import_statement"): line = node.start_point[0] + 1 + + def _julia_mod_name(n): + # identifier (`Foo`), scoped_identifier (`Base.Threads`), or + # import_path (relative `..Sibling`) -> the module name. Only bare + # identifiers were handled, so qualified/relative imports — and the + # scoped package of a `selected_import` — were silently dropped. + if n.type == "import_path": + ids = [c for c in n.children if c.type == "identifier"] + return _read_text(ids[-1], source) if ids else None + if n.type in ("identifier", "scoped_identifier"): + return _read_text(n, source) + return None + + def _emit_import(name): + if not name: + return + imp_nid = _make_id(name) + add_node(imp_nid, name, line) + add_edge(scope_nid, imp_nid, "imports", line, context="import") + for child in node.children: - if child.type == "identifier": - mod_name = _read_text(child, source) - imp_nid = _make_id(mod_name) - add_node(imp_nid, mod_name, line) - add_edge(scope_nid, imp_nid, "imports", line, context="import") + if child.type in ("identifier", "scoped_identifier", "import_path"): + _emit_import(_julia_mod_name(child)) elif child.type == "selected_import": - identifiers = [c for c in child.children if c.type == "identifier"] - if identifiers: - pkg_name = _read_text(identifiers[0], source) - pkg_nid = _make_id(pkg_name) - add_node(pkg_nid, pkg_name, line) - add_edge(scope_nid, pkg_nid, "imports", line, context="import") + # `import Base.Threads: nthreads` — the package (first named + # child) may itself be a scoped_identifier/import_path. + pkg = next( + (c for c in child.children + if c.type in ("identifier", "scoped_identifier", "import_path")), + None, + ) + if pkg is not None: + _emit_import(_julia_mod_name(pkg)) return for child in node.children: diff --git a/tests/fixtures/sample.jl b/tests/fixtures/sample.jl index 43409f4..0c1b66a 100644 --- a/tests/fixtures/sample.jl +++ b/tests/fixtures/sample.jl @@ -2,6 +2,8 @@ module Geometry using LinearAlgebra import Base: show +using Base.Threads +using ..ParentModule abstract type Shape end diff --git a/tests/test_languages.py b/tests/test_languages.py index c082e2d..28775a7 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -1446,6 +1446,20 @@ def test_julia_import_edges_have_import_context(): assert all(e.get("context") == "import" for e in import_edges) +def test_julia_qualified_and_relative_imports(): + """Qualified (`using Base.Threads`) and relative (`using ..Mod`) imports + must emit edges. + + The handler only matched bare identifiers, so scoped_identifier and + import_path forms — and the scoped package of a selected_import — were + silently dropped. + """ + r = extract_julia(FIXTURES / "sample.jl") + targets = [e["target"] for e in r["edges"] if e["relation"] == "imports"] + assert any("base_threads" in t for t in targets), "qualified import Base.Threads missing" + assert any("parentmodule" in t for t in targets), "relative import ParentModule missing" + + def test_julia_finds_inherits(): r = extract_julia(FIXTURES / "sample.jl") inherits = [e for e in r["edges"] if e["relation"] == "inherits"]