1use 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 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 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 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 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 SemanticTokenType::Variable
92 };
93
94 self.semantic_tokens.push(SemanticToken {
95 location: node.location,
96 token_type,
97 modifiers: vec![],
98 });
99
100 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 {
123 name,
124 prototype,
125 signature,
126 attributes,
127 body,
128 name_span: _,
129 declarator: _,
130 } => {
131 if let Some(sub_name) = name {
132 let token = SemanticToken {
134 location: node.location,
135 token_type: SemanticTokenType::FunctionDeclaration,
136 modifiers: vec![SemanticTokenModifier::Declaration],
137 };
138
139 self.semantic_tokens.push(token);
140
141 let mut signature_str = format!("sub {}", sub_name);
143 if let Some(sig_node) = signature {
144 signature_str.push_str(&format_signature_params(sig_node));
145 }
146
147 let hover = HoverInfo {
148 signature: signature_str,
149 documentation: self.extract_sub_documentation(node.location.start, body),
150 details: if attributes.is_empty() {
151 vec![]
152 } else {
153 vec![format!("Attributes: {}", attributes.join(", "))]
154 },
155 };
156
157 self.hover_info.insert(node.location, hover);
158 } else {
159 self.semantic_tokens.push(SemanticToken {
162 location: SourceLocation {
163 start: node.location.start,
164 end: node.location.start + 3, },
166 token_type: SemanticTokenType::Keyword,
167 modifiers: vec![],
168 });
169
170 let mut signature_str = "sub".to_string();
172 if let Some(sig_node) = signature {
173 signature_str.push_str(&format_signature_params(sig_node));
174 }
175 signature_str.push_str(" { ... }");
176
177 let mut details = vec!["Anonymous subroutine (closure)".to_string()];
178 if !attributes.is_empty() {
179 details.push(format!("Attributes: {}", attributes.join(", ")));
180 }
181
182 let hover = HoverInfo {
183 signature: signature_str,
184 documentation: self.extract_sub_documentation(node.location.start, body),
185 details,
186 };
187
188 self.hover_info.insert(node.location, hover);
189 }
190
191 {
192 let sub_scope = self.get_scope_for(node, ScopeKind::Subroutine);
194
195 if let Some(proto) = prototype {
196 self.analyze_node(proto, sub_scope);
197 }
198 if let Some(sig) = signature {
199 self.analyze_node(sig, sub_scope);
200 }
201
202 self.analyze_node(body, sub_scope);
203 }
204 }
205
206 NodeKind::Method { name, name_span: _, signature, attributes, body } => {
207 self.semantic_tokens.push(SemanticToken {
208 location: node.location, token_type: SemanticTokenType::FunctionDeclaration,
210 modifiers: vec![SemanticTokenModifier::Declaration],
211 });
212
213 let hover = HoverInfo {
215 signature: format!("method {}", name),
216 documentation: self.extract_sub_documentation(node.location.start, body),
217 details: if attributes.is_empty() {
218 vec![]
219 } else {
220 vec![format!("Attributes: {}", attributes.join(", "))]
221 },
222 };
223 self.hover_info.insert(node.location, hover);
224
225 let sub_scope = self.get_scope_for(node, ScopeKind::Subroutine);
227 if let Some(sig) = signature {
228 self.analyze_node(sig, sub_scope);
229 }
230 self.analyze_node(body, sub_scope);
231 }
232
233 NodeKind::FunctionCall { name, args } => {
234 {
236 let token_type = if is_control_keyword(name) {
237 SemanticTokenType::KeywordControl
238 } else if is_builtin_function(name) {
239 SemanticTokenType::Function
240 } else {
241 let symbols =
243 self.symbol_table.find_symbol(name, scope_id, SymbolKind::Subroutine);
244 if symbols.is_empty() {
245 SemanticTokenType::Function
246 } else {
247 SemanticTokenType::Function
248 }
249 };
250
251 self.semantic_tokens.push(SemanticToken {
252 location: node.location,
253 token_type,
254 modifiers: if is_builtin_function(name) && !is_control_keyword(name) {
255 vec![SemanticTokenModifier::DefaultLibrary]
256 } else {
257 vec![]
258 },
259 });
260
261 if let Some(doc) = get_builtin_documentation(name) {
263 let hover = HoverInfo {
264 signature: doc.signature.to_string(),
265 documentation: Some(doc.description.to_string()),
266 details: vec![],
267 };
268
269 self.hover_info.insert(node.location, hover);
270 }
271 }
272
273 for arg in args {
275 self.analyze_node(arg, scope_id);
276 }
277 }
278
279 NodeKind::Package { name, block, name_span: _ } => {
280 self.semantic_tokens.push(SemanticToken {
281 location: node.location,
282 token_type: SemanticTokenType::Namespace,
283 modifiers: vec![SemanticTokenModifier::Declaration],
284 });
285
286 let documentation = self
288 .extract_pod_name_section(name)
289 .or_else(|| self.extract_documentation(node.location.start));
290
291 let hover = HoverInfo {
292 signature: format!("package {}", name),
293 documentation,
294 details: vec![],
295 };
296
297 self.hover_info.insert(node.location, hover);
298
299 if let Some(block_node) = block {
300 let package_scope = self.get_scope_for(node, ScopeKind::Package);
301 self.analyze_node(block_node, package_scope);
302 }
303 }
304
305 NodeKind::String { value: _, interpolated: _ } => {
306 self.semantic_tokens.push(SemanticToken {
307 location: node.location,
308 token_type: SemanticTokenType::String,
309 modifiers: vec![],
310 });
311 }
312
313 NodeKind::Number { value: _ } => {
314 self.semantic_tokens.push(SemanticToken {
315 location: node.location,
316 token_type: SemanticTokenType::Number,
317 modifiers: vec![],
318 });
319 }
320
321 NodeKind::Regex { .. } => {
322 self.semantic_tokens.push(SemanticToken {
323 location: node.location,
324 token_type: SemanticTokenType::Regex,
325 modifiers: vec![],
326 });
327 }
328
329 NodeKind::Match { expr, .. } => {
330 self.semantic_tokens.push(SemanticToken {
331 location: node.location,
332 token_type: SemanticTokenType::Regex,
333 modifiers: vec![],
334 });
335 self.analyze_node(expr, scope_id);
336 }
337 NodeKind::Substitution { expr, .. } => {
338 self.semantic_tokens.push(SemanticToken {
340 location: node.location,
341 token_type: SemanticTokenType::Operator,
342 modifiers: vec![],
343 });
344 self.analyze_node(expr, scope_id);
345 }
346 NodeKind::Transliteration { expr, .. } => {
347 self.semantic_tokens.push(SemanticToken {
349 location: node.location,
350 token_type: SemanticTokenType::Operator,
351 modifiers: vec![],
352 });
353 self.analyze_node(expr, scope_id);
354 }
355
356 NodeKind::LabeledStatement { label: _, statement } => {
357 self.semantic_tokens.push(SemanticToken {
358 location: node.location,
359 token_type: SemanticTokenType::Label,
360 modifiers: vec![],
361 });
362
363 {
364 self.analyze_node(statement, scope_id);
365 }
366 }
367
368 NodeKind::If { condition, then_branch, elsif_branches, else_branch, .. } => {
370 self.analyze_node(condition, scope_id);
371 self.analyze_node(then_branch, scope_id);
372 for (elsif_cond, elsif_branch) in elsif_branches {
373 self.analyze_node(elsif_cond, scope_id);
374 self.analyze_node(elsif_branch, scope_id);
375 }
376 if let Some(else_node) = else_branch {
377 self.analyze_node(else_node, scope_id);
378 }
379 }
380
381 NodeKind::While { condition, body, continue_block: _, .. } => {
382 self.analyze_node(condition, scope_id);
383 self.analyze_node(body, scope_id);
384 }
385
386 NodeKind::For { init, condition, update, body, .. } => {
387 if let Some(init_node) = init {
388 self.analyze_node(init_node, scope_id);
389 }
390 if let Some(cond_node) = condition {
391 self.analyze_node(cond_node, scope_id);
392 }
393 if let Some(update_node) = update {
394 self.analyze_node(update_node, scope_id);
395 }
396 self.analyze_node(body, scope_id);
397 }
398
399 NodeKind::Foreach { variable, list, body, continue_block } => {
400 self.analyze_node(variable, scope_id);
401 self.analyze_node(list, scope_id);
402 self.analyze_node(body, scope_id);
403 if let Some(cb) = continue_block {
404 self.analyze_node(cb, scope_id);
405 }
406 }
407
408 NodeKind::Block { statements } => {
410 for stmt in statements {
411 self.analyze_node(stmt, scope_id);
412 }
413 }
414
415 NodeKind::Binary { left, right, .. } => {
416 self.analyze_node(left, scope_id);
417 self.analyze_node(right, scope_id);
418 }
419
420 NodeKind::Assignment { lhs, rhs, .. } => {
421 self.analyze_node(lhs, scope_id);
422 self.analyze_node(rhs, scope_id);
423 }
424
425 NodeKind::VariableListDeclaration {
427 declarator,
428 variables,
429 attributes,
430 initializer,
431 } => {
432 for var in variables {
434 if let NodeKind::Variable { sigil, name } = &var.kind {
435 let token_type = match declarator.as_str() {
436 "my" | "state" => SemanticTokenType::VariableDeclaration,
437 "our" => SemanticTokenType::Variable,
438 "local" => SemanticTokenType::Variable,
439 _ => SemanticTokenType::Variable,
440 };
441
442 let mut modifiers = vec![SemanticTokenModifier::Declaration];
443 if declarator == "state" || attributes.iter().any(|a| a == ":shared") {
444 modifiers.push(SemanticTokenModifier::Static);
445 }
446
447 self.semantic_tokens.push(SemanticToken {
448 location: var.location,
449 token_type,
450 modifiers,
451 });
452
453 let hover = HoverInfo {
455 signature: format!("{} {}{}", declarator, sigil, name),
456 documentation: self.extract_documentation(var.location.start),
457 details: if attributes.is_empty() {
458 vec![]
459 } else {
460 vec![format!("Attributes: {}", attributes.join(", "))]
461 },
462 };
463
464 self.hover_info.insert(var.location, hover);
465 } else if matches!(var.kind, NodeKind::NestedVariableList { .. }) {
466 self.register_nested_decl_vars(var, declarator, attributes, scope_id);
470 }
471 }
472
473 if let Some(init) = initializer {
474 self.analyze_node(init, scope_id);
475 }
476 }
477
478 NodeKind::NestedVariableList { items } => {
479 for item in items {
481 self.analyze_node(item, scope_id);
482 }
483 }
484
485 NodeKind::Ternary { condition, then_expr, else_expr } => {
486 self.analyze_node(condition, scope_id);
488 self.analyze_node(then_expr, scope_id);
489 self.analyze_node(else_expr, scope_id);
490 }
491
492 NodeKind::ArrayLiteral { elements } => {
493 for elem in elements {
495 self.analyze_node(elem, scope_id);
496 }
497 }
498
499 NodeKind::HashLiteral { pairs } => {
500 for (key, value) in pairs {
502 self.analyze_node(key, scope_id);
503 self.analyze_node(value, scope_id);
504 }
505 }
506
507 NodeKind::Try { body, catch_blocks, finally_block } => {
508 self.analyze_node(body, scope_id);
510
511 for (_var, catch_body) in catch_blocks {
512 self.analyze_node(catch_body, scope_id);
514 }
515
516 if let Some(finally) = finally_block {
517 self.analyze_node(finally, scope_id);
518 }
519 }
520
521 NodeKind::PhaseBlock { phase: _, phase_span: _, block } => {
522 self.semantic_tokens.push(SemanticToken {
524 location: node.location,
525 token_type: SemanticTokenType::Keyword,
526 modifiers: vec![],
527 });
528
529 self.analyze_node(block, scope_id);
530 }
531
532 NodeKind::ExpressionStatement { expression } => {
533 self.analyze_node(expression, scope_id);
536 }
537
538 NodeKind::Do { block } => {
539 self.analyze_node(block, scope_id);
542 }
543
544 NodeKind::Eval { block } => {
545 self.semantic_tokens.push(SemanticToken {
547 location: node.location,
548 token_type: SemanticTokenType::Keyword,
549 modifiers: vec![],
550 });
551
552 self.analyze_node(block, scope_id);
554 }
555
556 NodeKind::Defer { block } => {
557 self.semantic_tokens.push(SemanticToken {
559 location: node.location,
560 token_type: SemanticTokenType::Keyword,
561 modifiers: vec![],
562 });
563 self.analyze_node(block, scope_id);
564 }
565
566 NodeKind::VariableWithAttributes { variable, attributes } => {
567 self.analyze_node(variable, scope_id);
570
571 if attributes.iter().any(|a| a == ":shared" || a == ":lvalue") {
573 }
576 }
577
578 NodeKind::Unary { op, operand } => {
579 if matches!(op.as_str(), "++" | "--" | "!" | "-" | "~" | "\\") {
582 self.semantic_tokens.push(SemanticToken {
583 location: node.location,
584 token_type: SemanticTokenType::Operator,
585 modifiers: vec![],
586 });
587 }
588
589 if is_file_test_operator(op) {
591 self.semantic_tokens.push(SemanticToken {
592 location: node.location,
593 token_type: SemanticTokenType::Operator,
594 modifiers: vec![],
595 });
596 }
597
598 self.analyze_node(operand, scope_id);
599 }
600
601 NodeKind::Readline { filehandle } => {
602 self.semantic_tokens.push(SemanticToken {
604 location: node.location,
605 token_type: SemanticTokenType::Operator, modifiers: vec![],
607 });
608
609 if let Some(fh) = filehandle {
611 let hover = HoverInfo {
612 signature: format!("<{}>", fh),
613 documentation: match fh.as_str() {
614 "STDIN" => Some("Standard input filehandle".to_string()),
615 "STDOUT" => Some("Standard output filehandle".to_string()),
616 "STDERR" => Some("Standard error filehandle".to_string()),
617 _ => Some(format!("Read from filehandle {}", fh)),
618 },
619 details: vec![],
620 };
621 self.hover_info.insert(node.location, hover);
622 } else {
623 let hover = HoverInfo {
625 signature: "<>".to_string(),
626 documentation: Some("Read from command-line files or STDIN".to_string()),
627 details: vec![],
628 };
629 self.hover_info.insert(node.location, hover);
630 }
631 }
632
633 NodeKind::MethodCall { object, method, args } => {
635 self.analyze_node(object, scope_id);
636
637 if let Some(offset) =
638 self.find_substring_in_source_after(node, method, object.location.end)
639 {
640 self.semantic_tokens.push(SemanticToken {
641 location: SourceLocation { start: offset, end: offset + method.len() },
642 token_type: SemanticTokenType::Method,
643 modifiers: vec![],
644 });
645 }
646
647 for arg in args {
648 self.analyze_node(arg, scope_id);
649 }
650 }
651
652 NodeKind::IndirectCall { method, object, args } => {
653 if let Some(offset) = self.find_method_name_in_source(node, method) {
654 self.semantic_tokens.push(SemanticToken {
655 location: SourceLocation { start: offset, end: offset + method.len() },
656 token_type: SemanticTokenType::Method,
657 modifiers: vec![],
658 });
659 }
660 self.analyze_node(object, scope_id);
661 for arg in args {
662 self.analyze_node(arg, scope_id);
663 }
664 }
665
666 NodeKind::Use { module, args, .. } => {
667 self.semantic_tokens.push(SemanticToken {
668 location: SourceLocation {
669 start: node.location.start,
670 end: node.location.start + 3,
671 },
672 token_type: SemanticTokenType::Keyword,
673 modifiers: vec![],
674 });
675
676 let mut args_start = node.location.start + 3;
677 if let Some(offset) = self.find_substring_in_source(node, module) {
678 self.semantic_tokens.push(SemanticToken {
679 location: SourceLocation { start: offset, end: offset + module.len() },
680 token_type: SemanticTokenType::Namespace,
681 modifiers: vec![],
682 });
683 args_start = offset + module.len();
684 }
685
686 self.analyze_string_args(node, args, args_start);
687 }
688
689 NodeKind::No { module, args, .. } => {
690 self.semantic_tokens.push(SemanticToken {
691 location: SourceLocation {
692 start: node.location.start,
693 end: node.location.start + 2,
694 },
695 token_type: SemanticTokenType::Keyword,
696 modifiers: vec![],
697 });
698
699 let mut args_start = node.location.start + 2;
700 if let Some(offset) = self.find_substring_in_source(node, module) {
701 self.semantic_tokens.push(SemanticToken {
702 location: SourceLocation { start: offset, end: offset + module.len() },
703 token_type: SemanticTokenType::Namespace,
704 modifiers: vec![],
705 });
706 args_start = offset + module.len();
707 }
708
709 self.analyze_string_args(node, args, args_start);
710 }
711
712 NodeKind::Given { expr, body } => {
713 self.semantic_tokens.push(SemanticToken {
714 location: SourceLocation {
715 start: node.location.start,
716 end: node.location.start + 5,
717 }, token_type: SemanticTokenType::KeywordControl,
719 modifiers: vec![],
720 });
721 self.analyze_node(expr, scope_id);
722 self.analyze_node(body, scope_id);
723 }
724
725 NodeKind::When { condition, body } => {
726 self.semantic_tokens.push(SemanticToken {
727 location: SourceLocation {
728 start: node.location.start,
729 end: node.location.start + 4,
730 }, token_type: SemanticTokenType::KeywordControl,
732 modifiers: vec![],
733 });
734 self.analyze_node(condition, scope_id);
735 self.analyze_node(body, scope_id);
736 }
737
738 NodeKind::Default { body } => {
739 self.semantic_tokens.push(SemanticToken {
740 location: SourceLocation {
741 start: node.location.start,
742 end: node.location.start + 7,
743 }, token_type: SemanticTokenType::KeywordControl,
745 modifiers: vec![],
746 });
747 self.analyze_node(body, scope_id);
748 }
749
750 NodeKind::Return { value } => {
751 self.semantic_tokens.push(SemanticToken {
752 location: SourceLocation {
753 start: node.location.start,
754 end: node.location.start + 6,
755 }, token_type: SemanticTokenType::KeywordControl,
757 modifiers: vec![],
758 });
759 if let Some(v) = value {
760 self.analyze_node(v, scope_id);
761 }
762 }
763
764 NodeKind::Class { name, body, .. } => {
765 self.semantic_tokens.push(SemanticToken {
766 location: SourceLocation {
767 start: node.location.start,
768 end: node.location.start + 5,
769 }, token_type: SemanticTokenType::Keyword,
771 modifiers: vec![],
772 });
773
774 if let Some(offset) = self.find_substring_in_source(node, name) {
775 self.semantic_tokens.push(SemanticToken {
776 location: SourceLocation { start: offset, end: offset + name.len() },
777 token_type: SemanticTokenType::Class,
778 modifiers: vec![SemanticTokenModifier::Declaration],
779 });
780 }
781
782 let class_scope = self.get_scope_for(node, ScopeKind::Package);
783 self.analyze_node(body, class_scope);
784 }
785
786 NodeKind::Signature { parameters } => {
787 for param in parameters {
788 self.analyze_node(param, scope_id);
789 }
790 }
791
792 NodeKind::MandatoryParameter { variable }
793 | NodeKind::OptionalParameter { variable, .. }
794 | NodeKind::SlurpyParameter { variable }
795 | NodeKind::NamedParameter { variable } => {
796 self.analyze_node(variable, scope_id);
797 }
798
799 NodeKind::Diamond | NodeKind::Ellipsis => {
800 self.semantic_tokens.push(SemanticToken {
801 location: node.location,
802 token_type: SemanticTokenType::Operator,
803 modifiers: vec![],
804 });
805 }
806
807 NodeKind::Undef => {
808 self.semantic_tokens.push(SemanticToken {
809 location: node.location,
810 token_type: SemanticTokenType::Keyword,
811 modifiers: vec![],
812 });
813 }
814
815 NodeKind::Identifier { .. } => {
816 }
819
820 NodeKind::Heredoc { .. } => {
821 self.semantic_tokens.push(SemanticToken {
822 location: node.location,
823 token_type: SemanticTokenType::String,
824 modifiers: vec![],
825 });
826 }
827
828 NodeKind::Glob { .. } => {
829 self.semantic_tokens.push(SemanticToken {
830 location: node.location,
831 token_type: SemanticTokenType::Operator,
832 modifiers: vec![],
833 });
834 }
835
836 NodeKind::DataSection { .. } => {
837 self.semantic_tokens.push(SemanticToken {
838 location: node.location,
839 token_type: SemanticTokenType::Comment,
840 modifiers: vec![],
841 });
842 }
843
844 NodeKind::Prototype { .. } => {
845 self.semantic_tokens.push(SemanticToken {
846 location: node.location,
847 token_type: SemanticTokenType::Punctuation,
848 modifiers: vec![],
849 });
850 }
851
852 NodeKind::Typeglob { .. } => {
853 self.semantic_tokens.push(SemanticToken {
854 location: node.location,
855 token_type: SemanticTokenType::Variable,
856 modifiers: vec![],
857 });
858 }
859
860 NodeKind::Untie { variable } => {
861 self.analyze_node(variable, scope_id);
862 }
863
864 NodeKind::LoopControl { .. } => {
865 self.semantic_tokens.push(SemanticToken {
866 location: node.location,
867 token_type: SemanticTokenType::KeywordControl,
868 modifiers: vec![],
869 });
870 }
871
872 NodeKind::Goto { target } => {
873 self.semantic_tokens.push(SemanticToken {
874 location: node.location,
875 token_type: SemanticTokenType::KeywordControl,
876 modifiers: vec![],
877 });
878 self.analyze_node(target, scope_id);
879 }
880
881 NodeKind::MissingExpression
882 | NodeKind::MissingStatement
883 | NodeKind::MissingIdentifier
884 | NodeKind::MissingBlock => {
885 }
887
888 NodeKind::Tie { variable, package, args } => {
889 self.analyze_node(variable, scope_id);
890 self.analyze_node(package, scope_id);
891 for arg in args {
892 self.analyze_node(arg, scope_id);
893 }
894 }
895
896 NodeKind::StatementModifier { statement, condition, modifier } => {
897 if matches!(modifier.as_str(), "for" | "foreach" | "while" | "until") {
900 self.semantic_tokens.push(SemanticToken {
901 location: node.location,
902 token_type: SemanticTokenType::KeywordControl,
903 modifiers: vec![],
904 });
905 }
906 self.analyze_node(statement, scope_id);
907 self.analyze_node(condition, scope_id);
908 }
909
910 NodeKind::Format { name, .. } => {
911 self.semantic_tokens.push(SemanticToken {
912 location: node.location,
913 token_type: SemanticTokenType::FunctionDeclaration,
914 modifiers: vec![SemanticTokenModifier::Declaration],
915 });
916
917 let hover = HoverInfo {
918 signature: format!("format {} =", name),
919 documentation: None,
920 details: vec![],
921 };
922 self.hover_info.insert(node.location, hover);
923 }
924
925 NodeKind::Error { .. } | NodeKind::UnknownRest => {
926 }
928 }
929 }
930
931 fn register_nested_decl_vars(
940 &mut self,
941 node: &Node,
942 declarator: &str,
943 attributes: &[String],
944 scope_id: ScopeId,
945 ) {
946 match &node.kind {
947 NodeKind::NestedVariableList { items } => {
948 for item in items {
949 self.register_nested_decl_vars(item, declarator, attributes, scope_id);
950 }
951 }
952 NodeKind::Variable { sigil, name } => {
953 let token_type = match declarator {
954 "my" | "state" => SemanticTokenType::VariableDeclaration,
955 _ => SemanticTokenType::Variable,
956 };
957 let mut modifiers = vec![SemanticTokenModifier::Declaration];
958 if declarator == "state" || attributes.iter().any(|a| a == ":shared") {
959 modifiers.push(SemanticTokenModifier::Static);
960 }
961 self.semantic_tokens.push(SemanticToken {
962 location: node.location,
963 token_type,
964 modifiers,
965 });
966 let hover = HoverInfo {
967 signature: format!("{} {}{}", declarator, sigil, name),
968 documentation: self.extract_documentation(node.location.start),
969 details: if attributes.is_empty() {
970 vec![]
971 } else {
972 vec![format!("Attributes: {}", attributes.join(", "))]
973 },
974 };
975 self.hover_info.insert(node.location, hover);
976 }
977 _ => {}
979 }
980 }
981
982 pub(super) fn extract_documentation(&self, start: usize) -> Option<String> {
997 static POD_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
998 static COMMENT_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
999
1000 if self.source.is_empty() || start > self.source.len() {
1001 return None;
1002 }
1003 let before = &self.source[..start];
1004
1005 let pod_re = POD_RE
1007 .get_or_init(|| Regex::new(r"(?s)(=[a-zA-Z0-9].*?\r?\n=cut(?:\r?\n)?)\s*\z"))
1008 .as_ref()
1009 .ok()?;
1010 if let Some(caps) = pod_re.captures(before) {
1011 if let Some(pod_text) = caps.get(1) {
1012 return Some(pod_text.as_str().trim().to_string());
1013 }
1014 }
1015
1016 let comment_re =
1018 COMMENT_RE.get_or_init(|| Regex::new(r"(?m)(#.*\r?\n)+[\t ]*\z")).as_ref().ok()?;
1019 if let Some(caps) = comment_re.captures(before) {
1020 if let Some(comment_match) = caps.get(0) {
1021 let doc = comment_match
1023 .as_str()
1024 .lines()
1025 .map(|line| line.trim_start_matches('#').trim())
1026 .filter(|line| !line.is_empty())
1027 .collect::<Vec<_>>()
1028 .join(" ");
1029 return Some(doc);
1030 }
1031 }
1032
1033 None
1034 }
1035
1036 pub(super) fn extract_sub_documentation(&self, start: usize, body: &Node) -> Option<String> {
1056 self.extract_documentation(start).or_else(|| self.find_pod_in_node_body(body))
1057 }
1058
1059 pub(super) fn find_pod_in_node_body(&self, body: &Node) -> Option<String> {
1077 static BODY_POD_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
1078
1079 let start = body.location.start;
1080 let end = body.location.end;
1081 if self.source.is_empty() || end <= start || end > self.source.len() {
1082 return None;
1083 }
1084 let body_src = &self.source[start..end];
1085
1086 let pod_re = BODY_POD_RE
1087 .get_or_init(|| Regex::new(r"(?ms)^\s*(=[a-zA-Z0-9].*?\r?\n=cut)\b"))
1088 .as_ref()
1089 .ok()?;
1090 let caps = pod_re.captures(body_src)?;
1091 let pod_text = caps.get(1)?.as_str().trim().to_string();
1092 Some(pod_text)
1093 }
1094
1095 pub(super) fn extract_pod_name_section(&self, package_name: &str) -> Option<String> {
1100 if self.source.is_empty() {
1101 return None;
1102 }
1103
1104 let mut in_name_section = false;
1105 let mut name_lines: Vec<&str> = Vec::new();
1106
1107 for line in self.source.lines() {
1108 let trimmed: &str = line.trim();
1109 if trimmed.starts_with("=head1") {
1110 if in_name_section {
1111 break;
1112 }
1113 let heading = trimmed.strip_prefix("=head1").map(|s: &str| s.trim());
1114 if heading == Some("NAME") {
1115 in_name_section = true;
1116 continue;
1117 }
1118 } else if trimmed.starts_with("=cut") && in_name_section {
1119 break;
1120 } else if trimmed.starts_with('=') && in_name_section {
1121 break;
1122 } else if in_name_section && !trimmed.is_empty() {
1123 name_lines.push(trimmed);
1124 }
1125 }
1126
1127 if !name_lines.is_empty() {
1128 let name_doc = name_lines.join(" ");
1129 if name_doc.contains(package_name)
1130 || name_doc.contains(&package_name.replace("::", "-"))
1131 {
1132 return Some(name_doc);
1133 }
1134 }
1135
1136 None
1137 }
1138
1139 pub(super) fn get_scope_for(&self, node: &Node, kind: ScopeKind) -> ScopeId {
1141 for scope in self.symbol_table.scopes.values() {
1142 if scope.kind == kind
1143 && scope.location.start == node.location.start
1144 && scope.location.end == node.location.end
1145 {
1146 return scope.id;
1147 }
1148 }
1149 0
1150 }
1151
1152 pub(super) fn line_number(&self, offset: usize) -> usize {
1154 if self.source.is_empty() { 1 } else { self.source[..offset].lines().count() + 1 }
1155 }
1156
1157 pub(super) fn find_substring_in_source(&self, node: &Node, substring: &str) -> Option<usize> {
1159 if self.source.len() < node.location.end {
1160 return None;
1161 }
1162 let node_text = &self.source[node.location.start..node.location.end];
1163 if let Some(pos) = node_text.find(substring) {
1164 return Some(node.location.start + pos);
1165 }
1166 None
1167 }
1168
1169 pub(super) fn find_method_name_in_source(
1171 &self,
1172 node: &Node,
1173 method_name: &str,
1174 ) -> Option<usize> {
1175 self.find_substring_in_source(node, method_name)
1176 }
1177
1178 pub(super) fn find_substring_in_source_after(
1180 &self,
1181 node: &Node,
1182 substring: &str,
1183 after: usize,
1184 ) -> Option<usize> {
1185 if self.source.len() < node.location.end || after >= node.location.end {
1186 return None;
1187 }
1188
1189 let start_rel = after.saturating_sub(node.location.start);
1190
1191 let node_text = &self.source[node.location.start..node.location.end];
1192 if start_rel >= node_text.len() {
1193 return None;
1194 }
1195
1196 let text_to_search = &node_text[start_rel..];
1197 if let Some(pos) = text_to_search.find(substring) {
1198 return Some(node.location.start + start_rel + pos);
1199 }
1200 None
1201 }
1202
1203 pub(super) fn analyze_string_args(
1205 &mut self,
1206 node: &Node,
1207 args: &[String],
1208 start_offset: usize,
1209 ) {
1210 let mut current_offset = start_offset;
1211 for arg in args {
1212 if let Some(offset) = self.find_substring_in_source_after(node, arg, current_offset) {
1213 self.semantic_tokens.push(SemanticToken {
1214 location: SourceLocation { start: offset, end: offset + arg.len() },
1215 token_type: SemanticTokenType::String,
1216 modifiers: vec![],
1217 });
1218 current_offset = offset + arg.len();
1219 }
1220 }
1221 }
1222
1223 pub fn infer_type(&self, node: &Node) -> Option<String> {
1242 match &node.kind {
1243 NodeKind::Number { .. } => Some("number".to_string()),
1244 NodeKind::String { .. } => Some("string".to_string()),
1245 NodeKind::ArrayLiteral { .. } => Some("array".to_string()),
1246 NodeKind::HashLiteral { .. } => Some("hash".to_string()),
1247
1248 NodeKind::Variable { sigil, name } => {
1249 let kind = match sigil.as_str() {
1251 "$" => SymbolKind::scalar(),
1252 "@" => SymbolKind::array(),
1253 "%" => SymbolKind::hash(),
1254 _ => return None,
1255 };
1256
1257 let symbols = self.symbol_table.find_symbol(name, 0, kind);
1258 symbols.first()?;
1259
1260 match sigil.as_str() {
1262 "$" => Some("scalar".to_string()),
1263 "@" => Some("array".to_string()),
1264 "%" => Some("hash".to_string()),
1265 _ => None,
1266 }
1267 }
1268
1269 NodeKind::FunctionCall { name, .. } => {
1270 match name.as_str() {
1272 "scalar" => Some("scalar".to_string()),
1273 "ref" => Some("string".to_string()),
1274 "length" | "index" | "rindex" => Some("number".to_string()),
1275 "split" => Some("array".to_string()),
1276 "keys" | "values" => Some("array".to_string()),
1277 _ => None,
1278 }
1279 }
1280
1281 NodeKind::Binary { op, .. } => {
1282 match op.as_str() {
1284 "+" | "-" | "*" | "/" | "%" | "**" => Some("number".to_string()),
1285 "." | "x" => Some("string".to_string()),
1286 "==" | "!=" | "<" | ">" | "<=" | ">=" | "eq" | "ne" | "lt" | "gt" | "le"
1287 | "ge" => Some("boolean".to_string()),
1288 _ => None,
1289 }
1290 }
1291
1292 _ => None,
1293 }
1294 }
1295}
1296
1297fn format_signature_params(sig_node: &Node) -> String {
1302 let NodeKind::Signature { parameters } = &sig_node.kind else {
1303 return "(...)".to_string();
1304 };
1305
1306 let labels: Vec<String> = parameters
1307 .iter()
1308 .filter_map(|param| {
1309 let var = match ¶m.kind {
1310 NodeKind::MandatoryParameter { variable }
1311 | NodeKind::OptionalParameter { variable, .. }
1312 | NodeKind::SlurpyParameter { variable }
1313 | NodeKind::NamedParameter { variable } => variable.as_ref(),
1314 NodeKind::Variable { .. } => param,
1315 _ => return None,
1316 };
1317 if let NodeKind::Variable { sigil, name } = &var.kind {
1318 Some(format!("{}{}", sigil, name))
1319 } else {
1320 None
1321 }
1322 })
1323 .collect();
1324
1325 format!("({})", labels.join(", "))
1326}
1327
1328#[cfg(test)]
1329mod tests {
1330 use super::super::SemanticAnalyzer;
1331 use crate::{Node, NodeKind, SourceLocation};
1332 use perl_tdd_support::must;
1333
1334 fn loc() -> SourceLocation {
1335 SourceLocation { start: 0, end: 1 }
1336 }
1337
1338 fn var(sigil: &str, name: &str) -> Node {
1339 Node::new(NodeKind::Variable { sigil: sigil.to_string(), name: name.to_string() }, loc())
1340 }
1341
1342 #[test]
1346 fn analyze_node_nested_variable_list_direct() {
1347 let inner_var = var("$", "b");
1348 let inner_var2 = var("$", "c");
1349 let nested =
1350 Node::new(NodeKind::NestedVariableList { items: vec![inner_var, inner_var2] }, loc());
1351 let program = Node::new(NodeKind::Program { statements: vec![nested] }, loc());
1352 let analyzer = SemanticAnalyzer::analyze(&program);
1353 let _ = analyzer.semantic_tokens();
1356 }
1357
1358 #[test]
1361 fn register_nested_decl_vars_non_my_declarator() {
1362 let inner_a = var("$", "a");
1363 let inner_b = var("$", "b");
1364 let nested =
1365 Node::new(NodeKind::NestedVariableList { items: vec![inner_a, inner_b] }, loc());
1366 let decl = Node::new(
1367 NodeKind::VariableListDeclaration {
1368 declarator: "our".to_string(),
1369 variables: vec![nested],
1370 attributes: vec![],
1371 initializer: None,
1372 },
1373 loc(),
1374 );
1375 let program = Node::new(NodeKind::Program { statements: vec![decl] }, loc());
1376 let analyzer = SemanticAnalyzer::analyze(&program);
1377 assert!(
1380 !analyzer.semantic_tokens().is_empty(),
1381 "Expected semantic tokens for nested our-declaration"
1382 );
1383 }
1384
1385 #[test]
1388 fn register_nested_decl_vars_shared_attribute_and_hover_details() {
1389 let inner_a = var("$", "x");
1390 let nested = Node::new(NodeKind::NestedVariableList { items: vec![inner_a] }, loc());
1391 let decl = Node::new(
1392 NodeKind::VariableListDeclaration {
1393 declarator: "my".to_string(),
1394 variables: vec![nested],
1395 attributes: vec![":shared".to_string()],
1396 initializer: None,
1397 },
1398 loc(),
1399 );
1400 let program = Node::new(NodeKind::Program { statements: vec![decl] }, loc());
1401 let analyzer = SemanticAnalyzer::analyze(&program);
1402 let tokens = analyzer.semantic_tokens();
1404 assert!(!tokens.is_empty(), "Expected semantic tokens for :shared nested declaration");
1405 }
1406
1407 #[test]
1410 fn register_nested_decl_vars_undef_placeholder() {
1411 let inner_a = var("$", "a");
1412 let undef_placeholder = Node::new(NodeKind::Undef, loc());
1413 let nested = Node::new(
1414 NodeKind::NestedVariableList { items: vec![inner_a, undef_placeholder] },
1415 loc(),
1416 );
1417 let decl = Node::new(
1418 NodeKind::VariableListDeclaration {
1419 declarator: "my".to_string(),
1420 variables: vec![nested],
1421 attributes: vec![],
1422 initializer: None,
1423 },
1424 loc(),
1425 );
1426 let program = Node::new(NodeKind::Program { statements: vec![decl] }, loc());
1427 let analyzer = SemanticAnalyzer::analyze(&program);
1428 let _ = analyzer.semantic_tokens();
1431 }
1432
1433 #[test]
1436 fn analyze_parsed_nested_varlist_declaration() -> Result<(), Box<dyn std::error::Error>> {
1437 let code = "my ($a, ($b, $c)) = (1, (2, 3));";
1438 let mut parser = crate::Parser::new(code);
1439 let ast = must(parser.parse());
1440 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1441 let tokens = analyzer.semantic_tokens();
1443 assert!(!tokens.is_empty(), "Expected semantic tokens for nested my-declaration, got none");
1444 Ok(())
1445 }
1446}