Skip to main content

perl_lsp_type_hierarchy/
lib.rs

1//! Type hierarchy provider for Perl inheritance and package relationships.
2//!
3//! Supplies `textDocument/typeHierarchy` data for navigating parent/child
4//! package relationships in the Parse → Index → Navigate stages of the LSP workflow.
5//!
6//! # Client capability requirements
7//!
8//! Clients must advertise the type hierarchy capability to enable
9//! `textDocument/typeHierarchy` requests and responses.
10//!
11//! # Protocol compliance
12//!
13//! Implements the type hierarchy protocol with LSP symbol kind mappings and
14//! stable item identifiers for follow-up requests.
15//!
16//! # Examples
17//!
18//! ```ignore
19//! use perl_lsp_providers::ide::lsp_compat::type_hierarchy::TypeHierarchyProvider;
20//! use perl_parser_core::Parser;
21//!
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! let mut parser = Parser::new("package Parent; package Child; use parent 'Parent';");
24//! let _ast = parser.parse()?;
25//! let _provider = TypeHierarchyProvider::new();
26//! # Ok(())
27//! # }
28//! ```
29
30use perl_parser_core::PositionMapper;
31use perl_parser_core::ast::{Node, NodeKind};
32use perl_position_tracking::{WirePosition, WireRange};
33use serde::{Deserialize, Serialize};
34use std::collections::{BTreeMap, BTreeSet};
35
36/// Represents a type in the hierarchy
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct TypeHierarchyItem {
39    /// Fully qualified name of the type (e.g., package name)
40    pub name: String,
41    /// Kind of symbol (Class, Method, or Function)
42    pub kind: TypeHierarchySymbolKind,
43    /// URI of the document containing this type
44    pub uri: String,
45    /// Full range of the type declaration
46    pub range: WireRange,
47    /// Range of the type name for highlighting
48    pub selection_range: WireRange,
49    /// Optional detail string (e.g., "Perl Package")
50    pub detail: Option<String>,
51    /// Optional additional data for client use
52    pub data: Option<serde_json::Value>,
53}
54
55/// Kind of symbol in the type hierarchy (LSP protocol values)
56///
57/// This enum uses explicit discriminant values matching the LSP protocol
58/// SymbolKind values for direct wire serialization.
59#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
60pub enum TypeHierarchySymbolKind {
61    /// A class or package (LSP value 5)
62    Class = 5,
63    /// A method (LSP value 6)
64    Method = 6,
65    /// A function (LSP value 12)
66    Function = 12,
67}
68
69/// Index for tracking package hierarchy relationships
70#[derive(Default, Debug)]
71struct HierarchyIndex {
72    /// Map from child package to its parent packages
73    parents: BTreeMap<String, BTreeSet<String>>,
74    /// Map from parent package to its child packages
75    children: BTreeMap<String, BTreeSet<String>>,
76    /// Map from package to its composed roles (via `with`)
77    roles: BTreeMap<String, BTreeSet<String>>,
78}
79
80impl HierarchyIndex {
81    fn add_inheritance(&mut self, child: &str, parent: &str) {
82        self.parents.entry(child.to_string()).or_default().insert(parent.to_string());
83        self.children.entry(parent.to_string()).or_default().insert(child.to_string());
84    }
85
86    fn add_role(&mut self, package: &str, role: &str) {
87        self.roles.entry(package.to_string()).or_default().insert(role.to_string());
88    }
89
90    fn get_parents(&self, package: &str) -> Vec<String> {
91        self.parents.get(package).map(|set| set.iter().cloned().collect()).unwrap_or_default()
92    }
93
94    fn get_roles(&self, package: &str) -> Vec<String> {
95        self.roles.get(package).map(|set| set.iter().cloned().collect()).unwrap_or_default()
96    }
97
98    fn get_children(&self, package: &str) -> Vec<String> {
99        self.children.get(package).map(|set| set.iter().cloned().collect()).unwrap_or_default()
100    }
101}
102
103/// Provider for type hierarchy (inheritance) information
104pub struct TypeHierarchyProvider;
105
106impl Default for TypeHierarchyProvider {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl TypeHierarchyProvider {
113    /// Creates a new type hierarchy provider
114    pub fn new() -> Self {
115        Self
116    }
117
118    /// Build a hierarchy index from the AST
119    fn build_hierarchy_index(&self, ast: &Node) -> HierarchyIndex {
120        let mut index = HierarchyIndex::default();
121        let mut current_package = "main".to_string();
122
123        // Walk the AST in order, tracking package scope
124        self.index_hierarchy_recursive(ast, &mut index, &mut current_package);
125
126        index
127    }
128
129    fn index_hierarchy_recursive(
130        &self,
131        node: &Node,
132        index: &mut HierarchyIndex,
133        current_package: &mut String,
134    ) {
135        match &node.kind {
136            NodeKind::Package { name, block, name_span: _ } => {
137                if block.is_some() {
138                    // Block form: package Foo { ... }
139                    // Save current package, process block, restore
140                    let saved_package = current_package.clone();
141                    *current_package = name.clone();
142                    if let Some(blk) = block {
143                        self.index_hierarchy_recursive(blk, index, current_package);
144                    }
145                    *current_package = saved_package;
146                } else {
147                    // Linear form: package Foo;
148                    // Changes package scope for subsequent statements
149                    *current_package = name.clone();
150                }
151            }
152            NodeKind::Use { module, args, .. } => {
153                if module == "parent" || module == "base" {
154                    for arg in args {
155                        for parent in self.normalize_parent_arg(arg) {
156                            index.add_inheritance(current_package, &parent);
157                        }
158                    }
159                }
160            }
161            NodeKind::VariableDeclaration { declarator, variable, initializer, .. } => {
162                if declarator == "our"
163                    && let NodeKind::Variable { sigil, name: var_name } = &variable.kind
164                    && sigil == "@"
165                    && var_name == "ISA"
166                    && let Some(init) = initializer
167                {
168                    for parent in self.extract_isa_parents(init) {
169                        index.add_inheritance(current_package, &parent);
170                    }
171                }
172            }
173            NodeKind::VariableListDeclaration { declarator, variables, initializer, .. } => {
174                if declarator == "our" {
175                    // Check if any variable is @ISA
176                    for var in variables {
177                        if let NodeKind::Variable { sigil, name: var_name } = &var.kind
178                            && sigil == "@"
179                            && var_name == "ISA"
180                            && let Some(init) = initializer
181                        {
182                            for parent in self.extract_isa_parents(init) {
183                                index.add_inheritance(current_package, &parent);
184                            }
185                        }
186                    }
187                }
188            }
189            // Moose/Moo/Mouse: extends 'Parent', 'Parent2'  and  with 'Role', 'Role2'
190            NodeKind::ExpressionStatement { expression } => {
191                if let NodeKind::FunctionCall { name, args } = &expression.kind {
192                    match name.as_str() {
193                        "extends" => {
194                            for parent in Self::extract_names_from_args(args) {
195                                index.add_inheritance(current_package, &parent);
196                            }
197                        }
198                        "with" => {
199                            for role in Self::extract_names_from_args(args) {
200                                index.add_role(current_package, &role);
201                            }
202                        }
203                        _ => {}
204                    }
205                }
206            }
207            NodeKind::Program { statements } | NodeKind::Block { statements } => {
208                for stmt in statements {
209                    self.index_hierarchy_recursive(stmt, index, current_package);
210                }
211            }
212            _ => {
213                // Recurse into other nodes
214                if let Some(children) = self.get_children(node) {
215                    for child in children {
216                        self.index_hierarchy_recursive(child, index, current_package);
217                    }
218                }
219            }
220        }
221    }
222
223    /// Normalize parent argument (handle quotes, qw(), etc.)
224    fn normalize_parent_arg(&self, arg: &str) -> Vec<String> {
225        let arg = arg.trim();
226
227        // Handle qw(Base Other)
228        if arg.starts_with("qw(") && arg.ends_with(')') {
229            let content = &arg[3..arg.len() - 1];
230            return content.split_whitespace().map(|s| s.to_string()).collect();
231        }
232
233        // Handle qw{Base Other}, qw[Base Other], etc.
234        if arg.starts_with("qw") && arg.len() > 2 {
235            let delim_start = arg.chars().nth(2).unwrap_or(' ');
236            let delim_end = match delim_start {
237                '(' => ')',
238                '{' => '}',
239                '[' => ']',
240                '<' => '>',
241                _ => delim_start,
242            };
243            if let Some(start) = arg.find(delim_start)
244                && let Some(end) = arg.rfind(delim_end)
245            {
246                let content = &arg[start + 1..end];
247                return content.split_whitespace().map(|s| s.to_string()).collect();
248            }
249        }
250
251        // Remove quotes
252        let clean = arg.trim_matches('"').trim_matches('\'').trim_matches('`');
253        vec![clean.to_string()]
254    }
255
256    /// Extract package/role names from function call arguments (e.g., `extends 'A', 'B'`).
257    ///
258    /// Handles `String`, `Identifier`, and `ArrayLiteral` nodes. Hash literal
259    /// arguments (e.g., `{ -version => 0.01 }`) are harmlessly skipped.
260    fn extract_names_from_args(args: &[Node]) -> Vec<String> {
261        args.iter().flat_map(Self::collect_symbol_names).collect()
262    }
263
264    /// Collect symbol names from a single AST node (String, Identifier, or ArrayLiteral).
265    fn collect_symbol_names(node: &Node) -> Vec<String> {
266        match &node.kind {
267            NodeKind::String { value, .. } => {
268                let trimmed = value.trim().trim_matches('\'').trim_matches('"').trim();
269                if trimmed.is_empty() { Vec::new() } else { vec![trimmed.to_string()] }
270            }
271            NodeKind::Identifier { name } => {
272                let trimmed = name.trim();
273                if trimmed.is_empty() { Vec::new() } else { vec![trimmed.to_string()] }
274            }
275            NodeKind::ArrayLiteral { elements } => {
276                elements.iter().flat_map(Self::collect_symbol_names).collect()
277            }
278            _ => Vec::new(),
279        }
280    }
281
282    /// Extract parent classes from @ISA initialization
283    fn extract_isa_parents(&self, node: &Node) -> Vec<String> {
284        let mut parents = Vec::new();
285
286        match &node.kind {
287            NodeKind::ArrayLiteral { elements } => {
288                for elem in elements {
289                    match &elem.kind {
290                        NodeKind::String { value, .. } => {
291                            for parent in self.normalize_parent_arg(value) {
292                                parents.push(parent);
293                            }
294                        }
295                        NodeKind::Identifier { name } => {
296                            // Bareword
297                            parents.push(name.clone());
298                        }
299                        _ => {}
300                    }
301                }
302            }
303            NodeKind::String { value, .. } => {
304                for parent in self.normalize_parent_arg(value) {
305                    parents.push(parent);
306                }
307            }
308            NodeKind::Identifier { name } => {
309                // Bareword
310                parents.push(name.clone());
311            }
312            _ => {}
313        }
314
315        parents
316    }
317
318    /// Prepare type hierarchy at position
319    pub fn prepare(&self, ast: &Node, code: &str, offset: usize) -> Option<Vec<TypeHierarchyItem>> {
320        let position_mapper = PositionMapper::new(code);
321        // Find the node at the position
322        let target_node = self.find_node_at_offset(ast, offset)?;
323
324        // Check if it's a package or class declaration
325        match &target_node.kind {
326            NodeKind::Package { name, .. } => {
327                let item = self.create_type_item(
328                    name,
329                    target_node,
330                    &position_mapper,
331                    TypeHierarchySymbolKind::Class,
332                );
333                Some(vec![item])
334            }
335            NodeKind::Class { name, .. } => {
336                let item = self.create_type_item(
337                    name,
338                    target_node,
339                    &position_mapper,
340                    TypeHierarchySymbolKind::Class,
341                );
342                Some(vec![item])
343            }
344            NodeKind::Identifier { name } => {
345                // Check if this identifier is part of a package or ISA relationship
346                if self.is_package_identifier(ast, offset, name) {
347                    let item = TypeHierarchyItem {
348                        name: name.clone(),
349                        kind: TypeHierarchySymbolKind::Class,
350                        uri: "file:///current".to_string(),
351                        range: self.node_to_range(target_node, &position_mapper),
352                        selection_range: self.node_to_range(target_node, &position_mapper),
353                        detail: Some("Perl Package".to_string()),
354                        data: None,
355                    };
356                    Some(vec![item])
357                } else {
358                    None
359                }
360            }
361            _ => None,
362        }
363    }
364
365    /// Find supertypes (parent classes and composed roles)
366    pub fn find_supertypes(&self, ast: &Node, item: &TypeHierarchyItem) -> Vec<TypeHierarchyItem> {
367        let index = self.build_hierarchy_index(ast);
368        let parents = index.get_parents(&item.name);
369        let roles = index.get_roles(&item.name);
370
371        let parent_items = parents.into_iter().map(|name| TypeHierarchyItem {
372            name,
373            kind: TypeHierarchySymbolKind::Class,
374            uri: "file:///current".to_string(),
375            range: WireRange::default(),
376            selection_range: WireRange::default(),
377            detail: Some("Parent Class".to_string()),
378            data: None,
379        });
380
381        let role_items = roles.into_iter().map(|name| TypeHierarchyItem {
382            name,
383            kind: TypeHierarchySymbolKind::Class,
384            uri: "file:///current".to_string(),
385            range: WireRange::default(),
386            selection_range: WireRange::default(),
387            detail: Some("Role".to_string()),
388            data: None,
389        });
390
391        parent_items.chain(role_items).collect()
392    }
393
394    /// Compute the C3 Method Resolution Order (MRO) for a package.
395    ///
396    /// Returns a linearized list starting with `package` itself, followed by
397    /// ancestors in the order Perl's C3 MRO would search them. Each class
398    /// appears exactly once. If the C3 merge is inconsistent the algorithm
399    /// falls back to a depth-first left-to-right order with deduplication.
400    pub fn c3_mro(&self, ast: &Node, package: &str) -> Vec<String> {
401        let index = self.build_hierarchy_index(ast);
402        let mut result = Vec::new();
403        let mut visited = BTreeSet::new();
404        self.c3_linearize(package, &index, &mut result, &mut visited);
405        result
406    }
407
408    /// Recursive C3 linearization implementation.
409    fn c3_linearize(
410        &self,
411        package: &str,
412        index: &HierarchyIndex,
413        result: &mut Vec<String>,
414        visited: &mut BTreeSet<String>,
415    ) {
416        if visited.contains(package) {
417            return;
418        }
419        visited.insert(package.to_string());
420
421        let parents = index.get_parents(package);
422        if parents.is_empty() {
423            result.push(package.to_string());
424            return;
425        }
426
427        // Build the lists to merge: linearization of each parent + the parents list itself
428        let mut parent_mros: Vec<Vec<String>> = parents
429            .iter()
430            .map(|p| {
431                let mut sub_result = Vec::new();
432                let mut sub_visited = BTreeSet::new();
433                self.c3_linearize(p, index, &mut sub_result, &mut sub_visited);
434                sub_result
435            })
436            .collect();
437        // Append the direct parents list as the last list to merge
438        parent_mros.push(parents.clone());
439
440        // Prepend self
441        result.push(package.to_string());
442
443        // C3 merge
444        loop {
445            // Remove empty lists
446            parent_mros.retain(|list| !list.is_empty());
447            if parent_mros.is_empty() {
448                break;
449            }
450
451            // Find the first head that does not appear in any tail
452            let chosen = parent_mros.iter().find_map(|list| {
453                let candidate = list.first()?;
454                let in_tail =
455                    parent_mros.iter().any(|other| other.iter().skip(1).any(|n| n == candidate));
456                if in_tail { None } else { Some(candidate.clone()) }
457            });
458
459            match chosen {
460                Some(cls) => {
461                    if !result.contains(&cls) {
462                        result.push(cls.clone());
463                    }
464                    // Remove chosen from the front of all lists where it appears
465                    for list in &mut parent_mros {
466                        if list.first().is_some_and(|h| h == &cls) {
467                            list.remove(0);
468                        }
469                    }
470                }
471                None => {
472                    // Inconsistent hierarchy — fall back: take heads left-to-right
473                    for list in &parent_mros.clone() {
474                        if let Some(head) = list.first() {
475                            if !result.contains(head) {
476                                result.push(head.clone());
477                            }
478                        }
479                    }
480                    break;
481                }
482            }
483        }
484    }
485
486    /// Find subtypes (child classes) that inherit from this class
487    pub fn find_subtypes(&self, ast: &Node, item: &TypeHierarchyItem) -> Vec<TypeHierarchyItem> {
488        let index = self.build_hierarchy_index(ast);
489        let children = index.get_children(&item.name);
490
491        children
492            .into_iter()
493            .map(|name| TypeHierarchyItem {
494                name,
495                kind: TypeHierarchySymbolKind::Class,
496                uri: "file:///current".to_string(),
497                range: WireRange::default(),
498                selection_range: WireRange::default(),
499                detail: Some("Subclass".to_string()),
500                data: None,
501            })
502            .collect()
503    }
504
505    // Helper methods
506
507    fn find_node_at_offset<'a>(&self, node: &'a Node, offset: usize) -> Option<&'a Node> {
508        if offset >= node.location.start && offset < node.location.end {
509            // First check children
510            if let Some(children) = self.get_children(node) {
511                for child in children {
512                    if let Some(found) = self.find_node_at_offset(child, offset) {
513                        return Some(found);
514                    }
515                }
516            }
517            // Return this node if no child contains the offset
518            Some(node)
519        } else {
520            None
521        }
522    }
523
524    fn get_children<'a>(&self, node: &'a Node) -> Option<Vec<&'a Node>> {
525        match &node.kind {
526            NodeKind::Program { statements } => Some(statements.iter().collect()),
527            NodeKind::Block { statements } => Some(statements.iter().collect()),
528            NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
529                let mut children = vec![condition.as_ref(), then_branch.as_ref()];
530                for branch in elsif_branches {
531                    children.push(&branch.0);
532                    children.push(&branch.1);
533                }
534                if let Some(else_b) = else_branch {
535                    children.push(else_b.as_ref());
536                }
537                Some(children)
538            }
539            NodeKind::Package { block, .. } => block.as_ref().map(|b| vec![b.as_ref()]),
540            NodeKind::Class { body, .. } => Some(vec![body.as_ref()]),
541            NodeKind::Subroutine { body, .. } => Some(vec![body.as_ref()]),
542            NodeKind::Assignment { lhs, rhs, .. } => Some(vec![lhs.as_ref(), rhs.as_ref()]),
543            NodeKind::ExpressionStatement { expression } => Some(vec![expression.as_ref()]),
544            _ => None,
545        }
546    }
547
548    fn is_package_identifier(&self, _ast: &Node, _offset: usize, _name: &str) -> bool {
549        // Check if this identifier appears in a context that suggests it's a package
550        // For now, we'll return false as we need to match against strings not identifiers
551        false
552    }
553
554    fn create_type_item(
555        &self,
556        name: &str,
557        node: &Node,
558        position_mapper: &PositionMapper,
559        kind: TypeHierarchySymbolKind,
560    ) -> TypeHierarchyItem {
561        TypeHierarchyItem {
562            name: name.to_string(),
563            kind,
564            uri: "file:///current".to_string(),
565            range: self.node_to_range(node, position_mapper),
566            selection_range: self.node_to_range(node, position_mapper),
567            detail: Some(format!(
568                "Perl {}",
569                match kind {
570                    TypeHierarchySymbolKind::Class => "Package",
571                    TypeHierarchySymbolKind::Method => "Method",
572                    TypeHierarchySymbolKind::Function => "Function",
573                }
574            )),
575            data: None,
576        }
577    }
578
579    /// Convert node to LSP range using PositionMapper for UTF-16 compliance
580    fn node_to_range(&self, node: &Node, position_mapper: &PositionMapper) -> WireRange {
581        let start_pos = self.offset_to_position(node.location.start, position_mapper);
582        let end_pos = self.offset_to_position(node.location.end, position_mapper);
583        WireRange {
584            start: WirePosition { line: start_pos.0, character: start_pos.1 },
585            end: WirePosition { line: end_pos.0, character: end_pos.1 },
586        }
587    }
588
589    /// Convert byte offset to line/character position using PositionMapper for UTF-16 compliance
590    fn offset_to_position(&self, offset: usize, position_mapper: &PositionMapper) -> (u32, u32) {
591        let pos = position_mapper.byte_to_lsp_pos(offset);
592        (pos.line, pos.character)
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use perl_parser_core::parser::Parser;
600    use perl_tdd_support::{must, must_some};
601
602    #[test]
603    fn test_type_hierarchy_for_package() {
604        let code = r#"package MyClass;
605use parent 'BaseClass';
606
607sub new {
608    my $class = shift;
609    return bless {}, $class;
610}
611"#;
612        let mut parser = Parser::new(code);
613        let ast = must(parser.parse());
614        let provider = TypeHierarchyProvider::new();
615
616        // Position on "MyClass" (package starts at position 0)
617        let items = provider.prepare(&ast, code, 8);
618        assert!(items.is_some());
619        let items = must_some(items);
620        assert_eq!(items.len(), 1);
621        assert_eq!(items[0].name, "MyClass");
622
623        // Find supertypes
624        let supertypes = provider.find_supertypes(&ast, &items[0]);
625        assert_eq!(supertypes.len(), 1);
626        assert_eq!(supertypes[0].name, "BaseClass");
627    }
628
629    #[test]
630    fn test_type_hierarchy_with_isa() {
631        let code = r#"package Child;
632our @ISA = qw(Parent1 Parent2);
633"#;
634        let mut parser = Parser::new(code);
635        let ast = must(parser.parse());
636        let provider = TypeHierarchyProvider::new();
637
638        // Position on "Child"
639        let items = provider.prepare(&ast, code, 8);
640        assert!(items.is_some());
641        let items = must_some(items);
642        assert_eq!(items[0].name, "Child");
643
644        // Find supertypes - qw() parsing needs AST improvements
645        let supertypes = provider.find_supertypes(&ast, &items[0]);
646        // Just verify it doesn't panic for now
647        let _ = supertypes.len();
648    }
649
650    #[test]
651    fn test_find_subtypes() {
652        let code = r#"package Base;
653
654package Derived1;
655use parent 'Base';
656
657package Derived2;
658our @ISA = ('Base');
659
660package Unrelated;
661use parent 'Other';
662"#;
663        let mut parser = Parser::new(code);
664        let ast = must(parser.parse());
665        let provider = TypeHierarchyProvider::new();
666
667        // Create a Base item
668        let base_item = TypeHierarchyItem {
669            name: "Base".to_string(),
670            kind: TypeHierarchySymbolKind::Class,
671            uri: "file:///test".to_string(),
672            range: WireRange::default(),
673            selection_range: WireRange::default(),
674            detail: None,
675            data: None,
676        };
677
678        // Find subtypes
679        let subtypes = provider.find_subtypes(&ast, &base_item);
680        assert_eq!(subtypes.len(), 2, "Should find exactly 2 subtypes");
681
682        let subtype_names: Vec<String> = subtypes.iter().map(|t| t.name.clone()).collect();
683        assert!(subtype_names.contains(&"Derived1".to_string()), "Should find Derived1");
684        assert!(subtype_names.contains(&"Derived2".to_string()), "Should find Derived2");
685        assert!(!subtype_names.contains(&"Unrelated".to_string()), "Should not find Unrelated");
686    }
687
688    #[test]
689    fn test_qw_parsing() {
690        let code = r#"package Multi;
691our @ISA = qw(Parent1 Parent2 Parent3);
692"#;
693        let mut parser = Parser::new(code);
694        let ast = must(parser.parse());
695        let provider = TypeHierarchyProvider::new();
696
697        let items = provider.prepare(&ast, code, 8);
698        assert!(items.is_some());
699        let items = must_some(items);
700        assert_eq!(items[0].name, "Multi");
701
702        // Find supertypes - should handle qw() properly
703        let supertypes = provider.find_supertypes(&ast, &items[0]);
704        // For now just check it doesn't panic - full qw() support needs AST improvements
705        let _ = supertypes.len();
706    }
707
708    #[test]
709    fn test_moose_extends_single_parent() {
710        let code = r#"package Animal;
711
712package Dog;
713use Moose;
714extends 'Animal';
715"#;
716        let mut parser = Parser::new(code);
717        let ast = must(parser.parse());
718        let provider = TypeHierarchyProvider::new();
719
720        let dog_item = TypeHierarchyItem {
721            name: "Dog".to_string(),
722            kind: TypeHierarchySymbolKind::Class,
723            uri: "file:///test".to_string(),
724            range: WireRange::default(),
725            selection_range: WireRange::default(),
726            detail: None,
727            data: None,
728        };
729
730        let supertypes = provider.find_supertypes(&ast, &dog_item);
731        assert_eq!(supertypes.len(), 1, "Should find 1 parent via extends");
732        assert_eq!(supertypes[0].name, "Animal");
733        assert_eq!(
734            supertypes[0].detail.as_deref(),
735            Some("Parent Class"),
736            "extends parent should have 'Parent Class' detail"
737        );
738    }
739
740    #[test]
741    fn test_moose_extends_multiple_parents() {
742        let code = r#"package Readable;
743package Writable;
744
745package ReadWriteFile;
746use Moose;
747extends 'Readable', 'Writable';
748"#;
749        let mut parser = Parser::new(code);
750        let ast = must(parser.parse());
751        let provider = TypeHierarchyProvider::new();
752
753        let item = TypeHierarchyItem {
754            name: "ReadWriteFile".to_string(),
755            kind: TypeHierarchySymbolKind::Class,
756            uri: "file:///test".to_string(),
757            range: WireRange::default(),
758            selection_range: WireRange::default(),
759            detail: None,
760            data: None,
761        };
762
763        let supertypes = provider.find_supertypes(&ast, &item);
764        let names: Vec<&str> = supertypes.iter().map(|s| s.name.as_str()).collect();
765        assert!(names.contains(&"Readable"), "Should find Readable parent");
766        assert!(names.contains(&"Writable"), "Should find Writable parent");
767    }
768
769    #[test]
770    fn test_moose_with_role() {
771        let code = r#"package Printable;
772
773package Document;
774use Moose;
775with 'Printable';
776"#;
777        let mut parser = Parser::new(code);
778        let ast = must(parser.parse());
779        let provider = TypeHierarchyProvider::new();
780
781        let item = TypeHierarchyItem {
782            name: "Document".to_string(),
783            kind: TypeHierarchySymbolKind::Class,
784            uri: "file:///test".to_string(),
785            range: WireRange::default(),
786            selection_range: WireRange::default(),
787            detail: None,
788            data: None,
789        };
790
791        let supertypes = provider.find_supertypes(&ast, &item);
792        let role_types: Vec<&TypeHierarchyItem> =
793            supertypes.iter().filter(|s| s.detail.as_deref() == Some("Role")).collect();
794        assert_eq!(role_types.len(), 1, "Should find 1 role via with");
795        assert_eq!(role_types[0].name, "Printable");
796    }
797
798    #[test]
799    fn test_moose_with_multiple_roles() {
800        let code = r#"package Serializable;
801package Printable;
802
803package Report;
804use Moose;
805with 'Serializable', 'Printable';
806"#;
807        let mut parser = Parser::new(code);
808        let ast = must(parser.parse());
809        let provider = TypeHierarchyProvider::new();
810
811        let item = TypeHierarchyItem {
812            name: "Report".to_string(),
813            kind: TypeHierarchySymbolKind::Class,
814            uri: "file:///test".to_string(),
815            range: WireRange::default(),
816            selection_range: WireRange::default(),
817            detail: None,
818            data: None,
819        };
820
821        let supertypes = provider.find_supertypes(&ast, &item);
822        let role_names: Vec<&str> = supertypes
823            .iter()
824            .filter(|s| s.detail.as_deref() == Some("Role"))
825            .map(|s| s.name.as_str())
826            .collect();
827        assert!(role_names.contains(&"Serializable"), "Should find Serializable role");
828        assert!(role_names.contains(&"Printable"), "Should find Printable role");
829    }
830
831    #[test]
832    fn test_moose_extends_and_with_combined() {
833        let code = r#"package Base;
834package MyRole;
835
836package Child;
837use Moose;
838extends 'Base';
839with 'MyRole';
840"#;
841        let mut parser = Parser::new(code);
842        let ast = must(parser.parse());
843        let provider = TypeHierarchyProvider::new();
844
845        let item = TypeHierarchyItem {
846            name: "Child".to_string(),
847            kind: TypeHierarchySymbolKind::Class,
848            uri: "file:///test".to_string(),
849            range: WireRange::default(),
850            selection_range: WireRange::default(),
851            detail: None,
852            data: None,
853        };
854
855        let supertypes = provider.find_supertypes(&ast, &item);
856
857        let parent_names: Vec<&str> = supertypes
858            .iter()
859            .filter(|s| s.detail.as_deref() == Some("Parent Class"))
860            .map(|s| s.name.as_str())
861            .collect();
862        let role_names: Vec<&str> = supertypes
863            .iter()
864            .filter(|s| s.detail.as_deref() == Some("Role"))
865            .map(|s| s.name.as_str())
866            .collect();
867
868        assert_eq!(parent_names, vec!["Base"], "Should find Base as parent");
869        assert_eq!(role_names, vec!["MyRole"], "Should find MyRole as role");
870    }
871
872    #[test]
873    fn test_moo_extends_with() {
874        let code = r#"package MooParent;
875package MooRole;
876
877package MooChild;
878use Moo;
879extends 'MooParent';
880with 'MooRole';
881"#;
882        let mut parser = Parser::new(code);
883        let ast = must(parser.parse());
884        let provider = TypeHierarchyProvider::new();
885
886        let item = TypeHierarchyItem {
887            name: "MooChild".to_string(),
888            kind: TypeHierarchySymbolKind::Class,
889            uri: "file:///test".to_string(),
890            range: WireRange::default(),
891            selection_range: WireRange::default(),
892            detail: None,
893            data: None,
894        };
895
896        let supertypes = provider.find_supertypes(&ast, &item);
897        let names: Vec<&str> = supertypes.iter().map(|s| s.name.as_str()).collect();
898        assert!(names.contains(&"MooParent"), "Moo extends should work");
899        assert!(names.contains(&"MooRole"), "Moo with should work");
900    }
901
902    #[test]
903    fn test_mixed_use_parent_and_extends() {
904        let code = r#"package OldBase;
905package MooseBase;
906
907package Mixed;
908use parent 'OldBase';
909use Moose;
910extends 'MooseBase';
911"#;
912        let mut parser = Parser::new(code);
913        let ast = must(parser.parse());
914        let provider = TypeHierarchyProvider::new();
915
916        let item = TypeHierarchyItem {
917            name: "Mixed".to_string(),
918            kind: TypeHierarchySymbolKind::Class,
919            uri: "file:///test".to_string(),
920            range: WireRange::default(),
921            selection_range: WireRange::default(),
922            detail: None,
923            data: None,
924        };
925
926        let supertypes = provider.find_supertypes(&ast, &item);
927        let parent_names: Vec<&str> = supertypes
928            .iter()
929            .filter(|s| s.detail.as_deref() == Some("Parent Class"))
930            .map(|s| s.name.as_str())
931            .collect();
932        assert!(parent_names.contains(&"OldBase"), "use parent should still work");
933        assert!(parent_names.contains(&"MooseBase"), "extends should also work");
934    }
935
936    #[test]
937    fn test_extends_subtypes_reverse() {
938        // Verify that extends also populates the children (subtypes) direction
939        let code = r#"package Animal;
940
941package Dog;
942use Moose;
943extends 'Animal';
944
945package Cat;
946use Moo;
947extends 'Animal';
948"#;
949        let mut parser = Parser::new(code);
950        let ast = must(parser.parse());
951        let provider = TypeHierarchyProvider::new();
952
953        let animal_item = TypeHierarchyItem {
954            name: "Animal".to_string(),
955            kind: TypeHierarchySymbolKind::Class,
956            uri: "file:///test".to_string(),
957            range: WireRange::default(),
958            selection_range: WireRange::default(),
959            detail: None,
960            data: None,
961        };
962
963        let subtypes = provider.find_subtypes(&ast, &animal_item);
964        let subtype_names: Vec<&str> = subtypes.iter().map(|s| s.name.as_str()).collect();
965        assert!(subtype_names.contains(&"Dog"), "Dog should be a subtype of Animal");
966        assert!(subtype_names.contains(&"Cat"), "Cat should be a subtype of Animal");
967    }
968
969    #[test]
970    fn test_block_form_packages() {
971        let code = r#"package Outer {
972    package Inner;
973    use parent 'Outer';
974}
975package Other;
976use parent 'Outer';
977"#;
978        let mut parser = Parser::new(code);
979        let ast = must(parser.parse());
980        let provider = TypeHierarchyProvider::new();
981
982        let outer_item = TypeHierarchyItem {
983            name: "Outer".to_string(),
984            kind: TypeHierarchySymbolKind::Class,
985            uri: "file:///test".to_string(),
986            range: WireRange::default(),
987            selection_range: WireRange::default(),
988            detail: None,
989            data: None,
990        };
991
992        // Find subtypes - should handle block form packages
993        let subtypes = provider.find_subtypes(&ast, &outer_item);
994        // Both Inner and Other inherit from Outer
995        assert_eq!(subtypes.len(), 2, "Should find both Inner and Other as subtypes");
996    }
997}