1use crate::SourceLocation;
28use crate::ast::{Node, NodeKind};
29use regex::Regex;
30use std::collections::{HashMap, HashSet};
31use std::sync::OnceLock;
32
33const UNIVERSAL_METHODS: [&str; 4] = ["can", "isa", "DOES", "VERSION"];
34
35pub use perl_symbol::{SymbolKind, VarKind};
38
39#[derive(Debug, Clone)]
40pub struct Symbol {
57 pub name: String,
59 pub qualified_name: String,
61 pub kind: SymbolKind,
63 pub location: SourceLocation,
65 pub scope_id: ScopeId,
67 pub declaration: Option<String>,
69 pub documentation: Option<String>,
71 pub attributes: Vec<String>,
73}
74
75#[derive(Debug, Clone)]
76pub struct SymbolReference {
94 pub name: String,
96 pub kind: SymbolKind,
98 pub location: SourceLocation,
100 pub scope_id: ScopeId,
102 pub is_write: bool,
104}
105
106pub type ScopeId = usize;
108
109#[derive(Debug, Clone)]
110pub struct Scope {
129 pub id: ScopeId,
131 pub parent: Option<ScopeId>,
133 pub kind: ScopeKind,
135 pub location: SourceLocation,
137 pub symbols: HashSet<String>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum ScopeKind {
156 Global,
158 Package,
160 Subroutine,
162 Block,
164 Eval,
166}
167
168#[derive(Debug, Default)]
169pub struct SymbolTable {
195 pub symbols: HashMap<String, Vec<Symbol>>,
197 pub references: HashMap<String, Vec<SymbolReference>>,
199 pub scopes: HashMap<ScopeId, Scope>,
201 scope_stack: Vec<ScopeId>,
203 next_scope_id: ScopeId,
205 current_package: String,
207}
208
209pub fn is_universal_method(method_name: &str) -> bool {
214 UNIVERSAL_METHODS.contains(&method_name)
215}
216
217impl SymbolTable {
218 pub fn new() -> Self {
220 let mut table = SymbolTable {
221 symbols: HashMap::new(),
222 references: HashMap::new(),
223 scopes: HashMap::new(),
224 scope_stack: vec![0],
225 next_scope_id: 1,
226 current_package: "main".to_string(),
227 };
228
229 table.scopes.insert(
231 0,
232 Scope {
233 id: 0,
234 parent: None,
235 kind: ScopeKind::Global,
236 location: SourceLocation { start: 0, end: 0 },
237 symbols: HashSet::new(),
238 },
239 );
240
241 table
242 }
243
244 fn current_scope(&self) -> ScopeId {
246 *self.scope_stack.last().unwrap_or(&0)
247 }
248
249 fn push_scope(&mut self, kind: ScopeKind, location: SourceLocation) -> ScopeId {
251 let parent = self.current_scope();
252 let scope_id = self.next_scope_id;
253 self.next_scope_id += 1;
254
255 let scope =
256 Scope { id: scope_id, parent: Some(parent), kind, location, symbols: HashSet::new() };
257
258 self.scopes.insert(scope_id, scope);
259 self.scope_stack.push(scope_id);
260 scope_id
261 }
262
263 fn pop_scope(&mut self) {
265 self.scope_stack.pop();
266 }
267
268 fn add_symbol(&mut self, symbol: Symbol) {
270 let name = symbol.name.clone();
271 if let Some(scope) = self.scopes.get_mut(&symbol.scope_id) {
272 scope.symbols.insert(name.clone());
273 }
274 self.symbols.entry(name).or_default().push(symbol);
275 }
276
277 fn add_reference(&mut self, reference: SymbolReference) {
279 let name = reference.name.clone();
280 self.references.entry(name).or_default().push(reference);
281 }
282
283 pub fn find_symbol(&self, name: &str, from_scope: ScopeId, kind: SymbolKind) -> Vec<&Symbol> {
285 let mut results = Vec::new();
286 let mut current_scope_id = Some(from_scope);
287
288 while let Some(scope_id) = current_scope_id {
290 if let Some(scope) = self.scopes.get(&scope_id) {
291 if scope.symbols.contains(name) {
293 if let Some(symbols) = self.symbols.get(name) {
294 for symbol in symbols {
295 if symbol.scope_id == scope_id && symbol.kind == kind {
296 results.push(symbol);
297 }
298 }
299 }
300 }
301
302 if scope.kind != ScopeKind::Package {
304 if let Some(symbols) = self.symbols.get(name) {
305 for symbol in symbols {
306 if symbol.declaration.as_deref() == Some("our") && symbol.kind == kind {
307 results.push(symbol);
308 }
309 }
310 }
311 }
312
313 current_scope_id = scope.parent;
314 } else {
315 break;
316 }
317 }
318
319 results
320 }
321
322 pub fn find_references(&self, symbol: &Symbol) -> Vec<&SymbolReference> {
324 self.references
325 .get(&symbol.name)
326 .map(|refs| refs.iter().filter(|r| r.kind == symbol.kind).collect())
327 .unwrap_or_default()
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum FrameworkKind {
334 Moo,
336 MooRole,
338 Moose,
340 MooseRole,
342 RoleTiny,
344 RoleTinyWith,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349pub enum WebFrameworkKind {
351 Dancer,
353 Dancer2,
355 MojoliciousLite,
357 PlackBuilder,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub enum AsyncFrameworkKind {
364 AnyEvent,
366 EV,
368 Future,
370 FutureXS,
372 Promise,
374 PromiseXS,
376 POE,
378 IOAsync,
380 MojoRedis,
382 MojoPg,
384}
385
386#[derive(Debug, Clone, Default)]
387pub struct FrameworkFlags {
389 pub moo: bool,
391 pub class_accessor: bool,
393 pub kind: Option<FrameworkKind>,
395 pub web_framework: Option<WebFrameworkKind>,
397 pub async_framework: Option<AsyncFrameworkKind>,
399 pub catalyst_controller: bool,
401}
402
403pub struct SymbolExtractor {
405 table: SymbolTable,
406 source: String,
408 framework_flags: HashMap<String, FrameworkFlags>,
410 const_fast_enabled: bool,
412 readonly_enabled: bool,
414}
415
416impl Default for SymbolExtractor {
417 fn default() -> Self {
418 Self::new()
419 }
420}
421
422impl SymbolExtractor {
423 pub fn new() -> Self {
427 SymbolExtractor {
428 table: SymbolTable::new(),
429 source: String::new(),
430 framework_flags: HashMap::new(),
431 const_fast_enabled: false,
432 readonly_enabled: false,
433 }
434 }
435
436 pub fn new_with_source(source: &str) -> Self {
440 SymbolExtractor {
441 table: SymbolTable::new(),
442 source: source.to_string(),
443 framework_flags: HashMap::new(),
444 const_fast_enabled: false,
445 readonly_enabled: false,
446 }
447 }
448
449 pub fn extract(mut self, node: &Node) -> SymbolTable {
451 self.visit_node(node);
452 self.upgrade_package_symbols_from_framework_flags();
453 self.table
454 }
455
456 fn upgrade_package_symbols_from_framework_flags(&mut self) {
459 for (pkg_name, flags) in &self.framework_flags {
460 let Some(kind) = flags.kind else {
461 continue;
462 };
463 let new_kind = match kind {
464 FrameworkKind::Moo | FrameworkKind::Moose | FrameworkKind::RoleTinyWith => {
465 SymbolKind::Class
466 }
467 FrameworkKind::MooRole | FrameworkKind::MooseRole | FrameworkKind::RoleTiny => {
468 SymbolKind::Role
469 }
470 };
471 if let Some(symbols) = self.table.symbols.get_mut(pkg_name) {
472 for symbol in symbols.iter_mut() {
473 if symbol.kind == SymbolKind::Package {
474 symbol.kind = new_kind;
475 }
476 }
477 }
478 }
479 }
480
481 fn visit_node(&mut self, node: &Node) {
483 match &node.kind {
484 NodeKind::Program { statements } => {
485 self.visit_statement_list(statements);
486 }
487
488 NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } => {
489 let doc = self.extract_leading_comment(node.location.start);
490 self.handle_variable_declaration(
491 declarator,
492 variable,
493 attributes,
494 variable.location,
495 doc,
496 );
497 if let Some(init) = initializer {
498 self.visit_node(init);
499 }
500 }
501
502 NodeKind::VariableListDeclaration {
503 declarator,
504 variables,
505 attributes,
506 initializer,
507 } => {
508 let doc = self.extract_leading_comment(node.location.start);
509 for var in variables {
510 self.handle_variable_declaration(
511 declarator,
512 var,
513 attributes,
514 var.location,
515 doc.clone(),
516 );
517 }
518 if let Some(init) = initializer {
519 self.visit_node(init);
520 }
521 }
522
523 NodeKind::Variable { sigil, name } => {
524 let kind = match sigil.as_str() {
525 "$" => SymbolKind::scalar(),
526 "@" => SymbolKind::array(),
527 "%" => SymbolKind::hash(),
528 _ => return,
529 };
530
531 let reference = SymbolReference {
532 name: name.clone(),
533 kind,
534 location: node.location,
535 scope_id: self.table.current_scope(),
536 is_write: false, };
538
539 self.table.add_reference(reference);
540 }
541
542 NodeKind::Subroutine {
543 name,
544 prototype: _,
545 signature,
546 attributes,
547 body,
548 name_span: _,
549 } => {
550 let sub_name =
551 name.as_ref().map(|n| n.to_string()).unwrap_or_else(|| "<anon>".to_string());
552
553 if name.is_some() {
554 let documentation = self.extract_leading_comment(node.location.start);
555 let mut symbol_attributes = attributes.clone();
556 let documentation = if self.current_package_is_catalyst_controller()
557 && let Some((action_kind, action_details)) =
558 Self::catalyst_action_metadata(attributes)
559 {
560 symbol_attributes.push("framework=Catalyst".to_string());
561 symbol_attributes.push("catalyst_controller=true".to_string());
562 symbol_attributes.push("catalyst_action=true".to_string());
563 symbol_attributes.push(format!("catalyst_action_kind={action_kind}"));
564 if !action_details.is_empty() {
565 symbol_attributes.push(format!(
566 "catalyst_action_attributes={}",
567 action_details.join(", ")
568 ));
569 }
570
571 let action_doc = if action_details.is_empty() {
572 format!("Catalyst action ({action_kind})")
573 } else {
574 format!(
575 "Catalyst action ({action_kind}; {})",
576 action_details.join(", ")
577 )
578 };
579 match documentation {
580 Some(doc) => Some(format!("{doc}\n{action_doc}")),
581 None => Some(action_doc),
582 }
583 } else {
584 documentation
585 };
586 let symbol = Symbol {
587 name: sub_name.clone(),
588 qualified_name: format!("{}::{}", self.table.current_package, sub_name),
589 kind: SymbolKind::Subroutine,
590 location: node.location,
591 scope_id: self.table.current_scope(),
592 declaration: None,
593 documentation,
594 attributes: symbol_attributes,
595 };
596
597 self.table.add_symbol(symbol);
598 }
599
600 self.table.push_scope(ScopeKind::Subroutine, node.location);
602
603 if let Some(sig) = signature {
605 self.register_signature_params(sig);
606 }
607
608 self.visit_node(body);
609
610 self.table.pop_scope();
611 }
612
613 NodeKind::Package { name, block, name_span: _ } => {
614 let old_package = self.table.current_package.clone();
615 self.table.current_package = name.clone();
616 if Self::is_catalyst_controller_package_name(name) {
617 self.mark_catalyst_controller_package(name);
618 }
619
620 let documentation = self.extract_package_documentation(name, node.location);
621 let symbol = Symbol {
622 name: name.clone(),
623 qualified_name: name.clone(),
624 kind: SymbolKind::Package,
625 location: node.location,
626 scope_id: self.table.current_scope(),
627 declaration: None,
628 documentation,
629 attributes: vec![],
630 };
631
632 self.table.add_symbol(symbol);
633
634 if let Some(block_node) = block {
635 self.table.push_scope(ScopeKind::Package, node.location);
637 self.visit_node(block_node);
638 self.table.pop_scope();
639 self.table.current_package = old_package;
640 }
641 }
644
645 NodeKind::Block { statements } => {
646 self.table.push_scope(ScopeKind::Block, node.location);
647 self.visit_statement_list(statements);
648 self.table.pop_scope();
649 }
650
651 NodeKind::If { condition, then_branch, elsif_branches: _, else_branch } => {
652 self.visit_node(condition);
653
654 self.table.push_scope(ScopeKind::Block, then_branch.location);
655 self.visit_node(then_branch);
656 self.table.pop_scope();
657
658 if let Some(else_node) = else_branch {
659 self.table.push_scope(ScopeKind::Block, else_node.location);
660 self.visit_node(else_node);
661 self.table.pop_scope();
662 }
663 }
664
665 NodeKind::While { condition, body, continue_block: _ } => {
666 self.visit_node(condition);
667
668 self.table.push_scope(ScopeKind::Block, body.location);
669 self.visit_node(body);
670 self.table.pop_scope();
671 }
672
673 NodeKind::For { init, condition, update, body, .. } => {
674 self.table.push_scope(ScopeKind::Block, node.location);
675
676 if let Some(init_node) = init {
677 self.visit_node(init_node);
678 }
679 if let Some(cond_node) = condition {
680 self.visit_node(cond_node);
681 }
682 if let Some(update_node) = update {
683 self.visit_node(update_node);
684 }
685 self.visit_node(body);
686
687 self.table.pop_scope();
688 }
689
690 NodeKind::Foreach { variable, list, body, continue_block: _ } => {
691 self.table.push_scope(ScopeKind::Block, node.location);
692
693 self.handle_variable_declaration("my", variable, &[], variable.location, None);
695 self.visit_node(list);
696 self.visit_node(body);
697
698 self.table.pop_scope();
699 }
700
701 NodeKind::Assignment { lhs, rhs, .. } => {
703 self.mark_write_reference(lhs);
705 self.visit_node(lhs);
706 self.visit_node(rhs);
707 }
708
709 NodeKind::Binary { left, right, .. } => {
710 self.visit_node(left);
711 self.visit_node(right);
712 }
713
714 NodeKind::Unary { operand, .. } => {
715 self.visit_node(operand);
716 }
717
718 NodeKind::FunctionCall { name, args } => {
719 if self.const_fast_enabled
720 && name == "const"
721 && self.try_extract_const_fast_declaration(args)
722 {
723 return;
724 }
725 if self.readonly_enabled
726 && name == "Readonly"
727 && self.try_extract_readonly_declaration(args)
728 {
729 return;
730 }
731
732 let reference = SymbolReference {
734 name: name.clone(),
735 kind: SymbolKind::Subroutine,
736 location: node.location,
737 scope_id: self.table.current_scope(),
738 is_write: false,
739 };
740 self.table.add_reference(reference);
741
742 self.synthesize_plack_builder_symbols(name, args);
743 self.synthesize_ev_symbols(name, node.location);
744
745 for arg in args {
746 self.visit_node(arg);
747 }
748 }
749
750 NodeKind::MethodCall { object, method, args } => {
751 let location = self.method_reference_location(node, object, method);
754 self.table.add_reference(SymbolReference {
755 name: method.clone(),
756 kind: SymbolKind::Subroutine,
757 location,
758 scope_id: self.table.current_scope(),
759 is_write: false,
760 });
761
762 self.synthesize_async_framework_class_symbol(object);
763 self.synthesize_future_api_symbols(object, method, node.location);
764 self.visit_node(object);
765 for arg in args {
766 self.visit_node(arg);
767 }
768 }
769
770 NodeKind::ArrayLiteral { elements } => {
772 for elem in elements {
773 self.visit_node(elem);
774 }
775 }
776
777 NodeKind::HashLiteral { pairs } => {
778 for (key, value) in pairs {
779 self.visit_node(key);
780 self.visit_node(value);
781 }
782 }
783
784 NodeKind::Ternary { condition, then_expr, else_expr } => {
785 self.visit_node(condition);
786 self.visit_node(then_expr);
787 self.visit_node(else_expr);
788 }
789
790 NodeKind::LabeledStatement { label, statement } => {
791 let symbol = Symbol {
792 name: label.clone(),
793 qualified_name: label.clone(),
794 kind: SymbolKind::Label,
795 location: node.location,
796 scope_id: self.table.current_scope(),
797 declaration: None,
798 documentation: None,
799 attributes: vec![],
800 };
801
802 self.table.add_symbol(symbol);
803
804 {
805 self.visit_node(statement);
806 }
807 }
808
809 NodeKind::String { value, interpolated } => {
811 if *interpolated {
812 self.extract_vars_from_string(value, node.location);
814 }
815 }
816
817 NodeKind::Use { module, args, .. } => {
818 self.update_framework_context(module, args);
819 if module == "Const::Fast" {
820 self.const_fast_enabled = true;
821 }
822 if module == "Readonly" {
823 self.readonly_enabled = true;
824 }
825 if module == "EV" {
826 self.synthesize_ev_framework_symbol(node.location);
827 }
828 if module == "constant" {
829 self.synthesize_use_constant_symbols(args, node.location);
830 }
831 }
832
833 NodeKind::No { module: _, args: _, .. } => {
834 }
836
837 NodeKind::PhaseBlock { phase, phase_span: _, block } => {
838 let symbol = Symbol {
841 name: phase.clone(),
842 qualified_name: format!("{}::{}", self.table.current_package, phase),
843 kind: SymbolKind::Subroutine,
844 location: node.location,
845 scope_id: self.table.current_scope(),
846 declaration: None,
847 documentation: None,
848 attributes: vec![],
849 };
850 self.table.add_symbol(symbol);
851
852 self.table.push_scope(ScopeKind::Block, node.location);
853 self.visit_node(block);
854 self.table.pop_scope();
855 }
856
857 NodeKind::StatementModifier { statement, modifier: _, condition } => {
858 self.visit_node(statement);
859 self.visit_node(condition);
860 }
861
862 NodeKind::Do { block } | NodeKind::Eval { block } | NodeKind::Defer { block } => {
863 self.visit_node(block);
864 }
865
866 NodeKind::Try { body, catch_blocks, finally_block } => {
867 self.visit_node(body);
868 for (catch_var, catch_block) in catch_blocks {
869 self.table.push_scope(ScopeKind::Block, catch_block.location);
870 if let Some(full_name) = catch_var.as_deref() {
871 self.register_catch_variable(full_name, catch_block.location);
872 }
873 self.visit_node(catch_block);
874 self.table.pop_scope();
875 }
876 if let Some(finally) = finally_block {
877 self.visit_node(finally);
878 }
879 }
880
881 NodeKind::Given { expr, body } => {
882 self.visit_node(expr);
883 self.visit_node(body);
884 }
885
886 NodeKind::When { condition, body } => {
887 self.visit_node(condition);
888 self.visit_node(body);
889 }
890
891 NodeKind::Default { body } => {
892 self.visit_node(body);
893 }
894
895 NodeKind::Class { name, parents, body } => {
896 let documentation = self.extract_leading_comment(node.location.start);
897 if Self::is_catalyst_controller_package_name(name)
898 || parents.iter().any(|parent| parent == "Catalyst::Controller")
899 {
900 self.mark_catalyst_controller_package(name);
901 }
902 let symbol = Symbol {
903 name: name.clone(),
904 qualified_name: name.clone(),
905 kind: SymbolKind::Package, location: node.location,
907 scope_id: self.table.current_scope(),
908 declaration: None,
909 documentation,
910 attributes: vec![],
911 };
912 self.table.add_symbol(symbol);
913
914 self.table.push_scope(ScopeKind::Package, node.location);
915 self.visit_node(body);
916 self.table.pop_scope();
917 }
918
919 NodeKind::Method { name, signature, attributes, body } => {
920 let documentation = self.extract_leading_comment(node.location.start);
921 let mut symbol_attributes = Vec::with_capacity(attributes.len() + 1);
922 symbol_attributes.push("method".to_string());
923 symbol_attributes.extend(attributes.iter().cloned());
924 let symbol = Symbol {
925 name: name.clone(),
926 qualified_name: format!("{}::{}", self.table.current_package, name),
927 kind: SymbolKind::Method,
928 location: node.location,
929 scope_id: self.table.current_scope(),
930 declaration: None,
931 documentation,
932 attributes: symbol_attributes,
933 };
934 self.table.add_symbol(symbol);
935
936 self.table.push_scope(ScopeKind::Subroutine, node.location);
937
938 if let Some(sig) = signature {
940 self.register_signature_params(sig);
941 }
942
943 self.visit_node(body);
944 self.table.pop_scope();
945 }
946
947 NodeKind::Format { name, body: _ } => {
948 let symbol = Symbol {
949 name: name.clone(),
950 qualified_name: format!("{}::{}", self.table.current_package, name),
951 kind: SymbolKind::Format,
952 location: node.location,
953 scope_id: self.table.current_scope(),
954 declaration: None,
955 documentation: None,
956 attributes: vec![],
957 };
958 self.table.add_symbol(symbol);
959 }
960
961 NodeKind::Return { value } => {
962 if let Some(val) = value {
963 self.visit_node(val);
964 }
965 }
966
967 NodeKind::Tie { variable, package, args } => {
968 self.visit_node(variable);
969 self.visit_node(package);
970 for arg in args {
971 self.visit_node(arg);
972 }
973 }
974
975 NodeKind::Untie { variable } => {
976 self.visit_node(variable);
977 }
978
979 NodeKind::Goto { target } => match &target.kind {
980 NodeKind::Identifier { name } => {
981 self.table.add_reference(SymbolReference {
982 name: name.clone(),
983 kind: SymbolKind::Label,
984 location: target.location,
985 scope_id: self.table.current_scope(),
986 is_write: false,
987 });
988 }
989 NodeKind::Variable { sigil, name } if sigil == "&" => {
990 self.table.add_reference(SymbolReference {
991 name: name.clone(),
992 kind: SymbolKind::Subroutine,
993 location: target.location,
994 scope_id: self.table.current_scope(),
995 is_write: false,
996 });
997 }
998 _ => self.visit_node(target),
999 },
1000
1001 NodeKind::Regex { .. } => {}
1003 NodeKind::Match { expr, .. } => {
1004 self.visit_node(expr);
1005 }
1006 NodeKind::Substitution { expr, .. } => {
1007 self.visit_node(expr);
1008 }
1009 NodeKind::Transliteration { expr, .. } => {
1010 self.visit_node(expr);
1011 }
1012
1013 NodeKind::IndirectCall { method, object, args } => {
1014 self.table.add_reference(SymbolReference {
1015 name: method.clone(),
1016 kind: SymbolKind::Subroutine,
1017 location: node.location,
1018 scope_id: self.table.current_scope(),
1019 is_write: false,
1020 });
1021
1022 self.visit_node(object);
1023 for arg in args {
1024 self.visit_node(arg);
1025 }
1026 }
1027
1028 NodeKind::ExpressionStatement { expression } => {
1029 self.visit_node(expression);
1031 }
1032
1033 NodeKind::Number { .. }
1035 | NodeKind::Heredoc { .. }
1036 | NodeKind::Undef
1037 | NodeKind::Diamond
1038 | NodeKind::Ellipsis
1039 | NodeKind::Glob { .. }
1040 | NodeKind::Readline { .. }
1041 | NodeKind::Identifier { .. }
1042 | NodeKind::Typeglob { .. }
1043 | NodeKind::DataSection { .. }
1044 | NodeKind::LoopControl { .. }
1045 | NodeKind::MissingExpression
1046 | NodeKind::MissingStatement
1047 | NodeKind::MissingIdentifier
1048 | NodeKind::MissingBlock
1049 | NodeKind::UnknownRest => {
1050 }
1052
1053 NodeKind::Error { partial, .. } => {
1054 if let Some(partial_node) = partial {
1062 self.visit_node(partial_node);
1063 }
1064 }
1065
1066 _ => {
1067 tracing::warn!(kind = ?node.kind, "Unhandled node type in symbol extractor");
1069 }
1070 }
1071 }
1072
1073 fn visit_statement_list(&mut self, statements: &[Node]) {
1079 let mut idx = 0;
1080 while idx < statements.len() {
1081 if let Some(consumed) = self.try_extract_framework_declarations(statements, idx) {
1082 idx += consumed;
1083 continue;
1084 }
1085
1086 self.visit_node(&statements[idx]);
1087 idx += 1;
1088 }
1089 }
1090
1091 fn try_extract_framework_declarations(
1095 &mut self,
1096 statements: &[Node],
1097 idx: usize,
1098 ) -> Option<usize> {
1099 let flags = self.framework_flags.get(&self.table.current_package).cloned();
1100 let flags = flags.as_ref();
1101
1102 let is_moo = flags.is_some_and(|f| f.moo);
1103
1104 if is_moo {
1105 if let Some(consumed) = self.try_extract_moo_has_declaration(statements, idx) {
1106 return Some(consumed);
1107 }
1108 if let Some(consumed) = self.try_extract_method_modifier(statements, idx) {
1109 return Some(consumed);
1110 }
1111 if let Some(consumed) = self.try_extract_extends_with(statements, idx) {
1112 return Some(consumed);
1113 }
1114 if let Some(consumed) = self.try_extract_role_requires(statements, idx) {
1115 return Some(consumed);
1116 }
1117 }
1118
1119 if flags.is_some_and(|f| f.class_accessor)
1120 && self.try_extract_class_accessor_declaration(&statements[idx])
1121 {
1122 self.visit_node(&statements[idx]);
1124 return Some(1);
1125 }
1126
1127 if flags.is_some_and(|f| f.web_framework.is_some()) {
1128 if let Some(consumed) = self.try_extract_web_route_declaration(statements, idx) {
1129 return Some(consumed);
1130 }
1131 }
1132
1133 None
1134 }
1135
1136 fn try_extract_moo_has_declaration(
1140 &mut self,
1141 statements: &[Node],
1142 idx: usize,
1143 ) -> Option<usize> {
1144 let first = &statements[idx];
1145
1146 if idx + 1 < statements.len() {
1153 let second = &statements[idx + 1];
1154 let is_has_marker = matches!(
1155 &first.kind,
1156 NodeKind::ExpressionStatement { expression }
1157 if matches!(&expression.kind, NodeKind::Identifier { name } if name == "has")
1158 );
1159
1160 if is_has_marker {
1161 if let NodeKind::ExpressionStatement { expression } = &second.kind {
1162 let has_location =
1163 SourceLocation { start: first.location.start, end: second.location.end };
1164
1165 match &expression.kind {
1166 NodeKind::HashLiteral { pairs } => {
1167 self.synthesize_moo_has_pairs(pairs, has_location, false);
1168 self.visit_node(second);
1169 return Some(2);
1170 }
1171 NodeKind::ArrayLiteral { elements } => {
1172 if let Some(Node { kind: NodeKind::HashLiteral { pairs }, .. }) =
1173 elements.last()
1174 {
1175 let mut names = Vec::new();
1177 for el in elements.iter().take(elements.len() - 1) {
1178 names.extend(Self::collect_symbol_names(el));
1179 }
1180 if !names.is_empty() {
1181 self.synthesize_moo_has_attrs_with_options(
1182 &names,
1183 pairs,
1184 has_location,
1185 );
1186 self.visit_node(second);
1187 return Some(2);
1188 }
1189 }
1190 }
1191 _ => {}
1192 }
1193 }
1194 }
1195 }
1196
1197 if let NodeKind::ExpressionStatement { expression } = &first.kind
1200 && let NodeKind::HashLiteral { pairs } = &expression.kind
1201 {
1202 let has_embedded_marker = pairs.iter().any(|(key_node, _)| {
1203 matches!(
1204 &key_node.kind,
1205 NodeKind::Binary { op, left, .. }
1206 if op == "[]" && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
1207 )
1208 });
1209
1210 if has_embedded_marker {
1211 self.synthesize_moo_has_pairs(pairs, first.location, true);
1212 self.visit_node(first);
1213 return Some(1);
1214 }
1215 }
1216
1217 if let NodeKind::ExpressionStatement { expression } = &first.kind
1220 && let NodeKind::FunctionCall { name, args } = &expression.kind
1221 && name == "has"
1222 && !args.is_empty()
1223 {
1224 let options_hash_idx =
1225 args.iter().rposition(|a| matches!(a.kind, NodeKind::HashLiteral { .. }));
1226 if let Some(opts_idx) = options_hash_idx {
1227 if let NodeKind::HashLiteral { pairs } = &args[opts_idx].kind {
1228 let names: Vec<String> =
1229 args[..opts_idx].iter().flat_map(Self::collect_symbol_names).collect();
1230 if !names.is_empty() {
1231 self.synthesize_moo_has_attrs_with_options(&names, pairs, first.location);
1232 self.visit_node(first);
1233 return Some(1);
1234 }
1235 }
1236 }
1237 }
1238
1239 None
1240 }
1241
1242 fn try_extract_method_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1250 let first = &statements[idx];
1251
1252 if let NodeKind::ExpressionStatement { expression } = &first.kind
1254 && let NodeKind::FunctionCall { name, args } = &expression.kind
1255 && Self::is_moose_method_modifier(name)
1256 {
1257 let modifier_name = name.as_str();
1258 let method_names: Vec<String> =
1259 args.iter().flat_map(Self::collect_symbol_names).collect();
1260 if !method_names.is_empty() {
1261 let scope_id = self.table.current_scope();
1262 let package = self.table.current_package.clone();
1263 for method_name in method_names {
1264 self.table.add_symbol(Symbol {
1265 name: method_name.clone(),
1266 qualified_name: format!("{package}::{method_name}"),
1267 kind: SymbolKind::Subroutine,
1268 location: first.location,
1269 scope_id,
1270 declaration: Some(modifier_name.to_string()),
1271 documentation: Some(format!(
1272 "Method modifier `{modifier_name}` for `{method_name}`"
1273 )),
1274 attributes: vec![format!("modifier={modifier_name}")],
1275 });
1276 }
1277 return Some(1);
1278 }
1279 }
1280
1281 if idx + 1 >= statements.len() {
1282 return None;
1283 }
1284
1285 let second = &statements[idx + 1];
1286
1287 let modifier_name = match &first.kind {
1289 NodeKind::ExpressionStatement { expression } => match &expression.kind {
1290 NodeKind::Identifier { name } if Self::is_moose_method_modifier(name) => {
1291 name.as_str()
1292 }
1293 _ => return None,
1294 },
1295 _ => return None,
1296 };
1297
1298 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1300 return None;
1301 };
1302 let NodeKind::HashLiteral { pairs } = &expression.kind else {
1303 return None;
1304 };
1305
1306 let modifier_location =
1307 SourceLocation { start: first.location.start, end: second.location.end };
1308 let scope_id = self.table.current_scope();
1309 let package = self.table.current_package.clone();
1310
1311 for (key_node, _value_node) in pairs {
1312 let method_names = Self::collect_symbol_names(key_node);
1313 for method_name in method_names {
1314 self.table.add_symbol(Symbol {
1315 name: method_name.clone(),
1316 qualified_name: format!("{package}::{method_name}"),
1317 kind: SymbolKind::Subroutine,
1318 location: modifier_location,
1319 scope_id,
1320 declaration: Some(modifier_name.to_string()),
1321 documentation: Some(format!(
1322 "Method modifier `{modifier_name}` for `{method_name}`"
1323 )),
1324 attributes: vec![format!("modifier={modifier_name}")],
1325 });
1326 }
1327 }
1328
1329 self.visit_node(second);
1331
1332 Some(2)
1333 }
1334
1335 fn is_moose_method_modifier(name: &str) -> bool {
1336 matches!(name, "before" | "after" | "around" | "override" | "augment")
1337 }
1338
1339 fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1347 let first = &statements[idx];
1348
1349 if let NodeKind::ExpressionStatement { expression } = &first.kind
1351 && let NodeKind::FunctionCall { name, args } = &expression.kind
1352 && matches!(name.as_str(), "extends" | "with")
1353 {
1354 let keyword = name.as_str();
1355 let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
1356 if !names.is_empty() {
1357 if names.iter().any(|name| name == "Catalyst::Controller") {
1358 let package = self.table.current_package.clone();
1359 self.mark_catalyst_controller_package(&package);
1360 }
1361 let ref_kind =
1362 if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
1363 for ref_name in names {
1364 self.table.add_reference(SymbolReference {
1365 name: ref_name,
1366 kind: ref_kind,
1367 location: first.location,
1368 scope_id: self.table.current_scope(),
1369 is_write: false,
1370 });
1371 }
1372 return Some(1);
1373 }
1374 }
1375
1376 if idx + 1 >= statements.len() {
1377 return None;
1378 }
1379
1380 let second = &statements[idx + 1];
1381
1382 let keyword = match &first.kind {
1384 NodeKind::ExpressionStatement { expression } => match &expression.kind {
1385 NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
1386 name.as_str()
1387 }
1388 _ => return None,
1389 },
1390 _ => return None,
1391 };
1392
1393 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1395 return None;
1396 };
1397
1398 let names = Self::collect_symbol_names(expression);
1399 if names.is_empty() {
1400 return None;
1401 }
1402
1403 if names.iter().any(|name| name == "Catalyst::Controller") {
1404 let package = self.table.current_package.clone();
1405 self.mark_catalyst_controller_package(&package);
1406 }
1407
1408 let ref_location = SourceLocation { start: first.location.start, end: second.location.end };
1409
1410 let ref_kind = if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
1411
1412 for name in names {
1413 self.table.add_reference(SymbolReference {
1414 name,
1415 kind: ref_kind,
1416 location: ref_location,
1417 scope_id: self.table.current_scope(),
1418 is_write: false,
1419 });
1420 }
1421
1422 Some(2)
1423 }
1424
1425 fn try_extract_role_requires(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1432 let first = &statements[idx];
1433
1434 if let NodeKind::ExpressionStatement { expression } = &first.kind
1436 && let NodeKind::FunctionCall { name, args } = &expression.kind
1437 && name == "requires"
1438 {
1439 let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
1440 if !names.is_empty() {
1441 let scope_id = self.table.current_scope();
1442 let package = self.table.current_package.clone();
1443 for method_name in names {
1444 self.table.add_symbol(Symbol {
1445 name: method_name.clone(),
1446 qualified_name: format!("{package}::{method_name}"),
1447 kind: SymbolKind::Subroutine,
1448 location: first.location,
1449 scope_id,
1450 declaration: Some("requires".to_string()),
1451 documentation: Some(format!("Required method `{method_name}` from role")),
1452 attributes: vec!["requires=true".to_string()],
1453 });
1454 }
1455 return Some(1);
1456 }
1457 }
1458
1459 if idx + 1 >= statements.len() {
1460 return None;
1461 }
1462
1463 let second = &statements[idx + 1];
1464
1465 let is_requires = match &first.kind {
1467 NodeKind::ExpressionStatement { expression } => {
1468 matches!(&expression.kind, NodeKind::Identifier { name } if name == "requires")
1469 }
1470 _ => false,
1471 };
1472
1473 if !is_requires {
1474 return None;
1475 }
1476
1477 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1478 return None;
1479 };
1480
1481 let names = Self::collect_symbol_names(expression);
1482 if names.is_empty() {
1483 return None;
1484 }
1485
1486 let location = SourceLocation { start: first.location.start, end: second.location.end };
1487 let scope_id = self.table.current_scope();
1488 let package = self.table.current_package.clone();
1489
1490 for name in names {
1491 self.table.add_symbol(Symbol {
1492 name: name.clone(),
1493 qualified_name: format!("{package}::{name}"),
1494 kind: SymbolKind::Subroutine,
1495 location,
1496 scope_id,
1497 declaration: Some("requires".to_string()),
1498 documentation: Some(format!("Required method `{name}` from role")),
1499 attributes: vec!["requires=true".to_string()],
1500 });
1501 }
1502
1503 Some(2)
1504 }
1505
1506 fn synthesize_moo_has_pairs(
1508 &mut self,
1509 pairs: &[(Node, Node)],
1510 has_location: SourceLocation,
1511 require_embedded_marker: bool,
1512 ) {
1513 for (attr_expr, options_expr) in pairs {
1514 let Some(attr_expr) = Self::moo_attribute_expr(attr_expr, require_embedded_marker)
1515 else {
1516 continue;
1517 };
1518
1519 let attribute_names = Self::collect_symbol_names(attr_expr);
1520 if attribute_names.is_empty() {
1521 continue;
1522 }
1523
1524 if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
1525 self.synthesize_moo_has_attrs_with_options(
1526 &attribute_names,
1527 option_pairs,
1528 has_location,
1529 );
1530 }
1531 }
1532 }
1533
1534 fn synthesize_moo_has_attrs_with_options(
1536 &mut self,
1537 attribute_names: &[String],
1538 option_pairs: &[(Node, Node)],
1539 has_location: SourceLocation,
1540 ) {
1541 let scope_id = self.table.current_scope();
1542 let package = self.table.current_package.clone();
1543
1544 let options_expr = Node {
1547 kind: NodeKind::HashLiteral { pairs: option_pairs.to_vec() },
1548 location: has_location,
1549 };
1550
1551 let option_map = Self::extract_hash_options(&options_expr);
1552 let metadata = Self::attribute_metadata(&option_map);
1553 let generated_methods =
1554 Self::moo_accessor_names(attribute_names, &option_map, &options_expr);
1555
1556 for attribute_name in attribute_names {
1557 self.table.add_symbol(Symbol {
1558 name: attribute_name.clone(),
1559 qualified_name: format!("{package}::{attribute_name}"),
1560 kind: SymbolKind::scalar(),
1561 location: has_location,
1562 scope_id,
1563 declaration: Some("has".to_string()),
1564 documentation: Some(format!("Moo/Moose attribute `{attribute_name}`")),
1565 attributes: metadata.clone(),
1566 });
1567 }
1568
1569 let accessor_doc = Self::moo_accessor_doc(&option_map);
1571
1572 for method_name in generated_methods {
1573 self.table.add_symbol(Symbol {
1574 name: method_name.clone(),
1575 qualified_name: format!("{package}::{method_name}"),
1576 kind: SymbolKind::Subroutine,
1577 location: has_location,
1578 scope_id,
1579 declaration: Some("has".to_string()),
1580 documentation: Some(accessor_doc.clone()),
1581 attributes: metadata.clone(),
1582 });
1583 }
1584 }
1585
1586 fn moo_attribute_expr(attr_expr: &Node, require_embedded_marker: bool) -> Option<&Node> {
1588 if let NodeKind::Binary { op, left, right } = &attr_expr.kind
1589 && op == "[]"
1590 && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
1591 {
1592 return Some(right.as_ref());
1593 }
1594
1595 if require_embedded_marker { None } else { Some(attr_expr) }
1596 }
1597
1598 fn try_extract_web_route_declaration(
1607 &mut self,
1608 statements: &[Node],
1609 idx: usize,
1610 ) -> Option<usize> {
1611 let web_framework = self
1612 .framework_flags
1613 .get(&self.table.current_package)
1614 .and_then(|flags| flags.web_framework);
1615 let first = &statements[idx];
1616
1617 if let NodeKind::ExpressionStatement { expression } = &first.kind
1619 && let NodeKind::FunctionCall { name, args } = &expression.kind
1620 && matches!(name.as_str(), "get" | "post" | "put" | "del" | "delete" | "patch" | "any")
1621 {
1622 let method_name = name.as_str();
1623 if let Some(path_node) = args.first() {
1625 if let NodeKind::String { value, .. } = &path_node.kind {
1626 if let Some(path) = Self::normalize_symbol_name(value) {
1627 let http_method = match method_name {
1628 "get" => "GET",
1629 "post" => "POST",
1630 "put" => "PUT",
1631 "del" | "delete" => "DELETE",
1632 "patch" => "PATCH",
1633 "any" => "ANY",
1634 _ => method_name,
1635 };
1636 let scope_id = self.table.current_scope();
1637 self.table.add_symbol(Symbol {
1638 name: path.clone(),
1639 qualified_name: path.clone(),
1640 kind: SymbolKind::Subroutine,
1641 location: first.location,
1642 scope_id,
1643 declaration: Some(method_name.to_string()),
1644 documentation: Some(format!("{http_method} {path}")),
1645 attributes: vec![format!("http_method={http_method}")],
1646 });
1647
1648 if matches!(
1649 web_framework,
1650 Some(WebFrameworkKind::Dancer | WebFrameworkKind::Dancer2)
1651 ) && let Some(target_node) = args.get(1)
1652 {
1653 if let Some(target_name) =
1654 Self::collect_symbol_names(target_node).first().cloned()
1655 {
1656 self.table.add_reference(SymbolReference {
1657 name: target_name,
1658 kind: SymbolKind::Subroutine,
1659 location: target_node.location,
1660 scope_id: self.table.current_scope(),
1661 is_write: false,
1662 });
1663 }
1664 }
1665
1666 self.visit_node(first);
1667 return Some(1);
1668 }
1669 }
1670 }
1671 }
1672
1673 if idx + 1 >= statements.len() {
1674 return None;
1675 }
1676
1677 let second = &statements[idx + 1];
1678
1679 let method_name = match &first.kind {
1681 NodeKind::ExpressionStatement { expression } => match &expression.kind {
1682 NodeKind::Identifier { name }
1683 if matches!(
1684 name.as_str(),
1685 "get" | "post" | "put" | "del" | "delete" | "patch" | "any"
1686 ) =>
1687 {
1688 name.as_str()
1689 }
1690 _ => return None,
1691 },
1692 _ => return None,
1693 };
1694
1695 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1697 return None;
1698 };
1699 let NodeKind::HashLiteral { pairs } = &expression.kind else {
1700 return None;
1701 };
1702
1703 let (path_node, _handler_node) = pairs.first()?;
1705 let path = match &path_node.kind {
1706 NodeKind::String { value, .. } => Self::normalize_symbol_name(value)?,
1707 _ => return None,
1708 };
1709
1710 let http_method = match method_name {
1711 "get" => "GET",
1712 "post" => "POST",
1713 "put" => "PUT",
1714 "del" | "delete" => "DELETE",
1715 "patch" => "PATCH",
1716 "any" => "ANY",
1717 _ => method_name,
1718 };
1719
1720 let route_location =
1721 SourceLocation { start: first.location.start, end: second.location.end };
1722 let scope_id = self.table.current_scope();
1723
1724 self.table.add_symbol(Symbol {
1725 name: path.clone(),
1726 qualified_name: path.clone(),
1727 kind: SymbolKind::Subroutine,
1728 location: route_location,
1729 scope_id,
1730 declaration: Some(method_name.to_string()),
1731 documentation: Some(format!("{http_method} {path}")),
1732 attributes: vec![format!("http_method={http_method}")],
1733 });
1734
1735 self.visit_node(second);
1737
1738 Some(2)
1739 }
1740
1741 fn synthesize_plack_builder_symbols(&mut self, name: &str, args: &[Node]) {
1743 let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
1744 return;
1745 };
1746 if flags.web_framework != Some(WebFrameworkKind::PlackBuilder) || name != "builder" {
1747 return;
1748 }
1749
1750 let Some(block) = args.first() else {
1751 return;
1752 };
1753 let NodeKind::Block { statements } = &block.kind else {
1754 return;
1755 };
1756
1757 let scope_id = self.table.current_scope();
1758 let package = self.table.current_package.clone();
1759
1760 for statement in statements {
1761 let NodeKind::ExpressionStatement { expression } = &statement.kind else {
1762 continue;
1763 };
1764 let NodeKind::FunctionCall { name: stmt_name, args: stmt_args } = &expression.kind
1765 else {
1766 continue;
1767 };
1768
1769 match stmt_name.as_str() {
1770 "enable" => {
1771 self.synthesize_plack_enable_symbol(statement, stmt_args, scope_id, &package);
1772 }
1773 "mount" => {
1774 self.synthesize_plack_mount_symbol(statement, stmt_args, scope_id, &package);
1775 }
1776 _ => {}
1777 }
1778 }
1779 }
1780
1781 fn synthesize_plack_enable_symbol(
1782 &mut self,
1783 statement: &Node,
1784 args: &[Node],
1785 scope_id: ScopeId,
1786 _package: &str,
1787 ) {
1788 let Some(first) = args.first() else {
1789 return;
1790 };
1791 let Some(raw_name) = Self::single_symbol_name(first) else {
1792 return;
1793 };
1794 let middleware_name = if raw_name.contains("::") {
1795 raw_name
1796 } else {
1797 format!("Plack::Middleware::{raw_name}")
1798 };
1799 if middleware_name.is_empty() {
1800 return;
1801 }
1802
1803 if self.table.symbols.get(&middleware_name).is_some_and(|symbols| {
1804 symbols.iter().any(|symbol| {
1805 symbol.kind == SymbolKind::Package
1806 && symbol.declaration.as_deref() == Some("enable")
1807 && symbol
1808 .attributes
1809 .iter()
1810 .any(|attr| attr == &format!("middleware={middleware_name}"))
1811 })
1812 }) {
1813 return;
1814 }
1815
1816 self.table.add_symbol(Symbol {
1817 name: middleware_name.clone(),
1818 qualified_name: middleware_name.clone(),
1819 kind: SymbolKind::Package,
1820 location: statement.location,
1821 scope_id,
1822 declaration: Some("enable".to_string()),
1823 documentation: Some(format!("PSGI middleware {middleware_name}")),
1824 attributes: vec![
1825 "framework=Plack::Builder".to_string(),
1826 format!("middleware={middleware_name}"),
1827 ],
1828 });
1829 }
1830
1831 fn synthesize_plack_mount_symbol(
1832 &mut self,
1833 statement: &Node,
1834 args: &[Node],
1835 scope_id: ScopeId,
1836 _package: &str,
1837 ) {
1838 let Some(path_node) = args.first() else {
1839 return;
1840 };
1841 let Some(path) = Self::single_symbol_name(path_node) else {
1842 return;
1843 };
1844 if path.is_empty() {
1845 return;
1846 }
1847
1848 let target = args
1849 .get(1)
1850 .map(Self::value_summary)
1851 .filter(|s| !s.is_empty())
1852 .unwrap_or_else(|| "$app".to_string());
1853
1854 if self.table.symbols.get(&path).is_some_and(|symbols| {
1855 symbols.iter().any(|symbol| {
1856 symbol.kind == SymbolKind::Subroutine
1857 && symbol.declaration.as_deref() == Some("mount")
1858 && symbol.attributes.iter().any(|attr| attr == &format!("mount_path={path}"))
1859 })
1860 }) {
1861 return;
1862 }
1863
1864 self.table.add_symbol(Symbol {
1865 name: path.clone(),
1866 qualified_name: path.clone(),
1867 kind: SymbolKind::Subroutine,
1868 location: statement.location,
1869 scope_id,
1870 declaration: Some("mount".to_string()),
1871 documentation: Some(format!("PSGI mount {path} -> {target}")),
1872 attributes: vec![
1873 "framework=Plack::Builder".to_string(),
1874 format!("mount_path={path}"),
1875 format!("mount_target={target}"),
1876 ],
1877 });
1878 }
1879
1880 fn try_extract_class_accessor_declaration(&mut self, statement: &Node) -> bool {
1882 let NodeKind::ExpressionStatement { expression } = &statement.kind else {
1883 return false;
1884 };
1885
1886 let NodeKind::MethodCall { method, args, .. } = &expression.kind else {
1887 return false;
1888 };
1889
1890 let is_accessor_generator = matches!(
1891 method.as_str(),
1892 "mk_accessors" | "mk_ro_accessors" | "mk_rw_accessors" | "mk_wo_accessors"
1893 );
1894 if !is_accessor_generator {
1895 return false;
1896 }
1897
1898 let mut accessor_names = Vec::new();
1899 for arg in args {
1900 accessor_names.extend(Self::collect_symbol_names(arg));
1901 }
1902 if accessor_names.is_empty() {
1903 return false;
1904 }
1905
1906 let mut seen = HashSet::new();
1907 let scope_id = self.table.current_scope();
1908 let package = self.table.current_package.clone();
1909
1910 for accessor_name in accessor_names {
1911 if !seen.insert(accessor_name.clone()) {
1912 continue;
1913 }
1914
1915 self.table.add_symbol(Symbol {
1916 name: accessor_name.clone(),
1917 qualified_name: format!("{package}::{accessor_name}"),
1918 kind: SymbolKind::Subroutine,
1919 location: statement.location,
1920 scope_id,
1921 declaration: Some(method.clone()),
1922 documentation: Some("Generated accessor (Class::Accessor)".to_string()),
1923 attributes: vec!["framework=Class::Accessor".to_string()],
1924 });
1925 }
1926
1927 true
1928 }
1929
1930 fn synthesize_async_framework_class_symbol(&mut self, object: &Node) -> bool {
1932 let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
1933 return false;
1934 };
1935
1936 let (module_name, framework_name, exact_match) = match flags.async_framework {
1937 Some(AsyncFrameworkKind::AnyEvent) => ("AnyEvent", "AnyEvent", false),
1938 Some(AsyncFrameworkKind::EV) => ("EV", "EV", true),
1939 Some(AsyncFrameworkKind::Future) => ("Future", "Future", true),
1940 Some(AsyncFrameworkKind::FutureXS) => ("Future::XS", "Future::XS", true),
1941 Some(AsyncFrameworkKind::Promise) => ("Promise", "Promise", true),
1942 Some(AsyncFrameworkKind::PromiseXS) => ("Promise::XS", "Promise::XS", true),
1943 Some(AsyncFrameworkKind::POE) => ("POE", "POE", false),
1944 Some(AsyncFrameworkKind::IOAsync) => ("IO::Async", "IO::Async", false),
1945 Some(AsyncFrameworkKind::MojoRedis) => ("Mojo::Redis", "Mojo::Redis", true),
1946 Some(AsyncFrameworkKind::MojoPg) => ("Mojo::Pg", "Mojo::Pg", true),
1947 None => return false,
1948 };
1949
1950 let Some(name) = Self::single_symbol_name(object) else {
1951 return false;
1952 };
1953 if flags.async_framework == Some(AsyncFrameworkKind::AnyEvent) {
1954 if !matches!(
1955 name.as_str(),
1956 "AnyEvent" | "AnyEvent::CondVar" | "AnyEvent::Timer" | "AnyEvent::IO"
1957 ) {
1958 return false;
1959 }
1960 } else if exact_match {
1961 if name != module_name {
1962 return false;
1963 }
1964 } else if !name.starts_with(&format!("{module_name}::")) {
1965 return false;
1966 }
1967
1968 let already_synthesized = self.table.symbols.get(&name).is_some_and(|symbols| {
1969 symbols.iter().any(|symbol| {
1970 symbol.kind == SymbolKind::Class
1971 && symbol.declaration.as_deref() == Some(&format!("framework={framework_name}"))
1972 })
1973 });
1974 if already_synthesized {
1975 return true;
1976 }
1977
1978 let framework_attr = format!("framework={framework_name}");
1979
1980 self.table.add_symbol(Symbol {
1981 name: name.clone(),
1982 qualified_name: name.clone(),
1983 kind: SymbolKind::Class,
1984 location: object.location,
1985 scope_id: self.table.current_scope(),
1986 declaration: Some(framework_attr.clone()),
1987 documentation: Some(format!("Synthetic {framework_name} class")),
1988 attributes: vec![framework_attr],
1989 });
1990
1991 true
1992 }
1993
1994 fn synthesize_ev_framework_symbol(&mut self, location: SourceLocation) {
1996 let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
1997 return;
1998 };
1999 if flags.async_framework != Some(AsyncFrameworkKind::EV) {
2000 return;
2001 }
2002
2003 let name = "EV";
2004 if self.table.symbols.get(name).is_some_and(|symbols| {
2005 symbols.iter().any(|symbol| {
2006 symbol.kind == SymbolKind::Class
2007 && symbol.declaration.as_deref() == Some("framework=EV")
2008 })
2009 }) {
2010 return;
2011 }
2012
2013 self.table.add_symbol(Symbol {
2014 name: name.to_string(),
2015 qualified_name: name.to_string(),
2016 kind: SymbolKind::Class,
2017 location,
2018 scope_id: self.table.current_scope(),
2019 declaration: Some("framework=EV".to_string()),
2020 documentation: Some("Synthetic EV namespace".to_string()),
2021 attributes: vec!["framework=EV".to_string()],
2022 });
2023 }
2024
2025 fn synthesize_ev_symbols(&mut self, name: &str, location: SourceLocation) -> bool {
2027 let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
2028 return false;
2029 };
2030 if flags.async_framework != Some(AsyncFrameworkKind::EV) {
2031 return false;
2032 }
2033
2034 let Some(ev_suffix) = name.strip_prefix("EV::") else {
2035 return false;
2036 };
2037 if !matches!(ev_suffix, "timer" | "io" | "signal" | "idle") {
2038 return false;
2039 }
2040
2041 let already_synthesized = self.table.symbols.get(name).is_some_and(|symbols| {
2042 symbols.iter().any(|symbol| {
2043 symbol.kind == SymbolKind::Subroutine
2044 && symbol.declaration.as_deref() == Some("framework=EV")
2045 })
2046 });
2047 if already_synthesized {
2048 return true;
2049 }
2050
2051 self.table.add_symbol(Symbol {
2052 name: name.to_string(),
2053 qualified_name: name.to_string(),
2054 kind: SymbolKind::Subroutine,
2055 location,
2056 scope_id: self.table.current_scope(),
2057 declaration: Some("framework=EV".to_string()),
2058 documentation: Some(format!("Synthetic EV API `{ev_suffix}`")),
2059 attributes: vec!["framework=EV".to_string(), format!("ev_api={ev_suffix}")],
2060 });
2061
2062 true
2063 }
2064
2065 fn synthesize_future_api_symbols(
2072 &mut self,
2073 object: &Node,
2074 method: &str,
2075 location: SourceLocation,
2076 ) -> bool {
2077 let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
2078 return false;
2079 };
2080
2081 let (framework_name, root_name, chain_methods, class_entrypoints) =
2082 match flags.async_framework {
2083 Some(AsyncFrameworkKind::Future) => (
2084 "Future",
2085 "Future",
2086 vec!["then", "catch", "finally", "get", "is_done", "is_ready"],
2087 vec!["new", "done", "fail", "wait_all", "needs_all", "needs_any"],
2088 ),
2089 Some(AsyncFrameworkKind::FutureXS) => (
2090 "Future::XS",
2091 "Future::XS",
2092 vec!["then", "catch", "finally", "get", "is_done", "is_ready"],
2093 vec!["new", "done", "fail", "wait_all", "needs_all", "needs_any"],
2094 ),
2095 Some(AsyncFrameworkKind::Promise) => (
2096 "Promise",
2097 "Promise",
2098 vec!["then", "catch", "finally", "resolve", "reject"],
2099 vec!["new", "all", "race", "any"],
2100 ),
2101 Some(AsyncFrameworkKind::PromiseXS) => (
2102 "Promise::XS",
2103 "Promise::XS",
2104 vec!["then", "catch", "finally", "resolve", "reject"],
2105 vec!["new", "all", "race", "any"],
2106 ),
2107 _ => return false,
2108 };
2109
2110 let object_name = Self::single_symbol_name(object);
2111
2112 let should_synthesize = if chain_methods.contains(&method) {
2113 true
2114 } else if class_entrypoints.contains(&method) {
2115 object_name.is_some_and(|name| name == root_name)
2116 } else {
2117 false
2118 };
2119 if !should_synthesize {
2120 return false;
2121 }
2122
2123 let already_synthesized = self.table.symbols.get(method).is_some_and(|symbols| {
2124 symbols.iter().any(|symbol| {
2125 symbol.kind == SymbolKind::Subroutine
2126 && symbol.declaration.as_deref() == Some(&format!("framework={framework_name}"))
2127 && symbol.attributes.iter().any(|attr| attr == &format!("future_api={method}"))
2128 })
2129 });
2130 if already_synthesized {
2131 return true;
2132 }
2133
2134 self.table.add_symbol(Symbol {
2135 name: method.to_string(),
2136 qualified_name: format!("{framework_name}::{method}"),
2137 kind: SymbolKind::Subroutine,
2138 location,
2139 scope_id: self.table.current_scope(),
2140 declaration: Some(format!("framework={framework_name}")),
2141 documentation: Some(format!("Synthetic {framework_name} API `{method}`")),
2142 attributes: vec![format!("framework={framework_name}"), format!("future_api={method}")],
2143 });
2144
2145 true
2146 }
2147
2148 fn update_framework_context(&mut self, module: &str, args: &[String]) {
2150 let pkg = self.table.current_package.clone();
2151
2152 let framework_kind = match module {
2153 "Moo" | "Mouse" => Some(FrameworkKind::Moo),
2154 "Moo::Role" | "Mouse::Role" => Some(FrameworkKind::MooRole),
2155 "Moose" => Some(FrameworkKind::Moose),
2156 "Moose::Role" => Some(FrameworkKind::MooseRole),
2157 "Role::Tiny" => Some(FrameworkKind::RoleTiny),
2158 "Role::Tiny::With" => Some(FrameworkKind::RoleTinyWith),
2159 _ => None,
2160 };
2161
2162 if let Some(kind) = framework_kind {
2163 let flags = self.framework_flags.entry(pkg.clone()).or_default();
2164 flags.moo = true;
2165 flags.kind = Some(kind);
2166 return;
2167 }
2168
2169 if module == "Class::Accessor" {
2170 self.framework_flags.entry(pkg.clone()).or_default().class_accessor = true;
2171 return;
2172 }
2173
2174 let web_kind = match module {
2175 "Dancer" => Some(WebFrameworkKind::Dancer),
2176 "Dancer2" | "Dancer2::Core" => Some(WebFrameworkKind::Dancer2),
2177 "Mojolicious::Lite" => Some(WebFrameworkKind::MojoliciousLite),
2178 "Plack::Builder" => Some(WebFrameworkKind::PlackBuilder),
2179 _ => None,
2180 };
2181 if let Some(kind) = web_kind {
2182 self.framework_flags.entry(pkg.clone()).or_default().web_framework = Some(kind);
2183 return;
2184 }
2185
2186 if module == "IO::Async" || module.starts_with("IO::Async::") {
2187 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2188 Some(AsyncFrameworkKind::IOAsync);
2189 return;
2190 }
2191
2192 if module == "AnyEvent" {
2193 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2194 Some(AsyncFrameworkKind::AnyEvent);
2195 return;
2196 }
2197
2198 if module == "EV" {
2199 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2200 Some(AsyncFrameworkKind::EV);
2201 return;
2202 }
2203
2204 if module == "Future" {
2205 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2206 Some(AsyncFrameworkKind::Future);
2207 return;
2208 }
2209
2210 if module == "Future::XS" {
2211 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2212 Some(AsyncFrameworkKind::FutureXS);
2213 return;
2214 }
2215
2216 if module == "Promise" {
2217 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2218 Some(AsyncFrameworkKind::Promise);
2219 return;
2220 }
2221
2222 if module == "Promise::XS" {
2223 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2224 Some(AsyncFrameworkKind::PromiseXS);
2225 return;
2226 }
2227
2228 if module == "POE" || module.starts_with("POE::") {
2229 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2230 Some(AsyncFrameworkKind::POE);
2231 return;
2232 }
2233
2234 if module == "Mojo::Redis" {
2235 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2236 Some(AsyncFrameworkKind::MojoRedis);
2237 return;
2238 }
2239
2240 if module == "Mojo::Pg" {
2241 self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2242 Some(AsyncFrameworkKind::MojoPg);
2243 return;
2244 }
2245
2246 if matches!(module, "base" | "parent") {
2247 let has_class_accessor_parent = args
2248 .iter()
2249 .filter_map(|arg| Self::normalize_symbol_name(arg))
2250 .any(|arg| arg == "Class::Accessor");
2251 if has_class_accessor_parent {
2252 self.framework_flags.entry(pkg.clone()).or_default().class_accessor = true;
2253 }
2254 let has_catalyst_controller_parent = args
2255 .iter()
2256 .filter_map(|arg| Self::normalize_symbol_name(arg))
2257 .any(|arg| arg == "Catalyst::Controller");
2258 if has_catalyst_controller_parent {
2259 self.mark_catalyst_controller_package(&pkg);
2260 }
2261 }
2262 }
2263
2264 fn mark_catalyst_controller_package(&mut self, package: &str) {
2265 self.framework_flags.entry(package.to_string()).or_default().catalyst_controller = true;
2266 }
2267
2268 fn current_package_is_catalyst_controller(&self) -> bool {
2269 self.framework_flags
2270 .get(&self.table.current_package)
2271 .is_some_and(|flags| flags.catalyst_controller)
2272 || Self::is_catalyst_controller_package_name(&self.table.current_package)
2273 }
2274
2275 fn is_catalyst_controller_package_name(package: &str) -> bool {
2276 package.contains("::Controller::") || package.ends_with("::Controller")
2277 }
2278
2279 fn catalyst_action_metadata(attributes: &[String]) -> Option<(String, Vec<String>)> {
2280 let mut kind = None;
2281 let mut details = Vec::new();
2282 let mut seen = HashSet::new();
2283
2284 for attr in attributes {
2285 let attr_name = Self::attribute_base_name(attr);
2286 if !Self::is_catalyst_action_attribute(&attr_name) {
2287 continue;
2288 }
2289
2290 if kind.is_none()
2291 || matches!(kind.as_deref(), Some("Args" | "CaptureArgs" | "PathPart"))
2292 {
2293 if matches!(attr_name.as_str(), "Path" | "Local" | "Global" | "Regex" | "Chained") {
2294 kind = Some(attr_name.clone());
2295 } else if kind.is_none() {
2296 kind = Some(attr_name.clone());
2297 }
2298 }
2299
2300 if seen.insert(attr.clone()) {
2301 details.push(attr.clone());
2302 }
2303 }
2304
2305 if let Some(action_kind) = kind.as_deref()
2306 && matches!(action_kind, "Path" | "Local" | "Global" | "Regex" | "Chained")
2307 {
2308 details.retain(|attr| Self::attribute_base_name(attr) != action_kind);
2309 }
2310
2311 kind.map(|kind| (kind, details))
2312 }
2313
2314 fn is_catalyst_action_attribute(attr_name: &str) -> bool {
2315 matches!(
2316 attr_name,
2317 "Path" | "Local" | "Global" | "Regex" | "Chained" | "PathPart" | "Args" | "CaptureArgs"
2318 )
2319 }
2320
2321 fn attribute_base_name(attr: &str) -> String {
2322 attr.trim_start_matches(':')
2323 .split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == ':'))
2324 .next()
2325 .unwrap_or("")
2326 .to_string()
2327 }
2328
2329 fn extract_hash_options(node: &Node) -> HashMap<String, String> {
2331 let mut options = HashMap::new();
2332 let NodeKind::HashLiteral { pairs } = &node.kind else {
2333 return options;
2334 };
2335
2336 for (key_node, value_node) in pairs {
2337 let Some(key_name) = Self::single_symbol_name(key_node) else {
2338 continue;
2339 };
2340 let value_text = Self::value_summary(value_node);
2341 options.insert(key_name, value_text);
2342 }
2343
2344 options
2345 }
2346
2347 fn attribute_metadata(option_map: &HashMap<String, String>) -> Vec<String> {
2349 let preferred_order = [
2350 "is",
2351 "isa",
2352 "required",
2353 "lazy",
2354 "builder",
2355 "default",
2356 "reader",
2357 "writer",
2358 "accessor",
2359 "predicate",
2360 "clearer",
2361 "handles",
2362 ];
2363
2364 let mut metadata = Vec::new();
2365 for key in preferred_order {
2366 if let Some(value) = option_map.get(key) {
2367 metadata.push(format!("{key}={value}"));
2368 }
2369 }
2370 metadata
2371 }
2372
2373 fn moo_accessor_doc(option_map: &HashMap<String, String>) -> String {
2382 let mut parts = Vec::new();
2383
2384 if let Some(isa) = option_map.get("isa") {
2385 parts.push(format!("isa: {isa}"));
2386 }
2387 if let Some(is) = option_map.get("is") {
2388 parts.push(is.clone());
2389 }
2390
2391 if parts.is_empty() {
2392 "Generated accessor from Moo/Moose `has`".to_string()
2393 } else {
2394 format!("Moo/Moose accessor ({})", parts.join(", "))
2395 }
2396 }
2397
2398 fn moo_accessor_names(
2400 attribute_names: &[String],
2401 option_map: &HashMap<String, String>,
2402 options_expr: &Node,
2403 ) -> Vec<String> {
2404 let mut methods = Vec::new();
2405 let mut seen = HashSet::new();
2406
2407 for key in ["accessor", "reader", "writer", "predicate", "clearer", "builder"] {
2408 for name in Self::option_method_names(options_expr, key, attribute_names) {
2409 if seen.insert(name.clone()) {
2410 methods.push(name);
2411 }
2412 }
2413 }
2414
2415 for name in Self::handles_method_names(options_expr) {
2416 if seen.insert(name.clone()) {
2417 methods.push(name);
2418 }
2419 }
2420
2421 let has_explicit_accessor = option_map.contains_key("accessor")
2423 || option_map.contains_key("reader")
2424 || option_map.contains_key("writer");
2425 if !has_explicit_accessor {
2426 for attribute_name in attribute_names {
2427 if seen.insert(attribute_name.clone()) {
2428 methods.push(attribute_name.clone());
2429 }
2430 }
2431 }
2432
2433 methods
2434 }
2435
2436 fn find_hash_option_value<'a>(options_expr: &'a Node, key: &str) -> Option<&'a Node> {
2438 let NodeKind::HashLiteral { pairs } = &options_expr.kind else {
2439 return None;
2440 };
2441
2442 for (key_node, value_node) in pairs {
2443 if Self::single_symbol_name(key_node).as_deref() == Some(key) {
2444 return Some(value_node);
2445 }
2446 }
2447
2448 None
2449 }
2450
2451 fn option_method_names(
2453 options_expr: &Node,
2454 key: &str,
2455 attribute_names: &[String],
2456 ) -> Vec<String> {
2457 let Some(value_node) = Self::find_hash_option_value(options_expr, key) else {
2458 return Vec::new();
2459 };
2460
2461 let mut names = Self::collect_symbol_names(value_node);
2462 if !names.is_empty() {
2463 names.sort();
2464 names.dedup();
2465 return names;
2466 }
2467
2468 if !Self::is_truthy_shorthand(value_node) {
2470 return Vec::new();
2471 }
2472
2473 match key {
2474 "predicate" => attribute_names.iter().map(|name| format!("has_{name}")).collect(),
2475 "clearer" => attribute_names.iter().map(|name| format!("clear_{name}")).collect(),
2476 "builder" => attribute_names.iter().map(|name| format!("_build_{name}")).collect(),
2477 _ => Vec::new(),
2478 }
2479 }
2480
2481 fn is_truthy_shorthand(node: &Node) -> bool {
2483 match &node.kind {
2484 NodeKind::Number { value } => value.trim() == "1",
2485 NodeKind::Identifier { name } => {
2486 let lower = name.trim().to_ascii_lowercase();
2487 lower == "1" || lower == "true"
2488 }
2489 NodeKind::String { value, .. } => {
2490 Self::normalize_symbol_name(value).is_some_and(|value| {
2491 let lower = value.to_ascii_lowercase();
2492 value == "1" || lower == "true"
2493 })
2494 }
2495 _ => false,
2496 }
2497 }
2498
2499 fn handles_method_names(options_expr: &Node) -> Vec<String> {
2501 let Some(handles_node) = Self::find_hash_option_value(options_expr, "handles") else {
2502 return Vec::new();
2503 };
2504
2505 let mut names = Vec::new();
2506 match &handles_node.kind {
2507 NodeKind::HashLiteral { pairs } => {
2508 for (key_node, _) in pairs {
2509 names.extend(Self::collect_symbol_names(key_node));
2510 }
2511 }
2512 _ => {
2513 names.extend(Self::collect_symbol_names(handles_node));
2514 }
2515 }
2516
2517 names.sort();
2518 names.dedup();
2519 names
2520 }
2521
2522 fn collect_symbol_names(node: &Node) -> Vec<String> {
2524 match &node.kind {
2525 NodeKind::String { value, .. } => {
2526 Self::normalize_symbol_name(value).into_iter().collect()
2527 }
2528 NodeKind::Identifier { name } => {
2529 Self::normalize_symbol_name(name).into_iter().collect()
2530 }
2531 NodeKind::ArrayLiteral { elements } => {
2532 let mut names = Vec::new();
2533 for element in elements {
2534 names.extend(Self::collect_symbol_names(element));
2535 }
2536 names
2537 }
2538 _ => Vec::new(),
2539 }
2540 }
2541
2542 fn single_symbol_name(node: &Node) -> Option<String> {
2544 Self::collect_symbol_names(node).into_iter().next()
2545 }
2546
2547 fn normalize_symbol_name(raw: &str) -> Option<String> {
2549 let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
2550 if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
2551 }
2552
2553 fn value_summary(node: &Node) -> String {
2555 match &node.kind {
2556 NodeKind::String { value, .. } => {
2557 Self::normalize_symbol_name(value).unwrap_or_else(|| value.clone())
2558 }
2559 NodeKind::Identifier { name } => name.clone(),
2560 NodeKind::Variable { sigil, name } => format!("{sigil}{name}"),
2561 NodeKind::Number { value } => value.clone(),
2562 NodeKind::ArrayLiteral { elements } => {
2563 let mut entries = Vec::new();
2564 for element in elements {
2565 entries.extend(Self::collect_symbol_names(element));
2566 }
2567 entries.sort();
2568 entries.dedup();
2569 if entries.is_empty() {
2570 "array".to_string()
2571 } else {
2572 format!("[{}]", entries.join(","))
2573 }
2574 }
2575 NodeKind::HashLiteral { pairs } => {
2576 let mut entries = Vec::new();
2577 for (key_node, value_node) in pairs {
2578 let Some(key_name) = Self::single_symbol_name(key_node) else {
2579 continue;
2580 };
2581 if let Some(value_name) = Self::single_symbol_name(value_node) {
2582 entries.push(format!("{key_name}->{value_name}"));
2583 } else {
2584 entries.push(key_name);
2585 }
2586 }
2587 entries.sort();
2588 entries.dedup();
2589 if entries.is_empty() {
2590 "hash".to_string()
2591 } else {
2592 format!("{{{}}}", entries.join(","))
2593 }
2594 }
2595 NodeKind::Undef => "undef".to_string(),
2596 _ => "expr".to_string(),
2597 }
2598 }
2599
2600 fn method_reference_location(
2605 &self,
2606 call_node: &Node,
2607 object: &Node,
2608 method_name: &str,
2609 ) -> SourceLocation {
2610 if self.source.is_empty() {
2611 return call_node.location;
2612 }
2613
2614 let search_start = object.location.end.min(self.source.len());
2615 let search_end = search_start.saturating_add(160).min(self.source.len());
2616 if search_start >= search_end || !self.source.is_char_boundary(search_start) {
2617 return call_node.location;
2618 }
2619
2620 let window = &self.source[search_start..search_end];
2621 let Some(arrow_idx) = window.find("->") else {
2622 return call_node.location;
2623 };
2624
2625 let mut idx = arrow_idx + 2;
2626 while idx < window.len() {
2627 let b = window.as_bytes()[idx];
2628 if b.is_ascii_whitespace() {
2629 idx += 1;
2630 } else {
2631 break;
2632 }
2633 }
2634
2635 let suffix = &window[idx..];
2636 if suffix.starts_with(method_name) {
2637 let method_start = search_start + idx;
2638 return SourceLocation { start: method_start, end: method_start + method_name.len() };
2639 }
2640
2641 if let Some(rel_idx) = suffix.find(method_name) {
2642 let method_start = search_start + idx + rel_idx;
2643 return SourceLocation { start: method_start, end: method_start + method_name.len() };
2644 }
2645
2646 call_node.location
2647 }
2648
2649 fn extract_leading_comment(&self, start: usize) -> Option<String> {
2651 if self.source.is_empty() || start == 0 {
2652 return None;
2653 }
2654 let mut end = start.min(self.source.len());
2655 let bytes = self.source.as_bytes();
2656 while end > 0 && bytes[end - 1].is_ascii_whitespace() {
2658 end -= 1;
2659 }
2660
2661 while end > 0 && !self.source.is_char_boundary(end) {
2663 end -= 1;
2664 }
2665
2666 let prefix = &self.source[..end];
2667 let mut lines = prefix.lines().rev();
2668 let mut docs = Vec::new();
2669 for line in &mut lines {
2670 let trimmed = line.trim_start();
2671 if trimmed.starts_with('#') {
2672 let content = trimmed.trim_start_matches('#').trim_start();
2674 docs.push(content);
2675 } else {
2676 break;
2678 }
2679 }
2680 if docs.is_empty() {
2681 None
2682 } else {
2683 docs.reverse();
2684 let total_len: usize =
2686 docs.iter().map(|s| s.len()).sum::<usize>() + docs.len().saturating_sub(1);
2687 let mut result = String::with_capacity(total_len);
2688 for (i, doc) in docs.iter().enumerate() {
2689 if i > 0 {
2690 result.push('\n');
2691 }
2692 result.push_str(doc);
2693 }
2694 Some(result)
2695 }
2696 }
2697
2698 fn extract_package_documentation(
2705 &self,
2706 package_name: &str,
2707 location: SourceLocation,
2708 ) -> Option<String> {
2709 let leading = self.extract_leading_comment(location.start);
2711 if leading.is_some() {
2712 return leading;
2713 }
2714
2715 if self.source.is_empty() {
2717 return None;
2718 }
2719
2720 let mut in_name_section = false;
2722 let mut name_lines: Vec<&str> = Vec::new();
2723
2724 for line in self.source.lines() {
2725 let trimmed = line.trim();
2726 if trimmed.starts_with("=head1") {
2727 if in_name_section {
2728 break;
2730 }
2731 let heading = trimmed.strip_prefix("=head1").map(|s| s.trim());
2732 if heading == Some("NAME") {
2733 in_name_section = true;
2734 continue;
2735 }
2736 } else if trimmed.starts_with("=cut") && in_name_section {
2737 break;
2738 } else if trimmed.starts_with('=') && in_name_section {
2739 break;
2741 } else if in_name_section && !trimmed.is_empty() {
2742 name_lines.push(trimmed);
2743 }
2744 }
2745
2746 if !name_lines.is_empty() {
2747 let name_doc = name_lines.join(" ");
2748 if name_doc.contains(package_name)
2750 || name_doc.contains(&package_name.replace("::", "-"))
2751 {
2752 return Some(name_doc);
2753 }
2754 }
2755
2756 None
2757 }
2758
2759 fn register_signature_params(&mut self, sig: &Node) {
2765 let NodeKind::Signature { parameters } = &sig.kind else {
2766 return;
2767 };
2768 for param in parameters {
2769 let variable = match ¶m.kind {
2770 NodeKind::MandatoryParameter { variable } => variable.as_ref(),
2771 NodeKind::OptionalParameter { variable, .. } => variable.as_ref(),
2772 NodeKind::SlurpyParameter { variable } => variable.as_ref(),
2773 NodeKind::NamedParameter { variable } => variable.as_ref(),
2774 _ => continue,
2776 };
2777 self.handle_variable_declaration("my", variable, &[], variable.location, None);
2778 }
2779 }
2780
2781 fn handle_variable_declaration(
2783 &mut self,
2784 declarator: &str,
2785 variable: &Node,
2786 attributes: &[String],
2787 location: SourceLocation,
2788 documentation: Option<String>,
2789 ) {
2790 if let NodeKind::Variable { sigil, name } = &variable.kind {
2791 let kind = match sigil.as_str() {
2792 "$" => SymbolKind::scalar(),
2793 "@" => SymbolKind::array(),
2794 "%" => SymbolKind::hash(),
2795 _ => return,
2796 };
2797
2798 let symbol = Symbol {
2799 name: name.clone(),
2800 qualified_name: if declarator == "our" {
2801 format!("{}::{}", self.table.current_package, name)
2802 } else {
2803 name.clone()
2804 },
2805 kind,
2806 location,
2807 scope_id: self.table.current_scope(),
2808 declaration: Some(declarator.to_string()),
2809 documentation,
2810 attributes: attributes.to_vec(),
2811 };
2812
2813 self.table.add_symbol(symbol);
2814 }
2815 }
2816
2817 fn try_extract_const_fast_declaration(&mut self, args: &[Node]) -> bool {
2818 let mut matched = false;
2819
2820 for arg in args {
2821 match &arg.kind {
2822 NodeKind::VariableDeclaration { declarator, variable, .. } => {
2823 if self.add_constant_wrapper_symbol(
2824 variable,
2825 &[],
2826 declarator,
2827 "const",
2828 "Const::Fast read-only variable",
2829 ) {
2830 matched = true;
2831 }
2832 }
2833 NodeKind::VariableListDeclaration { declarator, variables, attributes, .. } => {
2834 let mut saw_decl = false;
2835 for variable in variables {
2836 if self.add_constant_wrapper_symbol(
2837 variable,
2838 attributes,
2839 declarator,
2840 "const",
2841 "Const::Fast read-only variable",
2842 ) {
2843 saw_decl = true;
2844 }
2845 }
2846 matched |= saw_decl;
2847 }
2848 _ => self.visit_node(arg),
2849 }
2850 }
2851
2852 matched
2853 }
2854
2855 fn try_extract_readonly_declaration(&mut self, args: &[Node]) -> bool {
2856 let mut matched = false;
2857
2858 for arg in args {
2859 match &arg.kind {
2860 NodeKind::VariableDeclaration { declarator, variable, attributes, .. } => {
2861 if self.add_constant_wrapper_symbol(
2862 variable,
2863 attributes,
2864 declarator,
2865 "Readonly",
2866 "Readonly read-only variable",
2867 ) {
2868 matched = true;
2869 }
2870 }
2871 NodeKind::VariableListDeclaration { declarator, variables, attributes, .. } => {
2872 let mut saw_decl = false;
2873 for variable in variables {
2874 if self.add_constant_wrapper_symbol(
2875 variable,
2876 attributes,
2877 declarator,
2878 "Readonly",
2879 "Readonly read-only variable",
2880 ) {
2881 saw_decl = true;
2882 }
2883 }
2884 matched |= saw_decl;
2885 }
2886 _ => self.visit_node(arg),
2887 }
2888 }
2889
2890 matched
2891 }
2892
2893 fn add_constant_wrapper_symbol(
2894 &mut self,
2895 variable: &Node,
2896 attributes: &[String],
2897 scope_declarator: &str,
2898 declarator: &str,
2899 documentation: &str,
2900 ) -> bool {
2901 match &variable.kind {
2902 NodeKind::Variable { name, .. } => {
2903 self.table.add_symbol(Symbol {
2904 name: name.clone(),
2905 qualified_name: if scope_declarator == "our" {
2906 format!("{}::{}", self.table.current_package, name)
2907 } else {
2908 name.clone()
2909 },
2910 kind: SymbolKind::Constant,
2911 location: variable.location,
2912 scope_id: self.table.current_scope(),
2913 declaration: Some(declarator.to_string()),
2914 documentation: Some(documentation.to_string()),
2915 attributes: attributes.to_vec(),
2916 });
2917 true
2918 }
2919 NodeKind::VariableWithAttributes { variable, attributes: inner_attributes } => {
2920 let mut merged = attributes.to_vec();
2921 merged.extend(inner_attributes.iter().cloned());
2922 self.add_constant_wrapper_symbol(
2923 variable,
2924 &merged,
2925 scope_declarator,
2926 declarator,
2927 documentation,
2928 )
2929 }
2930 _ => false,
2931 }
2932 }
2933
2934 fn synthesize_use_constant_symbols(&mut self, args: &[String], location: SourceLocation) {
2935 let constant_names = extract_constant_names_from_use_args(args);
2936 for name in constant_names {
2937 self.table.add_symbol(Symbol {
2938 name: name.clone(),
2939 qualified_name: format!("{}::{}", self.table.current_package, name),
2940 kind: SymbolKind::Constant,
2941 location,
2942 scope_id: self.table.current_scope(),
2943 declaration: Some("constant".to_string()),
2944 documentation: Some("use constant declaration".to_string()),
2945 attributes: vec![],
2946 });
2947 }
2948 }
2949
2950 fn register_catch_variable(&mut self, full_name: &str, catch_block_location: SourceLocation) {
2951 let (sigil, name) = split_variable_name(full_name);
2952 let kind = match sigil {
2953 "$" => SymbolKind::scalar(),
2954 "@" => SymbolKind::array(),
2955 "%" => SymbolKind::hash(),
2956 _ => return,
2957 };
2958 if name.is_empty() || name.contains("::") {
2959 return;
2960 }
2961
2962 let location = self
2963 .find_catch_variable_location(catch_block_location.start, full_name)
2964 .unwrap_or(SourceLocation {
2965 start: catch_block_location.start,
2966 end: catch_block_location.start,
2967 });
2968
2969 self.table.add_symbol(Symbol {
2970 name: name.to_string(),
2971 qualified_name: name.to_string(),
2972 kind,
2973 location,
2974 scope_id: self.table.current_scope(),
2975 declaration: Some("my".to_string()),
2976 documentation: Some("Exception variable bound by catch".to_string()),
2977 attributes: vec![],
2978 });
2979 }
2980
2981 fn find_catch_variable_location(
2982 &self,
2983 catch_body_start: usize,
2984 full_name: &str,
2985 ) -> Option<SourceLocation> {
2986 if self.source.is_empty()
2987 || full_name.is_empty()
2988 || catch_body_start == 0
2989 || catch_body_start > self.source.len()
2990 {
2991 return None;
2992 }
2993
2994 let window_start = catch_body_start.saturating_sub(256);
2995 let window = self.source.get(window_start..catch_body_start)?;
2996 let catch_start = window.rfind("catch")?;
2997 let search_start = catch_start + "catch".len();
2998 let var_offset = window[search_start..].rfind(full_name)? + search_start;
2999 let start = window_start + var_offset;
3000 let end = start + full_name.len();
3001
3002 Some(SourceLocation { start, end })
3003 }
3004
3005 fn mark_write_reference(&mut self, node: &Node) {
3007 if let NodeKind::Variable { .. } = &node.kind {
3010 }
3013 }
3014
3015 fn extract_vars_from_string(&mut self, value: &str, string_location: SourceLocation) {
3017 static SCALAR_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
3018
3019 let scalar_re = match SCALAR_RE
3022 .get_or_init(|| {
3023 Regex::new(
3024 r"\$((?:[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)|\{(?:[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)\})",
3025 )
3026 })
3027 .as_ref()
3028 {
3029 Ok(re) => re,
3030 Err(_) => return, };
3032
3033 let content = if value.len() >= 2 { &value[1..value.len() - 1] } else { value };
3035
3036 for cap in scalar_re.captures_iter(content) {
3037 if let Some(m) = cap.get(0) {
3038 let var_name = if m.as_str().starts_with("${") && m.as_str().ends_with("}") {
3039 &m.as_str()[2..m.as_str().len() - 1]
3041 } else {
3042 &m.as_str()[1..]
3044 };
3045
3046 let start_offset = string_location.start + 1 + m.start(); let end_offset = start_offset + m.len();
3050
3051 let reference = SymbolReference {
3052 name: var_name.to_string(),
3053 kind: SymbolKind::scalar(),
3054 location: SourceLocation { start: start_offset, end: end_offset },
3055 scope_id: self.table.current_scope(),
3056 is_write: false,
3057 };
3058
3059 self.table.add_reference(reference);
3060 }
3061 }
3062 }
3063}
3064
3065fn split_variable_name(full_name: &str) -> (&str, &str) {
3066 full_name
3067 .char_indices()
3068 .next()
3069 .map(|(idx, ch)| (&full_name[idx..idx + ch.len_utf8()], &full_name[idx + ch.len_utf8()..]))
3070 .unwrap_or(("", ""))
3071}
3072
3073fn extract_constant_names_from_use_args(args: &[String]) -> Vec<String> {
3075 fn push_unique(names: &mut Vec<String>, seen: &mut HashSet<String>, candidate: &str) {
3076 if seen.insert(candidate.to_string()) {
3077 names.push(candidate.to_string());
3078 }
3079 }
3080
3081 fn normalize_constant_name(token: &str) -> Option<&str> {
3082 let stripped = token.trim_matches(|c: char| {
3083 matches!(c, '\'' | '"' | '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';')
3084 });
3085 if stripped.is_empty() || stripped.starts_with('-') {
3086 return None;
3087 }
3088 stripped.chars().all(|c| c.is_alphanumeric() || c == '_').then_some(stripped)
3089 }
3090
3091 let mut names = Vec::new();
3092 let mut seen = HashSet::new();
3093 let Some(first) = args.first().map(String::as_str) else {
3094 return names;
3095 };
3096
3097 if first.starts_with("qw") {
3098 let (qw_words, remainder) = extract_qw_words(first);
3099 if remainder.trim().is_empty() {
3100 for word in qw_words {
3101 if let Some(candidate) = normalize_constant_name(&word) {
3102 push_unique(&mut names, &mut seen, candidate);
3103 }
3104 }
3105 return names;
3106 }
3107
3108 let content = first.trim_start_matches("qw").trim_start();
3109 let content = content
3110 .trim_start_matches(|c: char| "([{/<|!".contains(c))
3111 .trim_end_matches(|c: char| ")]}/|!>".contains(c));
3112 for word in content.split_whitespace() {
3113 if let Some(candidate) = normalize_constant_name(word) {
3114 push_unique(&mut names, &mut seen, candidate);
3115 }
3116 }
3117 return names;
3118 }
3119
3120 let starts_hash_form = first == "{"
3121 || first == "+{"
3122 || (first == "+" && args.get(1).map(String::as_str) == Some("{"));
3123 if starts_hash_form {
3124 let mut skipped_leading_plus = false;
3125 let mut iter = args.iter().peekable();
3126 while let Some(arg) = iter.next() {
3127 if arg == "+{" {
3128 skipped_leading_plus = true;
3129 continue;
3130 }
3131 if arg == "+" && !skipped_leading_plus {
3132 skipped_leading_plus = true;
3133 continue;
3134 }
3135 if arg == "{" || arg == "}" || arg == "," || arg == "=>" {
3136 continue;
3137 }
3138 if let Some(candidate) = normalize_constant_name(arg)
3139 && iter.peek().map(|s| s.as_str()) == Some("=>")
3140 {
3141 push_unique(&mut names, &mut seen, candidate);
3142 }
3143 }
3144 return names;
3145 }
3146
3147 if let Some(candidate) = normalize_constant_name(first) {
3148 push_unique(&mut names, &mut seen, candidate);
3149 }
3150
3151 names
3152}
3153
3154fn extract_qw_words(input: &str) -> (Vec<String>, String) {
3155 let chars: Vec<char> = input.chars().collect();
3156 let mut i = 0;
3157 let mut words = Vec::new();
3158 let mut remainder = String::new();
3159
3160 while i < chars.len() {
3161 if chars[i] == 'q'
3162 && i + 1 < chars.len()
3163 && chars[i + 1] == 'w'
3164 && (i == 0 || !chars[i - 1].is_alphanumeric())
3165 {
3166 let mut j = i + 2;
3167 while j < chars.len() && chars[j].is_whitespace() {
3168 j += 1;
3169 }
3170 if j >= chars.len() {
3171 remainder.push(chars[i]);
3172 i += 1;
3173 continue;
3174 }
3175
3176 let open = chars[j];
3177 let (close, is_paired_delimiter) = match open {
3178 '(' => (')', true),
3179 '[' => (']', true),
3180 '{' => ('}', true),
3181 '<' => ('>', true),
3182 _ => (open, false),
3183 };
3184 if open.is_alphanumeric() || open == '_' || open == '\'' || open == '"' {
3185 remainder.push(chars[i]);
3186 i += 1;
3187 continue;
3188 }
3189
3190 let mut k = j + 1;
3191 if is_paired_delimiter {
3192 let mut depth = 1usize;
3193 while k < chars.len() && depth > 0 {
3194 if chars[k] == open {
3195 depth += 1;
3196 } else if chars[k] == close {
3197 depth -= 1;
3198 }
3199 k += 1;
3200 }
3201 if depth != 0 {
3202 remainder.extend(chars[i..].iter());
3203 break;
3204 }
3205 k -= 1;
3206 } else {
3207 while k < chars.len() && chars[k] != close {
3208 k += 1;
3209 }
3210 if k >= chars.len() {
3211 remainder.extend(chars[i..].iter());
3212 break;
3213 }
3214 }
3215
3216 let content: String = chars[j + 1..k].iter().collect();
3217 for word in content.split_whitespace() {
3218 if !word.is_empty() {
3219 words.push(word.to_string());
3220 }
3221 }
3222 i = k + 1;
3223 continue;
3224 }
3225
3226 remainder.push(chars[i]);
3227 i += 1;
3228 }
3229
3230 (words, remainder)
3231}
3232
3233#[cfg(test)]
3234mod tests {
3235 use super::*;
3236 use crate::parser::Parser;
3237 use perl_tdd_support::{must, must_some};
3238
3239 #[test]
3240 fn test_symbol_extraction() {
3241 let code = r#"
3242package Foo;
3243
3244my $x = 42;
3245our $y = "hello";
3246
3247sub bar {
3248 my $z = $x + $y;
3249 return $z;
3250}
3251"#;
3252
3253 let mut parser = Parser::new(code);
3254 let ast = must(parser.parse());
3255
3256 let extractor = SymbolExtractor::new_with_source(code);
3257 let table = extractor.extract(&ast);
3258
3259 assert!(table.symbols.contains_key("Foo"));
3261 let foo_symbols = &table.symbols["Foo"];
3262 assert_eq!(foo_symbols.len(), 1);
3263 assert_eq!(foo_symbols[0].kind, SymbolKind::Package);
3264
3265 assert!(table.symbols.contains_key("x"));
3267 assert!(table.symbols.contains_key("y"));
3268 assert!(table.symbols.contains_key("z"));
3269
3270 assert!(table.symbols.contains_key("bar"));
3272 let bar_symbols = &table.symbols["bar"];
3273 assert_eq!(bar_symbols.len(), 1);
3274 assert_eq!(bar_symbols[0].kind, SymbolKind::Subroutine);
3275 }
3276
3277 #[test]
3280 fn test_method_node_uses_symbol_kind_method() {
3281 let code = r#"
3282class MyClass {
3283 method greet {
3284 return "hello";
3285 }
3286}
3287"#;
3288 let mut parser = Parser::new(code);
3289 let ast = must(parser.parse());
3290
3291 let extractor = SymbolExtractor::new_with_source(code);
3292 let table = extractor.extract(&ast);
3293
3294 assert!(table.symbols.contains_key("greet"), "expected 'greet' in symbol table");
3295 let greet_symbols = &table.symbols["greet"];
3296 assert_eq!(greet_symbols.len(), 1);
3297 assert_eq!(
3298 greet_symbols[0].kind,
3299 SymbolKind::Method,
3300 "NodeKind::Method should produce SymbolKind::Method, not Subroutine"
3301 );
3302 assert!(
3304 greet_symbols[0].attributes.contains(&"method".to_string()),
3305 "method symbol should have 'method' attribute"
3306 );
3307 }
3308
3309 #[test]
3312 fn test_subroutine_mandatory_params_in_symbol_table() {
3313 let code = r#"
3314sub foo ($x, $y) {
3315 return $x + $y;
3316}
3317"#;
3318 let mut parser = Parser::new(code);
3319 let ast = must(parser.parse());
3320
3321 let extractor = SymbolExtractor::new_with_source(code);
3322 let table = extractor.extract(&ast);
3323
3324 assert!(
3325 table.symbols.contains_key("x"),
3326 "mandatory parameter $x should be in the symbol table"
3327 );
3328 assert!(
3329 table.symbols.contains_key("y"),
3330 "mandatory parameter $y should be in the symbol table"
3331 );
3332
3333 let x_symbols = &table.symbols["x"];
3334 assert_eq!(x_symbols.len(), 1);
3335 assert_eq!(
3336 x_symbols[0].declaration,
3337 Some("my".to_string()),
3338 "$x should be declared as 'my'"
3339 );
3340
3341 let y_symbols = &table.symbols["y"];
3342 assert_eq!(y_symbols.len(), 1);
3343 assert_eq!(
3344 y_symbols[0].declaration,
3345 Some("my".to_string()),
3346 "$y should be declared as 'my'"
3347 );
3348 }
3349
3350 #[test]
3351 fn test_subroutine_optional_param_in_symbol_table() {
3352 let code = r#"
3353sub bar ($x, $y = 0) {
3354 return $x + $y;
3355}
3356"#;
3357 let mut parser = Parser::new(code);
3358 let ast = must(parser.parse());
3359
3360 let extractor = SymbolExtractor::new_with_source(code);
3361 let table = extractor.extract(&ast);
3362
3363 assert!(
3364 table.symbols.contains_key("x"),
3365 "mandatory parameter $x should be in the symbol table"
3366 );
3367 assert!(
3368 table.symbols.contains_key("y"),
3369 "optional parameter $y should be in the symbol table"
3370 );
3371 assert_eq!(
3372 table.symbols["y"][0].declaration,
3373 Some("my".to_string()),
3374 "optional parameter $y should be declared as 'my'"
3375 );
3376 }
3377
3378 #[test]
3379 fn test_subroutine_slurpy_param_in_symbol_table() {
3380 let code = r#"
3381sub baz ($x, @rest) {
3382 return scalar @rest;
3383}
3384"#;
3385 let mut parser = Parser::new(code);
3386 let ast = must(parser.parse());
3387
3388 let extractor = SymbolExtractor::new_with_source(code);
3389 let table = extractor.extract(&ast);
3390
3391 assert!(
3392 table.symbols.contains_key("x"),
3393 "mandatory parameter $x should be in the symbol table"
3394 );
3395 assert!(
3396 table.symbols.contains_key("rest"),
3397 "slurpy parameter @rest should be in the symbol table"
3398 );
3399 assert_eq!(
3400 table.symbols["rest"][0].declaration,
3401 Some("my".to_string()),
3402 "slurpy parameter @rest should be declared as 'my'"
3403 );
3404 }
3405
3406 #[test]
3407 fn test_method_signature_params_in_symbol_table() {
3408 let code = r#"
3409class Foo {
3410 method greet ($name) {
3411 return $name;
3412 }
3413}
3414"#;
3415 let mut parser = Parser::new(code);
3416 let ast = must(parser.parse());
3417
3418 let extractor = SymbolExtractor::new_with_source(code);
3419 let table = extractor.extract(&ast);
3420
3421 assert!(
3422 table.symbols.contains_key("name"),
3423 "method signature parameter $name should be in the symbol table"
3424 );
3425 assert_eq!(
3426 table.symbols["name"][0].declaration,
3427 Some("my".to_string()),
3428 "method parameter $name should be declared as 'my'"
3429 );
3430 }
3431
3432 #[test]
3433 fn test_empty_signature_no_crash() {
3434 let code = r#"
3437sub foo () {
3438 return 1;
3439}
3440"#;
3441 let mut parser = Parser::new(code);
3442 let ast = must(parser.parse());
3443
3444 let extractor = SymbolExtractor::new_with_source(code);
3445 let table = extractor.extract(&ast);
3446
3447 assert!(table.symbols.contains_key("foo"), "sub foo should be in the symbol table");
3449 assert_eq!(
3451 table.symbols.len(),
3452 1,
3453 "only 'foo' should be in the symbol table for an empty-signature sub"
3454 );
3455 }
3456
3457 #[test]
3458 fn test_hash_slurpy_param_in_symbol_table() {
3459 let code = r#"
3461sub configure ($x, %opts) {
3462 return $opts{key};
3463}
3464"#;
3465 let mut parser = Parser::new(code);
3466 let ast = must(parser.parse());
3467
3468 let extractor = SymbolExtractor::new_with_source(code);
3469 let table = extractor.extract(&ast);
3470
3471 assert!(
3472 table.symbols.contains_key("opts"),
3473 "hash slurpy parameter %opts should be in the symbol table"
3474 );
3475 assert_eq!(
3476 table.symbols["opts"][0].declaration,
3477 Some("my".to_string()),
3478 "hash slurpy parameter %opts should be declared as 'my'"
3479 );
3480 }
3481
3482 #[test]
3483 fn test_optional_param_location_is_variable_span() {
3484 let code = "sub bar ($x, $y = 0) { $x + $y }";
3488 let mut parser = Parser::new(code);
3489 let ast = must(parser.parse());
3490
3491 let extractor = SymbolExtractor::new_with_source(code);
3492 let table = extractor.extract(&ast);
3493
3494 let y_sym = &table.symbols["y"][0];
3497 let span_len = y_sym.location.end - y_sym.location.start;
3498 assert_eq!(
3500 span_len, 2,
3501 "symbol location should cover just '$y' (2 chars), not the full '$y = 0' (6 chars)"
3502 );
3503 }
3504
3505 #[test]
3506 fn test_goto_label_creates_label_reference() {
3507 let code = r#"
3508sub run {
3509 goto FINISH;
3510FINISH:
3511 return 1;
3512}
3513"#;
3514 let mut parser = Parser::new(code);
3515 let ast = must(parser.parse());
3516
3517 let extractor = SymbolExtractor::new_with_source(code);
3518 let table = extractor.extract(&ast);
3519 let references = must_some(table.references.get("FINISH"));
3520
3521 assert!(
3522 references.iter().any(|reference| reference.kind == SymbolKind::Label),
3523 "goto FINISH should produce a label reference"
3524 );
3525 }
3526
3527 #[test]
3528 fn test_goto_ampersand_creates_subroutine_reference() {
3529 let code = r#"
3530sub target { return 42; }
3531sub jump {
3532 goto ⌖
3533}
3534"#;
3535 let mut parser = Parser::new(code);
3536 let ast = must(parser.parse());
3537
3538 let extractor = SymbolExtractor::new_with_source(code);
3539 let table = extractor.extract(&ast);
3540 let references = must_some(table.references.get("target"));
3541
3542 assert!(
3543 references.iter().any(|reference| reference.kind == SymbolKind::Subroutine),
3544 "goto &target should produce a subroutine reference"
3545 );
3546 }
3547}