Skip to main content

perl_semantic_analyzer/analysis/
declaration.rs

1//! Declaration Provider for LSP
2//!
3//! Provides go-to-declaration functionality for finding where symbols are declared.
4//! Supports LocationLink for enhanced client experience.
5
6use crate::ast::{Node, NodeKind};
7use crate::symbol::is_universal_method;
8use crate::workspace_index::{SymKind, SymbolKey};
9use rustc_hash::FxHashMap;
10use std::sync::Arc;
11
12/// Parent-map from child node to parent node, stored as raw pointers.
13///
14/// # Safety Invariant
15///
16/// Every `*const Node` in this map (both keys and values) must be a pointer
17/// obtained by casting a shared reference (`&Node`) that was derived from the
18/// **same** `Arc<Node>` tree that was passed to [`DeclarationProvider::build_parent_map`].
19/// The pointed-to nodes must remain alive for the entire duration of any code
20/// that inspects the map.
21///
22/// Raw pointers are used as **hash keys only** for O(1) identity-based lookup.
23/// They are **never** dereferenced directly through this map.  Safe references
24/// are recovered via the companion `node_lookup` map
25/// (`FxHashMap<*const Node, &Node>`) that re-derives `&Node` from the live
26/// `Arc<Node>` tree at call time.
27///
28/// # Ownership and Lifetime
29///
30/// The `Arc<Node>` that backs the tree must outlive every `&ParentMap` borrow.
31/// In the LSP server this is guaranteed because both the `Arc<Node>` and the
32/// `ParentMap` are stored together in `DocumentState`, guarded by a
33/// `parking_lot::Mutex`.
34///
35/// # Thread Safety
36///
37/// `*const Node` is `!Send + !Sync`.  Consequently `ParentMap` is `!Send +
38/// !Sync` and must remain on the thread that owns the `Arc<Node>` tree.
39/// LSP request handlers satisfy this requirement because they process each
40/// request synchronously within a single thread context.
41pub type ParentMap = FxHashMap<*const Node, *const Node>;
42
43/// Provider for finding declarations in Perl source code.
44///
45/// This provider implements LSP go-to-declaration functionality with enhanced
46/// workspace navigation support. Maintains ≤1ms response time for symbol lookup
47/// operations through optimized AST traversal and parent mapping.
48///
49/// # Performance Characteristics
50/// - Declaration resolution: <500μs for typical Perl files
51/// - Memory usage: O(n) where n is AST node count
52/// - Parent map validation: Debug-only with cycle detection
53///
54/// # LSP Workflow Integration
55/// Parse → Index → Navigate → Complete → Analyze pipeline integration:
56/// 1. Parse: AST generation from Perl source
57/// 2. Index: Symbol table construction with qualified name resolution
58/// 3. Navigate: Declaration provider for go-to-definition requests
59/// 4. Complete: Symbol context for completion providers
60/// 5. Analyze: Cross-reference analysis for workspace refactoring
61pub struct DeclarationProvider<'a> {
62    /// The parsed AST for the current document
63    pub ast: Arc<Node>,
64    content: String,
65    document_uri: String,
66    parent_map: Option<&'a ParentMap>,
67    doc_version: i32,
68}
69
70/// Represents a location link from origin to target
71#[derive(Debug, Clone)]
72pub struct LocationLink {
73    /// The range of the symbol being targeted at the origin
74    pub origin_selection_range: (usize, usize),
75    /// The target URI
76    pub target_uri: String,
77    /// The full range of the target declaration
78    pub target_range: (usize, usize),
79    /// The range to select in the target (e.g., just the name)
80    pub target_selection_range: (usize, usize),
81}
82
83impl<'a> DeclarationProvider<'a> {
84    /// Creates a new declaration provider for the given AST and document.
85    ///
86    /// # Arguments
87    /// * `ast` - The parsed AST tree for declaration lookup
88    /// * `content` - The source code content for text extraction
89    /// * `document_uri` - The URI of the document being analyzed
90    ///
91    /// # Performance
92    /// - Initialization: <10μs for typical Perl files
93    /// - Memory overhead: Minimal, shares AST reference
94    ///
95    /// # Examples
96    /// ```rust,ignore
97    /// use perl_parser::declaration::DeclarationProvider;
98    /// use perl_parser::ast::Node;
99    /// use std::sync::Arc;
100    ///
101    /// let ast = Arc::new(Node::new_root());
102    /// let provider = DeclarationProvider::new(
103    ///     ast,
104    ///     "package MyPackage; sub example { }".to_string(),
105    ///     "file:///path/to/file.pl".to_string()
106    /// );
107    /// ```
108    pub fn new(ast: Arc<Node>, content: String, document_uri: String) -> Self {
109        Self {
110            ast,
111            content,
112            document_uri,
113            parent_map: None,
114            doc_version: 0, // Default to version 0 for simple use cases
115        }
116    }
117
118    /// Configures the provider with a pre-built parent map for enhanced traversal.
119    ///
120    /// The parent map enables efficient upward AST traversal for scope resolution
121    /// and context analysis. Debug builds include comprehensive validation.
122    ///
123    /// # Arguments
124    /// * `parent_map` - Mapping from child nodes to their parents
125    ///
126    /// # Performance
127    /// - Parent lookup: O(1) hash table access
128    /// - Validation overhead: Debug-only, ~100μs for large files
129    ///
130    /// # Panics
131    /// In debug builds, panics if:
132    /// - Parent map is empty for non-trivial AST
133    /// - Root node has a parent (cycle detection)
134    /// - Cycles detected in parent relationships
135    ///
136    /// # Examples
137    /// ```rust,ignore
138    /// use perl_parser::declaration::{DeclarationProvider, ParentMap};
139    /// use perl_parser::ast::Node;
140    /// use std::sync::Arc;
141    ///
142    /// let ast = Arc::new(Node::new_root());
143    /// let mut parent_map = ParentMap::default();
144    /// DeclarationProvider::build_parent_map(&ast, &mut parent_map, None);
145    ///
146    /// let provider = DeclarationProvider::new(
147    ///     ast, "content".to_string(), "uri".to_string()
148    /// ).with_parent_map(&parent_map);
149    /// ```
150    pub fn with_parent_map(mut self, parent_map: &'a ParentMap) -> Self {
151        #[cfg(debug_assertions)]
152        {
153            // If the AST has more than the root node, an empty map is suspicious.
154            // (Root has no parent, so a truly trivial AST may legitimately produce 0.)
155            debug_assert!(
156                !parent_map.is_empty(),
157                "DeclarationProvider: empty ParentMap (did you forget to rebuild after AST refresh?)"
158            );
159
160            // Root sanity check - root must have no parent
161            let root_ptr = &*self.ast as *const _;
162            debug_assert!(
163                !parent_map.contains_key(&root_ptr),
164                "Root node must have no parent in the parent map"
165            );
166
167            // Cycle detection - ensure no node is its own ancestor
168            Self::debug_assert_no_cycles(parent_map);
169        }
170        self.parent_map = Some(parent_map);
171        self
172    }
173
174    /// Sets the document version for staleness detection.
175    ///
176    /// Version tracking ensures the provider operates on current data
177    /// and prevents usage after document updates in LSP workflows.
178    ///
179    /// # Arguments
180    /// * `version` - Document version number from LSP client
181    ///
182    /// # Performance
183    /// - Version check: <1μs per operation
184    /// - Debug validation: Additional consistency checks
185    ///
186    /// # Examples
187    /// ```rust,ignore
188    /// use perl_parser::declaration::DeclarationProvider;
189    /// use perl_parser::ast::Node;
190    /// use std::sync::Arc;
191    ///
192    /// let provider = DeclarationProvider::new(
193    ///     Arc::new(Node::new_root()),
194    ///     "content".to_string(),
195    ///     "uri".to_string()
196    /// ).with_doc_version(42);
197    /// ```
198    pub fn with_doc_version(mut self, version: i32) -> Self {
199        self.doc_version = version;
200        self
201    }
202
203    /// Returns `true` if this provider is still fresh (version matches).
204    ///
205    /// In both debug and release builds: logs a warning and returns `false` on mismatch so
206    /// callers can return `None` early instead of operating on a stale AST snapshot.
207    #[inline]
208    #[track_caller]
209    fn is_fresh(&self, current_version: i32) -> bool {
210        if self.doc_version != current_version {
211            tracing::warn!(
212                provider_version = self.doc_version,
213                current_version,
214                "DeclarationProvider used after AST refresh — returning empty result"
215            );
216            return false;
217        }
218        true
219    }
220
221    /// Debug-only cycle detection for parent map
222    #[cfg(debug_assertions)]
223    fn debug_assert_no_cycles(parent_map: &ParentMap) {
224        // For each node in the map, climb up to ensure we don't hit a cycle
225        let cap = parent_map.len() + 1; // Max depth before assuming cycle
226
227        for (&child, _) in parent_map.iter() {
228            let mut current = child;
229            let mut depth = 0;
230
231            while depth < cap {
232                if let Some(&parent) = parent_map.get(&current) {
233                    current = parent;
234                    depth += 1;
235                } else {
236                    // Reached a node with no parent (root), no cycle
237                    break;
238                }
239            }
240
241            // If we exhausted the cap, we have a cycle
242            if depth >= cap {
243                tracing::warn!(
244                    depth_limit = cap,
245                    "Cycle detected in ParentMap - node is its own ancestor"
246                );
247                break;
248            }
249        }
250    }
251
252    /// Build a parent map for efficient scope walking
253    /// Builds a parent map for efficient upward AST traversal.
254    ///
255    /// Recursively traverses the AST to construct a mapping from each node
256    /// to its parent, enabling O(1) parent lookups for scope resolution.
257    ///
258    /// # Arguments
259    /// * `node` - Current node to process
260    /// * `map` - Mutable parent map to populate
261    /// * `parent` - Parent of the current node (None for root)
262    ///
263    /// # Performance
264    /// - Time complexity: O(n) where n is node count
265    /// - Space complexity: O(n) for parent pointers
266    /// - Typical build time: <100μs for 1000-node AST
267    ///
268    /// # Safety
269    /// Uses raw pointers for performance. Safe as long as AST nodes
270    /// remain valid during provider lifetime.
271    ///
272    /// # Examples
273    /// ```rust,ignore
274    /// use perl_parser::declaration::{DeclarationProvider, ParentMap};
275    /// use perl_parser::ast::Node;
276    ///
277    /// let ast = Node::new_root();
278    /// let mut parent_map = ParentMap::default();
279    /// DeclarationProvider::build_parent_map(&ast, &mut parent_map, None);
280    /// ```
281    pub fn build_parent_map(node: &Node, map: &mut ParentMap, parent: Option<*const Node>) {
282        if let Some(p) = parent {
283            // SAFETY invariant for the ParentMap:
284            //
285            // 1. `node` is a shared reference (`&Node`) obtained from a live `Arc<Node>`.
286            //    Casting it to `*const Node` produces a pointer that is valid for the
287            //    lifetime of that `Arc`.
288            //
289            // 2. `p` (the parent pointer) was obtained by the same cast in the previous
290            //    recursive frame, so it satisfies the same validity guarantee.
291            //
292            // 3. Neither pointer is **ever** dereferenced through this map.  The map stores
293            //    raw pointers purely as identity keys.  Callers that need to follow a parent
294            //    pointer back to a `&Node` must go through `build_node_lookup_map`, which
295            //    re-derives safe references from the same live `Arc<Node>` tree.
296            //
297            // 4. The caller (LSP runtime) is responsible for ensuring the `Arc<Node>` tree
298            //    remains alive for at least as long as any `&ParentMap` borrow.  In the LSP
299            //    server both the `Arc` and the `ParentMap` live inside `DocumentState`,
300            //    guarded by the same `parking_lot::Mutex`.
301            //
302            // 5. No interior mutability is introduced: `node` is not modified during
303            //    traversal.  The `ParentMap` itself is an exclusive (`&mut`) borrow during
304            //    construction and transitions to a shared borrow (`&`) afterwards.
305            map.insert(node as *const _, p);
306        }
307
308        for child in Self::get_children_static(node) {
309            // SAFETY: `child` is a child reference of `node`, both living in the same
310            // `Arc<Node>` allocation.  The same invariant from above applies.
311            Self::build_parent_map(child, map, Some(node as *const _));
312        }
313    }
314
315    /// Find the declaration of the symbol at the given position
316    pub fn find_declaration(
317        &self,
318        offset: usize,
319        current_version: i32,
320    ) -> Option<Vec<LocationLink>> {
321        // Guard against stale provider usage after AST refresh (both debug and release)
322        if !self.is_fresh(current_version) {
323            return None;
324        }
325
326        // Find the node at the cursor position
327        let node = self.find_node_at_offset(&self.ast, offset)?;
328
329        // Check what kind of node we're on
330        match &node.kind {
331            NodeKind::Variable { name, .. } => self.find_variable_declaration(node, name),
332            NodeKind::FunctionCall { name, .. } => self.find_subroutine_declaration(node, name),
333            NodeKind::MethodCall { method, object, .. } => {
334                self.find_method_declaration(node, method, object)
335            }
336            NodeKind::IndirectCall { method, object, .. } => {
337                // Handle indirect calls (e.g., "move $obj 10, 20" or "new Class")
338                self.find_method_declaration(node, method, object)
339            }
340            NodeKind::Identifier { name } => self.find_identifier_declaration(node, name),
341            NodeKind::Goto { target } => {
342                if let NodeKind::Identifier { name } = &target.kind {
343                    self.find_label_declaration(node, name)
344                        .or_else(|| self.find_subroutine_declaration(node, name))
345                } else {
346                    None
347                }
348            }
349            // Cursor on a `method` name at its declaration site — self-location.
350            // NodeKind::Method has no separate name-child node; the full Method node
351            // spans the keyword + name, so find_node_at_offset returns the Method node.
352            NodeKind::Method { name, .. } => {
353                let mut declarations = Vec::new();
354                self.collect_subroutine_declarations(&self.ast, name, &mut declarations);
355                declarations.first().map(|decl| {
356                    vec![self.create_location_link(
357                        node,
358                        decl,
359                        self.get_subroutine_name_range(decl),
360                    )]
361                })
362            }
363            // Handle string literals that are method names inside modifier calls:
364            // `before 'save' => sub { }` — cursor on 'save' navigates to sub save { }
365            NodeKind::String { value, .. } => self.find_modifier_target_declaration(node, value),
366            _ => None,
367        }
368    }
369
370    /// Find variable declaration using scope-aware lookup
371    fn find_variable_declaration(&self, usage: &Node, var_name: &str) -> Option<Vec<LocationLink>> {
372        // Walk upwards through scopes to find the nearest declaration
373        // SAFETY: `usage` is a shared reference into the `Arc<Node>` AST tree held by
374        // `DeclarationProvider<'a>`. The raw pointer is used only as a HashMap key for O(1)
375        // parent lookup and is never dereferenced directly; lookups go through `build_node_lookup_map`
376        // which re-derives safe `&Node` references from the same Arc tree.
377        let mut current_ptr: *const Node = usage as *const _;
378
379        // Build temporary parent map if not provided (for testing)
380        let temp_parent_map;
381        let parent_map = if let Some(pm) = self.parent_map {
382            pm
383        } else {
384            temp_parent_map = {
385                let mut map = FxHashMap::default();
386                Self::build_parent_map(&self.ast, &mut map, None);
387                map
388            };
389            &temp_parent_map
390        };
391        let node_lookup = self.build_node_lookup_map();
392
393        while let Some(&parent_ptr) = parent_map.get(&current_ptr) {
394            let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
395                break;
396            };
397
398            if matches!(parent.kind, NodeKind::Subroutine { .. } | NodeKind::Method { .. }) {
399                if let Some(links) =
400                    self.find_signature_parameter_declaration(parent, usage, var_name)
401                {
402                    return Some(links);
403                }
404            }
405
406            // Check siblings before this node in the current scope
407            for child in self.get_children(parent) {
408                // Stop when we reach or pass the usage node
409                if child.location.start >= usage.location.start {
410                    break;
411                }
412
413                // Check if this is a variable declaration matching our name
414                if let NodeKind::VariableDeclaration { variable, .. } = &child.kind {
415                    if let NodeKind::Variable { name, .. } = &variable.kind {
416                        if name == var_name {
417                            return Some(vec![LocationLink {
418                                origin_selection_range: (usage.location.start, usage.location.end),
419                                target_uri: self.document_uri.clone(),
420                                target_range: (child.location.start, child.location.end),
421                                target_selection_range: (
422                                    variable.location.start,
423                                    variable.location.end,
424                                ),
425                            }]);
426                        }
427                    }
428                }
429
430                // Also check variable list declarations
431                if let NodeKind::VariableListDeclaration { variables, .. } = &child.kind {
432                    for var in variables {
433                        if let NodeKind::Variable { name, .. } = &var.kind {
434                            if name == var_name {
435                                return Some(vec![LocationLink {
436                                    origin_selection_range: (
437                                        usage.location.start,
438                                        usage.location.end,
439                                    ),
440                                    target_uri: self.document_uri.clone(),
441                                    target_range: (child.location.start, child.location.end),
442                                    target_selection_range: (var.location.start, var.location.end),
443                                }]);
444                            }
445                        }
446                    }
447                }
448            }
449
450            current_ptr = parent_ptr;
451        }
452
453        None
454    }
455
456    fn find_signature_parameter_declaration(
457        &self,
458        declaration_site: &Node,
459        usage: &Node,
460        var_name: &str,
461    ) -> Option<Vec<LocationLink>> {
462        let signature = match &declaration_site.kind {
463            NodeKind::Subroutine { signature, .. } | NodeKind::Method { signature, .. } => {
464                signature.as_deref()?
465            }
466            _ => return None,
467        };
468
469        let NodeKind::Signature { parameters } = &signature.kind else {
470            return None;
471        };
472
473        for parameter in parameters {
474            let variable = match &parameter.kind {
475                NodeKind::MandatoryParameter { variable }
476                | NodeKind::OptionalParameter { variable, .. }
477                | NodeKind::SlurpyParameter { variable }
478                | NodeKind::NamedParameter { variable } => variable.as_ref(),
479                _ => continue,
480            };
481
482            let NodeKind::Variable { name, .. } = &variable.kind else {
483                continue;
484            };
485
486            if name == var_name {
487                return Some(vec![self.create_location_link(
488                    usage,
489                    parameter,
490                    (variable.location.start, variable.location.end),
491                )]);
492            }
493        }
494
495        None
496    }
497
498    /// Find subroutine declaration
499    fn find_subroutine_declaration(
500        &self,
501        node: &Node,
502        func_name: &str,
503    ) -> Option<Vec<LocationLink>> {
504        // Check if the function name is package-qualified (contains ::)
505        let (target_package, target_name) = if let Some(pos) = func_name.rfind("::") {
506            // Split into package and function name
507            let package = &func_name[..pos];
508            let name = &func_name[pos + 2..];
509            (Some(package), name)
510        } else {
511            // No package qualifier, use current package context
512            (self.find_current_package(node), func_name)
513        };
514
515        // Search for subroutines with the target name
516        let mut declarations = Vec::new();
517        self.collect_subroutine_declarations(&self.ast, target_name, &mut declarations);
518
519        // If we have a target package, find subs in that specific package
520        if let Some(pkg_name) = target_package {
521            if let Some(decl) =
522                declarations.iter().find(|d| self.find_current_package(d) == Some(pkg_name))
523            {
524                return Some(vec![self.create_location_link(
525                    node,
526                    decl,
527                    self.get_subroutine_name_range(decl),
528                )]);
529            }
530        }
531
532        // Otherwise return the first match
533        if let Some(decl) = declarations.first() {
534            return Some(vec![self.create_location_link(
535                node,
536                decl,
537                self.get_subroutine_name_range(decl),
538            )]);
539        }
540
541        None
542    }
543
544    /// Find method declaration with package resolution
545    fn find_method_declaration(
546        &self,
547        node: &Node,
548        method_name: &str,
549        object: &Node,
550    ) -> Option<Vec<LocationLink>> {
551        // Try to determine the package from the object
552        let package_name = match &object.kind {
553            NodeKind::Identifier { name } if name.chars().next()?.is_uppercase() => {
554                // Likely a package name (e.g., Foo->method)
555                Some(name.as_str())
556            }
557            _ => None,
558        };
559
560        if let Some(pkg) = package_name {
561            // Look for the method in the specific package
562            let mut declarations = Vec::new();
563            self.collect_subroutine_declarations(&self.ast, method_name, &mut declarations);
564
565            if let Some(decl) =
566                declarations.iter().find(|d| self.find_current_package(d) == Some(pkg))
567            {
568                return Some(vec![self.create_location_link(
569                    node,
570                    decl,
571                    self.get_subroutine_name_range(decl),
572                )]);
573            }
574
575            if is_universal_method(method_name)
576                && let Some(decl) =
577                    declarations.iter().find(|d| self.find_current_package(d) == Some("UNIVERSAL"))
578            {
579                return Some(vec![self.create_location_link(
580                    node,
581                    decl,
582                    self.get_subroutine_name_range(decl),
583                )]);
584            }
585        }
586
587        // Fall back to any subroutine with this name
588        self.find_subroutine_declaration(node, method_name)
589    }
590
591    /// Find declaration for an identifier
592    fn find_identifier_declaration(&self, node: &Node, name: &str) -> Option<Vec<LocationLink>> {
593        // `goto LABEL` should resolve to the statement label before considering
594        // sub/package/constant declarations.
595        if self.identifier_is_goto_target(node)
596            && let Some(links) = self.find_label_declaration(node, name)
597        {
598            return Some(links);
599        }
600
601        // Try to find as subroutine first
602        if let Some(links) = self.find_subroutine_declaration(node, name) {
603            return Some(links);
604        }
605
606        // Try to find as package
607        let packages = self.find_package_declarations(&self.ast, name);
608        if let Some(pkg) = packages.first() {
609            return Some(vec![self.create_location_link(
610                node,
611                pkg,
612                self.get_package_name_range(pkg),
613            )]);
614        }
615
616        // Try to find as constant (supporting multiple forms)
617        let constants = self.find_constant_declarations(&self.ast, name);
618        if let Some(const_decl) = constants.first() {
619            return Some(vec![self.create_location_link(
620                node,
621                const_decl,
622                self.get_constant_name_range_for(const_decl, name),
623            )]);
624        }
625
626        None
627    }
628
629    fn find_label_declaration(&self, origin: &Node, label_name: &str) -> Option<Vec<LocationLink>> {
630        let mut labels = Vec::new();
631        self.collect_label_declarations(&self.ast, label_name, &mut labels);
632        let labeled_stmt = labels.first().copied()?;
633
634        Some(vec![self.create_location_link(
635            origin,
636            labeled_stmt,
637            self.get_labeled_statement_label_range(labeled_stmt),
638        )])
639    }
640
641    fn collect_label_declarations<'b>(
642        &'b self,
643        node: &'b Node,
644        label_name: &str,
645        labels: &mut Vec<&'b Node>,
646    ) {
647        if let NodeKind::LabeledStatement { label, .. } = &node.kind
648            && label == label_name
649        {
650            labels.push(node);
651        }
652
653        for child in self.get_children(node) {
654            self.collect_label_declarations(child, label_name, labels);
655        }
656    }
657
658    fn get_labeled_statement_label_range(&self, node: &Node) -> (usize, usize) {
659        let NodeKind::LabeledStatement { label, .. } = &node.kind else {
660            return (node.location.start, node.location.end);
661        };
662
663        let start = node.location.start;
664        let end = node.location.end.min(self.content.len());
665        if start >= end {
666            return (node.location.start, node.location.end);
667        }
668
669        let text = &self.content[start..end];
670        let label_start = text.find(label).map_or(start, |idx| start + idx);
671        let label_end = label_start.saturating_add(label.len()).min(end);
672        (label_start, label_end)
673    }
674
675    fn identifier_is_goto_target(&self, node: &Node) -> bool {
676        let temp_parent_map;
677        let parent_map = if let Some(pm) = self.parent_map {
678            pm
679        } else {
680            temp_parent_map = {
681                let mut map = FxHashMap::default();
682                Self::build_parent_map(&self.ast, &mut map, None);
683                map
684            };
685            &temp_parent_map
686        };
687        let node_lookup = self.build_node_lookup_map();
688
689        let node_ptr = node as *const _;
690        let Some(parent_ptr) = parent_map.get(&node_ptr).copied() else {
691            return false;
692        };
693        let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
694            return false;
695        };
696
697        match &parent.kind {
698            NodeKind::Goto { target } => std::ptr::eq(target.as_ref(), node),
699            _ => false,
700        }
701    }
702
703    /// Find the definition of the method that a modifier string argument targets.
704    ///
705    /// When the cursor is on the string `'save'` in `before 'save' => sub { }`,
706    /// this walks up the parent map to confirm the string is the first argument
707    /// of a `before`/`after`/`around` function call, then returns the location of
708    /// `sub save { }`.
709    fn find_modifier_target_declaration(
710        &self,
711        string_node: &Node,
712        method_name: &str,
713    ) -> Option<Vec<LocationLink>> {
714        // Strip surrounding quotes from the raw token text ('save' → save, "save" → save).
715        let bare_name = method_name.trim().trim_matches('\'').trim_matches('"').trim();
716        if bare_name.is_empty() {
717            return None;
718        }
719
720        // Build parent map for upward traversal.
721        let temp_parent_map;
722        let parent_map = if let Some(pm) = self.parent_map {
723            pm
724        } else {
725            temp_parent_map = {
726                let mut map = FxHashMap::default();
727                Self::build_parent_map(&self.ast, &mut map, None);
728                map
729            };
730            &temp_parent_map
731        };
732        let node_lookup = self.build_node_lookup_map();
733
734        // Walk up: String → FunctionCall { name: "before"/"after"/"around" }
735        // The String node may be a direct child of the FunctionCall's args list,
736        // so its immediate parent should be the FunctionCall node.
737        let string_ptr: *const Node = string_node as *const _;
738        let parent_ptr = parent_map.get(&string_ptr).copied()?;
739        let parent = node_lookup.get(&parent_ptr).copied()?;
740
741        // Check direct parent is a modifier FunctionCall where the string is first arg.
742        if let NodeKind::FunctionCall { name, args } = &parent.kind {
743            if matches!(name.as_str(), "before" | "after" | "around" | "override") {
744                if args.first().map(|a| std::ptr::eq(a, string_node)).unwrap_or(false) {
745                    return self.find_subroutine_declaration(string_node, bare_name);
746                }
747            }
748        }
749
750        // The FunctionCall may be wrapped in an ExpressionStatement — check one
751        // level further up in case the parent is the statement wrapper.
752        let grandparent_ptr = parent_map.get(&parent_ptr).copied()?;
753        let grandparent = node_lookup.get(&grandparent_ptr).copied()?;
754
755        if let NodeKind::FunctionCall { name, args } = &grandparent.kind {
756            if matches!(name.as_str(), "before" | "after" | "around" | "override") {
757                if args.first().map(|a| std::ptr::eq(a, string_node)).unwrap_or(false) {
758                    return self.find_subroutine_declaration(string_node, bare_name);
759                }
760            }
761        }
762
763        None
764    }
765
766    /// Find the current package context for a node
767    fn find_current_package<'b>(&'b self, node: &Node) -> Option<&'b str> {
768        // SAFETY: `node` is a shared reference into the `Arc<Node>` AST tree held
769        // by `DeclarationProvider<'a>`.  The raw pointer is used only as a hash key
770        // to query the `parent_map`; it is never dereferenced.  Safe `&Node`
771        // references are recovered through `node_lookup`, which re-derives them
772        // from the same live `Arc<Node>` tree.
773        let mut current_ptr: *const Node = node as *const _;
774
775        // Build temporary parent map if not provided (for testing)
776        let temp_parent_map;
777        let parent_map = if let Some(pm) = self.parent_map {
778            pm
779        } else {
780            temp_parent_map = {
781                let mut map = FxHashMap::default();
782                Self::build_parent_map(&self.ast, &mut map, None);
783                map
784            };
785            &temp_parent_map
786        };
787        let node_lookup = self.build_node_lookup_map();
788
789        while let Some(&parent_ptr) = parent_map.get(&current_ptr) {
790            let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
791                break;
792            };
793
794            // Check siblings before this node for package declarations
795            for child in self.get_children(parent) {
796                if child.location.start >= node.location.start {
797                    break;
798                }
799
800                if let NodeKind::Package { name, .. } = &child.kind {
801                    return Some(name.as_str());
802                }
803            }
804
805            current_ptr = parent_ptr;
806        }
807
808        None
809    }
810
811    /// Create a location link
812    fn create_location_link(
813        &self,
814        origin: &Node,
815        target: &Node,
816        name_range: (usize, usize),
817    ) -> LocationLink {
818        LocationLink {
819            origin_selection_range: (origin.location.start, origin.location.end),
820            target_uri: self.document_uri.clone(),
821            target_range: (target.location.start, target.location.end),
822            target_selection_range: name_range,
823        }
824    }
825
826    // Helper methods
827
828    fn find_node_at_offset<'b>(&'b self, node: &'b Node, offset: usize) -> Option<&'b Node> {
829        if offset >= node.location.start && offset <= node.location.end {
830            // Check children first for more specific match
831            for child in self.get_children(node) {
832                if let Some(found) = self.find_node_at_offset(child, offset) {
833                    return Some(found);
834                }
835            }
836            return Some(node);
837        }
838        None
839    }
840
841    fn collect_subroutine_declarations<'b>(
842        &'b self,
843        node: &'b Node,
844        sub_name: &str,
845        subs: &mut Vec<&'b Node>,
846    ) {
847        match &node.kind {
848            NodeKind::Subroutine { name: Some(name_str), .. } if name_str == sub_name => {
849                subs.push(node);
850            }
851            // Method declarations (Perl 5.38+ native class / Object::Pad).
852            // NodeKind::Method.name is a bare String (not Option<String>).
853            NodeKind::Method { name: method_name, .. } if method_name == sub_name => {
854                subs.push(node);
855            }
856            _ => {}
857        }
858
859        for child in self.get_children(node) {
860            self.collect_subroutine_declarations(child, sub_name, subs);
861        }
862    }
863
864    fn find_package_declarations<'b>(&'b self, node: &'b Node, pkg_name: &str) -> Vec<&'b Node> {
865        let mut packages = Vec::new();
866        self.collect_package_declarations(node, pkg_name, &mut packages);
867        packages
868    }
869
870    fn collect_package_declarations<'b>(
871        &'b self,
872        node: &'b Node,
873        pkg_name: &str,
874        packages: &mut Vec<&'b Node>,
875    ) {
876        match &node.kind {
877            NodeKind::Package { name, .. } | NodeKind::Class { name, .. } if name == pkg_name => {
878                packages.push(node);
879            }
880            _ => {}
881        }
882
883        for child in self.get_children(node) {
884            self.collect_package_declarations(child, pkg_name, packages);
885        }
886    }
887
888    fn find_constant_declarations<'b>(&'b self, node: &'b Node, const_name: &str) -> Vec<&'b Node> {
889        let mut constants = Vec::new();
890        self.collect_constant_declarations(node, const_name, &mut constants);
891        constants
892    }
893
894    /// Strip leading -options from constant args
895    fn strip_constant_options<'b>(&self, args: &'b [String]) -> &'b [String] {
896        let mut i = 0;
897        while i < args.len() && args[i].starts_with('-') {
898            i += 1;
899        }
900        // Also skip a comma if present after options
901        if i < args.len() && args[i] == "," {
902            i += 1;
903        }
904        &args[i..]
905    }
906
907    fn collect_constant_declarations<'b>(
908        &'b self,
909        node: &'b Node,
910        const_name: &str,
911        constants: &mut Vec<&'b Node>,
912    ) {
913        if let NodeKind::Use { module, args, .. } = &node.kind {
914            if module == "constant" {
915                // Strip leading options like -strict, -nonstrict, -force
916                let stripped_args = self.strip_constant_options(args);
917
918                // Form 1: FOO => ...
919                if stripped_args.first().map(|s| s.as_str()) == Some(const_name) {
920                    constants.push(node);
921                    // keep scanning siblings too (there can be multiple `use constant`)
922                }
923
924                // Flattened args text once (cheap)
925                let args_text = stripped_args.join(" ");
926
927                // Form 2: { FOO => 1, BAR => 2 }
928                if self.contains_name_in_hash(&args_text, const_name) {
929                    constants.push(node);
930                }
931
932                // Form 3: qw(FOO BAR) / qw/FOO BAR/
933                if self.contains_name_in_qw(&args_text, const_name) {
934                    constants.push(node);
935                }
936            }
937        }
938
939        for child in self.get_children(node) {
940            self.collect_constant_declarations(child, const_name, constants);
941        }
942    }
943
944    /// Check if a byte is part of an ASCII identifier
945    #[inline]
946    fn is_ident_ascii(b: u8) -> bool {
947        matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_')
948    }
949
950    /// Iterate over all qw windows in the string
951    /// Handles both paired delimiters ((), [], {}, <>) and symmetric delimiters (|, !, #, etc.)
952    fn for_each_qw_window<F>(&self, s: &str, mut f: F) -> bool
953    where
954        F: FnMut(usize, usize) -> bool,
955    {
956        let b = s.as_bytes();
957        let mut i = 0;
958        while i + 1 < b.len() {
959            // find literal "qw"
960            if b[i] == b'q' && b[i + 1] == b'w' {
961                let mut j = i + 2;
962
963                // allow whitespace between qw and delimiter
964                while j < b.len() && (b[j] as char).is_ascii_whitespace() {
965                    j += 1;
966                }
967                if j >= b.len() {
968                    break;
969                }
970
971                let open = b[j] as char;
972
973                // "qwerty" guard: next non-ws must be a NON-word delimiter
974                // (i.e., not [A-Za-z0-9_])
975                if open.is_ascii_alphanumeric() || open == '_' {
976                    i += 1;
977                    continue;
978                }
979
980                // choose closing delimiter
981                let close = match open {
982                    '(' => ')',
983                    '[' => ']',
984                    '{' => '}',
985                    '<' => '>',
986                    _ => open, // symmetric delimiter (|, !, #, /, ~, ...)
987                };
988
989                // advance past opener and collect until closer
990                j += 1;
991                let start = j;
992                while j < b.len() && (b[j] as char) != close {
993                    j += 1;
994                }
995                if j <= b.len() {
996                    // Found the closing delimiter
997                    if f(start, j) {
998                        return true;
999                    }
1000                    // continue scanning after the closer
1001                    i = j + 1;
1002                    continue;
1003                } else {
1004                    // unclosed; stop scanning
1005                    break;
1006                }
1007            }
1008
1009            i += 1;
1010        }
1011        false
1012    }
1013
1014    /// Iterate over all {...} pairs in the string
1015    fn for_each_brace_window<F>(&self, s: &str, mut f: F) -> bool
1016    where
1017        F: FnMut(usize, usize) -> bool,
1018    {
1019        let b = s.as_bytes();
1020        let mut i = 0;
1021        while i < b.len() {
1022            if b[i] == b'{' {
1023                let start = i + 1;
1024                let mut nesting = 1;
1025                let mut j = i + 1;
1026                while j < b.len() {
1027                    match b[j] {
1028                        b'{' => nesting += 1,
1029                        b'}' => {
1030                            nesting -= 1;
1031                            if nesting == 0 {
1032                                break;
1033                            }
1034                        }
1035                        _ => {}
1036                    }
1037                    j += 1;
1038                }
1039
1040                if nesting == 0 {
1041                    // Found matching closing brace at j
1042                    if f(start, j) {
1043                        return true;
1044                    }
1045                    i = j + 1;
1046                    continue;
1047                }
1048            }
1049            i += 1;
1050        }
1051        false
1052    }
1053
1054    fn contains_name_in_hash(&self, s: &str, name: &str) -> bool {
1055        // for { FOO => 1, BAR => 2 } form - check all {...} pairs
1056        self.for_each_brace_window(s, |start, end| {
1057            // only scan that slice
1058            self.find_word(&s[start..end], name).is_some()
1059        })
1060    }
1061
1062    fn contains_name_in_qw(&self, s: &str, name: &str) -> bool {
1063        // looks for qw(...) / qw[...] / qw/.../ etc. with word boundaries
1064        self.for_each_qw_window(s, |start, end| {
1065            // tokens are whitespace separated
1066            s[start..end].split_whitespace().any(|tok| tok == name)
1067        })
1068    }
1069
1070    fn find_word(&self, hay: &str, needle: &str) -> Option<(usize, usize)> {
1071        if needle.is_empty() {
1072            return None;
1073        }
1074        let mut find_from = 0;
1075        while let Some(hit) = hay[find_from..].find(needle) {
1076            let start = find_from + hit;
1077            let end = start + needle.len();
1078            let left_ok = start == 0 || !Self::is_ident_ascii(hay.as_bytes()[start - 1]);
1079            let right_ok = end == hay.len()
1080                || !Self::is_ident_ascii(*hay.as_bytes().get(end).unwrap_or(&b' '));
1081            if left_ok && right_ok {
1082                return Some((start, end));
1083            }
1084            find_from = end;
1085        }
1086        None
1087    }
1088
1089    fn first_all_caps_word(&self, s: &str) -> Option<(usize, usize)> {
1090        // very small scanner: find FOO-ish
1091        let bytes = s.as_bytes();
1092        let mut i = 0;
1093        while i < bytes.len() {
1094            while i < bytes.len() && !Self::is_ident_ascii(bytes[i]) {
1095                i += 1;
1096            }
1097            let start = i;
1098            while i < bytes.len() && Self::is_ident_ascii(bytes[i]) {
1099                i += 1;
1100            }
1101            if start < i {
1102                let w = &s[start..i];
1103                if w.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') {
1104                    return Some((start, i));
1105                }
1106            }
1107        }
1108        None
1109    }
1110
1111    fn get_subroutine_name_range(&self, decl: &Node) -> (usize, usize) {
1112        if let NodeKind::Subroutine { name_span: Some(loc), .. } = &decl.kind {
1113            (loc.start, loc.end)
1114        } else {
1115            (decl.location.start, decl.location.end)
1116        }
1117    }
1118
1119    fn get_package_name_range(&self, decl: &Node) -> (usize, usize) {
1120        if let NodeKind::Package { name_span, .. } = &decl.kind {
1121            (name_span.start, name_span.end)
1122        } else {
1123            (decl.location.start, decl.location.end)
1124        }
1125    }
1126
1127    fn get_constant_name_range(&self, decl: &Node) -> (usize, usize) {
1128        let text = self.get_node_text(decl);
1129
1130        // Prefer an exact span if we can find the first occurrence with word boundaries
1131        if let NodeKind::Use { args, .. } = &decl.kind {
1132            let best_guess = args.first().map(|s| s.as_str()).unwrap_or("");
1133            if let Some((lo, hi)) = self.find_word(&text, best_guess) {
1134                let abs_lo = decl.location.start + lo;
1135                let abs_hi = decl.location.start + hi;
1136                return (abs_lo, abs_hi);
1137            }
1138        }
1139
1140        // Try any constant-looking all-caps token in the decl
1141        if let Some((lo, hi)) = self.first_all_caps_word(&text) {
1142            return (decl.location.start + lo, decl.location.start + hi);
1143        }
1144
1145        // Fallback to whole range
1146        (decl.location.start, decl.location.end)
1147    }
1148
1149    fn get_constant_name_range_for(&self, decl: &Node, name: &str) -> (usize, usize) {
1150        let text = self.get_node_text(decl);
1151
1152        // Fast path: try to find the exact word
1153        if let Some((lo, hi)) = self.find_word(&text, name) {
1154            return (decl.location.start + lo, decl.location.start + hi);
1155        }
1156
1157        // Try inside all qw(...) windows
1158        let mut found_range = None;
1159        self.for_each_qw_window(&text, |start, end| {
1160            // Find the exact token position within this qw window
1161            if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
1162                found_range =
1163                    Some((decl.location.start + start + lo, decl.location.start + start + hi));
1164                true // Stop searching
1165            } else {
1166                false // Continue to next window
1167            }
1168        });
1169        if let Some(range) = found_range {
1170            return range;
1171        }
1172
1173        // Try inside all { ... } blocks (hash form)
1174        self.for_each_brace_window(&text, |start, end| {
1175            if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
1176                found_range =
1177                    Some((decl.location.start + start + lo, decl.location.start + start + hi));
1178                true // Stop searching
1179            } else {
1180                false // Continue to next window
1181            }
1182        });
1183        if let Some(range) = found_range {
1184            return range;
1185        }
1186
1187        // Final fallback to heuristics
1188        self.get_constant_name_range(decl)
1189    }
1190
1191    fn get_children<'b>(&self, node: &'b Node) -> Vec<&'b Node> {
1192        Self::get_children_static(node)
1193    }
1194
1195    /// Build a lookup map from raw node pointers back to safe references.
1196    ///
1197    /// This map is the bridge that makes `ParentMap` safe to use: callers
1198    /// obtain a `*const Node` from the parent map and look it up here to
1199    /// recover a properly-lifetime-bounded `&Node`.  The raw pointer is
1200    /// used purely as an identity key — it is never dereferenced directly.
1201    fn build_node_lookup_map(&self) -> FxHashMap<*const Node, &Node> {
1202        let mut map = FxHashMap::default();
1203        Self::build_node_lookup(self.ast.as_ref(), &mut map);
1204        map
1205    }
1206
1207    fn build_node_lookup<'b>(node: &'b Node, map: &mut FxHashMap<*const Node, &'b Node>) {
1208        // SAFETY: `node` is a shared reference whose lifetime `'b` is tied to
1209        // `self.ast` (`Arc<Node>`).  We store the address as a raw-pointer key
1210        // alongside the same reference as the value.  The value is the safe
1211        // side of this pair — it is the only route through which the pointer
1212        // is ever turned back into usable data.
1213        map.insert(node as *const Node, node);
1214        for child in Self::get_children_static(node) {
1215            Self::build_node_lookup(child, map);
1216        }
1217    }
1218
1219    fn get_children_static(node: &Node) -> Vec<&Node> {
1220        match &node.kind {
1221            NodeKind::Program { statements } => statements.iter().collect(),
1222            NodeKind::Block { statements } => statements.iter().collect(),
1223            NodeKind::If { condition, then_branch, else_branch, .. } => {
1224                let mut children = vec![condition.as_ref(), then_branch.as_ref()];
1225                if let Some(else_b) = else_branch {
1226                    children.push(else_b.as_ref());
1227                }
1228                children
1229            }
1230            NodeKind::Binary { left, right, .. } => vec![left.as_ref(), right.as_ref()],
1231            NodeKind::Unary { operand, .. } => vec![operand.as_ref()],
1232            NodeKind::Return { value } => {
1233                if let Some(value) = value {
1234                    vec![value.as_ref()]
1235                } else {
1236                    vec![]
1237                }
1238            }
1239            NodeKind::VariableDeclaration { variable, initializer, .. } => {
1240                let mut children = vec![variable.as_ref()];
1241                if let Some(init) = initializer {
1242                    children.push(init.as_ref());
1243                }
1244                children
1245            }
1246            NodeKind::Method { signature, body, .. } => {
1247                let mut children = vec![body.as_ref()];
1248                if let Some(sig) = signature {
1249                    children.push(sig.as_ref());
1250                }
1251                children
1252            }
1253            NodeKind::Subroutine { signature, body, .. } => {
1254                let mut children = vec![body.as_ref()];
1255                if let Some(sig) = signature {
1256                    children.push(sig.as_ref());
1257                }
1258                children
1259            }
1260            NodeKind::FunctionCall { args, .. } => args.iter().collect(),
1261            NodeKind::MethodCall { object, args, .. } => {
1262                let mut children = vec![object.as_ref()];
1263                children.extend(args.iter());
1264                children
1265            }
1266            NodeKind::IndirectCall { object, args, .. } => {
1267                let mut children = vec![object.as_ref()];
1268                children.extend(args.iter());
1269                children
1270            }
1271            NodeKind::While { condition, body, .. } => {
1272                vec![condition.as_ref(), body.as_ref()]
1273            }
1274            NodeKind::For { init, condition, update, body, .. } => {
1275                let mut children = Vec::new();
1276                if let Some(i) = init {
1277                    children.push(i.as_ref());
1278                }
1279                if let Some(c) = condition {
1280                    children.push(c.as_ref());
1281                }
1282                if let Some(u) = update {
1283                    children.push(u.as_ref());
1284                }
1285                children.push(body.as_ref());
1286                children
1287            }
1288            NodeKind::Foreach { variable, list, body, .. } => {
1289                vec![variable.as_ref(), list.as_ref(), body.as_ref()]
1290            }
1291            NodeKind::ExpressionStatement { expression } => vec![expression.as_ref()],
1292            // Class body (Perl 5.38+ native class / Object::Pad) contains methods.
1293            NodeKind::Class { body, .. } => vec![body.as_ref()],
1294            // Package with optional inline block: `package Foo { ... }`.
1295            NodeKind::Package { block: Some(block), .. } => vec![block.as_ref()],
1296            _ => vec![],
1297        }
1298    }
1299
1300    /// Extracts the source code text for a given AST node.
1301    ///
1302    /// Returns the substring of the document content corresponding to
1303    /// the node's location range. Used for symbol name extraction and
1304    /// text-based analysis.
1305    ///
1306    /// # Arguments
1307    /// * `node` - AST node to extract text from
1308    ///
1309    /// # Performance
1310    /// - Time complexity: O(m) where m is node text length
1311    /// - Memory: Creates owned string copy
1312    /// - Typical latency: <10μs for identifier names
1313    ///
1314    /// # Examples
1315    /// ```rust,ignore
1316    /// use perl_parser::declaration::DeclarationProvider;
1317    /// use perl_parser::ast::Node;
1318    /// use std::sync::Arc;
1319    ///
1320    /// let provider = DeclarationProvider::new(
1321    ///     Arc::new(Node::new_root()),
1322    ///     "sub example { }".to_string(),
1323    ///     "uri".to_string()
1324    /// );
1325    /// // let text = provider.get_node_text(&some_node);
1326    /// ```
1327    pub fn get_node_text(&self, node: &Node) -> String {
1328        self.content[node.location.start..node.location.end].to_string()
1329    }
1330}
1331
1332/// Extracts a symbol key from the AST node at the given cursor position.
1333///
1334/// Analyzes the AST at a specific byte offset to identify the symbol under
1335/// the cursor for LSP operations. Supports function calls, variable references,
1336/// and package-qualified symbols with full Perl syntax coverage.
1337///
1338/// # Arguments
1339/// * `ast` - Root AST node to search within
1340/// * `offset` - Byte offset in the source document
1341/// * `current_pkg` - Current package context for symbol resolution
1342///
1343/// # Returns
1344/// * `Some(SymbolKey)` - Symbol found at position with package qualification
1345/// * `None` - No symbol at the given position
1346///
1347/// # Performance
1348/// - Search time: O(log n) average case with spatial indexing
1349/// - Worst case: O(n) for unbalanced AST traversal
1350/// - Typical latency: <50μs for LSP responsiveness
1351///
1352/// # Perl Parsing Context
1353/// Handles complex Perl symbol patterns:
1354/// - Package-qualified calls: `Package::function`
1355/// - Bare function calls: `function` (resolved in current package)
1356/// - Variable references: `$var`, `@array`, `%hash`
1357/// - Method calls: `$obj->method`
1358///
1359/// # Examples
1360/// ```rust,ignore
1361/// use perl_parser::declaration::symbol_at_cursor;
1362/// use perl_parser::ast::Node;
1363///
1364/// let ast = Node::new_root();
1365/// let symbol = symbol_at_cursor(&ast, 42, "MyPackage");
1366/// if let Some(sym) = symbol {
1367///     println!("Found symbol: {:?}", sym);
1368/// }
1369/// ```
1370fn symbol_at_cursor_internal(
1371    ast: &Node,
1372    offset: usize,
1373    current_pkg: &str,
1374    source_text: &str,
1375) -> Option<SymbolKey> {
1376    fn collect_node_path_at_offset<'a>(
1377        node: &'a Node,
1378        offset: usize,
1379        path: &mut Vec<&'a Node>,
1380    ) -> bool {
1381        if offset < node.location.start || offset > node.location.end {
1382            return false;
1383        }
1384
1385        path.push(node);
1386
1387        for child in get_node_children(node) {
1388            if collect_node_path_at_offset(child, offset, path) {
1389                return true;
1390            }
1391        }
1392
1393        true
1394    }
1395
1396    fn find_symbol_node_at_offset(ast: &Node, offset: usize) -> Option<(Vec<&Node>, &Node)> {
1397        let mut path = Vec::new();
1398        if !collect_node_path_at_offset(ast, offset, &mut path) {
1399            return None;
1400        }
1401
1402        let node = path
1403            .iter()
1404            .rev()
1405            .copied()
1406            .find(|node| {
1407                matches!(
1408                    node.kind,
1409                    NodeKind::Variable { .. }
1410                        | NodeKind::FunctionCall { .. }
1411                        | NodeKind::Subroutine { .. }
1412                        | NodeKind::Method { .. }
1413                        | NodeKind::MethodCall { .. }
1414                        | NodeKind::Use { .. }
1415                )
1416            })
1417            .or_else(|| path.last().copied())?;
1418
1419        Some((path, node))
1420    }
1421
1422    fn node_variable_name(node: &Node) -> Option<&str> {
1423        if let NodeKind::Variable { name, .. } = &node.kind { Some(name.as_str()) } else { None }
1424    }
1425
1426    fn normalize_symbol_name(raw: &str) -> Option<String> {
1427        let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
1428        if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
1429    }
1430
1431    fn token_at_offset_in_text(text: &str, rel_offset: usize) -> Option<String> {
1432        let bytes = text.as_bytes();
1433        if rel_offset >= bytes.len() {
1434            return None;
1435        }
1436        let is_ident = |b: u8| matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b':');
1437        if !is_ident(bytes[rel_offset]) {
1438            return None;
1439        }
1440
1441        let mut start = rel_offset;
1442        while start > 0 && is_ident(bytes[start - 1]) {
1443            start -= 1;
1444        }
1445        let mut end = rel_offset + 1;
1446        while end < bytes.len() && is_ident(bytes[end]) {
1447            end += 1;
1448        }
1449        Some(text[start..end].to_string())
1450    }
1451
1452    fn export_tag_members(module: &str, tag: &str) -> &'static [&'static str] {
1453        match (module, tag) {
1454            // POSIX tag sets commonly used in system scripts.
1455            ("POSIX", ":sys_wait_h") => {
1456                &["WEXITSTATUS", "WIFEXITED", "WIFSIGNALED", "WIFSTOPPED", "WTERMSIG"]
1457            }
1458            ("POSIX", ":fcntl_h") => &["F_GETFD", "F_SETFD", "F_GETFL", "F_SETFL", "FD_CLOEXEC"],
1459            ("POSIX", ":termios_h") => {
1460                &["B9600", "B19200", "B38400", "TCSANOW", "TCSADRAIN", "TCSAFLUSH"]
1461            }
1462            // File::Find exports.
1463            ("File::Find", ":find") => &["find", "finddepth"],
1464            // Fcntl exports.
1465            ("Fcntl", ":seek") => &["SEEK_SET", "SEEK_CUR", "SEEK_END"],
1466            ("Fcntl", ":lock") => &["LOCK_SH", "LOCK_EX", "LOCK_NB", "LOCK_UN"],
1467            // Encode exports.
1468            ("Encode", ":fallback") => &[
1469                "FB_DEFAULT",
1470                "FB_CROAK",
1471                "FB_QUIET",
1472                "FB_WARN",
1473                "FB_PERLQQ",
1474                "FB_HTMLCREF",
1475                "FB_XMLCREF",
1476            ],
1477            _ => &[],
1478        }
1479    }
1480
1481    fn tag_imports_symbol(module: &str, import_token: &str, symbol_name: &str) -> bool {
1482        if !import_token.starts_with(':') {
1483            return false;
1484        }
1485        export_tag_members(module, import_token).contains(&symbol_name)
1486    }
1487
1488    /// Pragmas and structural modules whose qw/string arguments are NOT
1489    /// imported symbol names. Cursor-on-arg for these should not resolve
1490    /// to a bogus `SymbolKey` — they carry inheritance lists, feature names,
1491    /// or other non-import semantics.
1492    const NON_IMPORT_PRAGMAS: &[&str] = &[
1493        "constant", // constant definitions, not imports
1494        "parent",   // inheritance: qw/string args are class names
1495        "base",     // legacy inheritance
1496        "vars",     // variable declarations, not imports
1497        "Exporter", // 'import' arg is a proxy method, not an imported symbol
1498        "mro",      // method resolution order pragma
1499        "if",       // conditional module load
1500        "lib",      // adds directories to @INC
1501        "feature",  // enables Perl feature flags
1502        "utf8",     // encoding pragma
1503    ];
1504
1505    fn use_args_import_symbol(module: &str, args: &[String], symbol_name: &str) -> bool {
1506        args.iter().any(|arg| {
1507            if arg == symbol_name || tag_imports_symbol(module, arg, symbol_name) {
1508                return true;
1509            }
1510
1511            if arg.starts_with("qw") {
1512                let content = arg
1513                    .trim_start_matches("qw")
1514                    .trim_start_matches(|c: char| "([{/<|!".contains(c))
1515                    .trim_end_matches(|c: char| ")]}/|!>".contains(c));
1516                return content
1517                    .split_whitespace()
1518                    .any(|tok| tok == symbol_name || tag_imports_symbol(module, tok, symbol_name));
1519            }
1520
1521            let bare = arg.trim().trim_matches('\'').trim_matches('"').trim();
1522            bare == symbol_name || tag_imports_symbol(module, bare, symbol_name)
1523        })
1524    }
1525
1526    fn find_import_source(ast: &Node, symbol_name: &str) -> Option<String> {
1527        /// Extract the module name from a `require Module;` statement node.
1528        ///
1529        /// Matches both `require Foo::Bar` (Identifier arg) and
1530        /// `require "Foo/Bar.pm"` forms, returning the module name as a
1531        /// `::` -separated string suitable for workspace lookup.
1532        fn require_module_name(node: &Node) -> Option<String> {
1533            let args = match &node.kind {
1534                NodeKind::FunctionCall { name, args } if name == "require" => args,
1535                _ => return None,
1536            };
1537            let arg = args.first()?;
1538            match &arg.kind {
1539                NodeKind::Identifier { name } => Some(name.clone()),
1540                NodeKind::String { value, .. } => {
1541                    // "Foo/Bar.pm" -> "Foo::Bar"
1542                    let cleaned = value.trim_matches('\'').trim_matches('"').trim();
1543                    let module = cleaned.trim_end_matches(".pm").replace('/', "::");
1544                    Some(module)
1545                }
1546                _ => None,
1547            }
1548        }
1549
1550        /// Check whether a MethodCall node is `Module->import(...)` and, if
1551        /// so, whether its argument list contains `symbol`.  Handles four
1552        /// argument forms:
1553        /// - bare string literals:  `->import('foo', 'bar')`
1554        /// - qw list as ArrayLit:   `->import(qw(foo bar))` → ArrayLiteral
1555        /// - Identifier nodes:      `->import(foo)` (unusual but legal)
1556        /// - String value trimming: quoted strings like `"'foo'"` from qw
1557        fn import_call_exports(
1558            method_node: &Node,
1559            expected_module: &str,
1560            symbol: &str,
1561            aliases: &std::collections::HashMap<String, String>,
1562        ) -> bool {
1563            let (object, method, args) = match &method_node.kind {
1564                NodeKind::MethodCall { object, method, args } => (object, method, args),
1565                _ => return false,
1566            };
1567            if method != "import" {
1568                return false;
1569            }
1570            // The object must be the same module name.
1571            let obj_name = match &object.kind {
1572                NodeKind::Identifier { name } => Some(name.as_str()),
1573                NodeKind::Variable { name, .. } => aliases.get(name).map(String::as_str),
1574                _ => return false,
1575            };
1576            let Some(obj_name) = obj_name else {
1577                return false;
1578            };
1579            if obj_name != expected_module {
1580                return false;
1581            }
1582            if args.is_empty() {
1583                // `Module->import()` default import set is module-specific and may
1584                // come from `@EXPORT` in another file.  We do not currently have
1585                // a workspace export table in this lookup path, so stay
1586                // conservative and do not claim symbol ownership here.
1587                return false;
1588            }
1589            // Walk the argument list looking for the symbol.
1590            for arg in args {
1591                if arg_node_matches_symbol(arg, expected_module, symbol) {
1592                    return true;
1593                }
1594            }
1595            false
1596        }
1597
1598        /// Check whether a single AST argument node matches `symbol`.
1599        /// Handles: String literals, Identifiers (including raw "qw(...)"),
1600        /// and ArrayLiteral (the AST form produced by `qw(...)` in expression
1601        /// context).
1602        fn arg_node_matches_symbol(arg: &Node, module: &str, symbol: &str) -> bool {
1603            match &arg.kind {
1604                NodeKind::String { value, .. } => {
1605                    // Strip surrounding single/double quotes that some code
1606                    // paths leave in the value (e.g. qw in quotes.rs).
1607                    let bare = value.trim_matches('\'').trim_matches('"');
1608                    bare == symbol || tag_imports_symbol(module, bare, symbol)
1609                }
1610                NodeKind::Identifier { name } => {
1611                    if name == symbol {
1612                        return true;
1613                    }
1614                    // qw(...) stored as a raw "qw(...)" Identifier string
1615                    // (from the Use-node code path that reuses this helper).
1616                    if name.starts_with("qw") {
1617                        let content = name
1618                            .trim_start_matches("qw")
1619                            .trim_start_matches(|c: char| "([{/<|!".contains(c))
1620                            .trim_end_matches(|c: char| ")]}/|!>".contains(c));
1621                        return content
1622                            .split_whitespace()
1623                            .any(|tok| tok == symbol || tag_imports_symbol(module, tok, symbol));
1624                    }
1625                    false
1626                }
1627                NodeKind::ArrayLiteral { elements } => {
1628                    // qw(...) in expression context → ArrayLiteral of String nodes
1629                    elements.iter().any(|el| arg_node_matches_symbol(el, module, symbol))
1630                }
1631                _ => false,
1632            }
1633        }
1634
1635        fn module_runtime_alias(expr: &Node) -> Option<(String, String)> {
1636            let (alias_name, call_node) = match &expr.kind {
1637                NodeKind::Assignment { lhs, rhs, op } if op == "=" => {
1638                    let NodeKind::Variable { name, .. } = &lhs.kind else {
1639                        return None;
1640                    };
1641                    (name.as_str(), rhs.as_ref())
1642                }
1643                NodeKind::VariableDeclaration { variable, initializer: Some(rhs), .. } => {
1644                    let NodeKind::Variable { name, .. } = &variable.kind else {
1645                        return None;
1646                    };
1647                    (name.as_str(), rhs.as_ref())
1648                }
1649                _ => return None,
1650            };
1651
1652            let NodeKind::FunctionCall { name, args } = &call_node.kind else {
1653                return None;
1654            };
1655            if !matches!(
1656                name.as_str(),
1657                "use_module"
1658                    | "require_module"
1659                    | "Module::Runtime::use_module"
1660                    | "Module::Runtime::require_module"
1661            ) {
1662                return None;
1663            }
1664            let first = args.first()?;
1665            let NodeKind::String { value, .. } = &first.kind else {
1666                return None;
1667            };
1668            let module = value.trim_matches('\'').trim_matches('"').trim();
1669            if module.is_empty() {
1670                return None;
1671            }
1672            Some((alias_name.to_string(), module.to_string()))
1673        }
1674
1675        /// Unwrap an ExpressionStatement to its inner expression, or return
1676        /// the node unchanged (handles the case where we're already at the
1677        /// expression level).
1678        fn inner_expr(node: &Node) -> &Node {
1679            if let NodeKind::ExpressionStatement { expression } = &node.kind {
1680                expression.as_ref()
1681            } else {
1682                node
1683            }
1684        }
1685
1686        /// Scan a flat statement list for a `require M; M->import(...)` pair
1687        /// that exports `symbol`.  The require and import calls do not have to
1688        /// be adjacent — the import just needs to appear anywhere in the same
1689        /// statement list after (or even before) the require.
1690        fn scan_statements_for_require_import(stmts: &[Node], symbol: &str) -> Option<String> {
1691            // Collect all `require Module` names present in this block.
1692            let mut required_modules: Vec<String> =
1693                stmts.iter().filter_map(|s| require_module_name(inner_expr(s))).collect();
1694            let mut aliases: std::collections::HashMap<String, String> =
1695                std::collections::HashMap::new();
1696            for stmt in stmts {
1697                if let Some((alias, module)) = module_runtime_alias(inner_expr(stmt)) {
1698                    aliases.insert(alias, module.clone());
1699                    if !required_modules.contains(&module) {
1700                        required_modules.push(module);
1701                    }
1702                }
1703            }
1704
1705            if required_modules.is_empty() {
1706                return None;
1707            }
1708
1709            // Check whether any `Module->import(...)` call in this block
1710            // exports our symbol, using the set of required modules.
1711            for stmt in stmts {
1712                let expr = inner_expr(stmt);
1713                for module in &required_modules {
1714                    if import_call_exports(expr, module, symbol, &aliases) {
1715                        return Some(module.clone());
1716                    }
1717                }
1718            }
1719            None
1720        }
1721
1722        fn find(node: &Node, name: &str) -> Option<String> {
1723            if let NodeKind::Use { module, args, .. } = &node.kind {
1724                // Skip structural pragmas — their args are not import-list symbols
1725                if NON_IMPORT_PRAGMAS.contains(&module.as_str()) {
1726                    // Fall through to children
1727                } else {
1728                    for arg in args {
1729                        if arg == name {
1730                            return Some(module.clone());
1731                        }
1732                        if tag_imports_symbol(module, arg, name) {
1733                            return Some(module.clone());
1734                        }
1735                        if arg.starts_with("qw") {
1736                            let content = arg
1737                                .trim_start_matches("qw")
1738                                .trim_start_matches(|c: char| "([{/<|!".contains(c))
1739                                .trim_end_matches(|c: char| ")]}/|!>".contains(c));
1740                            for import_token in content.split_whitespace() {
1741                                if import_token == name
1742                                    || tag_imports_symbol(module, import_token, name)
1743                                {
1744                                    return Some(module.clone());
1745                                }
1746                            }
1747                        } else {
1748                            // Parenthesized import list: use Foo ('bar', 'baz')
1749                            // The parser emits each token as a separate arg including commas
1750                            // and string literals with their surrounding quotes.
1751                            let bare = arg.trim().trim_matches('\'').trim_matches('"').trim();
1752                            if bare == name {
1753                                return Some(module.clone());
1754                            }
1755                            if tag_imports_symbol(module, bare, name) {
1756                                return Some(module.clone());
1757                            }
1758                        }
1759                    }
1760                }
1761            }
1762
1763            // Scan block/program statement lists for `require M; M->import(sym)` patterns.
1764            let stmts = match &node.kind {
1765                NodeKind::Program { statements } => Some(statements.as_slice()),
1766                NodeKind::Block { statements } => Some(statements.as_slice()),
1767                _ => None,
1768            };
1769            if let Some(statements) = stmts {
1770                if let Some(module) = scan_statements_for_require_import(statements, name) {
1771                    return Some(module);
1772                }
1773            }
1774
1775            for child in get_node_children(node) {
1776                if let Some(module) = find(child, name) {
1777                    return Some(module);
1778                }
1779            }
1780
1781            None
1782        }
1783
1784        find(ast, symbol_name)
1785    }
1786
1787    fn plack_builder_middleware_symbol(path: &[&Node], offset: usize) -> Option<SymbolKey> {
1788        let has_builder = path.iter().any(|ancestor| {
1789            matches!(ancestor.kind, NodeKind::FunctionCall { ref name, .. } if name == "builder")
1790        });
1791        if !has_builder {
1792            return None;
1793        }
1794
1795        let block = path.iter().rev().find_map(|ancestor| {
1796            if let NodeKind::Block { statements } = &ancestor.kind {
1797                Some(statements)
1798            } else {
1799                None
1800            }
1801        })?;
1802
1803        for statement in block {
1804            let NodeKind::ExpressionStatement { expression } = &statement.kind else {
1805                continue;
1806            };
1807            let NodeKind::FunctionCall { name, args } = &expression.kind else {
1808                continue;
1809            };
1810            if name != "enable" {
1811                continue;
1812            }
1813
1814            let Some(first) = args.first() else {
1815                continue;
1816            };
1817            if offset < first.location.start || offset > first.location.end {
1818                continue;
1819            }
1820
1821            let raw_name = match &first.kind {
1822                NodeKind::String { value, .. } => normalize_symbol_name(value)?,
1823                NodeKind::Identifier { name } => name.clone(),
1824                _ => continue,
1825            };
1826
1827            let middleware_name = if raw_name.contains("::") {
1828                raw_name
1829            } else {
1830                format!("Plack::Middleware::{raw_name}")
1831            };
1832
1833            return Some(SymbolKey {
1834                pkg: middleware_name.clone().into(),
1835                name: middleware_name.into(),
1836                sigil: None,
1837                kind: SymKind::Pack,
1838            });
1839        }
1840
1841        None
1842    }
1843
1844    fn looks_like_package_name(name: &str) -> bool {
1845        name.contains("::") || name.chars().next().is_some_and(|ch| ch.is_ascii_uppercase())
1846    }
1847
1848    fn infer_receiver_package(
1849        object: &Node,
1850        current_pkg: &str,
1851        receiver_packages: &std::collections::HashMap<String, String>,
1852    ) -> Option<String> {
1853        if let NodeKind::Identifier { name } = &object.kind {
1854            return Some(name.clone());
1855        }
1856
1857        if let Some(name) = node_variable_name(object) {
1858            if let Some(package_name) = receiver_packages.get(name) {
1859                return Some(package_name.clone());
1860            }
1861
1862            if matches!(name, "self" | "this" | "class") {
1863                return Some(current_pkg.to_string());
1864            }
1865
1866            if looks_like_package_name(name) {
1867                return Some(name.to_string());
1868            }
1869        }
1870
1871        None
1872    }
1873
1874    fn infer_constructor_package(
1875        rhs: &Node,
1876        current_pkg: &str,
1877        receiver_packages: &std::collections::HashMap<String, String>,
1878    ) -> Option<String> {
1879        match &rhs.kind {
1880            NodeKind::MethodCall { method, object, .. } if method == "new" => {
1881                infer_receiver_package(object, current_pkg, receiver_packages)
1882            }
1883            NodeKind::FunctionCall { name, .. } => {
1884                name.rsplit_once("::").map(|(package_name, _)| package_name.to_string())
1885            }
1886            _ => None,
1887        }
1888    }
1889
1890    fn record_receiver_assignment(
1891        node: &Node,
1892        offset: usize,
1893        current_pkg: &str,
1894        receiver_packages: &mut std::collections::HashMap<String, String>,
1895    ) {
1896        if node.location.start > offset {
1897            return;
1898        }
1899
1900        if node.location.end <= offset {
1901            match &node.kind {
1902                NodeKind::VariableDeclaration { variable, initializer, .. } => {
1903                    if let (Some(variable_name), Some(initializer)) =
1904                        (node_variable_name(variable), initializer.as_ref())
1905                    {
1906                        if let Some(package_name) =
1907                            infer_constructor_package(initializer, current_pkg, receiver_packages)
1908                        {
1909                            receiver_packages.insert(variable_name.to_string(), package_name);
1910                        }
1911                    }
1912                }
1913                NodeKind::Assignment { lhs, rhs, .. } => {
1914                    if let Some(variable_name) = node_variable_name(lhs) {
1915                        if let Some(package_name) =
1916                            infer_constructor_package(rhs, current_pkg, receiver_packages)
1917                        {
1918                            receiver_packages.insert(variable_name.to_string(), package_name);
1919                        }
1920                    }
1921                }
1922                _ => {}
1923            }
1924        }
1925
1926        for child in get_node_children(node) {
1927            if child.location.start <= offset {
1928                record_receiver_assignment(child, offset, current_pkg, receiver_packages);
1929            }
1930        }
1931    }
1932
1933    let (path, node) = find_symbol_node_at_offset(ast, offset)?;
1934
1935    if let Some(symbol_key) = plack_builder_middleware_symbol(&path, offset) {
1936        return Some(symbol_key);
1937    }
1938
1939    match &node.kind {
1940        NodeKind::Variable { sigil, name } => {
1941            // Variable already has sigil separated
1942            let sigil_char = sigil.chars().next();
1943            Some(SymbolKey {
1944                pkg: current_pkg.into(),
1945                name: name.clone().into(),
1946                sigil: sigil_char,
1947                kind: SymKind::Var,
1948            })
1949        }
1950        NodeKind::FunctionCall { name, .. } => {
1951            let (pkg, bare) = if let Some(idx) = name.rfind("::") {
1952                (name[..idx].to_string(), name[idx + 2..].to_string())
1953            } else {
1954                (
1955                    find_import_source(ast, name).unwrap_or_else(|| current_pkg.to_string()),
1956                    name.clone(),
1957                )
1958            };
1959            Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
1960        }
1961        NodeKind::Subroutine { name: Some(name), .. } => {
1962            let (pkg, bare) = if let Some(idx) = name.rfind("::") {
1963                (&name[..idx], &name[idx + 2..])
1964            } else {
1965                (current_pkg, name.as_str())
1966            };
1967            Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
1968        }
1969        // Method declaration (Perl 5.38+ native class / Object::Pad).
1970        // name is a bare String (not Option<String>) unlike NodeKind::Subroutine.
1971        NodeKind::Method { name, .. } => {
1972            let (pkg, bare) = if let Some(idx) = name.rfind("::") {
1973                (&name[..idx], &name[idx + 2..])
1974            } else {
1975                (current_pkg, name.as_str())
1976            };
1977            Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
1978        }
1979        NodeKind::MethodCall { object, method, .. } => {
1980            let mut receiver_packages = std::collections::HashMap::new();
1981            record_receiver_assignment(ast, offset, current_pkg, &mut receiver_packages);
1982            let pkg = infer_receiver_package(object, current_pkg, &receiver_packages)
1983                .unwrap_or_else(|| current_pkg.to_string());
1984            Some(SymbolKey {
1985                pkg: pkg.into(),
1986                name: method.clone().into(),
1987                sigil: None,
1988                kind: SymKind::Sub,
1989            })
1990        }
1991        NodeKind::Use { module, args, .. } => {
1992            if !NON_IMPORT_PRAGMAS.contains(&module.as_str())
1993                && !source_text.is_empty()
1994                && offset >= node.location.start
1995                && offset <= node.location.end
1996            {
1997                let rel_offset = offset.saturating_sub(node.location.start);
1998                if let Some(stmt_text) = source_text.get(node.location.start..node.location.end)
1999                    && let Some(token) = token_at_offset_in_text(stmt_text, rel_offset)
2000                    && token != *module
2001                    && token != "use"
2002                    && use_args_import_symbol(module, args, &token)
2003                {
2004                    return Some(SymbolKey {
2005                        pkg: module.clone().into(),
2006                        name: token.into(),
2007                        sigil: None,
2008                        kind: SymKind::Sub,
2009                    });
2010                }
2011            }
2012
2013            // When cursor is on a `use Module::Name` statement, resolve to the package
2014            Some(SymbolKey {
2015                pkg: module.clone().into(),
2016                name: module.clone().into(),
2017                sigil: None,
2018                kind: SymKind::Pack,
2019            })
2020        }
2021        _ => None,
2022    }
2023}
2024
2025/// Extract a symbol key at a cursor offset with access to source text.
2026///
2027/// This variant is used by LSP handlers when additional source-aware
2028/// disambiguation is needed (for example, barewords in `use ... qw(...)` lists).
2029pub fn symbol_at_cursor_with_source(
2030    ast: &Node,
2031    offset: usize,
2032    current_pkg: &str,
2033    source_text: &str,
2034) -> Option<SymbolKey> {
2035    symbol_at_cursor_internal(ast, offset, current_pkg, source_text)
2036}
2037
2038/// Extract a symbol key at a cursor offset.
2039///
2040/// This keeps the historical API and defers to [`symbol_at_cursor_with_source`]
2041/// without source text-specific disambiguation.
2042pub fn symbol_at_cursor(ast: &Node, offset: usize, current_pkg: &str) -> Option<SymbolKey> {
2043    symbol_at_cursor_internal(ast, offset, current_pkg, "")
2044}
2045
2046/// Determines the current package context at the given offset.
2047///
2048/// Scans the AST backwards from the offset to find the most recent
2049/// package declaration, providing proper context for symbol resolution
2050/// in Perl's package-based namespace system.
2051///
2052/// # Arguments
2053/// * `ast` - Root AST node to search within
2054/// * `offset` - Byte offset in the source document
2055///
2056/// # Returns
2057/// Package name as string slice, defaults to "main" if no package found
2058///
2059/// # Performance
2060/// - Search time: O(n) worst case, O(log n) typical
2061/// - Memory: Returns borrowed string slice (zero-copy)
2062/// - Caching: Results suitable for per-request caching
2063///
2064/// # Perl Parsing Context
2065/// Perl package semantics:
2066/// - `package Foo;` declarations change current namespace
2067/// - Scope continues until next package declaration, current block end, or EOF
2068/// - `package Foo { ... }` scopes the package to the explicit block
2069/// - Default package is "main" when no explicit declaration
2070/// - Package names follow Perl identifier rules (`::`-separated)
2071///
2072/// # Examples
2073/// ```rust,ignore
2074/// use perl_parser::declaration::current_package_at;
2075/// use perl_parser::ast::Node;
2076///
2077/// let ast = Node::new_root();
2078/// let pkg = current_package_at(&ast, 100);
2079/// println!("Current package: {}", pkg);
2080/// ```
2081pub fn current_package_at(ast: &Node, offset: usize) -> &str {
2082    fn package_in_statement_list<'a>(
2083        statements: &'a [Node],
2084        offset: usize,
2085        mut current_pkg: &'a str,
2086    ) -> &'a str {
2087        for child in statements {
2088            if child.location.start > offset {
2089                break;
2090            }
2091
2092            if child.location.start <= offset && offset <= child.location.end {
2093                return package_in_node(child, offset, current_pkg);
2094            }
2095
2096            if let NodeKind::Package { name, block: None, .. } = &child.kind {
2097                current_pkg = name.as_str();
2098            }
2099        }
2100
2101        current_pkg
2102    }
2103
2104    fn package_in_node<'a>(node: &'a Node, offset: usize, current_pkg: &'a str) -> &'a str {
2105        match &node.kind {
2106            NodeKind::Program { statements } | NodeKind::Block { statements } => {
2107                package_in_statement_list(statements, offset, current_pkg)
2108            }
2109            NodeKind::Package { name, block, .. } if node.location.start <= offset => {
2110                let package_name = name.as_str();
2111                if let Some(block) = block
2112                    && block.location.start <= offset
2113                    && offset <= block.location.end
2114                {
2115                    return package_in_node(block, offset, package_name);
2116                }
2117                package_name
2118            }
2119            _ => {
2120                for child in get_node_children(node) {
2121                    if child.location.start <= offset && offset <= child.location.end {
2122                        return package_in_node(child, offset, current_pkg);
2123                    }
2124                }
2125                current_pkg
2126            }
2127        }
2128    }
2129
2130    package_in_node(ast, offset, "main")
2131}
2132
2133/// Finds the most specific AST node containing the given byte offset.
2134///
2135/// Performs recursive descent through the AST to locate the deepest node
2136/// that encompasses the specified position. Essential for cursor-based
2137/// LSP operations like go-to-definition and hover.
2138///
2139/// # Arguments
2140/// * `node` - AST node to search within (typically root)
2141/// * `offset` - Byte offset in the source document
2142///
2143/// # Returns
2144/// * `Some(&Node)` - Deepest node containing the offset
2145/// * `None` - Offset is outside the node's range
2146///
2147/// # Performance
2148/// - Search time: O(log n) average, O(n) worst case
2149/// - Memory: Zero allocations, returns borrowed reference
2150/// - Spatial locality: Optimized for sequential offset queries
2151///
2152/// # LSP Integration
2153/// Core primitive for:
2154/// - Hover information: Find node for symbol details
2155/// - Go-to-definition: Identify symbol under cursor
2156/// - Completion: Determine context for suggestions
2157/// - Diagnostics: Map error positions to AST nodes
2158///
2159/// # Examples
2160/// ```rust,ignore
2161/// use perl_parser::declaration::find_node_at_offset;
2162/// use perl_parser::ast::Node;
2163///
2164/// let ast = Node::new_root();
2165/// if let Some(node) = find_node_at_offset(&ast, 42) {
2166///     println!("Found node: {:?}", node.kind);
2167/// }
2168/// ```
2169pub fn find_node_at_offset(node: &Node, offset: usize) -> Option<&Node> {
2170    if offset < node.location.start || offset > node.location.end {
2171        return None;
2172    }
2173
2174    // Check children first for more specific match
2175    let children = get_node_children(node);
2176    for child in children {
2177        if let Some(found) = find_node_at_offset(child, offset) {
2178            return Some(found);
2179        }
2180    }
2181
2182    // If no child contains the offset, return this node
2183    Some(node)
2184}
2185
2186/// Returns direct child nodes for a given AST node.
2187///
2188/// Provides generic access to child nodes across different node types,
2189/// essential for AST traversal algorithms and recursive analysis patterns.
2190///
2191/// # Arguments
2192/// * `node` - AST node to extract children from
2193///
2194/// # Returns
2195/// Vector of borrowed child node references
2196///
2197/// # Performance
2198/// - Time complexity: O(k) where k is child count
2199/// - Memory: Allocates vector for child references
2200/// - Typical latency: <5μs for common node types
2201///
2202/// # Examples
2203/// ```rust,ignore
2204/// use perl_parser::declaration::get_node_children;
2205/// use perl_parser::ast::Node;
2206///
2207/// let node = Node::new_root();
2208/// let children = get_node_children(&node);
2209/// println!("Node has {} children", children.len());
2210/// ```
2211pub fn get_node_children(node: &Node) -> Vec<&Node> {
2212    // Delegate to the AST node's own comprehensive children() method,
2213    // which handles all node kinds including Block, Package, MethodCall, etc.
2214    node.children()
2215}
2216
2217#[cfg(test)]
2218mod tests {
2219    use super::*;
2220    use crate::Parser;
2221    use std::sync::Arc;
2222
2223    /// Helper: parse source and return DeclarationProvider with version 0.
2224    fn make_provider(source: &str) -> DeclarationProvider<'static> {
2225        let mut parser = Parser::new(source);
2226        let ast = parser.parse().expect("parse must succeed");
2227        DeclarationProvider::new(Arc::new(ast), source.to_string(), "file:///test.pl".to_string())
2228    }
2229
2230    // =========================================================================
2231    // NodeKind::Method — changed lines in declaration.rs (#854, patch-coverage)
2232    //
2233    // find_declaration Method arm (lines ~352-362)
2234    // collect_subroutine_declarations Method arm (lines ~853-854)
2235    // collect_package_declarations Class arm (line ~877)
2236    // get_children_static Class arm (line ~1293)
2237    // get_children_static Package block arm (line ~1295)
2238    // symbol_at_cursor_internal Method arm (lines ~1971-1977)
2239    // =========================================================================
2240
2241    /// find_declaration on a Method node (cursor on the method name) returns
2242    /// Some([...]) — exercises the NodeKind::Method arm in find_declaration.
2243    ///
2244    /// Covered changed line: ~352  NodeKind::Method { name, .. } =>
2245    #[test]
2246    fn method_decl_find_declaration_self_locates() {
2247        let source = "class Foo { method greet { return 1; } }";
2248        let provider = make_provider(source);
2249        // "greet" starts at offset 19 (after "class Foo { method ").
2250        // The Method node has no separate name child, so find_node_at_offset
2251        // returns the Method node when the cursor is on the name characters.
2252        let offset = source.find("greet").expect("greet must be in source");
2253        let result = provider.find_declaration(offset, 0);
2254        assert!(
2255            result.is_some(),
2256            "find_declaration on a Method node must return Some; source={source:?} offset={offset}"
2257        );
2258    }
2259
2260    /// collect_subroutine_declarations finds a Method node by name.
2261    ///
2262    /// Covered changed line: ~853  NodeKind::Method { name: method_name, .. }
2263    #[test]
2264    fn method_decl_collect_subroutine_declarations_finds_method() {
2265        let source = "class Foo { method greet { return 1; } }";
2266        let provider = make_provider(source);
2267        let mut subs = Vec::new();
2268        provider.collect_subroutine_declarations(&provider.ast, "greet", &mut subs);
2269        assert!(
2270            !subs.is_empty(),
2271            "collect_subroutine_declarations must find the method 'greet'; got empty vec"
2272        );
2273        assert!(
2274            matches!(subs[0].kind, NodeKind::Method { ref name, .. } if name == "greet"),
2275            "collected declaration must be a Method node named 'greet'"
2276        );
2277    }
2278
2279    /// collect_package_declarations finds a Class node by name.
2280    ///
2281    /// Covered changed line: ~877  NodeKind::Class { name, .. } if name == pkg_name
2282    #[test]
2283    fn class_decl_collect_package_declarations_finds_class() {
2284        let source = "class Foo { method greet { return 1; } }";
2285        let provider = make_provider(source);
2286        let mut packages = Vec::new();
2287        provider.collect_package_declarations(&provider.ast, "Foo", &mut packages);
2288        assert!(
2289            !packages.is_empty(),
2290            "collect_package_declarations must find class 'Foo'; got empty vec"
2291        );
2292        assert!(
2293            matches!(packages[0].kind, NodeKind::Class { ref name, .. } if name == "Foo"),
2294            "collected declaration must be a Class node named 'Foo'"
2295        );
2296    }
2297
2298    /// get_children_static on a Class node returns the class body.
2299    ///
2300    /// Covered changed line: ~1293  NodeKind::Class { body, .. } => vec![body.as_ref()]
2301    #[test]
2302    fn get_children_static_class_returns_body() {
2303        let source = "class Foo { method greet { return 1; } }";
2304        let provider = make_provider(source);
2305        // Walk the AST to find the Class node.
2306        fn find_class(node: &Node) -> Option<&Node> {
2307            if matches!(node.kind, NodeKind::Class { .. }) {
2308                return Some(node);
2309            }
2310            for child in node.children() {
2311                if let Some(found) = find_class(child) {
2312                    return Some(found);
2313                }
2314            }
2315            None
2316        }
2317        let class_node = find_class(&provider.ast).expect("Class node must exist in parsed AST");
2318        let children = DeclarationProvider::get_children_static(class_node);
2319        assert!(
2320            !children.is_empty(),
2321            "get_children_static on Class must return the body; got empty vec"
2322        );
2323        // The single child must be the Block body.
2324        assert!(
2325            matches!(children[0].kind, NodeKind::Block { .. }),
2326            "Class child returned by get_children_static must be a Block"
2327        );
2328    }
2329
2330    /// get_children_static on a Package-with-block node returns the block.
2331    ///
2332    /// Covered changed line: ~1295  NodeKind::Package { block: Some(block), .. }
2333    #[test]
2334    fn get_children_static_package_block_returns_block() {
2335        let source = "package Foo { sub hello { return 1; } }";
2336        let provider = make_provider(source);
2337        fn find_package(node: &Node) -> Option<&Node> {
2338            if matches!(node.kind, NodeKind::Package { .. }) {
2339                return Some(node);
2340            }
2341            for child in node.children() {
2342                if let Some(found) = find_package(child) {
2343                    return Some(found);
2344                }
2345            }
2346            None
2347        }
2348        let package_node =
2349            find_package(&provider.ast).expect("Package node must exist in parsed AST");
2350        let children = DeclarationProvider::get_children_static(package_node);
2351        // package Foo { } has a block — get_children_static must return it.
2352        assert!(
2353            !children.is_empty(),
2354            "get_children_static on Package-with-block must return the block; got empty vec"
2355        );
2356    }
2357
2358    /// symbol_at_cursor on a Method declaration site returns a SymbolKey with
2359    /// name = method name and kind = Sub.
2360    ///
2361    /// Covered changed lines: ~1971-1977  NodeKind::Method { name, .. } => { ... }
2362    #[test]
2363    fn symbol_at_cursor_method_decl_returns_symbol_key() {
2364        let source = "class Foo { method greet { return 1; } }";
2365        let mut parser = Parser::new(source);
2366        let ast = parser.parse().expect("parse must succeed");
2367        let offset = source.find("greet").expect("greet must be in source");
2368        let result = symbol_at_cursor(&ast, offset, "Foo");
2369        assert!(
2370            result.is_some(),
2371            "symbol_at_cursor on a Method declaration must return Some; source={source:?}"
2372        );
2373        let key = result.unwrap();
2374        assert_eq!(key.name.as_ref(), "greet", "symbol name must be the method name");
2375        assert_eq!(key.kind, crate::workspace_index::SymKind::Sub, "method kind must be Sub");
2376    }
2377
2378    // =========================================================================
2379    // Boundary discriminator tests — ripr seam coverage for equality guards
2380    //
2381    // Each test exercises the FALSE side of a match guard (name == X conditions)
2382    // so ripr can confirm the boundary is exercised in both directions.
2383    // =========================================================================
2384
2385    /// Boundary discriminator: collect_subroutine_declarations does NOT collect a
2386    /// Subroutine node when its name does not equal sub_name (name_str != sub_name).
2387    ///
2388    /// Exercises the FALSE side of: name_str == sub_name (line ~848).
2389    #[test]
2390    fn subroutine_decl_boundary_discriminator_rejects_different_sub_name() {
2391        let source = "sub hello { return 1; }";
2392        let provider = make_provider(source);
2393        let mut subs = Vec::new();
2394        // Search for "goodbye" -- a name that does NOT exist in the AST.
2395        provider.collect_subroutine_declarations(&provider.ast, "goodbye", &mut subs);
2396        assert!(
2397            subs.is_empty(),
2398            "collect_subroutine_declarations must NOT collect hello when searching for goodbye; got {count} node(s)",
2399            count = subs.len()
2400        );
2401    }
2402
2403    /// Boundary discriminator: collect_subroutine_declarations does NOT collect a
2404    /// Method node when its name does not equal sub_name (method_name != sub_name).
2405    ///
2406    /// Exercises the FALSE side of: method_name == sub_name (line ~853).
2407    #[test]
2408    fn method_decl_boundary_discriminator_rejects_different_method_name() {
2409        let source = "class Foo { method greet { return 1; } }";
2410        let provider = make_provider(source);
2411        let mut subs = Vec::new();
2412        // Search for "farewell" -- a name that does NOT match the greet method.
2413        provider.collect_subroutine_declarations(&provider.ast, "farewell", &mut subs);
2414        assert!(
2415            subs.is_empty(),
2416            "collect_subroutine_declarations must NOT collect greet when searching for farewell; got {count} node(s)",
2417            count = subs.len()
2418        );
2419    }
2420
2421    /// Boundary discriminator: collect_package_declarations does NOT collect a
2422    /// Class or Package node when its name does not equal pkg_name (name != pkg_name).
2423    ///
2424    /// Exercises the FALSE side of: name == pkg_name (line ~877).
2425    #[test]
2426    fn class_decl_boundary_discriminator_rejects_different_class_name() {
2427        let source = "class Foo { method greet { return 1; } }";
2428        let provider = make_provider(source);
2429        let mut packages = Vec::new();
2430        // Search for "Bar" -- a class name that does NOT exist in the AST.
2431        provider.collect_package_declarations(&provider.ast, "Bar", &mut packages);
2432        assert!(
2433            packages.is_empty(),
2434            "collect_package_declarations must NOT collect Foo when searching for Bar; got {count} node(s)",
2435            count = packages.len()
2436        );
2437    }
2438
2439    // =========================================================================
2440    // Patch coverage tests -- cover specific changed lines not reached by the
2441    // existing tests above (Codecov Patch 95 gate, lines 847-849, 876, 1973).
2442    // =========================================================================
2443
2444    /// collect_subroutine_declarations finds a Subroutine node by name.
2445    ///
2446    /// Covered changed lines: ~847-849  match &node.kind { NodeKind::Subroutine { name: Some(name_str), .. } if ... => { subs.push(node) }
2447    /// (the Subroutine arm body, which is only hit when a sub with a matching name is visited)
2448    #[test]
2449    fn subroutine_decl_collect_subroutine_declarations_finds_subroutine() {
2450        let source = "sub hello { return 1; }";
2451        let provider = make_provider(source);
2452        let mut subs = Vec::new();
2453        provider.collect_subroutine_declarations(&provider.ast, "hello", &mut subs);
2454        assert!(
2455            !subs.is_empty(),
2456            "collect_subroutine_declarations must find the sub hello; got empty vec"
2457        );
2458        assert!(
2459            matches!(subs[0].kind, NodeKind::Subroutine { ref name, .. } if name.as_deref() == Some("hello")),
2460            "collected declaration must be a Subroutine node named hello"
2461        );
2462    }
2463
2464    /// collect_package_declarations finds a Package node by name (not just Class).
2465    ///
2466    /// Covered changed lines: ~876  match &node.kind { NodeKind::Package { name, .. } | NodeKind::Class { name, .. } if ...
2467    /// (exercises the Package arm and confirms the match head is instrumented)
2468    #[test]
2469    fn package_decl_collect_package_declarations_finds_package() {
2470        let source = "package Bar; sub hello { return 1; }";
2471        let provider = make_provider(source);
2472        let mut packages = Vec::new();
2473        provider.collect_package_declarations(&provider.ast, "Bar", &mut packages);
2474        assert!(
2475            !packages.is_empty(),
2476            "collect_package_declarations must find package Bar; got empty vec"
2477        );
2478        assert!(
2479            matches!(packages[0].kind, NodeKind::Package { ref name, .. } if name == "Bar"),
2480            "collected declaration must be a Package node named Bar"
2481        );
2482    }
2483
2484    /// symbol_at_cursor_with_source on a Method declaration returns a SymbolKey
2485    /// with source-text disambiguation active — exercises the Method arm of
2486    /// symbol_at_cursor_internal via the symbol_at_cursor_with_source wrapper.
2487    ///
2488    /// Covered changed lines: ~1971-1977  NodeKind::Method arm in symbol_at_cursor_internal
2489    /// Covers the bare-name path (line ~1975): no "::" in name, so pkg = current_pkg.
2490    #[test]
2491    fn symbol_at_cursor_with_source_method_decl_returns_symbol_key() {
2492        let source = "class Foo { method greet { return 1; } }";
2493        let mut parser = Parser::new(source);
2494        let ast = parser.parse().expect("parse must succeed");
2495        let offset = source.find("greet").expect("greet must be in source");
2496        let result = symbol_at_cursor_with_source(&ast, offset, "Foo", source);
2497        assert!(result.is_some(), "symbol_at_cursor_with_source on a Method must return Some");
2498        let key = result.unwrap();
2499        assert_eq!(key.name.as_ref(), "greet", "symbol name must be the bare method name");
2500        assert_eq!(key.pkg.as_ref(), "Foo", "pkg must be the current_pkg for bare method names");
2501    }
2502}