@@ -158,8 +158,9 @@ fn collect_interfaces(
158158
159159fn handle_interface_decl ( node : & Node , source : & [ u8 ] , symbols : & mut FileSymbols ) {
160160 let Some ( name_node) = node. child_by_field_name ( "name" ) else { return } ;
161+ let iface_name = node_text ( & name_node, source) . to_string ( ) ;
161162 symbols. definitions . push ( Definition {
162- name : node_text ( & name_node , source ) . to_string ( ) ,
163+ name : iface_name . clone ( ) ,
163164 kind : "interface" . to_string ( ) ,
164165 line : start_line ( node) ,
165166 end_line : Some ( end_line ( node) ) ,
@@ -168,6 +169,18 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols)
168169 cfg : None ,
169170 children : None ,
170171 } ) ;
172+
173+ // `interface X extends Y, Z` — tree-sitter-groovy 0.1.x exposes parent
174+ // interfaces as an unnamed `extends_interfaces` child wrapping a `type_list`.
175+ // collect_interfaces already recurses into `type_list`, so passing the
176+ // wrapper node works without a dedicated helper.
177+ for i in 0 ..node. child_count ( ) {
178+ let Some ( child) = node. child ( i) else { continue } ;
179+ if child. kind ( ) == "extends_interfaces" {
180+ collect_interfaces ( & child, & iface_name, source, symbols) ;
181+ break ;
182+ }
183+ }
171184}
172185
173186fn handle_enum_decl ( node : & Node , source : & [ u8 ] , symbols : & mut FileSymbols ) {
@@ -523,4 +536,37 @@ mod tests {
523536 assert ! ( rels. iter( ) . any( |c| c. implements. as_deref( ) == Some ( "I1" ) ) ) ;
524537 assert ! ( rels. iter( ) . any( |c| c. implements. as_deref( ) == Some ( "I2" ) ) ) ;
525538 }
539+
540+ #[ test]
541+ fn extracts_interface_inheritance ( ) {
542+ // `interface X extends Y, Z` — the grammar exposes parent interfaces
543+ // via an unnamed `extends_interfaces` child (not a field), distinct
544+ // from class declarations which use the `interfaces` field.
545+ let s = parse_groovy ( "interface Serializable extends Comparable, Cloneable {}" ) ;
546+ let rels: Vec < _ > = s. classes . iter ( ) . filter ( |c| c. name == "Serializable" ) . collect ( ) ;
547+ assert ! (
548+ rels. iter( ) . any( |c| c. implements. as_deref( ) == Some ( "Comparable" ) ) ,
549+ "missing implements=Comparable, got: {:?}" ,
550+ rels
551+ ) ;
552+ assert ! (
553+ rels. iter( ) . any( |c| c. implements. as_deref( ) == Some ( "Cloneable" ) ) ,
554+ "missing implements=Cloneable, got: {:?}" ,
555+ rels
556+ ) ;
557+ }
558+
559+ #[ test]
560+ fn interface_inheritance_line_tracks_extends_clause ( ) {
561+ // Engine-parity guard: the relation line should match the
562+ // `extends_interfaces` node's start line, not the `interface_declaration`'s
563+ // — `collect_interfaces` re-evaluates `start_line(interfaces)` on every
564+ // recursive call, and the WASM extractor must match.
565+ let s = parse_groovy ( "interface Serializable\n extends Comparable, Cloneable {}" ) ;
566+ let rels: Vec < _ > = s. classes . iter ( ) . filter ( |c| c. name == "Serializable" ) . collect ( ) ;
567+ assert ! ( !rels. is_empty( ) , "expected at least one ClassRelation" ) ;
568+ for rel in & rels {
569+ assert_eq ! ( rel. line, 2 , "line should track the extends clause, got: {:?}" , rel) ;
570+ }
571+ }
526572}
0 commit comments