Skip to main content

perl_semantic_analyzer/analysis/semantic/
node_analysis.rs

1//! AST node analysis — the `analyze_node` traversal and source-text helpers.
2//!
3//! This module provides the core recursive descent over the Perl AST that
4//! produces semantic tokens and hover information.  It is an `impl` block
5//! extension of `SemanticAnalyzer`; Rust allows splitting `impl` blocks across
6//! files within the same module, so the types defined in `mod.rs` are
7//! directly accessible here.
8
9use crate::SourceLocation;
10use crate::ast::{Node, NodeKind};
11use crate::symbol::{ScopeId, ScopeKind, SymbolKind};
12use regex::Regex;
13use std::sync::OnceLock;
14
15use super::SemanticAnalyzer;
16use super::builtins::{
17    get_builtin_documentation, is_builtin_function, is_control_keyword, is_file_test_operator,
18};
19use super::hover::HoverInfo;
20use super::tokens::{SemanticToken, SemanticTokenModifier, SemanticTokenType};
21
22impl SemanticAnalyzer {
23    /// Analyze a node and generate semantic information
24    pub(super) fn analyze_node(&mut self, node: &Node, scope_id: ScopeId) {
25        match &node.kind {
26            NodeKind::Program { statements } => {
27                for stmt in statements {
28                    self.analyze_node(stmt, scope_id);
29                }
30            }
31
32            NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } => {
33                // Add semantic token for declaration
34                if let NodeKind::Variable { sigil, name } = &variable.kind {
35                    let token_type = match declarator.as_str() {
36                        "my" | "state" => SemanticTokenType::VariableDeclaration,
37                        "our" => SemanticTokenType::Variable,
38                        "local" => SemanticTokenType::Variable,
39                        _ => SemanticTokenType::Variable,
40                    };
41
42                    let mut modifiers = vec![SemanticTokenModifier::Declaration];
43                    if declarator == "state" || attributes.iter().any(|a| a == ":shared") {
44                        modifiers.push(SemanticTokenModifier::Static);
45                    }
46
47                    self.semantic_tokens.push(SemanticToken {
48                        location: variable.location,
49                        token_type,
50                        modifiers,
51                    });
52
53                    // Add hover info
54                    let hover = HoverInfo {
55                        signature: format!("{} {}{}", declarator, sigil, name),
56                        documentation: self.extract_documentation(node.location.start),
57                        details: if attributes.is_empty() {
58                            vec![]
59                        } else {
60                            vec![format!("Attributes: {}", attributes.join(", "))]
61                        },
62                    };
63
64                    self.hover_info.insert(variable.location, hover);
65                }
66
67                if let Some(init) = initializer {
68                    self.analyze_node(init, scope_id);
69                }
70            }
71
72            NodeKind::Variable { sigil, name } => {
73                let kind = match sigil.as_str() {
74                    "$" => SymbolKind::scalar(),
75                    "@" => SymbolKind::array(),
76                    "%" => SymbolKind::hash(),
77                    _ => return,
78                };
79
80                // Find the symbol definition
81                let symbols = self.symbol_table.find_symbol(name, scope_id, kind);
82
83                let token_type = if let Some(symbol) = symbols.first() {
84                    match symbol.declaration.as_deref() {
85                        Some("my") | Some("state") => SemanticTokenType::Variable,
86                        Some("our") => SemanticTokenType::Variable,
87                        _ => SemanticTokenType::Variable,
88                    }
89                } else {
90                    // Undefined variable
91                    SemanticTokenType::Variable
92                };
93
94                self.semantic_tokens.push(SemanticToken {
95                    location: node.location,
96                    token_type,
97                    modifiers: vec![],
98                });
99
100                // Add hover info if we found the symbol
101                if let Some(symbol) = symbols.first() {
102                    let hover = HoverInfo {
103                        signature: format!(
104                            "{} {}{}",
105                            symbol.declaration.as_deref().unwrap_or(""),
106                            sigil,
107                            name
108                        )
109                        .trim()
110                        .to_string(),
111                        documentation: symbol.documentation.clone(),
112                        details: vec![format!(
113                            "Defined at line {}",
114                            self.line_number(symbol.location.start)
115                        )],
116                    };
117
118                    self.hover_info.insert(node.location, hover);
119                }
120            }
121
122            NodeKind::Subroutine { name, prototype, signature, attributes, body, name_span: _ } => {
123                if let Some(sub_name) = name {
124                    // Named subroutine
125                    let token = SemanticToken {
126                        location: node.location,
127                        token_type: SemanticTokenType::FunctionDeclaration,
128                        modifiers: vec![SemanticTokenModifier::Declaration],
129                    };
130
131                    self.semantic_tokens.push(token);
132
133                    // Add hover info
134                    let mut signature_str = format!("sub {}", sub_name);
135                    if let Some(sig_node) = signature {
136                        signature_str.push_str(&format_signature_params(sig_node));
137                    }
138
139                    let hover = HoverInfo {
140                        signature: signature_str,
141                        documentation: self.extract_documentation(node.location.start),
142                        details: if attributes.is_empty() {
143                            vec![]
144                        } else {
145                            vec![format!("Attributes: {}", attributes.join(", "))]
146                        },
147                    };
148
149                    self.hover_info.insert(node.location, hover);
150                } else {
151                    // Anonymous subroutine (closure)
152                    // Add semantic token for the 'sub' keyword
153                    self.semantic_tokens.push(SemanticToken {
154                        location: SourceLocation {
155                            start: node.location.start,
156                            end: node.location.start + 3, // "sub"
157                        },
158                        token_type: SemanticTokenType::Keyword,
159                        modifiers: vec![],
160                    });
161
162                    // Add hover info for anonymous subs
163                    let mut signature_str = "sub".to_string();
164                    if let Some(sig_node) = signature {
165                        signature_str.push_str(&format_signature_params(sig_node));
166                    }
167                    signature_str.push_str(" { ... }");
168
169                    let mut details = vec!["Anonymous subroutine (closure)".to_string()];
170                    if !attributes.is_empty() {
171                        details.push(format!("Attributes: {}", attributes.join(", ")));
172                    }
173
174                    let hover = HoverInfo {
175                        signature: signature_str,
176                        documentation: self.extract_documentation(node.location.start),
177                        details,
178                    };
179
180                    self.hover_info.insert(node.location, hover);
181                }
182
183                {
184                    // Get the subroutine scope from the symbol table
185                    let sub_scope = self.get_scope_for(node, ScopeKind::Subroutine);
186
187                    if let Some(proto) = prototype {
188                        self.analyze_node(proto, sub_scope);
189                    }
190                    if let Some(sig) = signature {
191                        self.analyze_node(sig, sub_scope);
192                    }
193
194                    self.analyze_node(body, sub_scope);
195                }
196            }
197
198            NodeKind::Method { name, signature, attributes, body } => {
199                self.semantic_tokens.push(SemanticToken {
200                    location: node.location, // Approximate, ideally name span
201                    token_type: SemanticTokenType::FunctionDeclaration,
202                    modifiers: vec![SemanticTokenModifier::Declaration],
203                });
204
205                // Add hover info
206                let hover = HoverInfo {
207                    signature: format!("method {}", name),
208                    documentation: self.extract_documentation(node.location.start),
209                    details: if attributes.is_empty() {
210                        vec![]
211                    } else {
212                        vec![format!("Attributes: {}", attributes.join(", "))]
213                    },
214                };
215                self.hover_info.insert(node.location, hover);
216
217                // Analyze body in new scope (assumed same as Subroutine scope kind for now)
218                let sub_scope = self.get_scope_for(node, ScopeKind::Subroutine);
219                if let Some(sig) = signature {
220                    self.analyze_node(sig, sub_scope);
221                }
222                self.analyze_node(body, sub_scope);
223            }
224
225            NodeKind::FunctionCall { name, args } => {
226                // Check if this is a built-in function
227                {
228                    let token_type = if is_control_keyword(name) {
229                        SemanticTokenType::KeywordControl
230                    } else if is_builtin_function(name) {
231                        SemanticTokenType::Function
232                    } else {
233                        // Check if it's a user-defined function
234                        let symbols =
235                            self.symbol_table.find_symbol(name, scope_id, SymbolKind::Subroutine);
236                        if symbols.is_empty() {
237                            SemanticTokenType::Function
238                        } else {
239                            SemanticTokenType::Function
240                        }
241                    };
242
243                    self.semantic_tokens.push(SemanticToken {
244                        location: node.location,
245                        token_type,
246                        modifiers: if is_builtin_function(name) && !is_control_keyword(name) {
247                            vec![SemanticTokenModifier::DefaultLibrary]
248                        } else {
249                            vec![]
250                        },
251                    });
252
253                    // Add hover for built-ins
254                    if let Some(doc) = get_builtin_documentation(name) {
255                        let hover = HoverInfo {
256                            signature: doc.signature.to_string(),
257                            documentation: Some(doc.description.to_string()),
258                            details: vec![],
259                        };
260
261                        self.hover_info.insert(node.location, hover);
262                    }
263                }
264
265                // Name is already a string, not a node
266                for arg in args {
267                    self.analyze_node(arg, scope_id);
268                }
269            }
270
271            NodeKind::Package { name, block, name_span: _ } => {
272                self.semantic_tokens.push(SemanticToken {
273                    location: node.location,
274                    token_type: SemanticTokenType::Namespace,
275                    modifiers: vec![SemanticTokenModifier::Declaration],
276                });
277
278                // Try POD docs first, then fall back to leading comments
279                let documentation = self
280                    .extract_pod_name_section(name)
281                    .or_else(|| self.extract_documentation(node.location.start));
282
283                let hover = HoverInfo {
284                    signature: format!("package {}", name),
285                    documentation,
286                    details: vec![],
287                };
288
289                self.hover_info.insert(node.location, hover);
290
291                if let Some(block_node) = block {
292                    let package_scope = self.get_scope_for(node, ScopeKind::Package);
293                    self.analyze_node(block_node, package_scope);
294                }
295            }
296
297            NodeKind::String { value: _, interpolated: _ } => {
298                self.semantic_tokens.push(SemanticToken {
299                    location: node.location,
300                    token_type: SemanticTokenType::String,
301                    modifiers: vec![],
302                });
303            }
304
305            NodeKind::Number { value: _ } => {
306                self.semantic_tokens.push(SemanticToken {
307                    location: node.location,
308                    token_type: SemanticTokenType::Number,
309                    modifiers: vec![],
310                });
311            }
312
313            NodeKind::Regex { .. } => {
314                self.semantic_tokens.push(SemanticToken {
315                    location: node.location,
316                    token_type: SemanticTokenType::Regex,
317                    modifiers: vec![],
318                });
319            }
320
321            NodeKind::Match { expr, .. } => {
322                self.semantic_tokens.push(SemanticToken {
323                    location: node.location,
324                    token_type: SemanticTokenType::Regex,
325                    modifiers: vec![],
326                });
327                self.analyze_node(expr, scope_id);
328            }
329            NodeKind::Substitution { expr, .. } => {
330                // Substitution operator: s/// - add semantic token for the operator
331                self.semantic_tokens.push(SemanticToken {
332                    location: node.location,
333                    token_type: SemanticTokenType::Operator,
334                    modifiers: vec![],
335                });
336                self.analyze_node(expr, scope_id);
337            }
338            NodeKind::Transliteration { expr, .. } => {
339                // Transliteration operator: tr/// or y/// - add semantic token for the operator
340                self.semantic_tokens.push(SemanticToken {
341                    location: node.location,
342                    token_type: SemanticTokenType::Operator,
343                    modifiers: vec![],
344                });
345                self.analyze_node(expr, scope_id);
346            }
347
348            NodeKind::LabeledStatement { label: _, statement } => {
349                self.semantic_tokens.push(SemanticToken {
350                    location: node.location,
351                    token_type: SemanticTokenType::Label,
352                    modifiers: vec![],
353                });
354
355                {
356                    self.analyze_node(statement, scope_id);
357                }
358            }
359
360            // Control flow keywords
361            NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
362                self.analyze_node(condition, scope_id);
363                self.analyze_node(then_branch, scope_id);
364                for (elsif_cond, elsif_branch) in elsif_branches {
365                    self.analyze_node(elsif_cond, scope_id);
366                    self.analyze_node(elsif_branch, scope_id);
367                }
368                if let Some(else_node) = else_branch {
369                    self.analyze_node(else_node, scope_id);
370                }
371            }
372
373            NodeKind::While { condition, body, continue_block: _ } => {
374                self.analyze_node(condition, scope_id);
375                self.analyze_node(body, scope_id);
376            }
377
378            NodeKind::For { init, condition, update, body, .. } => {
379                if let Some(init_node) = init {
380                    self.analyze_node(init_node, scope_id);
381                }
382                if let Some(cond_node) = condition {
383                    self.analyze_node(cond_node, scope_id);
384                }
385                if let Some(update_node) = update {
386                    self.analyze_node(update_node, scope_id);
387                }
388                self.analyze_node(body, scope_id);
389            }
390
391            NodeKind::Foreach { variable, list, body, continue_block } => {
392                self.analyze_node(variable, scope_id);
393                self.analyze_node(list, scope_id);
394                self.analyze_node(body, scope_id);
395                if let Some(cb) = continue_block {
396                    self.analyze_node(cb, scope_id);
397                }
398            }
399
400            // Recursively analyze other nodes
401            NodeKind::Block { statements } => {
402                for stmt in statements {
403                    self.analyze_node(stmt, scope_id);
404                }
405            }
406
407            NodeKind::Binary { left, right, .. } => {
408                self.analyze_node(left, scope_id);
409                self.analyze_node(right, scope_id);
410            }
411
412            NodeKind::Assignment { lhs, rhs, .. } => {
413                self.analyze_node(lhs, scope_id);
414                self.analyze_node(rhs, scope_id);
415            }
416
417            // Phase 1: Critical LSP Features (Issue #188)
418            NodeKind::VariableListDeclaration {
419                declarator,
420                variables,
421                attributes,
422                initializer,
423            } => {
424                // Handle multi-variable declarations like: my ($x, $y, $z) = (1, 2, 3);
425                for var in variables {
426                    if let NodeKind::Variable { sigil, name } = &var.kind {
427                        let token_type = match declarator.as_str() {
428                            "my" | "state" => SemanticTokenType::VariableDeclaration,
429                            "our" => SemanticTokenType::Variable,
430                            "local" => SemanticTokenType::Variable,
431                            _ => SemanticTokenType::Variable,
432                        };
433
434                        let mut modifiers = vec![SemanticTokenModifier::Declaration];
435                        if declarator == "state" || attributes.iter().any(|a| a == ":shared") {
436                            modifiers.push(SemanticTokenModifier::Static);
437                        }
438
439                        self.semantic_tokens.push(SemanticToken {
440                            location: var.location,
441                            token_type,
442                            modifiers,
443                        });
444
445                        // Add hover info
446                        let hover = HoverInfo {
447                            signature: format!("{} {}{}", declarator, sigil, name),
448                            documentation: self.extract_documentation(var.location.start),
449                            details: if attributes.is_empty() {
450                                vec![]
451                            } else {
452                                vec![format!("Attributes: {}", attributes.join(", "))]
453                            },
454                        };
455
456                        self.hover_info.insert(var.location, hover);
457                    }
458                }
459
460                if let Some(init) = initializer {
461                    self.analyze_node(init, scope_id);
462                }
463            }
464
465            NodeKind::Ternary { condition, then_expr, else_expr } => {
466                // Handle conditional expressions: $x ? $y : $z
467                self.analyze_node(condition, scope_id);
468                self.analyze_node(then_expr, scope_id);
469                self.analyze_node(else_expr, scope_id);
470            }
471
472            NodeKind::ArrayLiteral { elements } => {
473                // Handle array constructors: [1, 2, 3, 4]
474                for elem in elements {
475                    self.analyze_node(elem, scope_id);
476                }
477            }
478
479            NodeKind::HashLiteral { pairs } => {
480                // Handle hash constructors: { key1 => "value1", key2 => "value2" }
481                for (key, value) in pairs {
482                    self.analyze_node(key, scope_id);
483                    self.analyze_node(value, scope_id);
484                }
485            }
486
487            NodeKind::Try { body, catch_blocks, finally_block } => {
488                // Handle try/catch error handling
489                self.analyze_node(body, scope_id);
490
491                for (_var, catch_body) in catch_blocks {
492                    // Note: var is just a String (variable name), not a Node
493                    self.analyze_node(catch_body, scope_id);
494                }
495
496                if let Some(finally) = finally_block {
497                    self.analyze_node(finally, scope_id);
498                }
499            }
500
501            NodeKind::PhaseBlock { phase: _, phase_span: _, block } => {
502                // Handle BEGIN/END/INIT/CHECK/UNITCHECK blocks
503                self.semantic_tokens.push(SemanticToken {
504                    location: node.location,
505                    token_type: SemanticTokenType::Keyword,
506                    modifiers: vec![],
507                });
508
509                self.analyze_node(block, scope_id);
510            }
511
512            NodeKind::ExpressionStatement { expression } => {
513                // Handle expression statements: $x + 10;
514                // Just delegate to the wrapped expression
515                self.analyze_node(expression, scope_id);
516            }
517
518            NodeKind::Do { block } => {
519                // Handle do blocks: do { ... }
520                // Do blocks create expression context but maintain scope
521                self.analyze_node(block, scope_id);
522            }
523
524            NodeKind::Eval { block } => {
525                // Handle eval blocks: eval { dangerous_operation(); }
526                self.semantic_tokens.push(SemanticToken {
527                    location: node.location,
528                    token_type: SemanticTokenType::Keyword,
529                    modifiers: vec![],
530                });
531
532                // Eval blocks should create a new scope for error isolation
533                self.analyze_node(block, scope_id);
534            }
535
536            NodeKind::VariableWithAttributes { variable, attributes } => {
537                // Handle attributed variables: my $x :shared = 42;
538                // Analyze the base variable node
539                self.analyze_node(variable, scope_id);
540
541                // Add modifier tokens for special attributes
542                if attributes.iter().any(|a| a == ":shared" || a == ":lvalue") {
543                    // The variable node was already processed, so we just note the attributes
544                    // in the hover info (if we need to enhance it later)
545                }
546            }
547
548            NodeKind::Unary { op, operand } => {
549                // Handle unary operators: -$x, !$x, ++$x, $x++
550                // Add token for the operator itself (if needed for highlighting)
551                if matches!(op.as_str(), "++" | "--" | "!" | "-" | "~" | "\\") {
552                    self.semantic_tokens.push(SemanticToken {
553                        location: node.location,
554                        token_type: SemanticTokenType::Operator,
555                        modifiers: vec![],
556                    });
557                }
558
559                // Handle file test operators: -e, -d, -f, -r, -w, -x, -s, -z, -T, -B, etc.
560                if is_file_test_operator(op) {
561                    self.semantic_tokens.push(SemanticToken {
562                        location: node.location,
563                        token_type: SemanticTokenType::Operator,
564                        modifiers: vec![],
565                    });
566                }
567
568                self.analyze_node(operand, scope_id);
569            }
570
571            NodeKind::Readline { filehandle } => {
572                // Handle readline/diamond operator: <STDIN>, <$fh>, <>
573                self.semantic_tokens.push(SemanticToken {
574                    location: node.location,
575                    token_type: SemanticTokenType::Operator, // diamond operator is an I/O operator
576                    modifiers: vec![],
577                });
578
579                // Add hover info for common filehandles
580                if let Some(fh) = filehandle {
581                    let hover = HoverInfo {
582                        signature: format!("<{}>", fh),
583                        documentation: match fh.as_str() {
584                            "STDIN" => Some("Standard input filehandle".to_string()),
585                            "STDOUT" => Some("Standard output filehandle".to_string()),
586                            "STDERR" => Some("Standard error filehandle".to_string()),
587                            _ => Some(format!("Read from filehandle {}", fh)),
588                        },
589                        details: vec![],
590                    };
591                    self.hover_info.insert(node.location, hover);
592                } else {
593                    // Bare <> reads from ARGV or STDIN
594                    let hover = HoverInfo {
595                        signature: "<>".to_string(),
596                        documentation: Some("Read from command-line files or STDIN".to_string()),
597                        details: vec![],
598                    };
599                    self.hover_info.insert(node.location, hover);
600                }
601            }
602
603            // Phase 2/3 Handlers
604            NodeKind::MethodCall { object, method, args } => {
605                self.analyze_node(object, scope_id);
606
607                if let Some(offset) =
608                    self.find_substring_in_source_after(node, method, object.location.end)
609                {
610                    self.semantic_tokens.push(SemanticToken {
611                        location: SourceLocation { start: offset, end: offset + method.len() },
612                        token_type: SemanticTokenType::Method,
613                        modifiers: vec![],
614                    });
615                }
616
617                for arg in args {
618                    self.analyze_node(arg, scope_id);
619                }
620            }
621
622            NodeKind::IndirectCall { method, object, args } => {
623                if let Some(offset) = self.find_method_name_in_source(node, method) {
624                    self.semantic_tokens.push(SemanticToken {
625                        location: SourceLocation { start: offset, end: offset + method.len() },
626                        token_type: SemanticTokenType::Method,
627                        modifiers: vec![],
628                    });
629                }
630                self.analyze_node(object, scope_id);
631                for arg in args {
632                    self.analyze_node(arg, scope_id);
633                }
634            }
635
636            NodeKind::Use { module, args, .. } => {
637                self.semantic_tokens.push(SemanticToken {
638                    location: SourceLocation {
639                        start: node.location.start,
640                        end: node.location.start + 3,
641                    },
642                    token_type: SemanticTokenType::Keyword,
643                    modifiers: vec![],
644                });
645
646                let mut args_start = node.location.start + 3;
647                if let Some(offset) = self.find_substring_in_source(node, module) {
648                    self.semantic_tokens.push(SemanticToken {
649                        location: SourceLocation { start: offset, end: offset + module.len() },
650                        token_type: SemanticTokenType::Namespace,
651                        modifiers: vec![],
652                    });
653                    args_start = offset + module.len();
654                }
655
656                self.analyze_string_args(node, args, args_start);
657            }
658
659            NodeKind::No { module, args, .. } => {
660                self.semantic_tokens.push(SemanticToken {
661                    location: SourceLocation {
662                        start: node.location.start,
663                        end: node.location.start + 2,
664                    },
665                    token_type: SemanticTokenType::Keyword,
666                    modifiers: vec![],
667                });
668
669                let mut args_start = node.location.start + 2;
670                if let Some(offset) = self.find_substring_in_source(node, module) {
671                    self.semantic_tokens.push(SemanticToken {
672                        location: SourceLocation { start: offset, end: offset + module.len() },
673                        token_type: SemanticTokenType::Namespace,
674                        modifiers: vec![],
675                    });
676                    args_start = offset + module.len();
677                }
678
679                self.analyze_string_args(node, args, args_start);
680            }
681
682            NodeKind::Given { expr, body } => {
683                self.semantic_tokens.push(SemanticToken {
684                    location: SourceLocation {
685                        start: node.location.start,
686                        end: node.location.start + 5,
687                    }, // given
688                    token_type: SemanticTokenType::KeywordControl,
689                    modifiers: vec![],
690                });
691                self.analyze_node(expr, scope_id);
692                self.analyze_node(body, scope_id);
693            }
694
695            NodeKind::When { condition, body } => {
696                self.semantic_tokens.push(SemanticToken {
697                    location: SourceLocation {
698                        start: node.location.start,
699                        end: node.location.start + 4,
700                    }, // when
701                    token_type: SemanticTokenType::KeywordControl,
702                    modifiers: vec![],
703                });
704                self.analyze_node(condition, scope_id);
705                self.analyze_node(body, scope_id);
706            }
707
708            NodeKind::Default { body } => {
709                self.semantic_tokens.push(SemanticToken {
710                    location: SourceLocation {
711                        start: node.location.start,
712                        end: node.location.start + 7,
713                    }, // default
714                    token_type: SemanticTokenType::KeywordControl,
715                    modifiers: vec![],
716                });
717                self.analyze_node(body, scope_id);
718            }
719
720            NodeKind::Return { value } => {
721                self.semantic_tokens.push(SemanticToken {
722                    location: SourceLocation {
723                        start: node.location.start,
724                        end: node.location.start + 6,
725                    }, // return
726                    token_type: SemanticTokenType::KeywordControl,
727                    modifiers: vec![],
728                });
729                if let Some(v) = value {
730                    self.analyze_node(v, scope_id);
731                }
732            }
733
734            NodeKind::Class { name, body } => {
735                self.semantic_tokens.push(SemanticToken {
736                    location: SourceLocation {
737                        start: node.location.start,
738                        end: node.location.start + 5,
739                    }, // class
740                    token_type: SemanticTokenType::Keyword,
741                    modifiers: vec![],
742                });
743
744                if let Some(offset) = self.find_substring_in_source(node, name) {
745                    self.semantic_tokens.push(SemanticToken {
746                        location: SourceLocation { start: offset, end: offset + name.len() },
747                        token_type: SemanticTokenType::Class,
748                        modifiers: vec![SemanticTokenModifier::Declaration],
749                    });
750                }
751
752                let class_scope = self.get_scope_for(node, ScopeKind::Package);
753                self.analyze_node(body, class_scope);
754            }
755
756            NodeKind::Signature { parameters } => {
757                for param in parameters {
758                    self.analyze_node(param, scope_id);
759                }
760            }
761
762            NodeKind::MandatoryParameter { variable }
763            | NodeKind::OptionalParameter { variable, .. }
764            | NodeKind::SlurpyParameter { variable }
765            | NodeKind::NamedParameter { variable } => {
766                self.analyze_node(variable, scope_id);
767            }
768
769            NodeKind::Diamond | NodeKind::Ellipsis => {
770                self.semantic_tokens.push(SemanticToken {
771                    location: node.location,
772                    token_type: SemanticTokenType::Operator,
773                    modifiers: vec![],
774                });
775            }
776
777            NodeKind::Undef => {
778                self.semantic_tokens.push(SemanticToken {
779                    location: node.location,
780                    token_type: SemanticTokenType::Keyword,
781                    modifiers: vec![],
782                });
783            }
784
785            NodeKind::Identifier { .. } => {
786                // Bareword identifiers, usually left to lexical highlighting
787                // but we handle them to avoid the default case.
788            }
789
790            NodeKind::Heredoc { .. } => {
791                self.semantic_tokens.push(SemanticToken {
792                    location: node.location,
793                    token_type: SemanticTokenType::String,
794                    modifiers: vec![],
795                });
796            }
797
798            NodeKind::Glob { .. } => {
799                self.semantic_tokens.push(SemanticToken {
800                    location: node.location,
801                    token_type: SemanticTokenType::Operator,
802                    modifiers: vec![],
803                });
804            }
805
806            NodeKind::DataSection { .. } => {
807                self.semantic_tokens.push(SemanticToken {
808                    location: node.location,
809                    token_type: SemanticTokenType::Comment,
810                    modifiers: vec![],
811                });
812            }
813
814            NodeKind::Prototype { .. } => {
815                self.semantic_tokens.push(SemanticToken {
816                    location: node.location,
817                    token_type: SemanticTokenType::Punctuation,
818                    modifiers: vec![],
819                });
820            }
821
822            NodeKind::Typeglob { .. } => {
823                self.semantic_tokens.push(SemanticToken {
824                    location: node.location,
825                    token_type: SemanticTokenType::Variable,
826                    modifiers: vec![],
827                });
828            }
829
830            NodeKind::Untie { variable } => {
831                self.analyze_node(variable, scope_id);
832            }
833
834            NodeKind::LoopControl { .. } => {
835                self.semantic_tokens.push(SemanticToken {
836                    location: node.location,
837                    token_type: SemanticTokenType::KeywordControl,
838                    modifiers: vec![],
839                });
840            }
841
842            NodeKind::Goto { target } => {
843                self.semantic_tokens.push(SemanticToken {
844                    location: node.location,
845                    token_type: SemanticTokenType::KeywordControl,
846                    modifiers: vec![],
847                });
848                self.analyze_node(target, scope_id);
849            }
850
851            NodeKind::MissingExpression
852            | NodeKind::MissingStatement
853            | NodeKind::MissingIdentifier
854            | NodeKind::MissingBlock => {
855                // No tokens for missing constructs
856            }
857
858            NodeKind::Tie { variable, package, args } => {
859                self.analyze_node(variable, scope_id);
860                self.analyze_node(package, scope_id);
861                for arg in args {
862                    self.analyze_node(arg, scope_id);
863                }
864            }
865
866            NodeKind::StatementModifier { statement, condition, modifier } => {
867                // Handle postfix loop modifiers: for, while, until, foreach
868                // e.g., print $_ for @list; or $x++ while $x < 10;
869                if matches!(modifier.as_str(), "for" | "foreach" | "while" | "until") {
870                    self.semantic_tokens.push(SemanticToken {
871                        location: node.location,
872                        token_type: SemanticTokenType::KeywordControl,
873                        modifiers: vec![],
874                    });
875                }
876                self.analyze_node(statement, scope_id);
877                self.analyze_node(condition, scope_id);
878            }
879
880            NodeKind::Format { name, .. } => {
881                self.semantic_tokens.push(SemanticToken {
882                    location: node.location,
883                    token_type: SemanticTokenType::FunctionDeclaration,
884                    modifiers: vec![SemanticTokenModifier::Declaration],
885                });
886
887                let hover = HoverInfo {
888                    signature: format!("format {} =", name),
889                    documentation: None,
890                    details: vec![],
891                };
892                self.hover_info.insert(node.location, hover);
893            }
894
895            NodeKind::Error { .. } | NodeKind::UnknownRest => {
896                // No semantic tokens for error nodes
897            }
898        }
899    }
900
901    /// Extract documentation (POD or comments) preceding a position
902    pub(super) fn extract_documentation(&self, start: usize) -> Option<String> {
903        static POD_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
904        static COMMENT_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
905
906        if self.source.is_empty() {
907            return None;
908        }
909        let before = &self.source[..start];
910
911        // Check for POD blocks ending with =cut
912        let pod_re = POD_RE
913            .get_or_init(|| Regex::new(r"(?ms)(=[a-zA-Z0-9].*?\n=cut\n?)\s*$"))
914            .as_ref()
915            .ok()?;
916        if let Some(caps) = pod_re.captures(before) {
917            if let Some(pod_text) = caps.get(1) {
918                return Some(pod_text.as_str().trim().to_string());
919            }
920        }
921
922        // Check for consecutive comment lines
923        let comment_re =
924            COMMENT_RE.get_or_init(|| Regex::new(r"(?m)(#.*\n)+\s*$")).as_ref().ok()?;
925        if let Some(caps) = comment_re.captures(before) {
926            if let Some(comment_match) = caps.get(0) {
927                // Strip the # prefix from each comment line
928                let doc = comment_match
929                    .as_str()
930                    .lines()
931                    .map(|line| line.trim_start_matches('#').trim())
932                    .filter(|line| !line.is_empty())
933                    .collect::<Vec<_>>()
934                    .join(" ");
935                return Some(doc);
936            }
937        }
938
939        None
940    }
941
942    /// Extract the POD `=head1 NAME` section for a package.
943    ///
944    /// Scans the entire source for a `=head1 NAME` POD section and returns
945    /// its content if it mentions the given package name.
946    pub(super) fn extract_pod_name_section(&self, package_name: &str) -> Option<String> {
947        if self.source.is_empty() {
948            return None;
949        }
950
951        let mut in_name_section = false;
952        let mut name_lines: Vec<&str> = Vec::new();
953
954        for line in self.source.lines() {
955            let trimmed: &str = line.trim();
956            if trimmed.starts_with("=head1") {
957                if in_name_section {
958                    break;
959                }
960                let heading = trimmed.strip_prefix("=head1").map(|s: &str| s.trim());
961                if heading == Some("NAME") {
962                    in_name_section = true;
963                    continue;
964                }
965            } else if trimmed.starts_with("=cut") && in_name_section {
966                break;
967            } else if trimmed.starts_with('=') && in_name_section {
968                break;
969            } else if in_name_section && !trimmed.is_empty() {
970                name_lines.push(trimmed);
971            }
972        }
973
974        if !name_lines.is_empty() {
975            let name_doc = name_lines.join(" ");
976            if name_doc.contains(package_name)
977                || name_doc.contains(&package_name.replace("::", "-"))
978            {
979                return Some(name_doc);
980            }
981        }
982
983        None
984    }
985
986    /// Get scope id for a node by consulting the symbol table
987    pub(super) fn get_scope_for(&self, node: &Node, kind: ScopeKind) -> ScopeId {
988        for scope in self.symbol_table.scopes.values() {
989            if scope.kind == kind
990                && scope.location.start == node.location.start
991                && scope.location.end == node.location.end
992            {
993                return scope.id;
994            }
995        }
996        0
997    }
998
999    /// Get line number from byte offset (simplified version)
1000    pub(super) fn line_number(&self, offset: usize) -> usize {
1001        if self.source.is_empty() { 1 } else { self.source[..offset].lines().count() + 1 }
1002    }
1003
1004    /// Find substring in source within node's range
1005    pub(super) fn find_substring_in_source(&self, node: &Node, substring: &str) -> Option<usize> {
1006        if self.source.len() < node.location.end {
1007            return None;
1008        }
1009        let node_text = &self.source[node.location.start..node.location.end];
1010        if let Some(pos) = node_text.find(substring) {
1011            return Some(node.location.start + pos);
1012        }
1013        None
1014    }
1015
1016    /// Find method name in source within node's range
1017    pub(super) fn find_method_name_in_source(
1018        &self,
1019        node: &Node,
1020        method_name: &str,
1021    ) -> Option<usize> {
1022        self.find_substring_in_source(node, method_name)
1023    }
1024
1025    /// Find substring in source within node's range, starting search after a specific absolute offset
1026    pub(super) fn find_substring_in_source_after(
1027        &self,
1028        node: &Node,
1029        substring: &str,
1030        after: usize,
1031    ) -> Option<usize> {
1032        if self.source.len() < node.location.end || after >= node.location.end {
1033            return None;
1034        }
1035
1036        let start_rel = after.saturating_sub(node.location.start);
1037
1038        let node_text = &self.source[node.location.start..node.location.end];
1039        if start_rel >= node_text.len() {
1040            return None;
1041        }
1042
1043        let text_to_search = &node_text[start_rel..];
1044        if let Some(pos) = text_to_search.find(substring) {
1045            return Some(node.location.start + start_rel + pos);
1046        }
1047        None
1048    }
1049
1050    /// Analyze string arguments for highlighting (e.g. in use/no statements)
1051    pub(super) fn analyze_string_args(
1052        &mut self,
1053        node: &Node,
1054        args: &[String],
1055        start_offset: usize,
1056    ) {
1057        let mut current_offset = start_offset;
1058        for arg in args {
1059            if let Some(offset) = self.find_substring_in_source_after(node, arg, current_offset) {
1060                self.semantic_tokens.push(SemanticToken {
1061                    location: SourceLocation { start: offset, end: offset + arg.len() },
1062                    token_type: SemanticTokenType::String,
1063                    modifiers: vec![],
1064                });
1065                current_offset = offset + arg.len();
1066            }
1067        }
1068    }
1069
1070    /// Infer the type of a node based on its context and initialization.
1071    ///
1072    /// Provides basic type inference for Perl expressions to enhance hover
1073    /// information with derived type information. Supports common patterns:
1074    /// - Literal values (numbers, strings, arrays, hashes)
1075    /// - Variable references (looks up declaration)
1076    /// - Function calls (basic return type hints)
1077    ///
1078    /// In the semantic workflow (Parse -> Index -> Analyze), this method runs
1079    /// during the Analyze stage and consumes symbols produced during Index.
1080    ///
1081    /// # Arguments
1082    ///
1083    /// * `node` - The AST node to infer type for
1084    ///
1085    /// # Returns
1086    ///
1087    /// A string describing the inferred type, or None if type cannot be determined
1088    pub fn infer_type(&self, node: &Node) -> Option<String> {
1089        match &node.kind {
1090            NodeKind::Number { .. } => Some("number".to_string()),
1091            NodeKind::String { .. } => Some("string".to_string()),
1092            NodeKind::ArrayLiteral { .. } => Some("array".to_string()),
1093            NodeKind::HashLiteral { .. } => Some("hash".to_string()),
1094
1095            NodeKind::Variable { sigil, name } => {
1096                // Look up the variable in the symbol table
1097                let kind = match sigil.as_str() {
1098                    "$" => SymbolKind::scalar(),
1099                    "@" => SymbolKind::array(),
1100                    "%" => SymbolKind::hash(),
1101                    _ => return None,
1102                };
1103
1104                let symbols = self.symbol_table.find_symbol(name, 0, kind);
1105                symbols.first()?;
1106
1107                // Return the basic type based on sigil
1108                match sigil.as_str() {
1109                    "$" => Some("scalar".to_string()),
1110                    "@" => Some("array".to_string()),
1111                    "%" => Some("hash".to_string()),
1112                    _ => None,
1113                }
1114            }
1115
1116            NodeKind::FunctionCall { name, .. } => {
1117                // Basic return type inference for built-in functions
1118                match name.as_str() {
1119                    "scalar" => Some("scalar".to_string()),
1120                    "ref" => Some("string".to_string()),
1121                    "length" | "index" | "rindex" => Some("number".to_string()),
1122                    "split" => Some("array".to_string()),
1123                    "keys" | "values" => Some("array".to_string()),
1124                    _ => None,
1125                }
1126            }
1127
1128            NodeKind::Binary { op, .. } => {
1129                // Infer based on operator
1130                match op.as_str() {
1131                    "+" | "-" | "*" | "/" | "%" | "**" => Some("number".to_string()),
1132                    "." | "x" => Some("string".to_string()),
1133                    "==" | "!=" | "<" | ">" | "<=" | ">=" | "eq" | "ne" | "lt" | "gt" | "le"
1134                    | "ge" => Some("boolean".to_string()),
1135                    _ => None,
1136                }
1137            }
1138
1139            _ => None,
1140        }
1141    }
1142}
1143
1144/// Build a parenthesised parameter list string from a `Signature` AST node.
1145///
1146/// Extracts each parameter variable's sigil and name and joins them with
1147/// ", ".  Returns `"(...)"` as a safe fallback for any unrecognised structure.
1148fn format_signature_params(sig_node: &Node) -> String {
1149    let NodeKind::Signature { parameters } = &sig_node.kind else {
1150        return "(...)".to_string();
1151    };
1152
1153    let labels: Vec<String> = parameters
1154        .iter()
1155        .filter_map(|param| {
1156            let var = match &param.kind {
1157                NodeKind::MandatoryParameter { variable }
1158                | NodeKind::OptionalParameter { variable, .. }
1159                | NodeKind::SlurpyParameter { variable }
1160                | NodeKind::NamedParameter { variable } => variable.as_ref(),
1161                NodeKind::Variable { .. } => param,
1162                _ => return None,
1163            };
1164            if let NodeKind::Variable { sigil, name } = &var.kind {
1165                Some(format!("{}{}", sigil, name))
1166            } else {
1167                None
1168            }
1169        })
1170        .collect();
1171
1172    format!("({})", labels.join(", "))
1173}