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) <noreply@anthropic.com>
This commit is contained in:
Synvoya
2026-07-01 13:45:52 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 7e24c3b7e4
commit 984a6a8f0a
3 changed files with 47 additions and 11 deletions
+31 -11
View File
@@ -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:
+2
View File
@@ -2,6 +2,8 @@ module Geometry
using LinearAlgebra
import Base: show
using Base.Threads
using ..ParentModule
abstract type Shape end
+14
View File
@@ -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"]