@@ -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
177183fn 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}\n end\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\n end\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\n end\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