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