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