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