Skip to main content

perl_lsp_navigation/
type_definition.rs

1//! Type definition support for Perl LSP
2//!
3//! This module provides go-to-type-definition functionality,
4//! finding the type/class definition for variables and references.
5
6#[cfg(feature = "lsp-compat")]
7use perl_parser_core::ast::{Node, NodeKind};
8
9#[cfg(feature = "lsp-compat")]
10use lsp_types::LocationLink;
11#[cfg(feature = "lsp-compat")]
12use std::collections::HashMap;
13#[cfg(feature = "lsp-compat")]
14use std::str::FromStr;
15
16/// Provides go-to-type-definition functionality for Perl code.
17///
18/// Finds and locates type/class definitions for variables and references,
19/// enabling LSP clients to navigate to the source of type definitions.
20pub struct TypeDefinitionProvider;
21
22impl TypeDefinitionProvider {
23    /// Creates a new type definition provider instance.
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// Find type definition for a position in the AST
29    #[cfg(feature = "lsp-compat")]
30    pub fn find_type_definition(
31        &self,
32        ast: &Node,
33        line: u32,
34        character: u32,
35        uri: &str,
36        documents: &HashMap<String, String>,
37    ) -> Option<Vec<LocationLink>> {
38        // Get source text for position conversion
39        let source_text = documents.get(uri)?;
40
41        // Find the node at the given position
42        let target_node = self.find_node_at_position(ast, line, character, source_text)?;
43
44        // Get the type name from the node
45        let type_name = self.extract_type_name(&target_node)?;
46
47        // Find the package/class definition — search all open documents
48        self.find_package_definition_in_docs(&type_name, uri, documents)
49    }
50
51    /// Find a package definition across all open documents.
52    ///
53    /// Re-parses every document in `documents` (including the current file) and
54    /// collects all locations where `package <package_name>` is declared. This
55    /// enables cross-file go-to-type-definition (Fix A).
56    #[cfg(feature = "lsp-compat")]
57    fn find_package_definition_in_docs(
58        &self,
59        package_name: &str,
60        _origin_uri: &str,
61        documents: &HashMap<String, String>,
62    ) -> Option<Vec<LocationLink>> {
63        let mut locations = Vec::new();
64
65        for (doc_uri, source_text) in documents {
66            if let Ok(ast) = perl_parser_core::Parser::new(source_text).parse() {
67                self.find_package_in_node(&ast, package_name, doc_uri, source_text, &mut locations);
68            }
69        }
70
71        if !locations.is_empty() { Some(locations) } else { None }
72    }
73
74    /// Extract type name from a node
75    #[cfg(feature = "lsp-compat")]
76    fn extract_type_name(&self, node: &Node) -> Option<String> {
77        match &node.kind {
78            // Variable declaration with type: my ClassName $var
79            NodeKind::VariableDeclaration { variable, attributes, .. } => {
80                // Check if there's a type attribute (Perl 5.20+ style)
81                // Attributes are Vec<String>
82                for attr in attributes {
83                    // Check if the attribute looks like a package name
84                    if attr.contains("::") || attr.chars().next().is_some_and(|c| c.is_uppercase())
85                    {
86                        // Type is specified as an attribute
87                        return Some(attr.clone());
88                    }
89                }
90                // For typed variables, the type might be in the variable node itself
91                if let NodeKind::Variable { name, .. } = &variable.kind {
92                    // Check if name contains a type prefix pattern
93                    if name.contains("::") {
94                        // Extract package name from qualified variable
95                        let parts: Vec<&str> = name.split("::").collect();
96                        if parts.len() >= 2 {
97                            return Some(parts[..parts.len() - 1].join("::"));
98                        }
99                    }
100                }
101                None
102            }
103            // Method call: $obj->method
104            NodeKind::MethodCall { object, .. } => {
105                // Try to infer the type of the object
106                self.infer_object_type(object)
107            }
108            // Variable reference - look for its type
109            NodeKind::Variable { .. } => {
110                // Would need to track variable types through semantic analysis
111                // For now, return None and rely on context
112                None
113            }
114            // Package identifier or Package::method
115            NodeKind::Identifier { name } => {
116                if name.contains("::") {
117                    // Qualified name like Package::method
118                    let parts: Vec<&str> = name.split("::").collect();
119                    if parts.len() >= 2 {
120                        // Get the package name (everything except the last part)
121                        return Some(parts[..parts.len() - 1].join("::"));
122                    }
123                }
124                // Check if this identifier looks like a package name (starts with uppercase)
125                if name.chars().next().is_some_and(|c| c.is_uppercase()) {
126                    // Likely a package/class name
127                    return Some(name.clone());
128                }
129                None
130            }
131            // Constructor: Package->new or new Package
132            NodeKind::Binary { op, left, right } if op == "->" => {
133                // Handle Package->new pattern
134                if let NodeKind::Identifier { name: pkg } = &left.kind {
135                    if let NodeKind::Identifier { name: method } = &right.kind
136                        && method == "new"
137                    {
138                        return Some(pkg.clone());
139                    }
140                    // Also handle Package->method where we want Package
141                    return Some(pkg.clone());
142                }
143                None
144            }
145            // Blessed reference: bless $ref, 'Package'
146            NodeKind::FunctionCall { name, args } if name == "bless" => {
147                if args.len() >= 2 {
148                    // Second argument is the package name
149                    match &args[1].kind {
150                        NodeKind::String { value, .. } => Some(value.clone()),
151                        NodeKind::Identifier { name } => Some(name.clone()),
152                        NodeKind::Variable { name, .. } => {
153                            // Handle bless {}, $class where $class holds the package name
154                            Some(name.clone())
155                        }
156                        _ => None,
157                    }
158                } else if args.len() == 1 {
159                    // bless $ref (uses caller's package)
160                    None
161                } else {
162                    None
163                }
164            }
165            // ISA check: $obj isa Package
166            NodeKind::Binary { op, right, .. } if op == "isa" => match &right.kind {
167                NodeKind::String { value, .. } => Some(value.clone()),
168                NodeKind::Identifier { name } => Some(name.clone()),
169                _ => None,
170            },
171            // Expression statement - unwrap to inner expression
172            NodeKind::ExpressionStatement { expression } => self.extract_type_name(expression),
173            _ => None,
174        }
175    }
176
177    /// Try to infer the type of an object from its declaration or assignment
178    #[cfg(feature = "lsp-compat")]
179    fn infer_object_type(&self, object: &Node) -> Option<String> {
180        match &object.kind {
181            NodeKind::Variable { name, .. } => {
182                // Would need to track variable types through analysis
183                // For now, try common patterns like $self
184                if name == "$self" || name == "$this" {
185                    // Would need to find the enclosing package
186                    None
187                } else {
188                    None
189                }
190            }
191            // Direct constructor call
192            NodeKind::FunctionCall { name, .. } if name == "new" => {
193                // The package should be in the parent context
194                None
195            }
196            _ => None,
197        }
198    }
199
200    /// Find package definition in the AST (used in unit tests).
201    #[cfg(feature = "lsp-compat")]
202    #[cfg_attr(not(test), allow(dead_code))]
203    fn find_package_definition(
204        &self,
205        ast: &Node,
206        package_name: &str,
207        uri: &str,
208        source_text: &str,
209    ) -> Option<Vec<LocationLink>> {
210        let mut locations = Vec::new();
211        self.find_package_in_node(ast, package_name, uri, source_text, &mut locations);
212
213        if !locations.is_empty() { Some(locations) } else { None }
214    }
215
216    /// Recursively find package definitions
217    #[cfg(feature = "lsp-compat")]
218    fn find_package_in_node(
219        &self,
220        node: &Node,
221        package_name: &str,
222        uri: &str,
223        source_text: &str,
224        locations: &mut Vec<LocationLink>,
225    ) {
226        match &node.kind {
227            NodeKind::Package { name, .. } if name == package_name => {
228                // Convert byte offsets to LSP range using perl-parser-core utilities
229                let (target_start_line, target_start_char) =
230                    perl_parser_core::engine::position::offset_to_utf16_line_col(
231                        source_text,
232                        node.location.start,
233                    );
234                let (target_end_line, target_end_char) =
235                    perl_parser_core::engine::position::offset_to_utf16_line_col(
236                        source_text,
237                        node.location.end,
238                    );
239
240                let target_range = lsp_types::Range {
241                    start: lsp_types::Position {
242                        line: target_start_line,
243                        character: target_start_char,
244                    },
245                    end: lsp_types::Position { line: target_end_line, character: target_end_char },
246                };
247
248                // Create typed LocationLink for better UI experience
249                // Parse URI - if invalid, skip this location
250                if let Ok(target_uri) = lsp_types::Uri::from_str(uri) {
251                    locations.push(LocationLink {
252                        origin_selection_range: None, // Could be filled with the reference range
253                        target_uri,
254                        target_range,
255                        target_selection_range: target_range,
256                    });
257                }
258            }
259            _ => {}
260        }
261
262        // Recurse into children based on node type
263        self.visit_children(node, |child| {
264            self.find_package_in_node(child, package_name, uri, source_text, locations);
265        });
266    }
267
268    /// Helper to visit children of a node
269    #[cfg(feature = "lsp-compat")]
270    fn visit_children<F>(&self, node: &Node, mut f: F)
271    where
272        F: FnMut(&Node),
273    {
274        match &node.kind {
275            NodeKind::Program { statements } | NodeKind::Block { statements } => {
276                for stmt in statements {
277                    f(stmt);
278                }
279            }
280            NodeKind::Package { block: Some(b), .. } => {
281                f(b);
282            }
283            NodeKind::VariableDeclaration { variable, initializer, .. } => {
284                f(variable);
285                if let Some(init) = initializer {
286                    f(init);
287                }
288            }
289            NodeKind::Assignment { lhs, rhs, .. } => {
290                f(lhs);
291                f(rhs);
292            }
293            NodeKind::Binary { left, right, .. } => {
294                f(left);
295                f(right);
296            }
297            NodeKind::MethodCall { object, args, .. } => {
298                f(object);
299                for arg in args {
300                    f(arg);
301                }
302            }
303            NodeKind::FunctionCall { args, .. } => {
304                for arg in args {
305                    f(arg);
306                }
307            }
308            NodeKind::Subroutine { body, .. } => {
309                f(body);
310            }
311            NodeKind::ExpressionStatement { expression } => {
312                f(expression);
313            }
314            NodeKind::If { condition, then_branch, else_branch, .. } => {
315                f(condition);
316                f(then_branch);
317                if let Some(else_b) = else_branch {
318                    f(else_b);
319                }
320            }
321            NodeKind::While { condition, body, .. } => {
322                f(condition);
323                f(body);
324            }
325            NodeKind::For { init, condition, update, body, .. } => {
326                if let Some(i) = init {
327                    f(i);
328                }
329                if let Some(c) = condition {
330                    f(c);
331                }
332                if let Some(upd) = update {
333                    f(upd);
334                }
335                f(body);
336            }
337            NodeKind::Foreach { variable, list, body, continue_block } => {
338                f(variable);
339                if let Some(cb) = continue_block {
340                    f(cb);
341                }
342                f(list);
343                f(body);
344                if let Some(cb) = continue_block {
345                    f(cb);
346                }
347            }
348            _ => {
349                // Other node types don't have children we need to traverse
350            }
351        }
352    }
353
354    /// Find node at the given position
355    #[cfg(feature = "lsp-compat")]
356    fn find_node_at_position(
357        &self,
358        node: &Node,
359        line: u32,
360        character: u32,
361        source_text: &str,
362    ) -> Option<Node> {
363        // Convert UTF-16 line/char to byte offset using perl-parser-core
364        let offset = perl_parser_core::engine::position::utf16_line_col_to_offset(
365            source_text,
366            line,
367            character,
368        );
369
370        // Find the most specific node at this offset
371        self.find_node_at_offset(node, offset)
372    }
373
374    /// Find the most specific node containing the given offset
375    #[cfg(feature = "lsp-compat")]
376    fn find_node_at_offset(&self, node: &Node, offset: usize) -> Option<Node> {
377        // Check if offset is within this node's range
378        if offset < node.location.start || offset > node.location.end {
379            return None;
380        }
381
382        // Check children first for more specific match
383        let mut best_match = None;
384        self.visit_children(node, |child| {
385            if let Some(found) = self.find_node_at_offset(child, offset) {
386                // Prefer the smallest (most specific) node
387                if best_match.is_none()
388                    || found.location.end - found.location.start
389                        < best_match
390                            .as_ref()
391                            .map_or(usize::MAX, |n: &Node| n.location.end - n.location.start)
392                {
393                    best_match = Some(found);
394                }
395            }
396        });
397
398        // If we found a child, return it; otherwise return this node
399        best_match.or_else(|| Some(node.clone()))
400    }
401}
402
403impl Default for TypeDefinitionProvider {
404    fn default() -> Self {
405        Self::new()
406    }
407}
408
409#[cfg(all(test, feature = "lsp-compat"))]
410mod tests {
411    use super::*;
412    use perl_parser_core::Parser;
413    use perl_tdd_support::{must, must_some};
414
415    #[test]
416    fn test_find_package_definition() {
417        let code = r#"
418package MyClass;
419
420sub new {
421    my $class = shift;
422    bless {}, $class;
423}
424
425package main;
426
427my $obj = MyClass->new();
428$obj->method();
429"#;
430        let mut parser = Parser::new(code);
431        let ast = must(parser.parse());
432
433        let provider = TypeDefinitionProvider::new();
434        let uri = "file:///test.pl";
435
436        // Test finding MyClass definition
437        let locations = provider.find_package_definition(&ast, "MyClass", uri, code);
438        assert!(locations.is_some());
439        let locs = must_some(locations);
440        assert_eq!(locs.len(), 1);
441    }
442
443    #[test]
444    fn test_extract_type_from_constructor() {
445        let code = "my $obj = Package::Name->new();";
446        let mut parser = Parser::new(code);
447        let _ast = must(parser.parse());
448
449        let _provider = TypeDefinitionProvider::new();
450
451        // Would need to traverse to find the right node
452        // This is a simplified test
453    }
454
455    #[test]
456    fn test_full_type_definition_flow() {
457        let code = r#"
458package MyClass;
459
460sub new {
461    my $class = shift;
462    bless {}, $class;
463}
464
465package main;
466
467my $obj = MyClass->new();
468$obj->method();
469"#;
470        let mut parser = Parser::new(code);
471        let ast = must(parser.parse());
472
473        let provider = TypeDefinitionProvider::new();
474        let uri = "file:///test.pl";
475
476        let mut documents = std::collections::HashMap::new();
477        documents.insert(uri.to_string(), code.to_string());
478
479        // Line 10 (0-indexed: 10) is "my $obj = MyClass->new();"
480        // Character position 10 should be around "MyClass"
481        let line = 10;
482        let character = 10;
483
484        let locations = provider.find_type_definition(&ast, line, character, uri, &documents);
485
486        // Debug: print what we found
487        if let Some(ref locs) = locations {
488            eprintln!("Found {} locations", locs.len());
489            for loc in locs {
490                eprintln!("Location: {:?}", loc);
491            }
492        } else {
493            eprintln!("No locations found");
494
495            // Debug: try to find what node we're getting
496            // Use perl-parser-core for offset calculation
497            let offset =
498                perl_parser_core::engine::position::utf16_line_col_to_offset(code, line, character);
499            eprintln!("Offset: {}", offset);
500            if let Some(node) = provider.find_node_at_offset(&ast, offset) {
501                eprintln!("Node kind: {:?}", node.kind);
502                if let Some(type_name) = provider.extract_type_name(&node) {
503                    eprintln!("Extracted type name: {}", type_name);
504                } else {
505                    eprintln!("Could not extract type name from node");
506                }
507            } else {
508                eprintln!("Could not find node at offset");
509            }
510        }
511
512        assert!(locations.is_some(), "Should find type definition for MyClass->new()");
513        let locs = must_some(locations);
514        assert_eq!(locs.len(), 1, "Should find exactly one definition");
515    }
516}