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::workspace_index::{SymKind, SymbolKey};
8use rustc_hash::FxHashMap;
9use std::sync::Arc;
10
11/// Parent-map from child node to parent node, stored as raw pointers.
12///
13/// # Safety Invariant
14///
15/// Every `*const Node` in this map (both keys and values) must be a pointer
16/// obtained by casting a shared reference (`&Node`) that was derived from the
17/// **same** `Arc<Node>` tree that was passed to [`DeclarationProvider::build_parent_map`].
18/// The pointed-to nodes must remain alive for the entire duration of any code
19/// that inspects the map.
20///
21/// Raw pointers are used as **hash keys only** for O(1) identity-based lookup.
22/// They are **never** dereferenced directly through this map. Safe references
23/// are recovered via the companion `node_lookup` map
24/// (`FxHashMap<*const Node, &Node>`) that re-derives `&Node` from the live
25/// `Arc<Node>` tree at call time.
26///
27/// # Ownership and Lifetime
28///
29/// The `Arc<Node>` that backs the tree must outlive every `&ParentMap` borrow.
30/// In the LSP server this is guaranteed because both the `Arc<Node>` and the
31/// `ParentMap` are stored together in `DocumentState`, guarded by a
32/// `parking_lot::Mutex`.
33///
34/// # Thread Safety
35///
36/// `*const Node` is `!Send + !Sync`. Consequently `ParentMap` is `!Send +
37/// !Sync` and must remain on the thread that owns the `Arc<Node>` tree.
38/// LSP request handlers satisfy this requirement because they process each
39/// request synchronously within a single thread context.
40pub type ParentMap = FxHashMap<*const Node, *const Node>;
41
42/// Provider for finding declarations in Perl source code.
43///
44/// This provider implements LSP go-to-declaration functionality with enhanced
45/// workspace navigation support. Maintains ≤1ms response time for symbol lookup
46/// operations through optimized AST traversal and parent mapping.
47///
48/// # Performance Characteristics
49/// - Declaration resolution: <500μs for typical Perl files
50/// - Memory usage: O(n) where n is AST node count
51/// - Parent map validation: Debug-only with cycle detection
52///
53/// # LSP Workflow Integration
54/// Parse → Index → Navigate → Complete → Analyze pipeline integration:
55/// 1. Parse: AST generation from Perl source
56/// 2. Index: Symbol table construction with qualified name resolution
57/// 3. Navigate: Declaration provider for go-to-definition requests
58/// 4. Complete: Symbol context for completion providers
59/// 5. Analyze: Cross-reference analysis for workspace refactoring
60pub struct DeclarationProvider<'a> {
61 /// The parsed AST for the current document
62 pub ast: Arc<Node>,
63 content: String,
64 document_uri: String,
65 parent_map: Option<&'a ParentMap>,
66 doc_version: i32,
67}
68
69/// Represents a location link from origin to target
70#[derive(Debug, Clone)]
71pub struct LocationLink {
72 /// The range of the symbol being targeted at the origin
73 pub origin_selection_range: (usize, usize),
74 /// The target URI
75 pub target_uri: String,
76 /// The full range of the target declaration
77 pub target_range: (usize, usize),
78 /// The range to select in the target (e.g., just the name)
79 pub target_selection_range: (usize, usize),
80}
81
82impl<'a> DeclarationProvider<'a> {
83 /// Creates a new declaration provider for the given AST and document.
84 ///
85 /// # Arguments
86 /// * `ast` - The parsed AST tree for declaration lookup
87 /// * `content` - The source code content for text extraction
88 /// * `document_uri` - The URI of the document being analyzed
89 ///
90 /// # Performance
91 /// - Initialization: <10μs for typical Perl files
92 /// - Memory overhead: Minimal, shares AST reference
93 ///
94 /// # Examples
95 /// ```rust,ignore
96 /// use perl_parser::declaration::DeclarationProvider;
97 /// use perl_parser::ast::Node;
98 /// use std::sync::Arc;
99 ///
100 /// let ast = Arc::new(Node::new_root());
101 /// let provider = DeclarationProvider::new(
102 /// ast,
103 /// "package MyPackage; sub example { }".to_string(),
104 /// "file:///path/to/file.pl".to_string()
105 /// );
106 /// ```
107 pub fn new(ast: Arc<Node>, content: String, document_uri: String) -> Self {
108 Self {
109 ast,
110 content,
111 document_uri,
112 parent_map: None,
113 doc_version: 0, // Default to version 0 for simple use cases
114 }
115 }
116
117 /// Configures the provider with a pre-built parent map for enhanced traversal.
118 ///
119 /// The parent map enables efficient upward AST traversal for scope resolution
120 /// and context analysis. Debug builds include comprehensive validation.
121 ///
122 /// # Arguments
123 /// * `parent_map` - Mapping from child nodes to their parents
124 ///
125 /// # Performance
126 /// - Parent lookup: O(1) hash table access
127 /// - Validation overhead: Debug-only, ~100μs for large files
128 ///
129 /// # Panics
130 /// In debug builds, panics if:
131 /// - Parent map is empty for non-trivial AST
132 /// - Root node has a parent (cycle detection)
133 /// - Cycles detected in parent relationships
134 ///
135 /// # Examples
136 /// ```rust,ignore
137 /// use perl_parser::declaration::{DeclarationProvider, ParentMap};
138 /// use perl_parser::ast::Node;
139 /// use std::sync::Arc;
140 ///
141 /// let ast = Arc::new(Node::new_root());
142 /// let mut parent_map = ParentMap::default();
143 /// DeclarationProvider::build_parent_map(&ast, &mut parent_map, None);
144 ///
145 /// let provider = DeclarationProvider::new(
146 /// ast, "content".to_string(), "uri".to_string()
147 /// ).with_parent_map(&parent_map);
148 /// ```
149 pub fn with_parent_map(mut self, parent_map: &'a ParentMap) -> Self {
150 #[cfg(debug_assertions)]
151 {
152 // If the AST has more than the root node, an empty map is suspicious.
153 // (Root has no parent, so a truly trivial AST may legitimately produce 0.)
154 debug_assert!(
155 !parent_map.is_empty(),
156 "DeclarationProvider: empty ParentMap (did you forget to rebuild after AST refresh?)"
157 );
158
159 // Root sanity check - root must have no parent
160 let root_ptr = &*self.ast as *const _;
161 debug_assert!(
162 !parent_map.contains_key(&root_ptr),
163 "Root node must have no parent in the parent map"
164 );
165
166 // Cycle detection - ensure no node is its own ancestor
167 Self::debug_assert_no_cycles(parent_map);
168 }
169 self.parent_map = Some(parent_map);
170 self
171 }
172
173 /// Sets the document version for staleness detection.
174 ///
175 /// Version tracking ensures the provider operates on current data
176 /// and prevents usage after document updates in LSP workflows.
177 ///
178 /// # Arguments
179 /// * `version` - Document version number from LSP client
180 ///
181 /// # Performance
182 /// - Version check: <1μs per operation
183 /// - Debug validation: Additional consistency checks
184 ///
185 /// # Examples
186 /// ```rust,ignore
187 /// use perl_parser::declaration::DeclarationProvider;
188 /// use perl_parser::ast::Node;
189 /// use std::sync::Arc;
190 ///
191 /// let provider = DeclarationProvider::new(
192 /// Arc::new(Node::new_root()),
193 /// "content".to_string(),
194 /// "uri".to_string()
195 /// ).with_doc_version(42);
196 /// ```
197 pub fn with_doc_version(mut self, version: i32) -> Self {
198 self.doc_version = version;
199 self
200 }
201
202 /// Returns `true` if this provider is still fresh (version matches).
203 ///
204 /// In both debug and release builds: logs a warning and returns `false` on mismatch so
205 /// callers can return `None` early instead of operating on a stale AST snapshot.
206 #[inline]
207 #[track_caller]
208 fn is_fresh(&self, current_version: i32) -> bool {
209 if self.doc_version != current_version {
210 tracing::warn!(
211 provider_version = self.doc_version,
212 current_version,
213 "DeclarationProvider used after AST refresh — returning empty result"
214 );
215 return false;
216 }
217 true
218 }
219
220 /// Debug-only cycle detection for parent map
221 #[cfg(debug_assertions)]
222 fn debug_assert_no_cycles(parent_map: &ParentMap) {
223 // For each node in the map, climb up to ensure we don't hit a cycle
224 let cap = parent_map.len() + 1; // Max depth before assuming cycle
225
226 for (&child, _) in parent_map.iter() {
227 let mut current = child;
228 let mut depth = 0;
229
230 while depth < cap {
231 if let Some(&parent) = parent_map.get(¤t) {
232 current = parent;
233 depth += 1;
234 } else {
235 // Reached a node with no parent (root), no cycle
236 break;
237 }
238 }
239
240 // If we exhausted the cap, we have a cycle
241 if depth >= cap {
242 eprintln!(
243 "Cycle detected in ParentMap - node is its own ancestor (depth limit {})",
244 cap
245 );
246 break;
247 }
248 }
249 }
250
251 /// Build a parent map for efficient scope walking
252 /// Builds a parent map for efficient upward AST traversal.
253 ///
254 /// Recursively traverses the AST to construct a mapping from each node
255 /// to its parent, enabling O(1) parent lookups for scope resolution.
256 ///
257 /// # Arguments
258 /// * `node` - Current node to process
259 /// * `map` - Mutable parent map to populate
260 /// * `parent` - Parent of the current node (None for root)
261 ///
262 /// # Performance
263 /// - Time complexity: O(n) where n is node count
264 /// - Space complexity: O(n) for parent pointers
265 /// - Typical build time: <100μs for 1000-node AST
266 ///
267 /// # Safety
268 /// Uses raw pointers for performance. Safe as long as AST nodes
269 /// remain valid during provider lifetime.
270 ///
271 /// # Examples
272 /// ```rust,ignore
273 /// use perl_parser::declaration::{DeclarationProvider, ParentMap};
274 /// use perl_parser::ast::Node;
275 ///
276 /// let ast = Node::new_root();
277 /// let mut parent_map = ParentMap::default();
278 /// DeclarationProvider::build_parent_map(&ast, &mut parent_map, None);
279 /// ```
280 pub fn build_parent_map(node: &Node, map: &mut ParentMap, parent: Option<*const Node>) {
281 if let Some(p) = parent {
282 // SAFETY invariant for the ParentMap:
283 //
284 // 1. `node` is a shared reference (`&Node`) obtained from a live `Arc<Node>`.
285 // Casting it to `*const Node` produces a pointer that is valid for the
286 // lifetime of that `Arc`.
287 //
288 // 2. `p` (the parent pointer) was obtained by the same cast in the previous
289 // recursive frame, so it satisfies the same validity guarantee.
290 //
291 // 3. Neither pointer is **ever** dereferenced through this map. The map stores
292 // raw pointers purely as identity keys. Callers that need to follow a parent
293 // pointer back to a `&Node` must go through `build_node_lookup_map`, which
294 // re-derives safe references from the same live `Arc<Node>` tree.
295 //
296 // 4. The caller (LSP runtime) is responsible for ensuring the `Arc<Node>` tree
297 // remains alive for at least as long as any `&ParentMap` borrow. In the LSP
298 // server both the `Arc` and the `ParentMap` live inside `DocumentState`,
299 // guarded by the same `parking_lot::Mutex`.
300 //
301 // 5. No interior mutability is introduced: `node` is not modified during
302 // traversal. The `ParentMap` itself is an exclusive (`&mut`) borrow during
303 // construction and transitions to a shared borrow (`&`) afterwards.
304 map.insert(node as *const _, p);
305 }
306
307 for child in Self::get_children_static(node) {
308 // SAFETY: `child` is a child reference of `node`, both living in the same
309 // `Arc<Node>` allocation. The same invariant from above applies.
310 Self::build_parent_map(child, map, Some(node as *const _));
311 }
312 }
313
314 /// Find the declaration of the symbol at the given position
315 pub fn find_declaration(
316 &self,
317 offset: usize,
318 current_version: i32,
319 ) -> Option<Vec<LocationLink>> {
320 // Guard against stale provider usage after AST refresh (both debug and release)
321 if !self.is_fresh(current_version) {
322 return None;
323 }
324
325 // Find the node at the cursor position
326 let node = self.find_node_at_offset(&self.ast, offset)?;
327
328 // Check what kind of node we're on
329 match &node.kind {
330 NodeKind::Variable { name, .. } => self.find_variable_declaration(node, name),
331 NodeKind::FunctionCall { name, .. } => self.find_subroutine_declaration(node, name),
332 NodeKind::MethodCall { method, object, .. } => {
333 self.find_method_declaration(node, method, object)
334 }
335 NodeKind::IndirectCall { method, object, .. } => {
336 // Handle indirect calls (e.g., "move $obj 10, 20" or "new Class")
337 self.find_method_declaration(node, method, object)
338 }
339 NodeKind::Identifier { name } => self.find_identifier_declaration(node, name),
340 _ => None,
341 }
342 }
343
344 /// Find variable declaration using scope-aware lookup
345 fn find_variable_declaration(&self, usage: &Node, var_name: &str) -> Option<Vec<LocationLink>> {
346 // Walk upwards through scopes to find the nearest declaration
347 // SAFETY: `usage` is a shared reference into the `Arc<Node>` AST tree held by
348 // `DeclarationProvider<'a>`. The raw pointer is used only as a HashMap key for O(1)
349 // parent lookup and is never dereferenced directly; lookups go through `build_node_lookup_map`
350 // which re-derives safe `&Node` references from the same Arc tree.
351 let mut current_ptr: *const Node = usage as *const _;
352
353 // Build temporary parent map if not provided (for testing)
354 let temp_parent_map;
355 let parent_map = if let Some(pm) = self.parent_map {
356 pm
357 } else {
358 temp_parent_map = {
359 let mut map = FxHashMap::default();
360 Self::build_parent_map(&self.ast, &mut map, None);
361 map
362 };
363 &temp_parent_map
364 };
365 let node_lookup = self.build_node_lookup_map();
366
367 while let Some(&parent_ptr) = parent_map.get(¤t_ptr) {
368 let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
369 break;
370 };
371
372 // Check siblings before this node in the current scope
373 for child in self.get_children(parent) {
374 // Stop when we reach or pass the usage node
375 if child.location.start >= usage.location.start {
376 break;
377 }
378
379 // Check if this is a variable declaration matching our name
380 if let NodeKind::VariableDeclaration { variable, .. } = &child.kind {
381 if let NodeKind::Variable { name, .. } = &variable.kind {
382 if name == var_name {
383 return Some(vec![LocationLink {
384 origin_selection_range: (usage.location.start, usage.location.end),
385 target_uri: self.document_uri.clone(),
386 target_range: (child.location.start, child.location.end),
387 target_selection_range: (
388 variable.location.start,
389 variable.location.end,
390 ),
391 }]);
392 }
393 }
394 }
395
396 // Also check variable list declarations
397 if let NodeKind::VariableListDeclaration { variables, .. } = &child.kind {
398 for var in variables {
399 if let NodeKind::Variable { name, .. } = &var.kind {
400 if name == var_name {
401 return Some(vec![LocationLink {
402 origin_selection_range: (
403 usage.location.start,
404 usage.location.end,
405 ),
406 target_uri: self.document_uri.clone(),
407 target_range: (child.location.start, child.location.end),
408 target_selection_range: (var.location.start, var.location.end),
409 }]);
410 }
411 }
412 }
413 }
414 }
415
416 current_ptr = parent_ptr;
417 }
418
419 None
420 }
421
422 /// Find subroutine declaration
423 fn find_subroutine_declaration(
424 &self,
425 node: &Node,
426 func_name: &str,
427 ) -> Option<Vec<LocationLink>> {
428 // Check if the function name is package-qualified (contains ::)
429 let (target_package, target_name) = if let Some(pos) = func_name.rfind("::") {
430 // Split into package and function name
431 let package = &func_name[..pos];
432 let name = &func_name[pos + 2..];
433 (Some(package), name)
434 } else {
435 // No package qualifier, use current package context
436 (self.find_current_package(node), func_name)
437 };
438
439 // Search for subroutines with the target name
440 let mut declarations = Vec::new();
441 self.collect_subroutine_declarations(&self.ast, target_name, &mut declarations);
442
443 // If we have a target package, find subs in that specific package
444 if let Some(pkg_name) = target_package {
445 if let Some(decl) =
446 declarations.iter().find(|d| self.find_current_package(d) == Some(pkg_name))
447 {
448 return Some(vec![self.create_location_link(
449 node,
450 decl,
451 self.get_subroutine_name_range(decl),
452 )]);
453 }
454 }
455
456 // Otherwise return the first match
457 if let Some(decl) = declarations.first() {
458 return Some(vec![self.create_location_link(
459 node,
460 decl,
461 self.get_subroutine_name_range(decl),
462 )]);
463 }
464
465 None
466 }
467
468 /// Find method declaration with package resolution
469 fn find_method_declaration(
470 &self,
471 node: &Node,
472 method_name: &str,
473 object: &Node,
474 ) -> Option<Vec<LocationLink>> {
475 // Try to determine the package from the object
476 let package_name = match &object.kind {
477 NodeKind::Identifier { name } if name.chars().next()?.is_uppercase() => {
478 // Likely a package name (e.g., Foo->method)
479 Some(name.as_str())
480 }
481 _ => None,
482 };
483
484 if let Some(pkg) = package_name {
485 // Look for the method in the specific package
486 let mut declarations = Vec::new();
487 self.collect_subroutine_declarations(&self.ast, method_name, &mut declarations);
488
489 if let Some(decl) =
490 declarations.iter().find(|d| self.find_current_package(d) == Some(pkg))
491 {
492 return Some(vec![self.create_location_link(
493 node,
494 decl,
495 self.get_subroutine_name_range(decl),
496 )]);
497 }
498 }
499
500 // Fall back to any subroutine with this name
501 self.find_subroutine_declaration(node, method_name)
502 }
503
504 /// Find declaration for an identifier
505 fn find_identifier_declaration(&self, node: &Node, name: &str) -> Option<Vec<LocationLink>> {
506 // Try to find as subroutine first
507 if let Some(links) = self.find_subroutine_declaration(node, name) {
508 return Some(links);
509 }
510
511 // Try to find as package
512 let packages = self.find_package_declarations(&self.ast, name);
513 if let Some(pkg) = packages.first() {
514 return Some(vec![self.create_location_link(
515 node,
516 pkg,
517 self.get_package_name_range(pkg),
518 )]);
519 }
520
521 // Try to find as constant (supporting multiple forms)
522 let constants = self.find_constant_declarations(&self.ast, name);
523 if let Some(const_decl) = constants.first() {
524 return Some(vec![self.create_location_link(
525 node,
526 const_decl,
527 self.get_constant_name_range_for(const_decl, name),
528 )]);
529 }
530
531 None
532 }
533
534 /// Find the current package context for a node
535 fn find_current_package<'b>(&'b self, node: &Node) -> Option<&'b str> {
536 // SAFETY: `node` is a shared reference into the `Arc<Node>` AST tree held
537 // by `DeclarationProvider<'a>`. The raw pointer is used only as a hash key
538 // to query the `parent_map`; it is never dereferenced. Safe `&Node`
539 // references are recovered through `node_lookup`, which re-derives them
540 // from the same live `Arc<Node>` tree.
541 let mut current_ptr: *const Node = node as *const _;
542
543 // Build temporary parent map if not provided (for testing)
544 let temp_parent_map;
545 let parent_map = if let Some(pm) = self.parent_map {
546 pm
547 } else {
548 temp_parent_map = {
549 let mut map = FxHashMap::default();
550 Self::build_parent_map(&self.ast, &mut map, None);
551 map
552 };
553 &temp_parent_map
554 };
555 let node_lookup = self.build_node_lookup_map();
556
557 while let Some(&parent_ptr) = parent_map.get(¤t_ptr) {
558 let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
559 break;
560 };
561
562 // Check siblings before this node for package declarations
563 for child in self.get_children(parent) {
564 if child.location.start >= node.location.start {
565 break;
566 }
567
568 if let NodeKind::Package { name, .. } = &child.kind {
569 return Some(name.as_str());
570 }
571 }
572
573 current_ptr = parent_ptr;
574 }
575
576 None
577 }
578
579 /// Create a location link
580 fn create_location_link(
581 &self,
582 origin: &Node,
583 target: &Node,
584 name_range: (usize, usize),
585 ) -> LocationLink {
586 LocationLink {
587 origin_selection_range: (origin.location.start, origin.location.end),
588 target_uri: self.document_uri.clone(),
589 target_range: (target.location.start, target.location.end),
590 target_selection_range: name_range,
591 }
592 }
593
594 // Helper methods
595
596 fn find_node_at_offset<'b>(&'b self, node: &'b Node, offset: usize) -> Option<&'b Node> {
597 if offset >= node.location.start && offset <= node.location.end {
598 // Check children first for more specific match
599 for child in self.get_children(node) {
600 if let Some(found) = self.find_node_at_offset(child, offset) {
601 return Some(found);
602 }
603 }
604 return Some(node);
605 }
606 None
607 }
608
609 fn collect_subroutine_declarations<'b>(
610 &'b self,
611 node: &'b Node,
612 sub_name: &str,
613 subs: &mut Vec<&'b Node>,
614 ) {
615 if let NodeKind::Subroutine { name, .. } = &node.kind {
616 if let Some(name_str) = name {
617 if name_str == sub_name {
618 subs.push(node);
619 }
620 }
621 }
622
623 for child in self.get_children(node) {
624 self.collect_subroutine_declarations(child, sub_name, subs);
625 }
626 }
627
628 fn find_package_declarations<'b>(&'b self, node: &'b Node, pkg_name: &str) -> Vec<&'b Node> {
629 let mut packages = Vec::new();
630 self.collect_package_declarations(node, pkg_name, &mut packages);
631 packages
632 }
633
634 fn collect_package_declarations<'b>(
635 &'b self,
636 node: &'b Node,
637 pkg_name: &str,
638 packages: &mut Vec<&'b Node>,
639 ) {
640 if let NodeKind::Package { name, .. } = &node.kind {
641 if name == pkg_name {
642 packages.push(node);
643 }
644 }
645
646 for child in self.get_children(node) {
647 self.collect_package_declarations(child, pkg_name, packages);
648 }
649 }
650
651 fn find_constant_declarations<'b>(&'b self, node: &'b Node, const_name: &str) -> Vec<&'b Node> {
652 let mut constants = Vec::new();
653 self.collect_constant_declarations(node, const_name, &mut constants);
654 constants
655 }
656
657 /// Strip leading -options from constant args
658 fn strip_constant_options<'b>(&self, args: &'b [String]) -> &'b [String] {
659 let mut i = 0;
660 while i < args.len() && args[i].starts_with('-') {
661 i += 1;
662 }
663 // Also skip a comma if present after options
664 if i < args.len() && args[i] == "," {
665 i += 1;
666 }
667 &args[i..]
668 }
669
670 fn collect_constant_declarations<'b>(
671 &'b self,
672 node: &'b Node,
673 const_name: &str,
674 constants: &mut Vec<&'b Node>,
675 ) {
676 if let NodeKind::Use { module, args, .. } = &node.kind {
677 if module == "constant" {
678 // Strip leading options like -strict, -nonstrict, -force
679 let stripped_args = self.strip_constant_options(args);
680
681 // Form 1: FOO => ...
682 if stripped_args.first().map(|s| s.as_str()) == Some(const_name) {
683 constants.push(node);
684 // keep scanning siblings too (there can be multiple `use constant`)
685 }
686
687 // Flattened args text once (cheap)
688 let args_text = stripped_args.join(" ");
689
690 // Form 2: { FOO => 1, BAR => 2 }
691 if self.contains_name_in_hash(&args_text, const_name) {
692 constants.push(node);
693 }
694
695 // Form 3: qw(FOO BAR) / qw/FOO BAR/
696 if self.contains_name_in_qw(&args_text, const_name) {
697 constants.push(node);
698 }
699 }
700 }
701
702 for child in self.get_children(node) {
703 self.collect_constant_declarations(child, const_name, constants);
704 }
705 }
706
707 /// Check if a byte is part of an ASCII identifier
708 #[inline]
709 fn is_ident_ascii(b: u8) -> bool {
710 matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_')
711 }
712
713 /// Iterate over all qw windows in the string
714 /// Handles both paired delimiters ((), [], {}, <>) and symmetric delimiters (|, !, #, etc.)
715 fn for_each_qw_window<F>(&self, s: &str, mut f: F) -> bool
716 where
717 F: FnMut(usize, usize) -> bool,
718 {
719 let b = s.as_bytes();
720 let mut i = 0;
721 while i + 1 < b.len() {
722 // find literal "qw"
723 if b[i] == b'q' && b[i + 1] == b'w' {
724 let mut j = i + 2;
725
726 // allow whitespace between qw and delimiter
727 while j < b.len() && (b[j] as char).is_ascii_whitespace() {
728 j += 1;
729 }
730 if j >= b.len() {
731 break;
732 }
733
734 let open = b[j] as char;
735
736 // "qwerty" guard: next non-ws must be a NON-word delimiter
737 // (i.e., not [A-Za-z0-9_])
738 if open.is_ascii_alphanumeric() || open == '_' {
739 i += 1;
740 continue;
741 }
742
743 // choose closing delimiter
744 let close = match open {
745 '(' => ')',
746 '[' => ']',
747 '{' => '}',
748 '<' => '>',
749 _ => open, // symmetric delimiter (|, !, #, /, ~, ...)
750 };
751
752 // advance past opener and collect until closer
753 j += 1;
754 let start = j;
755 while j < b.len() && (b[j] as char) != close {
756 j += 1;
757 }
758 if j <= b.len() {
759 // Found the closing delimiter
760 if f(start, j) {
761 return true;
762 }
763 // continue scanning after the closer
764 i = j + 1;
765 continue;
766 } else {
767 // unclosed; stop scanning
768 break;
769 }
770 }
771
772 i += 1;
773 }
774 false
775 }
776
777 /// Iterate over all {...} pairs in the string
778 fn for_each_brace_window<F>(&self, s: &str, mut f: F) -> bool
779 where
780 F: FnMut(usize, usize) -> bool,
781 {
782 let b = s.as_bytes();
783 let mut i = 0;
784 while i < b.len() {
785 if b[i] == b'{' {
786 let start = i + 1;
787 let mut nesting = 1;
788 let mut j = i + 1;
789 while j < b.len() {
790 match b[j] {
791 b'{' => nesting += 1,
792 b'}' => {
793 nesting -= 1;
794 if nesting == 0 {
795 break;
796 }
797 }
798 _ => {}
799 }
800 j += 1;
801 }
802
803 if nesting == 0 {
804 // Found matching closing brace at j
805 if f(start, j) {
806 return true;
807 }
808 i = j + 1;
809 continue;
810 }
811 }
812 i += 1;
813 }
814 false
815 }
816
817 fn contains_name_in_hash(&self, s: &str, name: &str) -> bool {
818 // for { FOO => 1, BAR => 2 } form - check all {...} pairs
819 self.for_each_brace_window(s, |start, end| {
820 // only scan that slice
821 self.find_word(&s[start..end], name).is_some()
822 })
823 }
824
825 fn contains_name_in_qw(&self, s: &str, name: &str) -> bool {
826 // looks for qw(...) / qw[...] / qw/.../ etc. with word boundaries
827 self.for_each_qw_window(s, |start, end| {
828 // tokens are whitespace separated
829 s[start..end].split_whitespace().any(|tok| tok == name)
830 })
831 }
832
833 fn find_word(&self, hay: &str, needle: &str) -> Option<(usize, usize)> {
834 if needle.is_empty() {
835 return None;
836 }
837 let mut find_from = 0;
838 while let Some(hit) = hay[find_from..].find(needle) {
839 let start = find_from + hit;
840 let end = start + needle.len();
841 let left_ok = start == 0 || !Self::is_ident_ascii(hay.as_bytes()[start - 1]);
842 let right_ok = end == hay.len()
843 || !Self::is_ident_ascii(*hay.as_bytes().get(end).unwrap_or(&b' '));
844 if left_ok && right_ok {
845 return Some((start, end));
846 }
847 find_from = end;
848 }
849 None
850 }
851
852 fn first_all_caps_word(&self, s: &str) -> Option<(usize, usize)> {
853 // very small scanner: find FOO-ish
854 let bytes = s.as_bytes();
855 let mut i = 0;
856 while i < bytes.len() {
857 while i < bytes.len() && !Self::is_ident_ascii(bytes[i]) {
858 i += 1;
859 }
860 let start = i;
861 while i < bytes.len() && Self::is_ident_ascii(bytes[i]) {
862 i += 1;
863 }
864 if start < i {
865 let w = &s[start..i];
866 if w.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') {
867 return Some((start, i));
868 }
869 }
870 }
871 None
872 }
873
874 fn get_subroutine_name_range(&self, decl: &Node) -> (usize, usize) {
875 if let NodeKind::Subroutine { name_span: Some(loc), .. } = &decl.kind {
876 (loc.start, loc.end)
877 } else {
878 (decl.location.start, decl.location.end)
879 }
880 }
881
882 fn get_package_name_range(&self, decl: &Node) -> (usize, usize) {
883 if let NodeKind::Package { name_span, .. } = &decl.kind {
884 (name_span.start, name_span.end)
885 } else {
886 (decl.location.start, decl.location.end)
887 }
888 }
889
890 fn get_constant_name_range(&self, decl: &Node) -> (usize, usize) {
891 let text = self.get_node_text(decl);
892
893 // Prefer an exact span if we can find the first occurrence with word boundaries
894 if let NodeKind::Use { args, .. } = &decl.kind {
895 let best_guess = args.first().map(|s| s.as_str()).unwrap_or("");
896 if let Some((lo, hi)) = self.find_word(&text, best_guess) {
897 let abs_lo = decl.location.start + lo;
898 let abs_hi = decl.location.start + hi;
899 return (abs_lo, abs_hi);
900 }
901 }
902
903 // Try any constant-looking all-caps token in the decl
904 if let Some((lo, hi)) = self.first_all_caps_word(&text) {
905 return (decl.location.start + lo, decl.location.start + hi);
906 }
907
908 // Fallback to whole range
909 (decl.location.start, decl.location.end)
910 }
911
912 fn get_constant_name_range_for(&self, decl: &Node, name: &str) -> (usize, usize) {
913 let text = self.get_node_text(decl);
914
915 // Fast path: try to find the exact word
916 if let Some((lo, hi)) = self.find_word(&text, name) {
917 return (decl.location.start + lo, decl.location.start + hi);
918 }
919
920 // Try inside all qw(...) windows
921 let mut found_range = None;
922 self.for_each_qw_window(&text, |start, end| {
923 // Find the exact token position within this qw window
924 if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
925 found_range =
926 Some((decl.location.start + start + lo, decl.location.start + start + hi));
927 true // Stop searching
928 } else {
929 false // Continue to next window
930 }
931 });
932 if let Some(range) = found_range {
933 return range;
934 }
935
936 // Try inside all { ... } blocks (hash form)
937 self.for_each_brace_window(&text, |start, end| {
938 if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
939 found_range =
940 Some((decl.location.start + start + lo, decl.location.start + start + hi));
941 true // Stop searching
942 } else {
943 false // Continue to next window
944 }
945 });
946 if let Some(range) = found_range {
947 return range;
948 }
949
950 // Final fallback to heuristics
951 self.get_constant_name_range(decl)
952 }
953
954 fn get_children<'b>(&self, node: &'b Node) -> Vec<&'b Node> {
955 Self::get_children_static(node)
956 }
957
958 /// Build a lookup map from raw node pointers back to safe references.
959 ///
960 /// This map is the bridge that makes `ParentMap` safe to use: callers
961 /// obtain a `*const Node` from the parent map and look it up here to
962 /// recover a properly-lifetime-bounded `&Node`. The raw pointer is
963 /// used purely as an identity key — it is never dereferenced directly.
964 fn build_node_lookup_map(&self) -> FxHashMap<*const Node, &Node> {
965 let mut map = FxHashMap::default();
966 Self::build_node_lookup(self.ast.as_ref(), &mut map);
967 map
968 }
969
970 fn build_node_lookup<'b>(node: &'b Node, map: &mut FxHashMap<*const Node, &'b Node>) {
971 // SAFETY: `node` is a shared reference whose lifetime `'b` is tied to
972 // `self.ast` (`Arc<Node>`). We store the address as a raw-pointer key
973 // alongside the same reference as the value. The value is the safe
974 // side of this pair — it is the only route through which the pointer
975 // is ever turned back into usable data.
976 map.insert(node as *const Node, node);
977 for child in Self::get_children_static(node) {
978 Self::build_node_lookup(child, map);
979 }
980 }
981
982 fn get_children_static(node: &Node) -> Vec<&Node> {
983 match &node.kind {
984 NodeKind::Program { statements } => statements.iter().collect(),
985 NodeKind::Block { statements } => statements.iter().collect(),
986 NodeKind::If { condition, then_branch, else_branch, .. } => {
987 let mut children = vec![condition.as_ref(), then_branch.as_ref()];
988 if let Some(else_b) = else_branch {
989 children.push(else_b.as_ref());
990 }
991 children
992 }
993 NodeKind::Binary { left, right, .. } => vec![left.as_ref(), right.as_ref()],
994 NodeKind::Unary { operand, .. } => vec![operand.as_ref()],
995 NodeKind::VariableDeclaration { variable, initializer, .. } => {
996 let mut children = vec![variable.as_ref()];
997 if let Some(init) = initializer {
998 children.push(init.as_ref());
999 }
1000 children
1001 }
1002 NodeKind::Subroutine { signature, body, .. } => {
1003 let mut children = vec![body.as_ref()];
1004 if let Some(sig) = signature {
1005 children.push(sig.as_ref());
1006 }
1007 children
1008 }
1009 NodeKind::FunctionCall { args, .. } => args.iter().collect(),
1010 NodeKind::MethodCall { object, args, .. } => {
1011 let mut children = vec![object.as_ref()];
1012 children.extend(args.iter());
1013 children
1014 }
1015 NodeKind::IndirectCall { object, args, .. } => {
1016 let mut children = vec![object.as_ref()];
1017 children.extend(args.iter());
1018 children
1019 }
1020 NodeKind::While { condition, body, .. } => {
1021 vec![condition.as_ref(), body.as_ref()]
1022 }
1023 NodeKind::For { init, condition, update, body, .. } => {
1024 let mut children = Vec::new();
1025 if let Some(i) = init {
1026 children.push(i.as_ref());
1027 }
1028 if let Some(c) = condition {
1029 children.push(c.as_ref());
1030 }
1031 if let Some(u) = update {
1032 children.push(u.as_ref());
1033 }
1034 children.push(body.as_ref());
1035 children
1036 }
1037 NodeKind::Foreach { variable, list, body, .. } => {
1038 vec![variable.as_ref(), list.as_ref(), body.as_ref()]
1039 }
1040 NodeKind::ExpressionStatement { expression } => vec![expression.as_ref()],
1041 _ => vec![],
1042 }
1043 }
1044
1045 /// Extracts the source code text for a given AST node.
1046 ///
1047 /// Returns the substring of the document content corresponding to
1048 /// the node's location range. Used for symbol name extraction and
1049 /// text-based analysis.
1050 ///
1051 /// # Arguments
1052 /// * `node` - AST node to extract text from
1053 ///
1054 /// # Performance
1055 /// - Time complexity: O(m) where m is node text length
1056 /// - Memory: Creates owned string copy
1057 /// - Typical latency: <10μs for identifier names
1058 ///
1059 /// # Examples
1060 /// ```rust,ignore
1061 /// use perl_parser::declaration::DeclarationProvider;
1062 /// use perl_parser::ast::Node;
1063 /// use std::sync::Arc;
1064 ///
1065 /// let provider = DeclarationProvider::new(
1066 /// Arc::new(Node::new_root()),
1067 /// "sub example { }".to_string(),
1068 /// "uri".to_string()
1069 /// );
1070 /// // let text = provider.get_node_text(&some_node);
1071 /// ```
1072 pub fn get_node_text(&self, node: &Node) -> String {
1073 self.content[node.location.start..node.location.end].to_string()
1074 }
1075}
1076
1077/// Extracts a symbol key from the AST node at the given cursor position.
1078///
1079/// Analyzes the AST at a specific byte offset to identify the symbol under
1080/// the cursor for LSP operations. Supports function calls, variable references,
1081/// and package-qualified symbols with full Perl syntax coverage.
1082///
1083/// # Arguments
1084/// * `ast` - Root AST node to search within
1085/// * `offset` - Byte offset in the source document
1086/// * `current_pkg` - Current package context for symbol resolution
1087///
1088/// # Returns
1089/// * `Some(SymbolKey)` - Symbol found at position with package qualification
1090/// * `None` - No symbol at the given position
1091///
1092/// # Performance
1093/// - Search time: O(log n) average case with spatial indexing
1094/// - Worst case: O(n) for unbalanced AST traversal
1095/// - Typical latency: <50μs for LSP responsiveness
1096///
1097/// # Perl Parsing Context
1098/// Handles complex Perl symbol patterns:
1099/// - Package-qualified calls: `Package::function`
1100/// - Bare function calls: `function` (resolved in current package)
1101/// - Variable references: `$var`, `@array`, `%hash`
1102/// - Method calls: `$obj->method`
1103///
1104/// # Examples
1105/// ```rust,ignore
1106/// use perl_parser::declaration::symbol_at_cursor;
1107/// use perl_parser::ast::Node;
1108///
1109/// let ast = Node::new_root();
1110/// let symbol = symbol_at_cursor(&ast, 42, "MyPackage");
1111/// if let Some(sym) = symbol {
1112/// println!("Found symbol: {:?}", sym);
1113/// }
1114/// ```
1115pub fn symbol_at_cursor(ast: &Node, offset: usize, current_pkg: &str) -> Option<SymbolKey> {
1116 fn collect_node_path_at_offset<'a>(
1117 node: &'a Node,
1118 offset: usize,
1119 path: &mut Vec<&'a Node>,
1120 ) -> bool {
1121 if offset < node.location.start || offset > node.location.end {
1122 return false;
1123 }
1124
1125 path.push(node);
1126
1127 for child in get_node_children(node) {
1128 if collect_node_path_at_offset(child, offset, path) {
1129 return true;
1130 }
1131 }
1132
1133 true
1134 }
1135
1136 fn find_symbol_node_at_offset(ast: &Node, offset: usize) -> Option<&Node> {
1137 let mut path = Vec::new();
1138 if !collect_node_path_at_offset(ast, offset, &mut path) {
1139 return None;
1140 }
1141
1142 path.iter()
1143 .rev()
1144 .copied()
1145 .find(|node| {
1146 matches!(
1147 node.kind,
1148 NodeKind::Variable { .. }
1149 | NodeKind::FunctionCall { .. }
1150 | NodeKind::Subroutine { .. }
1151 | NodeKind::MethodCall { .. }
1152 | NodeKind::Use { .. }
1153 )
1154 })
1155 .or_else(|| path.last().copied())
1156 }
1157
1158 fn node_variable_name(node: &Node) -> Option<&str> {
1159 if let NodeKind::Variable { name, .. } = &node.kind { Some(name.as_str()) } else { None }
1160 }
1161
1162 fn looks_like_package_name(name: &str) -> bool {
1163 name.contains("::") || name.chars().next().is_some_and(|ch| ch.is_ascii_uppercase())
1164 }
1165
1166 fn infer_receiver_package(
1167 object: &Node,
1168 current_pkg: &str,
1169 receiver_packages: &std::collections::HashMap<String, String>,
1170 ) -> Option<String> {
1171 if let NodeKind::Identifier { name } = &object.kind {
1172 return Some(name.clone());
1173 }
1174
1175 if let Some(name) = node_variable_name(object) {
1176 if let Some(package_name) = receiver_packages.get(name) {
1177 return Some(package_name.clone());
1178 }
1179
1180 if matches!(name, "self" | "this" | "class") {
1181 return Some(current_pkg.to_string());
1182 }
1183
1184 if looks_like_package_name(name) {
1185 return Some(name.to_string());
1186 }
1187 }
1188
1189 None
1190 }
1191
1192 fn infer_constructor_package(
1193 rhs: &Node,
1194 current_pkg: &str,
1195 receiver_packages: &std::collections::HashMap<String, String>,
1196 ) -> Option<String> {
1197 match &rhs.kind {
1198 NodeKind::MethodCall { method, object, .. } if method == "new" => {
1199 infer_receiver_package(object, current_pkg, receiver_packages)
1200 }
1201 NodeKind::FunctionCall { name, .. } => {
1202 name.rsplit_once("::").map(|(package_name, _)| package_name.to_string())
1203 }
1204 _ => None,
1205 }
1206 }
1207
1208 fn record_receiver_assignment(
1209 node: &Node,
1210 offset: usize,
1211 current_pkg: &str,
1212 receiver_packages: &mut std::collections::HashMap<String, String>,
1213 ) {
1214 if node.location.start > offset {
1215 return;
1216 }
1217
1218 if node.location.end <= offset {
1219 match &node.kind {
1220 NodeKind::VariableDeclaration { variable, initializer, .. } => {
1221 if let (Some(variable_name), Some(initializer)) =
1222 (node_variable_name(variable), initializer.as_ref())
1223 {
1224 if let Some(package_name) =
1225 infer_constructor_package(initializer, current_pkg, receiver_packages)
1226 {
1227 receiver_packages.insert(variable_name.to_string(), package_name);
1228 }
1229 }
1230 }
1231 NodeKind::Assignment { lhs, rhs, .. } => {
1232 if let Some(variable_name) = node_variable_name(lhs) {
1233 if let Some(package_name) =
1234 infer_constructor_package(rhs, current_pkg, receiver_packages)
1235 {
1236 receiver_packages.insert(variable_name.to_string(), package_name);
1237 }
1238 }
1239 }
1240 _ => {}
1241 }
1242 }
1243
1244 for child in get_node_children(node) {
1245 if child.location.start <= offset {
1246 record_receiver_assignment(child, offset, current_pkg, receiver_packages);
1247 }
1248 }
1249 }
1250
1251 let node = find_symbol_node_at_offset(ast, offset)?;
1252 match &node.kind {
1253 NodeKind::Variable { sigil, name } => {
1254 // Variable already has sigil separated
1255 let sigil_char = sigil.chars().next();
1256 Some(SymbolKey {
1257 pkg: current_pkg.into(),
1258 name: name.clone().into(),
1259 sigil: sigil_char,
1260 kind: SymKind::Var,
1261 })
1262 }
1263 NodeKind::FunctionCall { name, .. } => {
1264 let (pkg, bare) = if let Some(idx) = name.rfind("::") {
1265 (&name[..idx], &name[idx + 2..])
1266 } else {
1267 (current_pkg, name.as_str())
1268 };
1269 Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
1270 }
1271 NodeKind::Subroutine { name: Some(name), .. } => {
1272 let (pkg, bare) = if let Some(idx) = name.rfind("::") {
1273 (&name[..idx], &name[idx + 2..])
1274 } else {
1275 (current_pkg, name.as_str())
1276 };
1277 Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
1278 }
1279 NodeKind::MethodCall { object, method, .. } => {
1280 let mut receiver_packages = std::collections::HashMap::new();
1281 record_receiver_assignment(ast, offset, current_pkg, &mut receiver_packages);
1282 let pkg = infer_receiver_package(object, current_pkg, &receiver_packages)
1283 .unwrap_or_else(|| current_pkg.to_string());
1284 Some(SymbolKey {
1285 pkg: pkg.into(),
1286 name: method.clone().into(),
1287 sigil: None,
1288 kind: SymKind::Sub,
1289 })
1290 }
1291 NodeKind::Use { module, .. } => {
1292 // When cursor is on a `use Module::Name` statement, resolve to the package
1293 Some(SymbolKey {
1294 pkg: module.clone().into(),
1295 name: module.clone().into(),
1296 sigil: None,
1297 kind: SymKind::Pack,
1298 })
1299 }
1300 _ => None,
1301 }
1302}
1303
1304/// Determines the current package context at the given offset.
1305///
1306/// Scans the AST backwards from the offset to find the most recent
1307/// package declaration, providing proper context for symbol resolution
1308/// in Perl's package-based namespace system.
1309///
1310/// # Arguments
1311/// * `ast` - Root AST node to search within
1312/// * `offset` - Byte offset in the source document
1313///
1314/// # Returns
1315/// Package name as string slice, defaults to "main" if no package found
1316///
1317/// # Performance
1318/// - Search time: O(n) worst case, O(log n) typical
1319/// - Memory: Returns borrowed string slice (zero-copy)
1320/// - Caching: Results suitable for per-request caching
1321///
1322/// # Perl Parsing Context
1323/// Perl package semantics:
1324/// - `package Foo;` declarations change current namespace
1325/// - Scope continues until next package declaration or EOF
1326/// - Default package is "main" when no explicit declaration
1327/// - Package names follow Perl identifier rules (`::`-separated)
1328///
1329/// # Examples
1330/// ```rust,ignore
1331/// use perl_parser::declaration::current_package_at;
1332/// use perl_parser::ast::Node;
1333///
1334/// let ast = Node::new_root();
1335/// let pkg = current_package_at(&ast, 100);
1336/// println!("Current package: {}", pkg);
1337/// ```
1338pub fn current_package_at(ast: &Node, offset: usize) -> &str {
1339 // Find the nearest package declaration before the offset
1340 fn scan<'a>(node: &'a Node, offset: usize, last: &mut Option<&'a str>) {
1341 if let NodeKind::Package { name, .. } = &node.kind {
1342 if node.location.start <= offset {
1343 *last = Some(name.as_str());
1344 }
1345 }
1346 for child in get_node_children(node) {
1347 if child.location.start <= offset {
1348 scan(child, offset, last);
1349 }
1350 }
1351 }
1352
1353 let mut last_pkg: Option<&str> = None;
1354 scan(ast, offset, &mut last_pkg);
1355 last_pkg.unwrap_or("main")
1356}
1357
1358/// Finds the most specific AST node containing the given byte offset.
1359///
1360/// Performs recursive descent through the AST to locate the deepest node
1361/// that encompasses the specified position. Essential for cursor-based
1362/// LSP operations like go-to-definition and hover.
1363///
1364/// # Arguments
1365/// * `node` - AST node to search within (typically root)
1366/// * `offset` - Byte offset in the source document
1367///
1368/// # Returns
1369/// * `Some(&Node)` - Deepest node containing the offset
1370/// * `None` - Offset is outside the node's range
1371///
1372/// # Performance
1373/// - Search time: O(log n) average, O(n) worst case
1374/// - Memory: Zero allocations, returns borrowed reference
1375/// - Spatial locality: Optimized for sequential offset queries
1376///
1377/// # LSP Integration
1378/// Core primitive for:
1379/// - Hover information: Find node for symbol details
1380/// - Go-to-definition: Identify symbol under cursor
1381/// - Completion: Determine context for suggestions
1382/// - Diagnostics: Map error positions to AST nodes
1383///
1384/// # Examples
1385/// ```rust,ignore
1386/// use perl_parser::declaration::find_node_at_offset;
1387/// use perl_parser::ast::Node;
1388///
1389/// let ast = Node::new_root();
1390/// if let Some(node) = find_node_at_offset(&ast, 42) {
1391/// println!("Found node: {:?}", node.kind);
1392/// }
1393/// ```
1394pub fn find_node_at_offset(node: &Node, offset: usize) -> Option<&Node> {
1395 if offset < node.location.start || offset > node.location.end {
1396 return None;
1397 }
1398
1399 // Check children first for more specific match
1400 let children = get_node_children(node);
1401 for child in children {
1402 if let Some(found) = find_node_at_offset(child, offset) {
1403 return Some(found);
1404 }
1405 }
1406
1407 // If no child contains the offset, return this node
1408 Some(node)
1409}
1410
1411/// Returns direct child nodes for a given AST node.
1412///
1413/// Provides generic access to child nodes across different node types,
1414/// essential for AST traversal algorithms and recursive analysis patterns.
1415///
1416/// # Arguments
1417/// * `node` - AST node to extract children from
1418///
1419/// # Returns
1420/// Vector of borrowed child node references
1421///
1422/// # Performance
1423/// - Time complexity: O(k) where k is child count
1424/// - Memory: Allocates vector for child references
1425/// - Typical latency: <5μs for common node types
1426///
1427/// # Examples
1428/// ```rust,ignore
1429/// use perl_parser::declaration::get_node_children;
1430/// use perl_parser::ast::Node;
1431///
1432/// let node = Node::new_root();
1433/// let children = get_node_children(&node);
1434/// println!("Node has {} children", children.len());
1435/// ```
1436pub fn get_node_children(node: &Node) -> Vec<&Node> {
1437 // Delegate to the AST node's own comprehensive children() method,
1438 // which handles all node kinds including Block, Package, MethodCall, etc.
1439 node.children()
1440}