Skip to content

Commit 47b9c1f

Browse files
committed
fix(native/julia): handle parameterized structs, qualified defs, qualified selected imports (#1098)
1 parent b940cf2 commit 47b9c1f

2 files changed

Lines changed: 110 additions & 26 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/codegraph-core/src/extractors/julia.rs

Lines changed: 109 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,12 @@ fn handle_function_def(
9393
if let Some(call_sig) = signature_call(node) {
9494
if let Some(func_name_node) = call_sig.child(0) {
9595
let base = node_text(&func_name_node, source);
96+
// For qualified names (`function Base.show ... end` inside a module),
97+
// the LHS is a `scoped_identifier` already containing the qualifier —
98+
// skip the module prefix to avoid producing `Outer.Base.show`.
9699
let name = match current_module {
97-
Some(m) => format!("{}.{}", m, base),
98-
None => base.to_string(),
100+
Some(m) if !base.contains('.') => format!("{}.{}", m, base),
101+
_ => base.to_string(),
99102
};
100103
let params = extract_julia_params(&call_sig, source);
101104
symbols.definitions.push(Definition {
@@ -122,8 +125,8 @@ fn handle_function_def(
122125
};
123126
let base = node_text(&name_node, source);
124127
let name = match current_module {
125-
Some(m) => format!("{}.{}", m, base),
126-
None => base.to_string(),
128+
Some(m) if !base.contains('.') => format!("{}.{}", m, base),
129+
_ => base.to_string(),
127130
};
128131
symbols.definitions.push(Definition {
129132
name,
@@ -156,9 +159,12 @@ fn handle_assignment(
156159
None => return,
157160
};
158161
let base = node_text(&func_name_node, source);
162+
// For qualified short-form definitions like `Foo.bar(x, y) = x + y`,
163+
// `func_name_node` is a `scoped_identifier` already containing the
164+
// qualifier — skip the module prefix to avoid producing `Outer.Foo.bar`.
159165
let name = match current_module {
160-
Some(m) => format!("{}.{}", m, base),
161-
None => base.to_string(),
166+
Some(m) if !base.contains('.') => format!("{}.{}", m, base),
167+
_ => base.to_string(),
162168
};
163169
let params = extract_julia_params(&lhs, source);
164170

@@ -176,8 +182,9 @@ fn handle_assignment(
176182

177183
fn handle_struct_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
178184
// struct_definition: `struct` type_head <fields> `end`
179-
// type_head is either a bare `identifier` (no supertype) or a
180-
// `binary_expression` of the form `Name <: Super`.
185+
// type_head wraps the name and optional supertype. The name may be a
186+
// bare `identifier`, a `parameterized_identifier` (e.g. `Vec{T}`), or
187+
// either of those nested inside a `binary_expression` (`Name <: Super`).
181188
let type_head = match find_child(node, "type_head") {
182189
Some(th) => th,
183190
None => return,
@@ -186,26 +193,24 @@ fn handle_struct_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
186193
let (name_node, supertype): (Node, Option<Node>) = if let Some(bin) =
187194
find_child(&type_head, "binary_expression")
188195
{
189-
// First identifier is the struct name, last identifier (after `<:`) is the supertype.
190-
let mut name_id: Option<Node> = None;
191-
let mut super_id: Option<Node> = None;
196+
// Walk into each side of the binary expression to find the base-name
197+
// identifier — handles parameterized forms like `Vec{T} <: AbstractArray{T,1}`.
198+
let mut sides: Vec<Node> = Vec::new();
192199
for i in 0..bin.child_count() {
193200
if let Some(c) = bin.child(i) {
194-
if c.kind() == "identifier" {
195-
if name_id.is_none() {
196-
name_id = Some(c);
197-
} else {
198-
super_id = Some(c);
199-
}
201+
if c.kind() != "operator" {
202+
sides.push(c);
200203
}
201204
}
202205
}
206+
let name_id = sides.first().and_then(|n| find_base_name(n));
207+
let super_id = sides.get(1).and_then(|n| find_base_name(n));
203208
match name_id {
204209
Some(n) => (n, super_id),
205210
None => return,
206211
}
207-
} else if let Some(id) = find_child(&type_head, "identifier") {
208-
(id, None)
212+
} else if let Some(n) = find_base_name(&type_head) {
213+
(n, None)
209214
} else {
210215
return;
211216
};
@@ -267,7 +272,7 @@ fn handle_abstract_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
267272
{
268273
Some(n) => n,
269274
None => match find_child(node, "type_head") {
270-
Some(th) => match find_abstract_name(&th) {
275+
Some(th) => match find_base_name(&th) {
271276
Some(n) => n,
272277
// Mirror the TS extractor: skip rather than emit a garbled
273278
// definition name (e.g. raw `Name{T} <: Super{T,1}` text).
@@ -295,7 +300,12 @@ fn handle_abstract_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
295300
/// into common wrapper kinds (binary expressions, parametrized identifiers,
296301
/// type-parameter lists). Returns `None` when no identifier can be located —
297302
/// callers should skip emitting a definition in that case.
298-
fn find_abstract_name<'a>(node: &Node<'a>) -> Option<Node<'a>> {
303+
fn find_base_name<'a>(node: &Node<'a>) -> Option<Node<'a>> {
304+
// The node itself may already be the identifier (e.g. when called on a
305+
// direct side of a binary_expression like `Point <: AbstractPoint`).
306+
if node.kind() == "identifier" {
307+
return Some(*node);
308+
}
299309
// Direct identifier child wins.
300310
if let Some(id) = find_child(node, "identifier") {
301311
return Some(id);
@@ -310,7 +320,7 @@ fn find_abstract_name<'a>(node: &Node<'a>) -> Option<Node<'a>> {
310320
| "parameterized_identifier"
311321
| "type_parameter_list"
312322
| "type_argument_list" => {
313-
if let Some(found) = find_abstract_name(&child) {
323+
if let Some(found) = find_base_name(&child) {
314324
return Some(found);
315325
}
316326
}
@@ -391,19 +401,24 @@ fn handle_import(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
391401
}
392402
}
393403
"selected_import" => {
394-
// First identifier is the source module; the rest are imported names.
404+
// First identifier-bearing node is the source module; the rest
405+
// are imported names. The module may itself be a
406+
// `scoped_identifier` (e.g. `import Foo.Bar: baz`) — handle it
407+
// alongside bare `identifier` and use the trailing segment as
408+
// the display name, mirroring the outer loop.
395409
let mut first = true;
396410
for j in 0..child.child_count() {
397411
let Some(part) = child.child(j) else { continue };
398-
if part.kind() == "identifier" {
412+
if part.kind() == "identifier" || part.kind() == "scoped_identifier" {
399413
let txt = node_text(&part, source).to_string();
400414
if first {
401415
if source_str.is_empty() {
402416
source_str = txt.clone();
403417
}
404418
first = false;
405419
} else {
406-
names.push(txt);
420+
let last = txt.rsplit('.').next().unwrap_or(&txt).to_string();
421+
names.push(last);
407422
}
408423
}
409424
}
@@ -669,4 +684,73 @@ mod tests {
669684
assert!(!call_names.contains(&"greet"));
670685
assert!(call_names.contains(&"println"));
671686
}
687+
688+
#[test]
689+
fn extracts_parameterized_struct_base_name() {
690+
// Parameterized struct names (e.g. `Vec{T}`) must record the base
691+
// identifier — not be silently dropped or include type-parameter text.
692+
let s = parse_jl("struct Vec{T} <: AbstractArray{T,1}\n data::Vector{T}\nend\n");
693+
let names: Vec<&str> = s.definitions.iter().map(|d| d.name.as_str()).collect();
694+
assert!(
695+
names.contains(&"Vec"),
696+
"expected base name `Vec`, got {names:?}"
697+
);
698+
assert!(
699+
!names.iter().any(|n| n.contains('{') || n.contains('<')),
700+
"definition name leaked raw type-head text: {names:?}"
701+
);
702+
// Supertype should still resolve to the base identifier `AbstractArray`.
703+
assert_eq!(s.classes.len(), 1);
704+
assert_eq!(s.classes[0].name, "Vec");
705+
assert_eq!(s.classes[0].extends.as_deref(), Some("AbstractArray"));
706+
}
707+
708+
#[test]
709+
fn qualified_short_form_method_does_not_double_prefix() {
710+
// `Foo.bar(x, y) = x + y` inside `module Outer` must record `Foo.bar`,
711+
// not `Outer.Foo.bar` — the scoped_identifier already carries the
712+
// qualifier.
713+
let s = parse_jl("module Outer\n Foo.bar(x, y) = x + y\nend\n");
714+
let names: Vec<&str> = s.definitions.iter().map(|d| d.name.as_str()).collect();
715+
assert!(names.contains(&"Foo.bar"), "got {names:?}");
716+
assert!(
717+
!names.iter().any(|n| *n == "Outer.Foo.bar"),
718+
"qualified method got double-prefixed: {names:?}"
719+
);
720+
}
721+
722+
#[test]
723+
fn qualified_function_def_does_not_double_prefix() {
724+
// `function Base.show(io, x) ... end` inside `module Foo` must record
725+
// `Base.show`, not `Foo.Base.show`.
726+
let s = parse_jl(
727+
"module Foo\n function Base.show(io, x)\n println(io, x)\n end\nend\n",
728+
);
729+
let names: Vec<&str> = s.definitions.iter().map(|d| d.name.as_str()).collect();
730+
assert!(names.contains(&"Base.show"), "got {names:?}");
731+
assert!(
732+
!names.iter().any(|n| *n == "Foo.Base.show"),
733+
"qualified function def got double-prefixed: {names:?}"
734+
);
735+
}
736+
737+
#[test]
738+
fn selected_import_handles_qualified_module() {
739+
// `import Foo.Bar: baz` — module is a scoped_identifier. The import
740+
// must record `Foo.Bar` as the source and `baz` as the imported name,
741+
// not the malformed `source="baz", names=["baz"]`.
742+
let s = parse_jl("import LinearAlgebra.BLAS: gemm\n");
743+
assert_eq!(s.imports.len(), 1);
744+
assert_eq!(s.imports[0].source, "LinearAlgebra.BLAS");
745+
assert!(
746+
s.imports[0].names.contains(&"gemm".to_string()),
747+
"expected `gemm` in imported names, got {:?}",
748+
s.imports[0].names
749+
);
750+
assert!(
751+
!s.imports[0].names.contains(&"LinearAlgebra.BLAS".to_string()),
752+
"source module leaked into names: {:?}",
753+
s.imports[0].names
754+
);
755+
}
672756
}

0 commit comments

Comments
 (0)