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            // Handle string literals that are method names inside modifier calls:
350            // `before 'save' => sub { }` — cursor on 'save' navigates to sub save { }
351            NodeKind::String { value, .. } => self.find_modifier_target_declaration(node, value),
352            _ => None,
353        }
354    }
355
356    /// Find variable declaration using scope-aware lookup
357    fn find_variable_declaration(&self, usage: &Node, var_name: &str) -> Option<Vec<LocationLink>> {
358        // Walk upwards through scopes to find the nearest declaration
359        // SAFETY: `usage` is a shared reference into the `Arc<Node>` AST tree held by
360        // `DeclarationProvider<'a>`. The raw pointer is used only as a HashMap key for O(1)
361        // parent lookup and is never dereferenced directly; lookups go through `build_node_lookup_map`
362        // which re-derives safe `&Node` references from the same Arc tree.
363        let mut current_ptr: *const Node = usage as *const _;
364
365        // Build temporary parent map if not provided (for testing)
366        let temp_parent_map;
367        let parent_map = if let Some(pm) = self.parent_map {
368            pm
369        } else {
370            temp_parent_map = {
371                let mut map = FxHashMap::default();
372                Self::build_parent_map(&self.ast, &mut map, None);
373                map
374            };
375            &temp_parent_map
376        };
377        let node_lookup = self.build_node_lookup_map();
378
379        while let Some(&parent_ptr) = parent_map.get(&current_ptr) {
380            let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
381                break;
382            };
383
384            if matches!(parent.kind, NodeKind::Subroutine { .. } | NodeKind::Method { .. }) {
385                if let Some(links) =
386                    self.find_signature_parameter_declaration(parent, usage, var_name)
387                {
388                    return Some(links);
389                }
390            }
391
392            // Check siblings before this node in the current scope
393            for child in self.get_children(parent) {
394                // Stop when we reach or pass the usage node
395                if child.location.start >= usage.location.start {
396                    break;
397                }
398
399                // Check if this is a variable declaration matching our name
400                if let NodeKind::VariableDeclaration { variable, .. } = &child.kind {
401                    if let NodeKind::Variable { name, .. } = &variable.kind {
402                        if name == var_name {
403                            return Some(vec![LocationLink {
404                                origin_selection_range: (usage.location.start, usage.location.end),
405                                target_uri: self.document_uri.clone(),
406                                target_range: (child.location.start, child.location.end),
407                                target_selection_range: (
408                                    variable.location.start,
409                                    variable.location.end,
410                                ),
411                            }]);
412                        }
413                    }
414                }
415
416                // Also check variable list declarations
417                if let NodeKind::VariableListDeclaration { variables, .. } = &child.kind {
418                    for var in variables {
419                        if let NodeKind::Variable { name, .. } = &var.kind {
420                            if name == var_name {
421                                return Some(vec![LocationLink {
422                                    origin_selection_range: (
423                                        usage.location.start,
424                                        usage.location.end,
425                                    ),
426                                    target_uri: self.document_uri.clone(),
427                                    target_range: (child.location.start, child.location.end),
428                                    target_selection_range: (var.location.start, var.location.end),
429                                }]);
430                            }
431                        }
432                    }
433                }
434            }
435
436            current_ptr = parent_ptr;
437        }
438
439        None
440    }
441
442    fn find_signature_parameter_declaration(
443        &self,
444        declaration_site: &Node,
445        usage: &Node,
446        var_name: &str,
447    ) -> Option<Vec<LocationLink>> {
448        let signature = match &declaration_site.kind {
449            NodeKind::Subroutine { signature, .. } | NodeKind::Method { signature, .. } => {
450                signature.as_deref()?
451            }
452            _ => return None,
453        };
454
455        let NodeKind::Signature { parameters } = &signature.kind else {
456            return None;
457        };
458
459        for parameter in parameters {
460            let variable = match &parameter.kind {
461                NodeKind::MandatoryParameter { variable }
462                | NodeKind::OptionalParameter { variable, .. }
463                | NodeKind::SlurpyParameter { variable }
464                | NodeKind::NamedParameter { variable } => variable.as_ref(),
465                _ => continue,
466            };
467
468            let NodeKind::Variable { name, .. } = &variable.kind else {
469                continue;
470            };
471
472            if name == var_name {
473                return Some(vec![self.create_location_link(
474                    usage,
475                    parameter,
476                    (variable.location.start, variable.location.end),
477                )]);
478            }
479        }
480
481        None
482    }
483
484    /// Find subroutine declaration
485    fn find_subroutine_declaration(
486        &self,
487        node: &Node,
488        func_name: &str,
489    ) -> Option<Vec<LocationLink>> {
490        // Check if the function name is package-qualified (contains ::)
491        let (target_package, target_name) = if let Some(pos) = func_name.rfind("::") {
492            // Split into package and function name
493            let package = &func_name[..pos];
494            let name = &func_name[pos + 2..];
495            (Some(package), name)
496        } else {
497            // No package qualifier, use current package context
498            (self.find_current_package(node), func_name)
499        };
500
501        // Search for subroutines with the target name
502        let mut declarations = Vec::new();
503        self.collect_subroutine_declarations(&self.ast, target_name, &mut declarations);
504
505        // If we have a target package, find subs in that specific package
506        if let Some(pkg_name) = target_package {
507            if let Some(decl) =
508                declarations.iter().find(|d| self.find_current_package(d) == Some(pkg_name))
509            {
510                return Some(vec![self.create_location_link(
511                    node,
512                    decl,
513                    self.get_subroutine_name_range(decl),
514                )]);
515            }
516        }
517
518        // Otherwise return the first match
519        if let Some(decl) = declarations.first() {
520            return Some(vec![self.create_location_link(
521                node,
522                decl,
523                self.get_subroutine_name_range(decl),
524            )]);
525        }
526
527        None
528    }
529
530    /// Find method declaration with package resolution
531    fn find_method_declaration(
532        &self,
533        node: &Node,
534        method_name: &str,
535        object: &Node,
536    ) -> Option<Vec<LocationLink>> {
537        // Try to determine the package from the object
538        let package_name = match &object.kind {
539            NodeKind::Identifier { name } if name.chars().next()?.is_uppercase() => {
540                // Likely a package name (e.g., Foo->method)
541                Some(name.as_str())
542            }
543            _ => None,
544        };
545
546        if let Some(pkg) = package_name {
547            // Look for the method in the specific package
548            let mut declarations = Vec::new();
549            self.collect_subroutine_declarations(&self.ast, method_name, &mut declarations);
550
551            if let Some(decl) =
552                declarations.iter().find(|d| self.find_current_package(d) == Some(pkg))
553            {
554                return Some(vec![self.create_location_link(
555                    node,
556                    decl,
557                    self.get_subroutine_name_range(decl),
558                )]);
559            }
560
561            if is_universal_method(method_name)
562                && let Some(decl) =
563                    declarations.iter().find(|d| self.find_current_package(d) == Some("UNIVERSAL"))
564            {
565                return Some(vec![self.create_location_link(
566                    node,
567                    decl,
568                    self.get_subroutine_name_range(decl),
569                )]);
570            }
571        }
572
573        // Fall back to any subroutine with this name
574        self.find_subroutine_declaration(node, method_name)
575    }
576
577    /// Find declaration for an identifier
578    fn find_identifier_declaration(&self, node: &Node, name: &str) -> Option<Vec<LocationLink>> {
579        // `goto LABEL` should resolve to the statement label before considering
580        // sub/package/constant declarations.
581        if self.identifier_is_goto_target(node)
582            && let Some(links) = self.find_label_declaration(node, name)
583        {
584            return Some(links);
585        }
586
587        // Try to find as subroutine first
588        if let Some(links) = self.find_subroutine_declaration(node, name) {
589            return Some(links);
590        }
591
592        // Try to find as package
593        let packages = self.find_package_declarations(&self.ast, name);
594        if let Some(pkg) = packages.first() {
595            return Some(vec![self.create_location_link(
596                node,
597                pkg,
598                self.get_package_name_range(pkg),
599            )]);
600        }
601
602        // Try to find as constant (supporting multiple forms)
603        let constants = self.find_constant_declarations(&self.ast, name);
604        if let Some(const_decl) = constants.first() {
605            return Some(vec![self.create_location_link(
606                node,
607                const_decl,
608                self.get_constant_name_range_for(const_decl, name),
609            )]);
610        }
611
612        None
613    }
614
615    fn find_label_declaration(&self, origin: &Node, label_name: &str) -> Option<Vec<LocationLink>> {
616        let mut labels = Vec::new();
617        self.collect_label_declarations(&self.ast, label_name, &mut labels);
618        let labeled_stmt = labels.first().copied()?;
619
620        Some(vec![self.create_location_link(
621            origin,
622            labeled_stmt,
623            self.get_labeled_statement_label_range(labeled_stmt),
624        )])
625    }
626
627    fn collect_label_declarations<'b>(
628        &'b self,
629        node: &'b Node,
630        label_name: &str,
631        labels: &mut Vec<&'b Node>,
632    ) {
633        if let NodeKind::LabeledStatement { label, .. } = &node.kind
634            && label == label_name
635        {
636            labels.push(node);
637        }
638
639        for child in self.get_children(node) {
640            self.collect_label_declarations(child, label_name, labels);
641        }
642    }
643
644    fn get_labeled_statement_label_range(&self, node: &Node) -> (usize, usize) {
645        let NodeKind::LabeledStatement { label, .. } = &node.kind else {
646            return (node.location.start, node.location.end);
647        };
648
649        let start = node.location.start;
650        let end = node.location.end.min(self.content.len());
651        if start >= end {
652            return (node.location.start, node.location.end);
653        }
654
655        let text = &self.content[start..end];
656        let label_start = text.find(label).map_or(start, |idx| start + idx);
657        let label_end = label_start.saturating_add(label.len()).min(end);
658        (label_start, label_end)
659    }
660
661    fn identifier_is_goto_target(&self, node: &Node) -> bool {
662        let temp_parent_map;
663        let parent_map = if let Some(pm) = self.parent_map {
664            pm
665        } else {
666            temp_parent_map = {
667                let mut map = FxHashMap::default();
668                Self::build_parent_map(&self.ast, &mut map, None);
669                map
670            };
671            &temp_parent_map
672        };
673        let node_lookup = self.build_node_lookup_map();
674
675        let node_ptr = node as *const _;
676        let Some(parent_ptr) = parent_map.get(&node_ptr).copied() else {
677            return false;
678        };
679        let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
680            return false;
681        };
682
683        match &parent.kind {
684            NodeKind::Goto { target } => std::ptr::eq(target.as_ref(), node),
685            _ => false,
686        }
687    }
688
689    /// Find the definition of the method that a modifier string argument targets.
690    ///
691    /// When the cursor is on the string `'save'` in `before 'save' => sub { }`,
692    /// this walks up the parent map to confirm the string is the first argument
693    /// of a `before`/`after`/`around` function call, then returns the location of
694    /// `sub save { }`.
695    fn find_modifier_target_declaration(
696        &self,
697        string_node: &Node,
698        method_name: &str,
699    ) -> Option<Vec<LocationLink>> {
700        // Strip surrounding quotes from the raw token text ('save' → save, "save" → save).
701        let bare_name = method_name.trim().trim_matches('\'').trim_matches('"').trim();
702        if bare_name.is_empty() {
703            return None;
704        }
705
706        // Build parent map for upward traversal.
707        let temp_parent_map;
708        let parent_map = if let Some(pm) = self.parent_map {
709            pm
710        } else {
711            temp_parent_map = {
712                let mut map = FxHashMap::default();
713                Self::build_parent_map(&self.ast, &mut map, None);
714                map
715            };
716            &temp_parent_map
717        };
718        let node_lookup = self.build_node_lookup_map();
719
720        // Walk up: String → FunctionCall { name: "before"/"after"/"around" }
721        // The String node may be a direct child of the FunctionCall's args list,
722        // so its immediate parent should be the FunctionCall node.
723        let string_ptr: *const Node = string_node as *const _;
724        let parent_ptr = parent_map.get(&string_ptr).copied()?;
725        let parent = node_lookup.get(&parent_ptr).copied()?;
726
727        // Check direct parent is a modifier FunctionCall where the string is first arg.
728        if let NodeKind::FunctionCall { name, args } = &parent.kind {
729            if matches!(name.as_str(), "before" | "after" | "around" | "override") {
730                if args.first().map(|a| std::ptr::eq(a, string_node)).unwrap_or(false) {
731                    return self.find_subroutine_declaration(string_node, bare_name);
732                }
733            }
734        }
735
736        // The FunctionCall may be wrapped in an ExpressionStatement — check one
737        // level further up in case the parent is the statement wrapper.
738        let grandparent_ptr = parent_map.get(&parent_ptr).copied()?;
739        let grandparent = node_lookup.get(&grandparent_ptr).copied()?;
740
741        if let NodeKind::FunctionCall { name, args } = &grandparent.kind {
742            if matches!(name.as_str(), "before" | "after" | "around" | "override") {
743                if args.first().map(|a| std::ptr::eq(a, string_node)).unwrap_or(false) {
744                    return self.find_subroutine_declaration(string_node, bare_name);
745                }
746            }
747        }
748
749        None
750    }
751
752    /// Find the current package context for a node
753    fn find_current_package<'b>(&'b self, node: &Node) -> Option<&'b str> {
754        // SAFETY: `node` is a shared reference into the `Arc<Node>` AST tree held
755        // by `DeclarationProvider<'a>`.  The raw pointer is used only as a hash key
756        // to query the `parent_map`; it is never dereferenced.  Safe `&Node`
757        // references are recovered through `node_lookup`, which re-derives them
758        // from the same live `Arc<Node>` tree.
759        let mut current_ptr: *const Node = node as *const _;
760
761        // Build temporary parent map if not provided (for testing)
762        let temp_parent_map;
763        let parent_map = if let Some(pm) = self.parent_map {
764            pm
765        } else {
766            temp_parent_map = {
767                let mut map = FxHashMap::default();
768                Self::build_parent_map(&self.ast, &mut map, None);
769                map
770            };
771            &temp_parent_map
772        };
773        let node_lookup = self.build_node_lookup_map();
774
775        while let Some(&parent_ptr) = parent_map.get(&current_ptr) {
776            let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
777                break;
778            };
779
780            // Check siblings before this node for package declarations
781            for child in self.get_children(parent) {
782                if child.location.start >= node.location.start {
783                    break;
784                }
785
786                if let NodeKind::Package { name, .. } = &child.kind {
787                    return Some(name.as_str());
788                }
789            }
790
791            current_ptr = parent_ptr;
792        }
793
794        None
795    }
796
797    /// Create a location link
798    fn create_location_link(
799        &self,
800        origin: &Node,
801        target: &Node,
802        name_range: (usize, usize),
803    ) -> LocationLink {
804        LocationLink {
805            origin_selection_range: (origin.location.start, origin.location.end),
806            target_uri: self.document_uri.clone(),
807            target_range: (target.location.start, target.location.end),
808            target_selection_range: name_range,
809        }
810    }
811
812    // Helper methods
813
814    fn find_node_at_offset<'b>(&'b self, node: &'b Node, offset: usize) -> Option<&'b Node> {
815        if offset >= node.location.start && offset <= node.location.end {
816            // Check children first for more specific match
817            for child in self.get_children(node) {
818                if let Some(found) = self.find_node_at_offset(child, offset) {
819                    return Some(found);
820                }
821            }
822            return Some(node);
823        }
824        None
825    }
826
827    fn collect_subroutine_declarations<'b>(
828        &'b self,
829        node: &'b Node,
830        sub_name: &str,
831        subs: &mut Vec<&'b Node>,
832    ) {
833        if let NodeKind::Subroutine { name, .. } = &node.kind {
834            if let Some(name_str) = name {
835                if name_str == sub_name {
836                    subs.push(node);
837                }
838            }
839        }
840
841        for child in self.get_children(node) {
842            self.collect_subroutine_declarations(child, sub_name, subs);
843        }
844    }
845
846    fn find_package_declarations<'b>(&'b self, node: &'b Node, pkg_name: &str) -> Vec<&'b Node> {
847        let mut packages = Vec::new();
848        self.collect_package_declarations(node, pkg_name, &mut packages);
849        packages
850    }
851
852    fn collect_package_declarations<'b>(
853        &'b self,
854        node: &'b Node,
855        pkg_name: &str,
856        packages: &mut Vec<&'b Node>,
857    ) {
858        if let NodeKind::Package { name, .. } = &node.kind {
859            if name == pkg_name {
860                packages.push(node);
861            }
862        }
863
864        for child in self.get_children(node) {
865            self.collect_package_declarations(child, pkg_name, packages);
866        }
867    }
868
869    fn find_constant_declarations<'b>(&'b self, node: &'b Node, const_name: &str) -> Vec<&'b Node> {
870        let mut constants = Vec::new();
871        self.collect_constant_declarations(node, const_name, &mut constants);
872        constants
873    }
874
875    /// Strip leading -options from constant args
876    fn strip_constant_options<'b>(&self, args: &'b [String]) -> &'b [String] {
877        let mut i = 0;
878        while i < args.len() && args[i].starts_with('-') {
879            i += 1;
880        }
881        // Also skip a comma if present after options
882        if i < args.len() && args[i] == "," {
883            i += 1;
884        }
885        &args[i..]
886    }
887
888    fn collect_constant_declarations<'b>(
889        &'b self,
890        node: &'b Node,
891        const_name: &str,
892        constants: &mut Vec<&'b Node>,
893    ) {
894        if let NodeKind::Use { module, args, .. } = &node.kind {
895            if module == "constant" {
896                // Strip leading options like -strict, -nonstrict, -force
897                let stripped_args = self.strip_constant_options(args);
898
899                // Form 1: FOO => ...
900                if stripped_args.first().map(|s| s.as_str()) == Some(const_name) {
901                    constants.push(node);
902                    // keep scanning siblings too (there can be multiple `use constant`)
903                }
904
905                // Flattened args text once (cheap)
906                let args_text = stripped_args.join(" ");
907
908                // Form 2: { FOO => 1, BAR => 2 }
909                if self.contains_name_in_hash(&args_text, const_name) {
910                    constants.push(node);
911                }
912
913                // Form 3: qw(FOO BAR) / qw/FOO BAR/
914                if self.contains_name_in_qw(&args_text, const_name) {
915                    constants.push(node);
916                }
917            }
918        }
919
920        for child in self.get_children(node) {
921            self.collect_constant_declarations(child, const_name, constants);
922        }
923    }
924
925    /// Check if a byte is part of an ASCII identifier
926    #[inline]
927    fn is_ident_ascii(b: u8) -> bool {
928        matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_')
929    }
930
931    /// Iterate over all qw windows in the string
932    /// Handles both paired delimiters ((), [], {}, <>) and symmetric delimiters (|, !, #, etc.)
933    fn for_each_qw_window<F>(&self, s: &str, mut f: F) -> bool
934    where
935        F: FnMut(usize, usize) -> bool,
936    {
937        let b = s.as_bytes();
938        let mut i = 0;
939        while i + 1 < b.len() {
940            // find literal "qw"
941            if b[i] == b'q' && b[i + 1] == b'w' {
942                let mut j = i + 2;
943
944                // allow whitespace between qw and delimiter
945                while j < b.len() && (b[j] as char).is_ascii_whitespace() {
946                    j += 1;
947                }
948                if j >= b.len() {
949                    break;
950                }
951
952                let open = b[j] as char;
953
954                // "qwerty" guard: next non-ws must be a NON-word delimiter
955                // (i.e., not [A-Za-z0-9_])
956                if open.is_ascii_alphanumeric() || open == '_' {
957                    i += 1;
958                    continue;
959                }
960
961                // choose closing delimiter
962                let close = match open {
963                    '(' => ')',
964                    '[' => ']',
965                    '{' => '}',
966                    '<' => '>',
967                    _ => open, // symmetric delimiter (|, !, #, /, ~, ...)
968                };
969
970                // advance past opener and collect until closer
971                j += 1;
972                let start = j;
973                while j < b.len() && (b[j] as char) != close {
974                    j += 1;
975                }
976                if j <= b.len() {
977                    // Found the closing delimiter
978                    if f(start, j) {
979                        return true;
980                    }
981                    // continue scanning after the closer
982                    i = j + 1;
983                    continue;
984                } else {
985                    // unclosed; stop scanning
986                    break;
987                }
988            }
989
990            i += 1;
991        }
992        false
993    }
994
995    /// Iterate over all {...} pairs in the string
996    fn for_each_brace_window<F>(&self, s: &str, mut f: F) -> bool
997    where
998        F: FnMut(usize, usize) -> bool,
999    {
1000        let b = s.as_bytes();
1001        let mut i = 0;
1002        while i < b.len() {
1003            if b[i] == b'{' {
1004                let start = i + 1;
1005                let mut nesting = 1;
1006                let mut j = i + 1;
1007                while j < b.len() {
1008                    match b[j] {
1009                        b'{' => nesting += 1,
1010                        b'}' => {
1011                            nesting -= 1;
1012                            if nesting == 0 {
1013                                break;
1014                            }
1015                        }
1016                        _ => {}
1017                    }
1018                    j += 1;
1019                }
1020
1021                if nesting == 0 {
1022                    // Found matching closing brace at j
1023                    if f(start, j) {
1024                        return true;
1025                    }
1026                    i = j + 1;
1027                    continue;
1028                }
1029            }
1030            i += 1;
1031        }
1032        false
1033    }
1034
1035    fn contains_name_in_hash(&self, s: &str, name: &str) -> bool {
1036        // for { FOO => 1, BAR => 2 } form - check all {...} pairs
1037        self.for_each_brace_window(s, |start, end| {
1038            // only scan that slice
1039            self.find_word(&s[start..end], name).is_some()
1040        })
1041    }
1042
1043    fn contains_name_in_qw(&self, s: &str, name: &str) -> bool {
1044        // looks for qw(...) / qw[...] / qw/.../ etc. with word boundaries
1045        self.for_each_qw_window(s, |start, end| {
1046            // tokens are whitespace separated
1047            s[start..end].split_whitespace().any(|tok| tok == name)
1048        })
1049    }
1050
1051    fn find_word(&self, hay: &str, needle: &str) -> Option<(usize, usize)> {
1052        if needle.is_empty() {
1053            return None;
1054        }
1055        let mut find_from = 0;
1056        while let Some(hit) = hay[find_from..].find(needle) {
1057            let start = find_from + hit;
1058            let end = start + needle.len();
1059            let left_ok = start == 0 || !Self::is_ident_ascii(hay.as_bytes()[start - 1]);
1060            let right_ok = end == hay.len()
1061                || !Self::is_ident_ascii(*hay.as_bytes().get(end).unwrap_or(&b' '));
1062            if left_ok && right_ok {
1063                return Some((start, end));
1064            }
1065            find_from = end;
1066        }
1067        None
1068    }
1069
1070    fn first_all_caps_word(&self, s: &str) -> Option<(usize, usize)> {
1071        // very small scanner: find FOO-ish
1072        let bytes = s.as_bytes();
1073        let mut i = 0;
1074        while i < bytes.len() {
1075            while i < bytes.len() && !Self::is_ident_ascii(bytes[i]) {
1076                i += 1;
1077            }
1078            let start = i;
1079            while i < bytes.len() && Self::is_ident_ascii(bytes[i]) {
1080                i += 1;
1081            }
1082            if start < i {
1083                let w = &s[start..i];
1084                if w.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') {
1085                    return Some((start, i));
1086                }
1087            }
1088        }
1089        None
1090    }
1091
1092    fn get_subroutine_name_range(&self, decl: &Node) -> (usize, usize) {
1093        if let NodeKind::Subroutine { name_span: Some(loc), .. } = &decl.kind {
1094            (loc.start, loc.end)
1095        } else {
1096            (decl.location.start, decl.location.end)
1097        }
1098    }
1099
1100    fn get_package_name_range(&self, decl: &Node) -> (usize, usize) {
1101        if let NodeKind::Package { name_span, .. } = &decl.kind {
1102            (name_span.start, name_span.end)
1103        } else {
1104            (decl.location.start, decl.location.end)
1105        }
1106    }
1107
1108    fn get_constant_name_range(&self, decl: &Node) -> (usize, usize) {
1109        let text = self.get_node_text(decl);
1110
1111        // Prefer an exact span if we can find the first occurrence with word boundaries
1112        if let NodeKind::Use { args, .. } = &decl.kind {
1113            let best_guess = args.first().map(|s| s.as_str()).unwrap_or("");
1114            if let Some((lo, hi)) = self.find_word(&text, best_guess) {
1115                let abs_lo = decl.location.start + lo;
1116                let abs_hi = decl.location.start + hi;
1117                return (abs_lo, abs_hi);
1118            }
1119        }
1120
1121        // Try any constant-looking all-caps token in the decl
1122        if let Some((lo, hi)) = self.first_all_caps_word(&text) {
1123            return (decl.location.start + lo, decl.location.start + hi);
1124        }
1125
1126        // Fallback to whole range
1127        (decl.location.start, decl.location.end)
1128    }
1129
1130    fn get_constant_name_range_for(&self, decl: &Node, name: &str) -> (usize, usize) {
1131        let text = self.get_node_text(decl);
1132
1133        // Fast path: try to find the exact word
1134        if let Some((lo, hi)) = self.find_word(&text, name) {
1135            return (decl.location.start + lo, decl.location.start + hi);
1136        }
1137
1138        // Try inside all qw(...) windows
1139        let mut found_range = None;
1140        self.for_each_qw_window(&text, |start, end| {
1141            // Find the exact token position within this qw window
1142            if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
1143                found_range =
1144                    Some((decl.location.start + start + lo, decl.location.start + start + hi));
1145                true // Stop searching
1146            } else {
1147                false // Continue to next window
1148            }
1149        });
1150        if let Some(range) = found_range {
1151            return range;
1152        }
1153
1154        // Try inside all { ... } blocks (hash form)
1155        self.for_each_brace_window(&text, |start, end| {
1156            if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
1157                found_range =
1158                    Some((decl.location.start + start + lo, decl.location.start + start + hi));
1159                true // Stop searching
1160            } else {
1161                false // Continue to next window
1162            }
1163        });
1164        if let Some(range) = found_range {
1165            return range;
1166        }
1167
1168        // Final fallback to heuristics
1169        self.get_constant_name_range(decl)
1170    }
1171
1172    fn get_children<'b>(&self, node: &'b Node) -> Vec<&'b Node> {
1173        Self::get_children_static(node)
1174    }
1175
1176    /// Build a lookup map from raw node pointers back to safe references.
1177    ///
1178    /// This map is the bridge that makes `ParentMap` safe to use: callers
1179    /// obtain a `*const Node` from the parent map and look it up here to
1180    /// recover a properly-lifetime-bounded `&Node`.  The raw pointer is
1181    /// used purely as an identity key — it is never dereferenced directly.
1182    fn build_node_lookup_map(&self) -> FxHashMap<*const Node, &Node> {
1183        let mut map = FxHashMap::default();
1184        Self::build_node_lookup(self.ast.as_ref(), &mut map);
1185        map
1186    }
1187
1188    fn build_node_lookup<'b>(node: &'b Node, map: &mut FxHashMap<*const Node, &'b Node>) {
1189        // SAFETY: `node` is a shared reference whose lifetime `'b` is tied to
1190        // `self.ast` (`Arc<Node>`).  We store the address as a raw-pointer key
1191        // alongside the same reference as the value.  The value is the safe
1192        // side of this pair — it is the only route through which the pointer
1193        // is ever turned back into usable data.
1194        map.insert(node as *const Node, node);
1195        for child in Self::get_children_static(node) {
1196            Self::build_node_lookup(child, map);
1197        }
1198    }
1199
1200    fn get_children_static(node: &Node) -> Vec<&Node> {
1201        match &node.kind {
1202            NodeKind::Program { statements } => statements.iter().collect(),
1203            NodeKind::Block { statements } => statements.iter().collect(),
1204            NodeKind::If { condition, then_branch, else_branch, .. } => {
1205                let mut children = vec![condition.as_ref(), then_branch.as_ref()];
1206                if let Some(else_b) = else_branch {
1207                    children.push(else_b.as_ref());
1208                }
1209                children
1210            }
1211            NodeKind::Binary { left, right, .. } => vec![left.as_ref(), right.as_ref()],
1212            NodeKind::Unary { operand, .. } => vec![operand.as_ref()],
1213            NodeKind::Return { value } => {
1214                if let Some(value) = value {
1215                    vec![value.as_ref()]
1216                } else {
1217                    vec![]
1218                }
1219            }
1220            NodeKind::VariableDeclaration { variable, initializer, .. } => {
1221                let mut children = vec![variable.as_ref()];
1222                if let Some(init) = initializer {
1223                    children.push(init.as_ref());
1224                }
1225                children
1226            }
1227            NodeKind::Method { signature, body, .. } => {
1228                let mut children = vec![body.as_ref()];
1229                if let Some(sig) = signature {
1230                    children.push(sig.as_ref());
1231                }
1232                children
1233            }
1234            NodeKind::Subroutine { signature, body, .. } => {
1235                let mut children = vec![body.as_ref()];
1236                if let Some(sig) = signature {
1237                    children.push(sig.as_ref());
1238                }
1239                children
1240            }
1241            NodeKind::FunctionCall { args, .. } => args.iter().collect(),
1242            NodeKind::MethodCall { object, args, .. } => {
1243                let mut children = vec![object.as_ref()];
1244                children.extend(args.iter());
1245                children
1246            }
1247            NodeKind::IndirectCall { object, args, .. } => {
1248                let mut children = vec![object.as_ref()];
1249                children.extend(args.iter());
1250                children
1251            }
1252            NodeKind::While { condition, body, .. } => {
1253                vec![condition.as_ref(), body.as_ref()]
1254            }
1255            NodeKind::For { init, condition, update, body, .. } => {
1256                let mut children = Vec::new();
1257                if let Some(i) = init {
1258                    children.push(i.as_ref());
1259                }
1260                if let Some(c) = condition {
1261                    children.push(c.as_ref());
1262                }
1263                if let Some(u) = update {
1264                    children.push(u.as_ref());
1265                }
1266                children.push(body.as_ref());
1267                children
1268            }
1269            NodeKind::Foreach { variable, list, body, .. } => {
1270                vec![variable.as_ref(), list.as_ref(), body.as_ref()]
1271            }
1272            NodeKind::ExpressionStatement { expression } => vec![expression.as_ref()],
1273            _ => vec![],
1274        }
1275    }
1276
1277    /// Extracts the source code text for a given AST node.
1278    ///
1279    /// Returns the substring of the document content corresponding to
1280    /// the node's location range. Used for symbol name extraction and
1281    /// text-based analysis.
1282    ///
1283    /// # Arguments
1284    /// * `node` - AST node to extract text from
1285    ///
1286    /// # Performance
1287    /// - Time complexity: O(m) where m is node text length
1288    /// - Memory: Creates owned string copy
1289    /// - Typical latency: <10μs for identifier names
1290    ///
1291    /// # Examples
1292    /// ```rust,ignore
1293    /// use perl_parser::declaration::DeclarationProvider;
1294    /// use perl_parser::ast::Node;
1295    /// use std::sync::Arc;
1296    ///
1297    /// let provider = DeclarationProvider::new(
1298    ///     Arc::new(Node::new_root()),
1299    ///     "sub example { }".to_string(),
1300    ///     "uri".to_string()
1301    /// );
1302    /// // let text = provider.get_node_text(&some_node);
1303    /// ```
1304    pub fn get_node_text(&self, node: &Node) -> String {
1305        self.content[node.location.start..node.location.end].to_string()
1306    }
1307}
1308
1309/// Extracts a symbol key from the AST node at the given cursor position.
1310///
1311/// Analyzes the AST at a specific byte offset to identify the symbol under
1312/// the cursor for LSP operations. Supports function calls, variable references,
1313/// and package-qualified symbols with full Perl syntax coverage.
1314///
1315/// # Arguments
1316/// * `ast` - Root AST node to search within
1317/// * `offset` - Byte offset in the source document
1318/// * `current_pkg` - Current package context for symbol resolution
1319///
1320/// # Returns
1321/// * `Some(SymbolKey)` - Symbol found at position with package qualification
1322/// * `None` - No symbol at the given position
1323///
1324/// # Performance
1325/// - Search time: O(log n) average case with spatial indexing
1326/// - Worst case: O(n) for unbalanced AST traversal
1327/// - Typical latency: <50μs for LSP responsiveness
1328///
1329/// # Perl Parsing Context
1330/// Handles complex Perl symbol patterns:
1331/// - Package-qualified calls: `Package::function`
1332/// - Bare function calls: `function` (resolved in current package)
1333/// - Variable references: `$var`, `@array`, `%hash`
1334/// - Method calls: `$obj->method`
1335///
1336/// # Examples
1337/// ```rust,ignore
1338/// use perl_parser::declaration::symbol_at_cursor;
1339/// use perl_parser::ast::Node;
1340///
1341/// let ast = Node::new_root();
1342/// let symbol = symbol_at_cursor(&ast, 42, "MyPackage");
1343/// if let Some(sym) = symbol {
1344///     println!("Found symbol: {:?}", sym);
1345/// }
1346/// ```
1347fn symbol_at_cursor_internal(
1348    ast: &Node,
1349    offset: usize,
1350    current_pkg: &str,
1351    source_text: &str,
1352) -> Option<SymbolKey> {
1353    fn collect_node_path_at_offset<'a>(
1354        node: &'a Node,
1355        offset: usize,
1356        path: &mut Vec<&'a Node>,
1357    ) -> bool {
1358        if offset < node.location.start || offset > node.location.end {
1359            return false;
1360        }
1361
1362        path.push(node);
1363
1364        for child in get_node_children(node) {
1365            if collect_node_path_at_offset(child, offset, path) {
1366                return true;
1367            }
1368        }
1369
1370        true
1371    }
1372
1373    fn find_symbol_node_at_offset(ast: &Node, offset: usize) -> Option<(Vec<&Node>, &Node)> {
1374        let mut path = Vec::new();
1375        if !collect_node_path_at_offset(ast, offset, &mut path) {
1376            return None;
1377        }
1378
1379        let node = path
1380            .iter()
1381            .rev()
1382            .copied()
1383            .find(|node| {
1384                matches!(
1385                    node.kind,
1386                    NodeKind::Variable { .. }
1387                        | NodeKind::FunctionCall { .. }
1388                        | NodeKind::Subroutine { .. }
1389                        | NodeKind::MethodCall { .. }
1390                        | NodeKind::Use { .. }
1391                )
1392            })
1393            .or_else(|| path.last().copied())?;
1394
1395        Some((path, node))
1396    }
1397
1398    fn node_variable_name(node: &Node) -> Option<&str> {
1399        if let NodeKind::Variable { name, .. } = &node.kind { Some(name.as_str()) } else { None }
1400    }
1401
1402    fn normalize_symbol_name(raw: &str) -> Option<String> {
1403        let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
1404        if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
1405    }
1406
1407    fn token_at_offset_in_text(text: &str, rel_offset: usize) -> Option<String> {
1408        let bytes = text.as_bytes();
1409        if rel_offset >= bytes.len() {
1410            return None;
1411        }
1412        let is_ident = |b: u8| matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b':');
1413        if !is_ident(bytes[rel_offset]) {
1414            return None;
1415        }
1416
1417        let mut start = rel_offset;
1418        while start > 0 && is_ident(bytes[start - 1]) {
1419            start -= 1;
1420        }
1421        let mut end = rel_offset + 1;
1422        while end < bytes.len() && is_ident(bytes[end]) {
1423            end += 1;
1424        }
1425        Some(text[start..end].to_string())
1426    }
1427
1428    fn export_tag_members(module: &str, tag: &str) -> &'static [&'static str] {
1429        match (module, tag) {
1430            // POSIX tag sets commonly used in system scripts.
1431            ("POSIX", ":sys_wait_h") => {
1432                &["WEXITSTATUS", "WIFEXITED", "WIFSIGNALED", "WIFSTOPPED", "WTERMSIG"]
1433            }
1434            ("POSIX", ":fcntl_h") => &["F_GETFD", "F_SETFD", "F_GETFL", "F_SETFL", "FD_CLOEXEC"],
1435            ("POSIX", ":termios_h") => {
1436                &["B9600", "B19200", "B38400", "TCSANOW", "TCSADRAIN", "TCSAFLUSH"]
1437            }
1438            // File::Find exports.
1439            ("File::Find", ":find") => &["find", "finddepth"],
1440            // Fcntl exports.
1441            ("Fcntl", ":seek") => &["SEEK_SET", "SEEK_CUR", "SEEK_END"],
1442            ("Fcntl", ":lock") => &["LOCK_SH", "LOCK_EX", "LOCK_NB", "LOCK_UN"],
1443            // Encode exports.
1444            ("Encode", ":fallback") => &[
1445                "FB_DEFAULT",
1446                "FB_CROAK",
1447                "FB_QUIET",
1448                "FB_WARN",
1449                "FB_PERLQQ",
1450                "FB_HTMLCREF",
1451                "FB_XMLCREF",
1452            ],
1453            _ => &[],
1454        }
1455    }
1456
1457    fn tag_imports_symbol(module: &str, import_token: &str, symbol_name: &str) -> bool {
1458        if !import_token.starts_with(':') {
1459            return false;
1460        }
1461        export_tag_members(module, import_token).contains(&symbol_name)
1462    }
1463
1464    /// Pragmas and structural modules whose qw/string arguments are NOT
1465    /// imported symbol names. Cursor-on-arg for these should not resolve
1466    /// to a bogus `SymbolKey` — they carry inheritance lists, feature names,
1467    /// or other non-import semantics.
1468    const NON_IMPORT_PRAGMAS: &[&str] = &[
1469        "constant", // constant definitions, not imports
1470        "parent",   // inheritance: qw/string args are class names
1471        "base",     // legacy inheritance
1472        "vars",     // variable declarations, not imports
1473        "Exporter", // 'import' arg is a proxy method, not an imported symbol
1474        "mro",      // method resolution order pragma
1475        "if",       // conditional module load
1476        "lib",      // adds directories to @INC
1477        "feature",  // enables Perl feature flags
1478        "utf8",     // encoding pragma
1479    ];
1480
1481    fn use_args_import_symbol(module: &str, args: &[String], symbol_name: &str) -> bool {
1482        args.iter().any(|arg| {
1483            if arg == symbol_name || tag_imports_symbol(module, arg, symbol_name) {
1484                return true;
1485            }
1486
1487            if arg.starts_with("qw") {
1488                let content = arg
1489                    .trim_start_matches("qw")
1490                    .trim_start_matches(|c: char| "([{/<|!".contains(c))
1491                    .trim_end_matches(|c: char| ")]}/|!>".contains(c));
1492                return content
1493                    .split_whitespace()
1494                    .any(|tok| tok == symbol_name || tag_imports_symbol(module, tok, symbol_name));
1495            }
1496
1497            let bare = arg.trim().trim_matches('\'').trim_matches('"').trim();
1498            bare == symbol_name || tag_imports_symbol(module, bare, symbol_name)
1499        })
1500    }
1501
1502    fn find_import_source(ast: &Node, symbol_name: &str) -> Option<String> {
1503        /// Extract the module name from a `require Module;` statement node.
1504        ///
1505        /// Matches both `require Foo::Bar` (Identifier arg) and
1506        /// `require "Foo/Bar.pm"` forms, returning the module name as a
1507        /// `::` -separated string suitable for workspace lookup.
1508        fn require_module_name(node: &Node) -> Option<String> {
1509            let args = match &node.kind {
1510                NodeKind::FunctionCall { name, args } if name == "require" => args,
1511                _ => return None,
1512            };
1513            let arg = args.first()?;
1514            match &arg.kind {
1515                NodeKind::Identifier { name } => Some(name.clone()),
1516                NodeKind::String { value, .. } => {
1517                    // "Foo/Bar.pm" -> "Foo::Bar"
1518                    let cleaned = value.trim_matches('\'').trim_matches('"').trim();
1519                    let module = cleaned.trim_end_matches(".pm").replace('/', "::");
1520                    Some(module)
1521                }
1522                _ => None,
1523            }
1524        }
1525
1526        /// Check whether a MethodCall node is `Module->import(...)` and, if
1527        /// so, whether its argument list contains `symbol`.  Handles four
1528        /// argument forms:
1529        /// - bare string literals:  `->import('foo', 'bar')`
1530        /// - qw list as ArrayLit:   `->import(qw(foo bar))` → ArrayLiteral
1531        /// - Identifier nodes:      `->import(foo)` (unusual but legal)
1532        /// - String value trimming: quoted strings like `"'foo'"` from qw
1533        fn import_call_exports(
1534            method_node: &Node,
1535            expected_module: &str,
1536            symbol: &str,
1537            aliases: &std::collections::HashMap<String, String>,
1538        ) -> bool {
1539            let (object, method, args) = match &method_node.kind {
1540                NodeKind::MethodCall { object, method, args } => (object, method, args),
1541                _ => return false,
1542            };
1543            if method != "import" {
1544                return false;
1545            }
1546            // The object must be the same module name.
1547            let obj_name = match &object.kind {
1548                NodeKind::Identifier { name } => Some(name.as_str()),
1549                NodeKind::Variable { name, .. } => aliases.get(name).map(String::as_str),
1550                _ => return false,
1551            };
1552            let Some(obj_name) = obj_name else {
1553                return false;
1554            };
1555            if obj_name != expected_module {
1556                return false;
1557            }
1558            if args.is_empty() {
1559                // `Module->import()` default import set is module-specific and may
1560                // come from `@EXPORT` in another file.  We do not currently have
1561                // a workspace export table in this lookup path, so stay
1562                // conservative and do not claim symbol ownership here.
1563                return false;
1564            }
1565            // Walk the argument list looking for the symbol.
1566            for arg in args {
1567                if arg_node_matches_symbol(arg, expected_module, symbol) {
1568                    return true;
1569                }
1570            }
1571            false
1572        }
1573
1574        /// Check whether a single AST argument node matches `symbol`.
1575        /// Handles: String literals, Identifiers (including raw "qw(...)"),
1576        /// and ArrayLiteral (the AST form produced by `qw(...)` in expression
1577        /// context).
1578        fn arg_node_matches_symbol(arg: &Node, module: &str, symbol: &str) -> bool {
1579            match &arg.kind {
1580                NodeKind::String { value, .. } => {
1581                    // Strip surrounding single/double quotes that some code
1582                    // paths leave in the value (e.g. qw in quotes.rs).
1583                    let bare = value.trim_matches('\'').trim_matches('"');
1584                    bare == symbol || tag_imports_symbol(module, bare, symbol)
1585                }
1586                NodeKind::Identifier { name } => {
1587                    if name == symbol {
1588                        return true;
1589                    }
1590                    // qw(...) stored as a raw "qw(...)" Identifier string
1591                    // (from the Use-node code path that reuses this helper).
1592                    if name.starts_with("qw") {
1593                        let content = name
1594                            .trim_start_matches("qw")
1595                            .trim_start_matches(|c: char| "([{/<|!".contains(c))
1596                            .trim_end_matches(|c: char| ")]}/|!>".contains(c));
1597                        return content
1598                            .split_whitespace()
1599                            .any(|tok| tok == symbol || tag_imports_symbol(module, tok, symbol));
1600                    }
1601                    false
1602                }
1603                NodeKind::ArrayLiteral { elements } => {
1604                    // qw(...) in expression context → ArrayLiteral of String nodes
1605                    elements.iter().any(|el| arg_node_matches_symbol(el, module, symbol))
1606                }
1607                _ => false,
1608            }
1609        }
1610
1611        fn module_runtime_alias(expr: &Node) -> Option<(String, String)> {
1612            let (alias_name, call_node) = match &expr.kind {
1613                NodeKind::Assignment { lhs, rhs, op } if op == "=" => {
1614                    let NodeKind::Variable { name, .. } = &lhs.kind else {
1615                        return None;
1616                    };
1617                    (name.as_str(), rhs.as_ref())
1618                }
1619                NodeKind::VariableDeclaration { variable, initializer: Some(rhs), .. } => {
1620                    let NodeKind::Variable { name, .. } = &variable.kind else {
1621                        return None;
1622                    };
1623                    (name.as_str(), rhs.as_ref())
1624                }
1625                _ => return None,
1626            };
1627
1628            let NodeKind::FunctionCall { name, args } = &call_node.kind else {
1629                return None;
1630            };
1631            if !matches!(
1632                name.as_str(),
1633                "use_module"
1634                    | "require_module"
1635                    | "Module::Runtime::use_module"
1636                    | "Module::Runtime::require_module"
1637            ) {
1638                return None;
1639            }
1640            let first = args.first()?;
1641            let NodeKind::String { value, .. } = &first.kind else {
1642                return None;
1643            };
1644            let module = value.trim_matches('\'').trim_matches('"').trim();
1645            if module.is_empty() {
1646                return None;
1647            }
1648            Some((alias_name.to_string(), module.to_string()))
1649        }
1650
1651        /// Unwrap an ExpressionStatement to its inner expression, or return
1652        /// the node unchanged (handles the case where we're already at the
1653        /// expression level).
1654        fn inner_expr(node: &Node) -> &Node {
1655            if let NodeKind::ExpressionStatement { expression } = &node.kind {
1656                expression.as_ref()
1657            } else {
1658                node
1659            }
1660        }
1661
1662        /// Scan a flat statement list for a `require M; M->import(...)` pair
1663        /// that exports `symbol`.  The require and import calls do not have to
1664        /// be adjacent — the import just needs to appear anywhere in the same
1665        /// statement list after (or even before) the require.
1666        fn scan_statements_for_require_import(stmts: &[Node], symbol: &str) -> Option<String> {
1667            // Collect all `require Module` names present in this block.
1668            let mut required_modules: Vec<String> =
1669                stmts.iter().filter_map(|s| require_module_name(inner_expr(s))).collect();
1670            let mut aliases: std::collections::HashMap<String, String> =
1671                std::collections::HashMap::new();
1672            for stmt in stmts {
1673                if let Some((alias, module)) = module_runtime_alias(inner_expr(stmt)) {
1674                    aliases.insert(alias, module.clone());
1675                    if !required_modules.contains(&module) {
1676                        required_modules.push(module);
1677                    }
1678                }
1679            }
1680
1681            if required_modules.is_empty() {
1682                return None;
1683            }
1684
1685            // Check whether any `Module->import(...)` call in this block
1686            // exports our symbol, using the set of required modules.
1687            for stmt in stmts {
1688                let expr = inner_expr(stmt);
1689                for module in &required_modules {
1690                    if import_call_exports(expr, module, symbol, &aliases) {
1691                        return Some(module.clone());
1692                    }
1693                }
1694            }
1695            None
1696        }
1697
1698        fn find(node: &Node, name: &str) -> Option<String> {
1699            if let NodeKind::Use { module, args, .. } = &node.kind {
1700                // Skip structural pragmas — their args are not import-list symbols
1701                if NON_IMPORT_PRAGMAS.contains(&module.as_str()) {
1702                    // Fall through to children
1703                } else {
1704                    for arg in args {
1705                        if arg == name {
1706                            return Some(module.clone());
1707                        }
1708                        if tag_imports_symbol(module, arg, name) {
1709                            return Some(module.clone());
1710                        }
1711                        if arg.starts_with("qw") {
1712                            let content = arg
1713                                .trim_start_matches("qw")
1714                                .trim_start_matches(|c: char| "([{/<|!".contains(c))
1715                                .trim_end_matches(|c: char| ")]}/|!>".contains(c));
1716                            for import_token in content.split_whitespace() {
1717                                if import_token == name
1718                                    || tag_imports_symbol(module, import_token, name)
1719                                {
1720                                    return Some(module.clone());
1721                                }
1722                            }
1723                        } else {
1724                            // Parenthesized import list: use Foo ('bar', 'baz')
1725                            // The parser emits each token as a separate arg including commas
1726                            // and string literals with their surrounding quotes.
1727                            let bare = arg.trim().trim_matches('\'').trim_matches('"').trim();
1728                            if bare == name {
1729                                return Some(module.clone());
1730                            }
1731                            if tag_imports_symbol(module, bare, name) {
1732                                return Some(module.clone());
1733                            }
1734                        }
1735                    }
1736                }
1737            }
1738
1739            // Scan block/program statement lists for `require M; M->import(sym)` patterns.
1740            let stmts = match &node.kind {
1741                NodeKind::Program { statements } => Some(statements.as_slice()),
1742                NodeKind::Block { statements } => Some(statements.as_slice()),
1743                _ => None,
1744            };
1745            if let Some(statements) = stmts {
1746                if let Some(module) = scan_statements_for_require_import(statements, name) {
1747                    return Some(module);
1748                }
1749            }
1750
1751            for child in get_node_children(node) {
1752                if let Some(module) = find(child, name) {
1753                    return Some(module);
1754                }
1755            }
1756
1757            None
1758        }
1759
1760        find(ast, symbol_name)
1761    }
1762
1763    fn plack_builder_middleware_symbol(path: &[&Node], offset: usize) -> Option<SymbolKey> {
1764        let has_builder = path.iter().any(|ancestor| {
1765            matches!(ancestor.kind, NodeKind::FunctionCall { ref name, .. } if name == "builder")
1766        });
1767        if !has_builder {
1768            return None;
1769        }
1770
1771        let block = path.iter().rev().find_map(|ancestor| {
1772            if let NodeKind::Block { statements } = &ancestor.kind {
1773                Some(statements)
1774            } else {
1775                None
1776            }
1777        })?;
1778
1779        for statement in block {
1780            let NodeKind::ExpressionStatement { expression } = &statement.kind else {
1781                continue;
1782            };
1783            let NodeKind::FunctionCall { name, args } = &expression.kind else {
1784                continue;
1785            };
1786            if name != "enable" {
1787                continue;
1788            }
1789
1790            let Some(first) = args.first() else {
1791                continue;
1792            };
1793            if offset < first.location.start || offset > first.location.end {
1794                continue;
1795            }
1796
1797            let raw_name = match &first.kind {
1798                NodeKind::String { value, .. } => normalize_symbol_name(value)?,
1799                NodeKind::Identifier { name } => name.clone(),
1800                _ => continue,
1801            };
1802
1803            let middleware_name = if raw_name.contains("::") {
1804                raw_name
1805            } else {
1806                format!("Plack::Middleware::{raw_name}")
1807            };
1808
1809            return Some(SymbolKey {
1810                pkg: middleware_name.clone().into(),
1811                name: middleware_name.into(),
1812                sigil: None,
1813                kind: SymKind::Pack,
1814            });
1815        }
1816
1817        None
1818    }
1819
1820    fn looks_like_package_name(name: &str) -> bool {
1821        name.contains("::") || name.chars().next().is_some_and(|ch| ch.is_ascii_uppercase())
1822    }
1823
1824    fn infer_receiver_package(
1825        object: &Node,
1826        current_pkg: &str,
1827        receiver_packages: &std::collections::HashMap<String, String>,
1828    ) -> Option<String> {
1829        if let NodeKind::Identifier { name } = &object.kind {
1830            return Some(name.clone());
1831        }
1832
1833        if let Some(name) = node_variable_name(object) {
1834            if let Some(package_name) = receiver_packages.get(name) {
1835                return Some(package_name.clone());
1836            }
1837
1838            if matches!(name, "self" | "this" | "class") {
1839                return Some(current_pkg.to_string());
1840            }
1841
1842            if looks_like_package_name(name) {
1843                return Some(name.to_string());
1844            }
1845        }
1846
1847        None
1848    }
1849
1850    fn infer_constructor_package(
1851        rhs: &Node,
1852        current_pkg: &str,
1853        receiver_packages: &std::collections::HashMap<String, String>,
1854    ) -> Option<String> {
1855        match &rhs.kind {
1856            NodeKind::MethodCall { method, object, .. } if method == "new" => {
1857                infer_receiver_package(object, current_pkg, receiver_packages)
1858            }
1859            NodeKind::FunctionCall { name, .. } => {
1860                name.rsplit_once("::").map(|(package_name, _)| package_name.to_string())
1861            }
1862            _ => None,
1863        }
1864    }
1865
1866    fn record_receiver_assignment(
1867        node: &Node,
1868        offset: usize,
1869        current_pkg: &str,
1870        receiver_packages: &mut std::collections::HashMap<String, String>,
1871    ) {
1872        if node.location.start > offset {
1873            return;
1874        }
1875
1876        if node.location.end <= offset {
1877            match &node.kind {
1878                NodeKind::VariableDeclaration { variable, initializer, .. } => {
1879                    if let (Some(variable_name), Some(initializer)) =
1880                        (node_variable_name(variable), initializer.as_ref())
1881                    {
1882                        if let Some(package_name) =
1883                            infer_constructor_package(initializer, current_pkg, receiver_packages)
1884                        {
1885                            receiver_packages.insert(variable_name.to_string(), package_name);
1886                        }
1887                    }
1888                }
1889                NodeKind::Assignment { lhs, rhs, .. } => {
1890                    if let Some(variable_name) = node_variable_name(lhs) {
1891                        if let Some(package_name) =
1892                            infer_constructor_package(rhs, current_pkg, receiver_packages)
1893                        {
1894                            receiver_packages.insert(variable_name.to_string(), package_name);
1895                        }
1896                    }
1897                }
1898                _ => {}
1899            }
1900        }
1901
1902        for child in get_node_children(node) {
1903            if child.location.start <= offset {
1904                record_receiver_assignment(child, offset, current_pkg, receiver_packages);
1905            }
1906        }
1907    }
1908
1909    let (path, node) = find_symbol_node_at_offset(ast, offset)?;
1910
1911    if let Some(symbol_key) = plack_builder_middleware_symbol(&path, offset) {
1912        return Some(symbol_key);
1913    }
1914
1915    match &node.kind {
1916        NodeKind::Variable { sigil, name } => {
1917            // Variable already has sigil separated
1918            let sigil_char = sigil.chars().next();
1919            Some(SymbolKey {
1920                pkg: current_pkg.into(),
1921                name: name.clone().into(),
1922                sigil: sigil_char,
1923                kind: SymKind::Var,
1924            })
1925        }
1926        NodeKind::FunctionCall { name, .. } => {
1927            let (pkg, bare) = if let Some(idx) = name.rfind("::") {
1928                (name[..idx].to_string(), name[idx + 2..].to_string())
1929            } else {
1930                (
1931                    find_import_source(ast, name).unwrap_or_else(|| current_pkg.to_string()),
1932                    name.clone(),
1933                )
1934            };
1935            Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
1936        }
1937        NodeKind::Subroutine { name: Some(name), .. } => {
1938            let (pkg, bare) = if let Some(idx) = name.rfind("::") {
1939                (&name[..idx], &name[idx + 2..])
1940            } else {
1941                (current_pkg, name.as_str())
1942            };
1943            Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
1944        }
1945        NodeKind::MethodCall { object, method, .. } => {
1946            let mut receiver_packages = std::collections::HashMap::new();
1947            record_receiver_assignment(ast, offset, current_pkg, &mut receiver_packages);
1948            let pkg = infer_receiver_package(object, current_pkg, &receiver_packages)
1949                .unwrap_or_else(|| current_pkg.to_string());
1950            Some(SymbolKey {
1951                pkg: pkg.into(),
1952                name: method.clone().into(),
1953                sigil: None,
1954                kind: SymKind::Sub,
1955            })
1956        }
1957        NodeKind::Use { module, args, .. } => {
1958            if !NON_IMPORT_PRAGMAS.contains(&module.as_str())
1959                && !source_text.is_empty()
1960                && offset >= node.location.start
1961                && offset <= node.location.end
1962            {
1963                let rel_offset = offset.saturating_sub(node.location.start);
1964                if let Some(stmt_text) = source_text.get(node.location.start..node.location.end)
1965                    && let Some(token) = token_at_offset_in_text(stmt_text, rel_offset)
1966                    && token != *module
1967                    && token != "use"
1968                    && use_args_import_symbol(module, args, &token)
1969                {
1970                    return Some(SymbolKey {
1971                        pkg: module.clone().into(),
1972                        name: token.into(),
1973                        sigil: None,
1974                        kind: SymKind::Sub,
1975                    });
1976                }
1977            }
1978
1979            // When cursor is on a `use Module::Name` statement, resolve to the package
1980            Some(SymbolKey {
1981                pkg: module.clone().into(),
1982                name: module.clone().into(),
1983                sigil: None,
1984                kind: SymKind::Pack,
1985            })
1986        }
1987        _ => None,
1988    }
1989}
1990
1991/// Extract a symbol key at a cursor offset with access to source text.
1992///
1993/// This variant is used by LSP handlers when additional source-aware
1994/// disambiguation is needed (for example, barewords in `use ... qw(...)` lists).
1995pub fn symbol_at_cursor_with_source(
1996    ast: &Node,
1997    offset: usize,
1998    current_pkg: &str,
1999    source_text: &str,
2000) -> Option<SymbolKey> {
2001    symbol_at_cursor_internal(ast, offset, current_pkg, source_text)
2002}
2003
2004/// Extract a symbol key at a cursor offset.
2005///
2006/// This keeps the historical API and defers to [`symbol_at_cursor_with_source`]
2007/// without source text-specific disambiguation.
2008pub fn symbol_at_cursor(ast: &Node, offset: usize, current_pkg: &str) -> Option<SymbolKey> {
2009    symbol_at_cursor_internal(ast, offset, current_pkg, "")
2010}
2011
2012/// Determines the current package context at the given offset.
2013///
2014/// Scans the AST backwards from the offset to find the most recent
2015/// package declaration, providing proper context for symbol resolution
2016/// in Perl's package-based namespace system.
2017///
2018/// # Arguments
2019/// * `ast` - Root AST node to search within
2020/// * `offset` - Byte offset in the source document
2021///
2022/// # Returns
2023/// Package name as string slice, defaults to "main" if no package found
2024///
2025/// # Performance
2026/// - Search time: O(n) worst case, O(log n) typical
2027/// - Memory: Returns borrowed string slice (zero-copy)
2028/// - Caching: Results suitable for per-request caching
2029///
2030/// # Perl Parsing Context
2031/// Perl package semantics:
2032/// - `package Foo;` declarations change current namespace
2033/// - Scope continues until next package declaration or EOF
2034/// - Default package is "main" when no explicit declaration
2035/// - Package names follow Perl identifier rules (`::`-separated)
2036///
2037/// # Examples
2038/// ```rust,ignore
2039/// use perl_parser::declaration::current_package_at;
2040/// use perl_parser::ast::Node;
2041///
2042/// let ast = Node::new_root();
2043/// let pkg = current_package_at(&ast, 100);
2044/// println!("Current package: {}", pkg);
2045/// ```
2046pub fn current_package_at(ast: &Node, offset: usize) -> &str {
2047    // Find the nearest package declaration before the offset
2048    fn scan<'a>(node: &'a Node, offset: usize, last: &mut Option<&'a str>) {
2049        if let NodeKind::Package { name, .. } = &node.kind {
2050            if node.location.start <= offset {
2051                *last = Some(name.as_str());
2052            }
2053        }
2054        for child in get_node_children(node) {
2055            if child.location.start <= offset {
2056                scan(child, offset, last);
2057            }
2058        }
2059    }
2060
2061    let mut last_pkg: Option<&str> = None;
2062    scan(ast, offset, &mut last_pkg);
2063    last_pkg.unwrap_or("main")
2064}
2065
2066/// Finds the most specific AST node containing the given byte offset.
2067///
2068/// Performs recursive descent through the AST to locate the deepest node
2069/// that encompasses the specified position. Essential for cursor-based
2070/// LSP operations like go-to-definition and hover.
2071///
2072/// # Arguments
2073/// * `node` - AST node to search within (typically root)
2074/// * `offset` - Byte offset in the source document
2075///
2076/// # Returns
2077/// * `Some(&Node)` - Deepest node containing the offset
2078/// * `None` - Offset is outside the node's range
2079///
2080/// # Performance
2081/// - Search time: O(log n) average, O(n) worst case
2082/// - Memory: Zero allocations, returns borrowed reference
2083/// - Spatial locality: Optimized for sequential offset queries
2084///
2085/// # LSP Integration
2086/// Core primitive for:
2087/// - Hover information: Find node for symbol details
2088/// - Go-to-definition: Identify symbol under cursor
2089/// - Completion: Determine context for suggestions
2090/// - Diagnostics: Map error positions to AST nodes
2091///
2092/// # Examples
2093/// ```rust,ignore
2094/// use perl_parser::declaration::find_node_at_offset;
2095/// use perl_parser::ast::Node;
2096///
2097/// let ast = Node::new_root();
2098/// if let Some(node) = find_node_at_offset(&ast, 42) {
2099///     println!("Found node: {:?}", node.kind);
2100/// }
2101/// ```
2102pub fn find_node_at_offset(node: &Node, offset: usize) -> Option<&Node> {
2103    if offset < node.location.start || offset > node.location.end {
2104        return None;
2105    }
2106
2107    // Check children first for more specific match
2108    let children = get_node_children(node);
2109    for child in children {
2110        if let Some(found) = find_node_at_offset(child, offset) {
2111            return Some(found);
2112        }
2113    }
2114
2115    // If no child contains the offset, return this node
2116    Some(node)
2117}
2118
2119/// Returns direct child nodes for a given AST node.
2120///
2121/// Provides generic access to child nodes across different node types,
2122/// essential for AST traversal algorithms and recursive analysis patterns.
2123///
2124/// # Arguments
2125/// * `node` - AST node to extract children from
2126///
2127/// # Returns
2128/// Vector of borrowed child node references
2129///
2130/// # Performance
2131/// - Time complexity: O(k) where k is child count
2132/// - Memory: Allocates vector for child references
2133/// - Typical latency: <5μs for common node types
2134///
2135/// # Examples
2136/// ```rust,ignore
2137/// use perl_parser::declaration::get_node_children;
2138/// use perl_parser::ast::Node;
2139///
2140/// let node = Node::new_root();
2141/// let children = get_node_children(&node);
2142/// println!("Node has {} children", children.len());
2143/// ```
2144pub fn get_node_children(node: &Node) -> Vec<&Node> {
2145    // Delegate to the AST node's own comprehensive children() method,
2146    // which handles all node kinds including Block, Package, MethodCall, etc.
2147    node.children()
2148}