Skip to main content

perl_lsp/features/
implementation_provider.rs

1//! Implementation provider for finding implementations of types/interfaces
2//!
3//! This provider finds:
4//! - Subclasses that inherit from a base class
5//! - Overridden methods in derived classes
6
7use crate::type_hierarchy::TypeHierarchyProvider;
8use crate::util::uri::parse_uri;
9use lsp_types::LocationLink;
10use lsp_types::{Position as LspPosition, Range as LspRange};
11use perl_parser::ast::{Node, NodeKind};
12use perl_parser::workspace_index::WorkspaceIndex;
13use std::collections::HashMap;
14
15/// Provider for finding implementations of types and interfaces in Perl code
16///
17/// This provider locates implementations and inheritance relationships in Perl codebases:
18/// - Subclasses that inherit from a base class using `@ISA` or `use parent`
19/// - Overridden methods in derived classes
20/// - Package implementations and blessed type relationships
21///
22/// # LSP Workflow Integration
23///
24/// Integrates with the Parse → Index → Navigate → Complete → Analyze workflow:
25/// - **Parse**: AST analysis identifies package and method definitions
26/// - **Index**: Workspace indexing tracks inheritance relationships
27/// - **Navigate**: Provides go-to-implementation functionality
28/// - **Complete**: No direct integration (implementations don't affect completion)
29/// - **Analyze**: Implementation analysis supports refactoring decisions
30///
31/// # Performance
32///
33/// - Implementation finding: <100ms for typical inheritance hierarchies
34/// - Memory usage: <5MB for implementation metadata
35/// - Workspace indexing: Leverages cached inheritance relationships
36pub struct ImplementationProvider {
37    workspace_index: Option<std::sync::Arc<WorkspaceIndex>>,
38}
39
40impl ImplementationProvider {
41    /// Create a new implementation provider with optional workspace indexing
42    ///
43    /// # Arguments
44    ///
45    /// * `workspace_index` - Optional workspace index for cross-file inheritance tracking
46    ///
47    /// # Returns
48    ///
49    /// A new [`ImplementationProvider`] configured for finding Perl implementations
50    ///
51    /// # Examples
52    ///
53    /// ```rust,ignore
54    /// use perl_lsp_rs_core::providers::navigation::ImplementationProvider;
55    ///
56    /// // Without workspace indexing (single-file analysis)
57    /// let provider = ImplementationProvider::new(None);
58    ///
59    /// // With workspace indexing (cross-file inheritance)
60    /// # use std::sync::Arc;
61    /// # use perl_parser::workspace_index::WorkspaceIndex;
62    /// # let workspace_index = Arc::new(WorkspaceIndex::new());
63    /// let provider = ImplementationProvider::new(Some(workspace_index));
64    /// ```
65    pub fn new(workspace_index: Option<std::sync::Arc<WorkspaceIndex>>) -> Self {
66        Self { workspace_index }
67    }
68
69    /// Find implementations at the given position
70    pub fn find_implementations(
71        &self,
72        ast: &Node,
73        line: u32,
74        character: u32,
75        uri: &str,
76        documents: &HashMap<String, String>,
77    ) -> Vec<LocationLink> {
78        // Find the node at position
79        let source = documents.get(uri);
80        let target_node = match self.find_node_at_position(ast, line, character, source) {
81            Some(node) => node,
82            None => return Vec::new(),
83        };
84
85        // Compute the enclosing package for the cursor position (Fix B).
86        // This is needed when the cursor lands on a Subroutine node so that
87        // `extract_implementation_target` can use the real package instead of "main".
88        let byte_offset = source
89            .map(|src| crate::position::utf16_line_col_to_offset(src, line, character))
90            .unwrap_or(0);
91        let current_package = crate::declaration::current_package_at(ast, byte_offset);
92
93        // Extract what we're looking for implementations of
94        match self.extract_implementation_target(&target_node, current_package) {
95            Some(ImplementationTarget::Package(name)) => {
96                self.find_package_implementations(&name, documents)
97            }
98            Some(ImplementationTarget::Method { package, method }) => {
99                self.find_method_implementations(&package, &method, documents)
100            }
101            Some(ImplementationTarget::BlessedType(name)) => {
102                // For blessed types, find package implementations
103                self.find_package_implementations(&name, documents)
104            }
105            None => Vec::new(),
106        }
107    }
108
109    /// Find all implementations of a package (subclasses).
110    fn find_package_implementations(
111        &self,
112        base_package: &str,
113        documents: &HashMap<String, String>,
114    ) -> Vec<LocationLink> {
115        self.find_subclass_locations(base_package, documents)
116            .into_iter()
117            .map(|(_name, link)| link)
118            .collect()
119    }
120
121    /// Find subclasses with their package names, for use in method resolution.
122    ///
123    /// Returns `(subclass_package_name, LocationLink_pointing_to_package_declaration)`.
124    fn find_subclass_locations(
125        &self,
126        base_package: &str,
127        documents: &HashMap<String, String>,
128    ) -> Vec<(String, LocationLink)> {
129        let mut results: Vec<(String, LocationLink)> = Vec::new();
130
131        let _hierarchy_provider = TypeHierarchyProvider::new();
132
133        for (uri, content) in documents {
134            if let Ok(ast) = crate::Parser::new(content).parse() {
135                self.find_inheriting_packages_named(&ast, base_package, uri, content, &mut results);
136            }
137        }
138
139        // If we have workspace index, use it for more comprehensive results
140        if let Some(ref index) = self.workspace_index {
141            let symbols = index.find_symbols(base_package);
142            for symbol in symbols {
143                if symbol.kind == crate::workspace_index::SymbolKind::Class
144                    || symbol.kind == crate::workspace_index::SymbolKind::Package
145                {
146                    if let Some(container) = &symbol.container_name {
147                        if container.contains(base_package) {
148                            let target_uri = parse_uri(&symbol.uri);
149                            // Convert internal Position to LSP Position (LSP uses 0-based, internal uses 1-based)
150                            let lsp_start = LspPosition::new(
151                                symbol.range.start.line - 1,
152                                symbol.range.start.column - 1,
153                            );
154                            let lsp_end = LspPosition::new(
155                                symbol.range.end.line - 1,
156                                symbol.range.end.column - 1,
157                            );
158                            results.push((
159                                symbol.name.clone(),
160                                LocationLink {
161                                    origin_selection_range: None,
162                                    target_uri,
163                                    target_range: LspRange::new(lsp_start, lsp_end),
164                                    target_selection_range: LspRange::new(lsp_start, lsp_end),
165                                },
166                            ));
167                        }
168                    }
169                }
170            }
171        }
172
173        results
174    }
175
176    /// Find method implementations (overrides) in subclasses
177    fn find_method_implementations(
178        &self,
179        package: &str,
180        method: &str,
181        documents: &HashMap<String, String>,
182    ) -> Vec<LocationLink> {
183        let mut results = Vec::new();
184
185        // First find all subclasses (with their package names for scoped method lookup)
186        let subclasses = self.find_subclass_locations(package, documents);
187
188        // Then find the method in each subclass, restricted to the subclass package scope
189        for (subclass_name, subclass_link) in &subclasses {
190            if let Some(doc_content) = documents.get(subclass_link.target_uri.as_str()) {
191                if let Ok(ast) = crate::Parser::new(doc_content).parse() {
192                    self.find_method_in_package(
193                        &ast,
194                        method,
195                        subclass_name,
196                        subclass_link.target_uri.as_str(),
197                        doc_content,
198                        &mut results,
199                    );
200                }
201            }
202        }
203
204        results
205    }
206
207    /// Find packages that inherit from base_package in AST, returning named pairs.
208    fn find_inheriting_packages_named(
209        &self,
210        node: &Node,
211        base_package: &str,
212        uri: &str,
213        source: &str,
214        results: &mut Vec<(String, LocationLink)>,
215    ) {
216        let mut current_package = String::new();
217        // Track the package node's range so results point to `package Foo;` not `use parent` (Fix C).
218        let mut current_package_range = LspRange::default();
219        self.find_inheriting_packages_recursive(
220            node,
221            base_package,
222            uri,
223            source,
224            &mut current_package,
225            &mut current_package_range,
226            results,
227        );
228    }
229
230    fn find_inheriting_packages_recursive(
231        &self,
232        node: &Node,
233        base_package: &str,
234        uri: &str,
235        source: &str,
236        current_package: &mut String,
237        current_package_range: &mut LspRange,
238        results: &mut Vec<(String, LocationLink)>,
239    ) {
240        match &node.kind {
241            NodeKind::Package { name, .. } => {
242                *current_package = name.clone();
243                // Record this package node's range for use when we later find `use parent` (Fix C).
244                *current_package_range = self.node_to_range(node, source);
245            }
246            NodeKind::Use { module, args, .. } if module == "base" || module == "parent" => {
247                // Check if any arg matches base_package.
248                // Args are raw token texts (e.g., `'Animal'` with quotes), so strip them first.
249                for arg in args {
250                    let normalized = Self::strip_arg_quotes(arg);
251                    if normalized == base_package {
252                        // Point at the enclosing package declaration, not the `use parent` line (Fix C).
253                        let pkg_range = *current_package_range;
254                        let target_uri = parse_uri(uri);
255                        results.push((
256                            current_package.clone(),
257                            LocationLink {
258                                origin_selection_range: None,
259                                target_uri,
260                                target_range: pkg_range,
261                                target_selection_range: pkg_range,
262                            },
263                        ));
264                    }
265                }
266            }
267            NodeKind::VariableDeclaration { declarator, variable, initializer, .. } => {
268                if declarator == "our" {
269                    if let NodeKind::Variable { sigil, name } = &variable.kind {
270                        if sigil == "@" && name == "ISA" {
271                            if let Some(init) = initializer {
272                                if self.contains_parent(init, base_package) {
273                                    let pkg_range = *current_package_range;
274                                    let target_uri = parse_uri(uri);
275                                    results.push((
276                                        current_package.clone(),
277                                        LocationLink {
278                                            origin_selection_range: None,
279                                            target_uri,
280                                            target_range: pkg_range,
281                                            target_selection_range: pkg_range,
282                                        },
283                                    ));
284                                }
285                            }
286                        }
287                    }
288                }
289            }
290            NodeKind::Program { statements } | NodeKind::Block { statements } => {
291                for stmt in statements {
292                    self.find_inheriting_packages_recursive(
293                        stmt,
294                        base_package,
295                        uri,
296                        source,
297                        current_package,
298                        current_package_range,
299                        results,
300                    );
301                }
302            }
303            _ => {}
304        }
305    }
306
307    /// Find method definitions in AST (any package). Kept for future workspace-index integration.
308    #[allow(dead_code)]
309    fn find_method_in_ast(
310        &self,
311        node: &Node,
312        method_name: &str,
313        uri: &str,
314        source: &str,
315        results: &mut Vec<LocationLink>,
316    ) {
317        match &node.kind {
318            NodeKind::Subroutine { name: Some(name), .. } => {
319                let (_, name_bare) = perl_parser::qualified_name::split_qualified_name(name);
320                let (_, method_bare) =
321                    perl_parser::qualified_name::split_qualified_name(method_name);
322                if name_bare == method_bare {
323                    let target_uri = parse_uri(uri);
324                    results.push(LocationLink {
325                        origin_selection_range: None,
326                        target_uri,
327                        target_range: self.node_to_range(node, source),
328                        target_selection_range: self.node_to_range(node, source),
329                    });
330                }
331            }
332            NodeKind::Program { statements } | NodeKind::Block { statements } => {
333                for stmt in statements {
334                    self.find_method_in_ast(stmt, method_name, uri, source, results);
335                }
336            }
337            _ => {}
338        }
339    }
340
341    /// Find a method defined within a specific package in the AST.
342    ///
343    /// Only emits a result when `method_name` appears after `package_name;` in
344    /// linear-form source (before the next package declaration), or inside a
345    /// block-form package (`package Name { ... }`). This prevents cross-package
346    /// false positives when all subclasses live in the same file.
347    fn find_method_in_package(
348        &self,
349        node: &Node,
350        method_name: &str,
351        package_name: &str,
352        uri: &str,
353        source: &str,
354        results: &mut Vec<LocationLink>,
355    ) {
356        let mut current_package: Option<String> = None;
357        self.find_method_in_package_with_scope(
358            node,
359            method_name,
360            package_name,
361            uri,
362            source,
363            &mut current_package,
364            results,
365        );
366    }
367
368    fn find_method_in_package_with_scope(
369        &self,
370        node: &Node,
371        method_name: &str,
372        package_name: &str,
373        uri: &str,
374        source: &str,
375        current_package: &mut Option<String>,
376        results: &mut Vec<LocationLink>,
377    ) {
378        if let NodeKind::Program { statements } | NodeKind::Block { statements } = &node.kind {
379            for stmt in statements {
380                match &stmt.kind {
381                    NodeKind::Package { name, block: Some(inner), .. } => {
382                        let previous_package = current_package.clone();
383                        *current_package = Some(name.clone());
384                        self.find_method_in_package_with_scope(
385                            inner,
386                            method_name,
387                            package_name,
388                            uri,
389                            source,
390                            current_package,
391                            results,
392                        );
393                        *current_package = previous_package;
394                    }
395                    NodeKind::Package { name, .. } => {
396                        *current_package = Some(name.clone());
397                    }
398                    NodeKind::Subroutine { name: Some(sub_name), .. } => {
399                        if current_package.as_deref() == Some(package_name)
400                            && *sub_name == method_name
401                        {
402                            let target_uri = parse_uri(uri);
403                            results.push(LocationLink {
404                                origin_selection_range: None,
405                                target_uri,
406                                target_range: self.node_to_range(stmt, source),
407                                target_selection_range: self.node_to_range(stmt, source),
408                            });
409                        }
410                    }
411                    _ => {
412                        self.find_method_in_package_with_scope(
413                            stmt,
414                            method_name,
415                            package_name,
416                            uri,
417                            source,
418                            current_package,
419                            results,
420                        );
421                    }
422                }
423            }
424            return;
425        }
426
427        if let NodeKind::Package { name, block: Some(inner), .. } = &node.kind {
428            let previous_package = current_package.clone();
429            *current_package = Some(name.clone());
430            self.find_method_in_package_with_scope(
431                inner,
432                method_name,
433                package_name,
434                uri,
435                source,
436                current_package,
437                results,
438            );
439            *current_package = previous_package;
440        }
441    }
442
443    /// Extract implementation target from node.
444    ///
445    /// `current_package` is the package name enclosing the cursor position,
446    /// determined by `crate::declaration::current_package_at`. It replaces the
447    /// previous hardcoded `"main"` for `Subroutine` nodes (Fix B).
448    fn extract_implementation_target(
449        &self,
450        node: &Node,
451        current_package: &str,
452    ) -> Option<ImplementationTarget> {
453        match &node.kind {
454            NodeKind::Package { name, .. } => Some(ImplementationTarget::Package(name.clone())),
455            NodeKind::Subroutine { name: Some(method), .. } => Some(ImplementationTarget::Method {
456                package: current_package.to_string(),
457                method: method.clone(),
458            }),
459            NodeKind::Identifier { name } if name.contains("::") => {
460                let parts: Vec<&str> = name.split("::").collect();
461                if parts.len() == 2 {
462                    Some(ImplementationTarget::Method {
463                        package: parts[0].to_string(),
464                        method: parts[1].to_string(),
465                    })
466                } else if parts.len() > 2 {
467                    Some(ImplementationTarget::Package(parts[..parts.len() - 1].join("::")))
468                } else {
469                    None
470                }
471            }
472            _ => None,
473        }
474    }
475
476    /// Find node at position
477    fn find_node_at_position(
478        &self,
479        node: &Node,
480        line: u32,
481        character: u32,
482        source: Option<&String>,
483    ) -> Option<Node> {
484        if let Some(src) = source {
485            let (start_line, start_col) =
486                crate::position::offset_to_utf16_line_col(src, node.location.start);
487            let (end_line, end_col) =
488                crate::position::offset_to_utf16_line_col(src, node.location.end);
489
490            if line >= start_line && line <= end_line {
491                if (line == start_line && character >= start_col)
492                    || (line == end_line && character <= end_col)
493                    || (line > start_line && line < end_line)
494                {
495                    // Check children first for more specific match
496                    match &node.kind {
497                        NodeKind::Program { statements } | NodeKind::Block { statements } => {
498                            for stmt in statements {
499                                if let Some(child) =
500                                    self.find_node_at_position(stmt, line, character, source)
501                                {
502                                    return Some(child);
503                                }
504                            }
505                        }
506                        _ => {}
507                    }
508                    return Some(node.clone());
509                }
510            }
511        }
512        None
513    }
514
515    /// Convert node to LSP range
516    fn node_to_range(&self, node: &Node, source: &str) -> LspRange {
517        let (start_line, start_col) =
518            crate::position::offset_to_utf16_line_col(source, node.location.start);
519        let (end_line, end_col) =
520            crate::position::offset_to_utf16_line_col(source, node.location.end);
521
522        LspRange {
523            start: LspPosition::new(start_line, start_col),
524            end: LspPosition::new(end_line, end_col),
525        }
526    }
527
528    /// Extract parent name from use statement argument (not needed anymore)
529    fn _extract_parent_name(&self, node: &Node) -> Option<String> {
530        match &node.kind {
531            NodeKind::String { value, .. } => Some(value.clone()),
532            NodeKind::Identifier { name } => Some(name.clone()),
533            _ => None,
534        }
535    }
536
537    /// Check if initializer contains parent
538    fn contains_parent(&self, node: &Node, parent: &str) -> bool {
539        match &node.kind {
540            NodeKind::String { value, .. } => value == parent,
541            NodeKind::ArrayLiteral { elements, .. } => {
542                elements.iter().any(|e| self.contains_parent(e, parent))
543            }
544            _ => false,
545        }
546    }
547
548    /// Strip surrounding quotes from a raw `use parent`/`use base` argument token.
549    ///
550    /// The parser stores args as raw token text (e.g., `'Animal'` or `"Animal"`).
551    /// This removes a single layer of matching `'` or `"` delimiters.
552    fn strip_arg_quotes(raw: &str) -> &str {
553        let s = raw.trim();
554        if s.len() >= 2 {
555            if (s.starts_with('\'') && s.ends_with('\''))
556                || (s.starts_with('"') && s.ends_with('"'))
557            {
558                return &s[1..s.len() - 1];
559            }
560        }
561        s
562    }
563}
564
565enum ImplementationTarget {
566    Package(String),
567    Method {
568        package: String,
569        method: String,
570    },
571    #[allow(dead_code)]
572    BlessedType(String),
573}