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(¤t) {
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(¤t_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 ¶meter.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(¤t_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}