1use crate::SourceLocation;
28use crate::ast::{Node, NodeKind};
29use regex::Regex;
30use std::collections::{HashMap, HashSet};
31use std::sync::OnceLock;
32
33pub use perl_symbol_types::{SymbolKind, VarKind};
36
37#[derive(Debug, Clone)]
38pub struct Symbol {
55 pub name: String,
57 pub qualified_name: String,
59 pub kind: SymbolKind,
61 pub location: SourceLocation,
63 pub scope_id: ScopeId,
65 pub declaration: Option<String>,
67 pub documentation: Option<String>,
69 pub attributes: Vec<String>,
71}
72
73#[derive(Debug, Clone)]
74pub struct SymbolReference {
92 pub name: String,
94 pub kind: SymbolKind,
96 pub location: SourceLocation,
98 pub scope_id: ScopeId,
100 pub is_write: bool,
102}
103
104pub type ScopeId = usize;
106
107#[derive(Debug, Clone)]
108pub struct Scope {
127 pub id: ScopeId,
129 pub parent: Option<ScopeId>,
131 pub kind: ScopeKind,
133 pub location: SourceLocation,
135 pub symbols: HashSet<String>,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum ScopeKind {
154 Global,
156 Package,
158 Subroutine,
160 Block,
162 Eval,
164}
165
166#[derive(Debug, Default)]
167pub struct SymbolTable {
193 pub symbols: HashMap<String, Vec<Symbol>>,
195 pub references: HashMap<String, Vec<SymbolReference>>,
197 pub scopes: HashMap<ScopeId, Scope>,
199 scope_stack: Vec<ScopeId>,
201 next_scope_id: ScopeId,
203 current_package: String,
205}
206
207impl SymbolTable {
208 pub fn new() -> Self {
210 let mut table = SymbolTable {
211 symbols: HashMap::new(),
212 references: HashMap::new(),
213 scopes: HashMap::new(),
214 scope_stack: vec![0],
215 next_scope_id: 1,
216 current_package: "main".to_string(),
217 };
218
219 table.scopes.insert(
221 0,
222 Scope {
223 id: 0,
224 parent: None,
225 kind: ScopeKind::Global,
226 location: SourceLocation { start: 0, end: 0 },
227 symbols: HashSet::new(),
228 },
229 );
230
231 table
232 }
233
234 fn current_scope(&self) -> ScopeId {
236 *self.scope_stack.last().unwrap_or(&0)
237 }
238
239 fn push_scope(&mut self, kind: ScopeKind, location: SourceLocation) -> ScopeId {
241 let parent = self.current_scope();
242 let scope_id = self.next_scope_id;
243 self.next_scope_id += 1;
244
245 let scope =
246 Scope { id: scope_id, parent: Some(parent), kind, location, symbols: HashSet::new() };
247
248 self.scopes.insert(scope_id, scope);
249 self.scope_stack.push(scope_id);
250 scope_id
251 }
252
253 fn pop_scope(&mut self) {
255 self.scope_stack.pop();
256 }
257
258 fn add_symbol(&mut self, symbol: Symbol) {
260 let name = symbol.name.clone();
261 if let Some(scope) = self.scopes.get_mut(&symbol.scope_id) {
262 scope.symbols.insert(name.clone());
263 }
264 self.symbols.entry(name).or_default().push(symbol);
265 }
266
267 fn add_reference(&mut self, reference: SymbolReference) {
269 let name = reference.name.clone();
270 self.references.entry(name).or_default().push(reference);
271 }
272
273 pub fn find_symbol(&self, name: &str, from_scope: ScopeId, kind: SymbolKind) -> Vec<&Symbol> {
275 let mut results = Vec::new();
276 let mut current_scope_id = Some(from_scope);
277
278 while let Some(scope_id) = current_scope_id {
280 if let Some(scope) = self.scopes.get(&scope_id) {
281 if scope.symbols.contains(name) {
283 if let Some(symbols) = self.symbols.get(name) {
284 for symbol in symbols {
285 if symbol.scope_id == scope_id && symbol.kind == kind {
286 results.push(symbol);
287 }
288 }
289 }
290 }
291
292 if scope.kind != ScopeKind::Package {
294 if let Some(symbols) = self.symbols.get(name) {
295 for symbol in symbols {
296 if symbol.declaration.as_deref() == Some("our") && symbol.kind == kind {
297 results.push(symbol);
298 }
299 }
300 }
301 }
302
303 current_scope_id = scope.parent;
304 } else {
305 break;
306 }
307 }
308
309 results
310 }
311
312 pub fn find_references(&self, symbol: &Symbol) -> Vec<&SymbolReference> {
314 self.references
315 .get(&symbol.name)
316 .map(|refs| refs.iter().filter(|r| r.kind == symbol.kind).collect())
317 .unwrap_or_default()
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum FrameworkKind {
324 Moo,
326 MooRole,
328 Moose,
330 MooseRole,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum WebFrameworkKind {
337 Dancer2,
339 MojoliciousLite,
341}
342
343#[derive(Debug, Clone, Default)]
344pub struct FrameworkFlags {
346 pub moo: bool,
348 pub class_accessor: bool,
350 pub kind: Option<FrameworkKind>,
352 pub web_framework: Option<WebFrameworkKind>,
354}
355
356pub struct SymbolExtractor {
358 table: SymbolTable,
359 source: String,
361 framework_flags: HashMap<String, FrameworkFlags>,
363}
364
365impl Default for SymbolExtractor {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371impl SymbolExtractor {
372 pub fn new() -> Self {
376 SymbolExtractor {
377 table: SymbolTable::new(),
378 source: String::new(),
379 framework_flags: HashMap::new(),
380 }
381 }
382
383 pub fn new_with_source(source: &str) -> Self {
387 SymbolExtractor {
388 table: SymbolTable::new(),
389 source: source.to_string(),
390 framework_flags: HashMap::new(),
391 }
392 }
393
394 pub fn extract(mut self, node: &Node) -> SymbolTable {
396 self.visit_node(node);
397 self.upgrade_package_symbols_from_framework_flags();
398 self.table
399 }
400
401 fn upgrade_package_symbols_from_framework_flags(&mut self) {
404 for (pkg_name, flags) in &self.framework_flags {
405 let Some(kind) = flags.kind else {
406 continue;
407 };
408 let new_kind = match kind {
409 FrameworkKind::Moo | FrameworkKind::Moose => SymbolKind::Class,
410 FrameworkKind::MooRole | FrameworkKind::MooseRole => SymbolKind::Role,
411 };
412 if let Some(symbols) = self.table.symbols.get_mut(pkg_name) {
413 for symbol in symbols.iter_mut() {
414 if symbol.kind == SymbolKind::Package {
415 symbol.kind = new_kind;
416 }
417 }
418 }
419 }
420 }
421
422 fn visit_node(&mut self, node: &Node) {
424 match &node.kind {
425 NodeKind::Program { statements } => {
426 self.visit_statement_list(statements);
427 }
428
429 NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } => {
430 let doc = self.extract_leading_comment(node.location.start);
431 self.handle_variable_declaration(
432 declarator,
433 variable,
434 attributes,
435 variable.location,
436 doc,
437 );
438 if let Some(init) = initializer {
439 self.visit_node(init);
440 }
441 }
442
443 NodeKind::VariableListDeclaration {
444 declarator,
445 variables,
446 attributes,
447 initializer,
448 } => {
449 let doc = self.extract_leading_comment(node.location.start);
450 for var in variables {
451 self.handle_variable_declaration(
452 declarator,
453 var,
454 attributes,
455 var.location,
456 doc.clone(),
457 );
458 }
459 if let Some(init) = initializer {
460 self.visit_node(init);
461 }
462 }
463
464 NodeKind::Variable { sigil, name } => {
465 let kind = match sigil.as_str() {
466 "$" => SymbolKind::scalar(),
467 "@" => SymbolKind::array(),
468 "%" => SymbolKind::hash(),
469 _ => return,
470 };
471
472 let reference = SymbolReference {
473 name: name.clone(),
474 kind,
475 location: node.location,
476 scope_id: self.table.current_scope(),
477 is_write: false, };
479
480 self.table.add_reference(reference);
481 }
482
483 NodeKind::Subroutine {
484 name,
485 prototype: _,
486 signature: _,
487 attributes,
488 body,
489 name_span: _,
490 } => {
491 let sub_name =
492 name.as_ref().map(|n| n.to_string()).unwrap_or_else(|| "<anon>".to_string());
493
494 if name.is_some() {
495 let documentation = self.extract_leading_comment(node.location.start);
496 let symbol = Symbol {
497 name: sub_name.clone(),
498 qualified_name: format!("{}::{}", self.table.current_package, sub_name),
499 kind: SymbolKind::Subroutine,
500 location: node.location,
501 scope_id: self.table.current_scope(),
502 declaration: None,
503 documentation,
504 attributes: attributes.clone(),
505 };
506
507 self.table.add_symbol(symbol);
508 }
509
510 self.table.push_scope(ScopeKind::Subroutine, node.location);
512
513 {
514 self.visit_node(body);
515 }
516
517 self.table.pop_scope();
518 }
519
520 NodeKind::Package { name, block, name_span: _ } => {
521 let old_package = self.table.current_package.clone();
522 self.table.current_package = name.clone();
523
524 let documentation = self.extract_package_documentation(name, node.location);
525 let symbol = Symbol {
526 name: name.clone(),
527 qualified_name: name.clone(),
528 kind: SymbolKind::Package,
529 location: node.location,
530 scope_id: self.table.current_scope(),
531 declaration: None,
532 documentation,
533 attributes: vec![],
534 };
535
536 self.table.add_symbol(symbol);
537
538 if let Some(block_node) = block {
539 self.table.push_scope(ScopeKind::Package, node.location);
541 self.visit_node(block_node);
542 self.table.pop_scope();
543 self.table.current_package = old_package;
544 }
545 }
548
549 NodeKind::Block { statements } => {
550 self.table.push_scope(ScopeKind::Block, node.location);
551 self.visit_statement_list(statements);
552 self.table.pop_scope();
553 }
554
555 NodeKind::If { condition, then_branch, elsif_branches: _, else_branch } => {
556 self.visit_node(condition);
557
558 self.table.push_scope(ScopeKind::Block, then_branch.location);
559 self.visit_node(then_branch);
560 self.table.pop_scope();
561
562 if let Some(else_node) = else_branch {
563 self.table.push_scope(ScopeKind::Block, else_node.location);
564 self.visit_node(else_node);
565 self.table.pop_scope();
566 }
567 }
568
569 NodeKind::While { condition, body, continue_block: _ } => {
570 self.visit_node(condition);
571
572 self.table.push_scope(ScopeKind::Block, body.location);
573 self.visit_node(body);
574 self.table.pop_scope();
575 }
576
577 NodeKind::For { init, condition, update, body, .. } => {
578 self.table.push_scope(ScopeKind::Block, node.location);
579
580 if let Some(init_node) = init {
581 self.visit_node(init_node);
582 }
583 if let Some(cond_node) = condition {
584 self.visit_node(cond_node);
585 }
586 if let Some(update_node) = update {
587 self.visit_node(update_node);
588 }
589 self.visit_node(body);
590
591 self.table.pop_scope();
592 }
593
594 NodeKind::Foreach { variable, list, body, continue_block: _ } => {
595 self.table.push_scope(ScopeKind::Block, node.location);
596
597 self.handle_variable_declaration("my", variable, &[], variable.location, None);
599 self.visit_node(list);
600 self.visit_node(body);
601
602 self.table.pop_scope();
603 }
604
605 NodeKind::Assignment { lhs, rhs, .. } => {
607 self.mark_write_reference(lhs);
609 self.visit_node(lhs);
610 self.visit_node(rhs);
611 }
612
613 NodeKind::Binary { left, right, .. } => {
614 self.visit_node(left);
615 self.visit_node(right);
616 }
617
618 NodeKind::Unary { operand, .. } => {
619 self.visit_node(operand);
620 }
621
622 NodeKind::FunctionCall { name, args } => {
623 let reference = SymbolReference {
625 name: name.clone(),
626 kind: SymbolKind::Subroutine,
627 location: node.location,
628 scope_id: self.table.current_scope(),
629 is_write: false,
630 };
631 self.table.add_reference(reference);
632
633 for arg in args {
634 self.visit_node(arg);
635 }
636 }
637
638 NodeKind::MethodCall { object, method, args } => {
639 let location = self.method_reference_location(node, object, method);
642 self.table.add_reference(SymbolReference {
643 name: method.clone(),
644 kind: SymbolKind::Subroutine,
645 location,
646 scope_id: self.table.current_scope(),
647 is_write: false,
648 });
649
650 self.visit_node(object);
651 for arg in args {
652 self.visit_node(arg);
653 }
654 }
655
656 NodeKind::ArrayLiteral { elements } => {
658 for elem in elements {
659 self.visit_node(elem);
660 }
661 }
662
663 NodeKind::HashLiteral { pairs } => {
664 for (key, value) in pairs {
665 self.visit_node(key);
666 self.visit_node(value);
667 }
668 }
669
670 NodeKind::Ternary { condition, then_expr, else_expr } => {
671 self.visit_node(condition);
672 self.visit_node(then_expr);
673 self.visit_node(else_expr);
674 }
675
676 NodeKind::LabeledStatement { label, statement } => {
677 let symbol = Symbol {
678 name: label.clone(),
679 qualified_name: label.clone(),
680 kind: SymbolKind::Label,
681 location: node.location,
682 scope_id: self.table.current_scope(),
683 declaration: None,
684 documentation: None,
685 attributes: vec![],
686 };
687
688 self.table.add_symbol(symbol);
689
690 {
691 self.visit_node(statement);
692 }
693 }
694
695 NodeKind::String { value, interpolated } => {
697 if *interpolated {
698 self.extract_vars_from_string(value, node.location);
700 }
701 }
702
703 NodeKind::Use { module, args, .. } => {
704 self.update_framework_context(module, args);
705 }
706
707 NodeKind::No { module: _, args: _, .. } => {
708 }
710
711 NodeKind::PhaseBlock { phase: _, phase_span: _, block } => {
712 self.visit_node(block);
714 }
715
716 NodeKind::StatementModifier { statement, modifier: _, condition } => {
717 self.visit_node(statement);
718 self.visit_node(condition);
719 }
720
721 NodeKind::Do { block } | NodeKind::Eval { block } => {
722 self.visit_node(block);
723 }
724
725 NodeKind::Try { body, catch_blocks, finally_block } => {
726 self.visit_node(body);
727 for (_, catch_block) in catch_blocks {
728 self.visit_node(catch_block);
729 }
730 if let Some(finally) = finally_block {
731 self.visit_node(finally);
732 }
733 }
734
735 NodeKind::Given { expr, body } => {
736 self.visit_node(expr);
737 self.visit_node(body);
738 }
739
740 NodeKind::When { condition, body } => {
741 self.visit_node(condition);
742 self.visit_node(body);
743 }
744
745 NodeKind::Default { body } => {
746 self.visit_node(body);
747 }
748
749 NodeKind::Class { name, body } => {
750 let documentation = self.extract_leading_comment(node.location.start);
751 let symbol = Symbol {
752 name: name.clone(),
753 qualified_name: name.clone(),
754 kind: SymbolKind::Package, location: node.location,
756 scope_id: self.table.current_scope(),
757 declaration: None,
758 documentation,
759 attributes: vec![],
760 };
761 self.table.add_symbol(symbol);
762
763 self.table.push_scope(ScopeKind::Package, node.location);
764 self.visit_node(body);
765 self.table.pop_scope();
766 }
767
768 NodeKind::Method { name, signature: _, attributes, body } => {
769 let documentation = self.extract_leading_comment(node.location.start);
770 let mut symbol_attributes = Vec::with_capacity(attributes.len() + 1);
771 symbol_attributes.push("method".to_string());
772 symbol_attributes.extend(attributes.iter().cloned());
773 let symbol = Symbol {
774 name: name.clone(),
775 qualified_name: format!("{}::{}", self.table.current_package, name),
776 kind: SymbolKind::Method,
777 location: node.location,
778 scope_id: self.table.current_scope(),
779 declaration: None,
780 documentation,
781 attributes: symbol_attributes,
782 };
783 self.table.add_symbol(symbol);
784
785 self.table.push_scope(ScopeKind::Subroutine, node.location);
786 self.visit_node(body);
787 self.table.pop_scope();
788 }
789
790 NodeKind::Format { name, body: _ } => {
791 let symbol = Symbol {
792 name: name.clone(),
793 qualified_name: format!("{}::{}", self.table.current_package, name),
794 kind: SymbolKind::Format,
795 location: node.location,
796 scope_id: self.table.current_scope(),
797 declaration: None,
798 documentation: None,
799 attributes: vec![],
800 };
801 self.table.add_symbol(symbol);
802 }
803
804 NodeKind::Return { value } => {
805 if let Some(val) = value {
806 self.visit_node(val);
807 }
808 }
809
810 NodeKind::Tie { variable, package, args } => {
811 self.visit_node(variable);
812 self.visit_node(package);
813 for arg in args {
814 self.visit_node(arg);
815 }
816 }
817
818 NodeKind::Untie { variable } | NodeKind::Goto { target: variable } => {
819 self.visit_node(variable);
820 }
821
822 NodeKind::Regex { .. } => {}
824 NodeKind::Match { expr, .. } => {
825 self.visit_node(expr);
826 }
827 NodeKind::Substitution { expr, .. } => {
828 self.visit_node(expr);
829 }
830 NodeKind::Transliteration { expr, .. } => {
831 self.visit_node(expr);
832 }
833
834 NodeKind::IndirectCall { method, object, args } => {
835 self.table.add_reference(SymbolReference {
836 name: method.clone(),
837 kind: SymbolKind::Subroutine,
838 location: node.location,
839 scope_id: self.table.current_scope(),
840 is_write: false,
841 });
842
843 self.visit_node(object);
844 for arg in args {
845 self.visit_node(arg);
846 }
847 }
848
849 NodeKind::ExpressionStatement { expression } => {
850 self.visit_node(expression);
852 }
853
854 NodeKind::Number { .. }
856 | NodeKind::Heredoc { .. }
857 | NodeKind::Undef
858 | NodeKind::Diamond
859 | NodeKind::Ellipsis
860 | NodeKind::Glob { .. }
861 | NodeKind::Readline { .. }
862 | NodeKind::Identifier { .. }
863 | NodeKind::Typeglob { .. }
864 | NodeKind::DataSection { .. }
865 | NodeKind::LoopControl { .. }
866 | NodeKind::MissingExpression
867 | NodeKind::MissingStatement
868 | NodeKind::MissingIdentifier
869 | NodeKind::MissingBlock
870 | NodeKind::UnknownRest
871 | NodeKind::Error { .. } => {
872 }
874
875 _ => {
876 eprintln!("Warning: Unhandled node type in symbol extractor: {:?}", node.kind);
878 }
879 }
880 }
881
882 fn visit_statement_list(&mut self, statements: &[Node]) {
888 let mut idx = 0;
889 while idx < statements.len() {
890 if let Some(consumed) = self.try_extract_framework_declarations(statements, idx) {
891 idx += consumed;
892 continue;
893 }
894
895 self.visit_node(&statements[idx]);
896 idx += 1;
897 }
898 }
899
900 fn try_extract_framework_declarations(
904 &mut self,
905 statements: &[Node],
906 idx: usize,
907 ) -> Option<usize> {
908 let flags = self.framework_flags.get(&self.table.current_package).cloned();
909 let flags = flags.as_ref();
910
911 let is_moo = flags.is_some_and(|f| f.moo);
912
913 if is_moo {
914 if let Some(consumed) = self.try_extract_moo_has_declaration(statements, idx) {
915 return Some(consumed);
916 }
917 if let Some(consumed) = self.try_extract_method_modifier(statements, idx) {
918 return Some(consumed);
919 }
920 if let Some(consumed) = self.try_extract_extends_with(statements, idx) {
921 return Some(consumed);
922 }
923 if let Some(consumed) = self.try_extract_role_requires(statements, idx) {
924 return Some(consumed);
925 }
926 }
927
928 if flags.is_some_and(|f| f.class_accessor)
929 && self.try_extract_class_accessor_declaration(&statements[idx])
930 {
931 self.visit_node(&statements[idx]);
933 return Some(1);
934 }
935
936 if flags.is_some_and(|f| f.web_framework.is_some()) {
937 if let Some(consumed) = self.try_extract_web_route_declaration(statements, idx) {
938 return Some(consumed);
939 }
940 }
941
942 None
943 }
944
945 fn try_extract_moo_has_declaration(
949 &mut self,
950 statements: &[Node],
951 idx: usize,
952 ) -> Option<usize> {
953 let first = &statements[idx];
954
955 if idx + 1 < statements.len() {
962 let second = &statements[idx + 1];
963 let is_has_marker = matches!(
964 &first.kind,
965 NodeKind::ExpressionStatement { expression }
966 if matches!(&expression.kind, NodeKind::Identifier { name } if name == "has")
967 );
968
969 if is_has_marker {
970 if let NodeKind::ExpressionStatement { expression } = &second.kind {
971 let has_location =
972 SourceLocation { start: first.location.start, end: second.location.end };
973
974 match &expression.kind {
975 NodeKind::HashLiteral { pairs } => {
976 self.synthesize_moo_has_pairs(pairs, has_location, false);
977 self.visit_node(second);
978 return Some(2);
979 }
980 NodeKind::ArrayLiteral { elements } => {
981 if let Some(Node { kind: NodeKind::HashLiteral { pairs }, .. }) =
982 elements.last()
983 {
984 let mut names = Vec::new();
986 for el in elements.iter().take(elements.len() - 1) {
987 names.extend(Self::collect_symbol_names(el));
988 }
989 if !names.is_empty() {
990 self.synthesize_moo_has_attrs_with_options(
991 &names,
992 pairs,
993 has_location,
994 );
995 self.visit_node(second);
996 return Some(2);
997 }
998 }
999 }
1000 _ => {}
1001 }
1002 }
1003 }
1004 }
1005
1006 if let NodeKind::ExpressionStatement { expression } = &first.kind
1009 && let NodeKind::HashLiteral { pairs } = &expression.kind
1010 {
1011 let has_embedded_marker = pairs.iter().any(|(key_node, _)| {
1012 matches!(
1013 &key_node.kind,
1014 NodeKind::Binary { op, left, .. }
1015 if op == "[]" && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
1016 )
1017 });
1018
1019 if has_embedded_marker {
1020 self.synthesize_moo_has_pairs(pairs, first.location, true);
1021 self.visit_node(first);
1022 return Some(1);
1023 }
1024 }
1025
1026 if let NodeKind::ExpressionStatement { expression } = &first.kind
1029 && let NodeKind::FunctionCall { name, args } = &expression.kind
1030 && name == "has"
1031 && !args.is_empty()
1032 {
1033 let options_hash_idx =
1034 args.iter().rposition(|a| matches!(a.kind, NodeKind::HashLiteral { .. }));
1035 if let Some(opts_idx) = options_hash_idx {
1036 if let NodeKind::HashLiteral { pairs } = &args[opts_idx].kind {
1037 let names: Vec<String> =
1038 args[..opts_idx].iter().flat_map(Self::collect_symbol_names).collect();
1039 if !names.is_empty() {
1040 self.synthesize_moo_has_attrs_with_options(&names, pairs, first.location);
1041 self.visit_node(first);
1042 return Some(1);
1043 }
1044 }
1045 }
1046 }
1047
1048 None
1049 }
1050
1051 fn try_extract_method_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1059 let first = &statements[idx];
1060
1061 if let NodeKind::ExpressionStatement { expression } = &first.kind
1063 && let NodeKind::FunctionCall { name, args } = &expression.kind
1064 && matches!(name.as_str(), "around" | "before" | "after")
1065 {
1066 let modifier_name = name.as_str();
1067 let method_names: Vec<String> =
1068 args.first().map(Self::collect_symbol_names).unwrap_or_default();
1069 if !method_names.is_empty() {
1070 let scope_id = self.table.current_scope();
1071 let package = self.table.current_package.clone();
1072 for method_name in method_names {
1073 self.table.add_symbol(Symbol {
1074 name: method_name.clone(),
1075 qualified_name: format!("{package}::{method_name}"),
1076 kind: SymbolKind::Subroutine,
1077 location: first.location,
1078 scope_id,
1079 declaration: Some(modifier_name.to_string()),
1080 documentation: Some(format!(
1081 "Method modifier `{modifier_name}` for `{method_name}`"
1082 )),
1083 attributes: vec![format!("modifier={modifier_name}")],
1084 });
1085 }
1086 return Some(1);
1087 }
1088 }
1089
1090 if idx + 1 >= statements.len() {
1091 return None;
1092 }
1093
1094 let second = &statements[idx + 1];
1095
1096 let modifier_name = match &first.kind {
1098 NodeKind::ExpressionStatement { expression } => match &expression.kind {
1099 NodeKind::Identifier { name }
1100 if matches!(name.as_str(), "around" | "before" | "after") =>
1101 {
1102 name.as_str()
1103 }
1104 _ => return None,
1105 },
1106 _ => return None,
1107 };
1108
1109 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1111 return None;
1112 };
1113 let NodeKind::HashLiteral { pairs } = &expression.kind else {
1114 return None;
1115 };
1116
1117 let modifier_location =
1118 SourceLocation { start: first.location.start, end: second.location.end };
1119 let scope_id = self.table.current_scope();
1120 let package = self.table.current_package.clone();
1121
1122 for (key_node, _value_node) in pairs {
1123 let method_names = Self::collect_symbol_names(key_node);
1124 for method_name in method_names {
1125 self.table.add_symbol(Symbol {
1126 name: method_name.clone(),
1127 qualified_name: format!("{package}::{method_name}"),
1128 kind: SymbolKind::Subroutine,
1129 location: modifier_location,
1130 scope_id,
1131 declaration: Some(modifier_name.to_string()),
1132 documentation: Some(format!(
1133 "Method modifier `{modifier_name}` for `{method_name}`"
1134 )),
1135 attributes: vec![format!("modifier={modifier_name}")],
1136 });
1137 }
1138 }
1139
1140 self.visit_node(second);
1142
1143 Some(2)
1144 }
1145
1146 fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1154 let first = &statements[idx];
1155
1156 if let NodeKind::ExpressionStatement { expression } = &first.kind
1158 && let NodeKind::FunctionCall { name, args } = &expression.kind
1159 && matches!(name.as_str(), "extends" | "with")
1160 {
1161 let keyword = name.as_str();
1162 let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
1163 if !names.is_empty() {
1164 let ref_kind =
1165 if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
1166 for ref_name in names {
1167 self.table.add_reference(SymbolReference {
1168 name: ref_name,
1169 kind: ref_kind,
1170 location: first.location,
1171 scope_id: self.table.current_scope(),
1172 is_write: false,
1173 });
1174 }
1175 return Some(1);
1176 }
1177 }
1178
1179 if idx + 1 >= statements.len() {
1180 return None;
1181 }
1182
1183 let second = &statements[idx + 1];
1184
1185 let keyword = match &first.kind {
1187 NodeKind::ExpressionStatement { expression } => match &expression.kind {
1188 NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
1189 name.as_str()
1190 }
1191 _ => return None,
1192 },
1193 _ => return None,
1194 };
1195
1196 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1198 return None;
1199 };
1200
1201 let names = Self::collect_symbol_names(expression);
1202 if names.is_empty() {
1203 return None;
1204 }
1205
1206 let ref_location = SourceLocation { start: first.location.start, end: second.location.end };
1207
1208 let ref_kind = if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
1209
1210 for name in names {
1211 self.table.add_reference(SymbolReference {
1212 name,
1213 kind: ref_kind,
1214 location: ref_location,
1215 scope_id: self.table.current_scope(),
1216 is_write: false,
1217 });
1218 }
1219
1220 Some(2)
1221 }
1222
1223 fn try_extract_role_requires(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1230 let first = &statements[idx];
1231
1232 if let NodeKind::ExpressionStatement { expression } = &first.kind
1234 && let NodeKind::FunctionCall { name, args } = &expression.kind
1235 && name == "requires"
1236 {
1237 let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
1238 if !names.is_empty() {
1239 let scope_id = self.table.current_scope();
1240 let package = self.table.current_package.clone();
1241 for method_name in names {
1242 self.table.add_symbol(Symbol {
1243 name: method_name.clone(),
1244 qualified_name: format!("{package}::{method_name}"),
1245 kind: SymbolKind::Subroutine,
1246 location: first.location,
1247 scope_id,
1248 declaration: Some("requires".to_string()),
1249 documentation: Some(format!("Required method `{method_name}` from role")),
1250 attributes: vec!["requires=true".to_string()],
1251 });
1252 }
1253 return Some(1);
1254 }
1255 }
1256
1257 if idx + 1 >= statements.len() {
1258 return None;
1259 }
1260
1261 let second = &statements[idx + 1];
1262
1263 let is_requires = match &first.kind {
1265 NodeKind::ExpressionStatement { expression } => {
1266 matches!(&expression.kind, NodeKind::Identifier { name } if name == "requires")
1267 }
1268 _ => false,
1269 };
1270
1271 if !is_requires {
1272 return None;
1273 }
1274
1275 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1276 return None;
1277 };
1278
1279 let names = Self::collect_symbol_names(expression);
1280 if names.is_empty() {
1281 return None;
1282 }
1283
1284 let location = SourceLocation { start: first.location.start, end: second.location.end };
1285 let scope_id = self.table.current_scope();
1286 let package = self.table.current_package.clone();
1287
1288 for name in names {
1289 self.table.add_symbol(Symbol {
1290 name: name.clone(),
1291 qualified_name: format!("{package}::{name}"),
1292 kind: SymbolKind::Subroutine,
1293 location,
1294 scope_id,
1295 declaration: Some("requires".to_string()),
1296 documentation: Some(format!("Required method `{name}` from role")),
1297 attributes: vec!["requires=true".to_string()],
1298 });
1299 }
1300
1301 Some(2)
1302 }
1303
1304 fn synthesize_moo_has_pairs(
1306 &mut self,
1307 pairs: &[(Node, Node)],
1308 has_location: SourceLocation,
1309 require_embedded_marker: bool,
1310 ) {
1311 for (attr_expr, options_expr) in pairs {
1312 let Some(attr_expr) = Self::moo_attribute_expr(attr_expr, require_embedded_marker)
1313 else {
1314 continue;
1315 };
1316
1317 let attribute_names = Self::collect_symbol_names(attr_expr);
1318 if attribute_names.is_empty() {
1319 continue;
1320 }
1321
1322 if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
1323 self.synthesize_moo_has_attrs_with_options(
1324 &attribute_names,
1325 option_pairs,
1326 has_location,
1327 );
1328 }
1329 }
1330 }
1331
1332 fn synthesize_moo_has_attrs_with_options(
1334 &mut self,
1335 attribute_names: &[String],
1336 option_pairs: &[(Node, Node)],
1337 has_location: SourceLocation,
1338 ) {
1339 let scope_id = self.table.current_scope();
1340 let package = self.table.current_package.clone();
1341
1342 let options_expr = Node {
1345 kind: NodeKind::HashLiteral { pairs: option_pairs.to_vec() },
1346 location: has_location,
1347 };
1348
1349 let option_map = Self::extract_hash_options(&options_expr);
1350 let metadata = Self::attribute_metadata(&option_map);
1351 let generated_methods =
1352 Self::moo_accessor_names(attribute_names, &option_map, &options_expr);
1353
1354 for attribute_name in attribute_names {
1355 self.table.add_symbol(Symbol {
1356 name: attribute_name.clone(),
1357 qualified_name: format!("{package}::{attribute_name}"),
1358 kind: SymbolKind::scalar(),
1359 location: has_location,
1360 scope_id,
1361 declaration: Some("has".to_string()),
1362 documentation: Some(format!("Moo/Moose attribute `{attribute_name}`")),
1363 attributes: metadata.clone(),
1364 });
1365 }
1366
1367 let accessor_doc = Self::moo_accessor_doc(&option_map);
1369
1370 for method_name in generated_methods {
1371 self.table.add_symbol(Symbol {
1372 name: method_name.clone(),
1373 qualified_name: format!("{package}::{method_name}"),
1374 kind: SymbolKind::Subroutine,
1375 location: has_location,
1376 scope_id,
1377 declaration: Some("has".to_string()),
1378 documentation: Some(accessor_doc.clone()),
1379 attributes: metadata.clone(),
1380 });
1381 }
1382 }
1383
1384 fn moo_attribute_expr(attr_expr: &Node, require_embedded_marker: bool) -> Option<&Node> {
1386 if let NodeKind::Binary { op, left, right } = &attr_expr.kind
1387 && op == "[]"
1388 && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
1389 {
1390 return Some(right.as_ref());
1391 }
1392
1393 if require_embedded_marker { None } else { Some(attr_expr) }
1394 }
1395
1396 fn try_extract_web_route_declaration(
1405 &mut self,
1406 statements: &[Node],
1407 idx: usize,
1408 ) -> Option<usize> {
1409 let first = &statements[idx];
1410
1411 if let NodeKind::ExpressionStatement { expression } = &first.kind
1413 && let NodeKind::FunctionCall { name, args } = &expression.kind
1414 && matches!(name.as_str(), "get" | "post" | "put" | "del" | "delete" | "patch" | "any")
1415 {
1416 let method_name = name.as_str();
1417 if let Some(path_node) = args.first() {
1419 if let NodeKind::String { value, .. } = &path_node.kind {
1420 if let Some(path) = Self::normalize_symbol_name(value) {
1421 let http_method = match method_name {
1422 "get" => "GET",
1423 "post" => "POST",
1424 "put" => "PUT",
1425 "del" | "delete" => "DELETE",
1426 "patch" => "PATCH",
1427 "any" => "ANY",
1428 _ => method_name,
1429 };
1430 let scope_id = self.table.current_scope();
1431 self.table.add_symbol(Symbol {
1432 name: path.clone(),
1433 qualified_name: path.clone(),
1434 kind: SymbolKind::Subroutine,
1435 location: first.location,
1436 scope_id,
1437 declaration: Some(method_name.to_string()),
1438 documentation: Some(format!("{http_method} {path}")),
1439 attributes: vec![format!("http_method={http_method}")],
1440 });
1441 self.visit_node(first);
1442 return Some(1);
1443 }
1444 }
1445 }
1446 }
1447
1448 if idx + 1 >= statements.len() {
1449 return None;
1450 }
1451
1452 let second = &statements[idx + 1];
1453
1454 let method_name = match &first.kind {
1456 NodeKind::ExpressionStatement { expression } => match &expression.kind {
1457 NodeKind::Identifier { name }
1458 if matches!(
1459 name.as_str(),
1460 "get" | "post" | "put" | "del" | "delete" | "patch" | "any"
1461 ) =>
1462 {
1463 name.as_str()
1464 }
1465 _ => return None,
1466 },
1467 _ => return None,
1468 };
1469
1470 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1472 return None;
1473 };
1474 let NodeKind::HashLiteral { pairs } = &expression.kind else {
1475 return None;
1476 };
1477
1478 let (path_node, _handler_node) = pairs.first()?;
1480 let path = match &path_node.kind {
1481 NodeKind::String { value, .. } => Self::normalize_symbol_name(value)?,
1482 _ => return None,
1483 };
1484
1485 let http_method = match method_name {
1486 "get" => "GET",
1487 "post" => "POST",
1488 "put" => "PUT",
1489 "del" | "delete" => "DELETE",
1490 "patch" => "PATCH",
1491 "any" => "ANY",
1492 _ => method_name,
1493 };
1494
1495 let route_location =
1496 SourceLocation { start: first.location.start, end: second.location.end };
1497 let scope_id = self.table.current_scope();
1498
1499 self.table.add_symbol(Symbol {
1500 name: path.clone(),
1501 qualified_name: path.clone(),
1502 kind: SymbolKind::Subroutine,
1503 location: route_location,
1504 scope_id,
1505 declaration: Some(method_name.to_string()),
1506 documentation: Some(format!("{http_method} {path}")),
1507 attributes: vec![format!("http_method={http_method}")],
1508 });
1509
1510 self.visit_node(second);
1512
1513 Some(2)
1514 }
1515
1516 fn try_extract_class_accessor_declaration(&mut self, statement: &Node) -> bool {
1518 let NodeKind::ExpressionStatement { expression } = &statement.kind else {
1519 return false;
1520 };
1521
1522 let NodeKind::MethodCall { method, args, .. } = &expression.kind else {
1523 return false;
1524 };
1525
1526 let is_accessor_generator = matches!(
1527 method.as_str(),
1528 "mk_accessors" | "mk_ro_accessors" | "mk_rw_accessors" | "mk_wo_accessors"
1529 );
1530 if !is_accessor_generator {
1531 return false;
1532 }
1533
1534 let mut accessor_names = Vec::new();
1535 for arg in args {
1536 accessor_names.extend(Self::collect_symbol_names(arg));
1537 }
1538 if accessor_names.is_empty() {
1539 return false;
1540 }
1541
1542 let mut seen = HashSet::new();
1543 let scope_id = self.table.current_scope();
1544 let package = self.table.current_package.clone();
1545
1546 for accessor_name in accessor_names {
1547 if !seen.insert(accessor_name.clone()) {
1548 continue;
1549 }
1550
1551 self.table.add_symbol(Symbol {
1552 name: accessor_name.clone(),
1553 qualified_name: format!("{package}::{accessor_name}"),
1554 kind: SymbolKind::Subroutine,
1555 location: statement.location,
1556 scope_id,
1557 declaration: Some(method.clone()),
1558 documentation: Some("Generated accessor (Class::Accessor)".to_string()),
1559 attributes: vec!["framework=Class::Accessor".to_string()],
1560 });
1561 }
1562
1563 true
1564 }
1565
1566 fn update_framework_context(&mut self, module: &str, args: &[String]) {
1568 let pkg = self.table.current_package.clone();
1569
1570 let framework_kind = match module {
1571 "Moo" | "Mouse" => Some(FrameworkKind::Moo),
1572 "Moo::Role" | "Mouse::Role" => Some(FrameworkKind::MooRole),
1573 "Moose" => Some(FrameworkKind::Moose),
1574 "Moose::Role" => Some(FrameworkKind::MooseRole),
1575 _ => None,
1576 };
1577
1578 if let Some(kind) = framework_kind {
1579 let flags = self.framework_flags.entry(pkg).or_default();
1580 flags.moo = true;
1581 flags.kind = Some(kind);
1582 return;
1583 }
1584
1585 if module == "Class::Accessor" {
1586 self.framework_flags.entry(pkg).or_default().class_accessor = true;
1587 return;
1588 }
1589
1590 let web_kind = match module {
1591 "Dancer2" | "Dancer2::Core" => Some(WebFrameworkKind::Dancer2),
1592 "Mojolicious::Lite" => Some(WebFrameworkKind::MojoliciousLite),
1593 _ => None,
1594 };
1595 if let Some(kind) = web_kind {
1596 self.framework_flags.entry(pkg).or_default().web_framework = Some(kind);
1597 return;
1598 }
1599
1600 if matches!(module, "base" | "parent") {
1601 let has_class_accessor_parent = args
1602 .iter()
1603 .filter_map(|arg| Self::normalize_symbol_name(arg))
1604 .any(|arg| arg == "Class::Accessor");
1605 if has_class_accessor_parent {
1606 self.framework_flags.entry(pkg).or_default().class_accessor = true;
1607 }
1608 }
1609 }
1610
1611 fn extract_hash_options(node: &Node) -> HashMap<String, String> {
1613 let mut options = HashMap::new();
1614 let NodeKind::HashLiteral { pairs } = &node.kind else {
1615 return options;
1616 };
1617
1618 for (key_node, value_node) in pairs {
1619 let Some(key_name) = Self::single_symbol_name(key_node) else {
1620 continue;
1621 };
1622 let value_text = Self::value_summary(value_node);
1623 options.insert(key_name, value_text);
1624 }
1625
1626 options
1627 }
1628
1629 fn attribute_metadata(option_map: &HashMap<String, String>) -> Vec<String> {
1631 let preferred_order = [
1632 "is",
1633 "isa",
1634 "required",
1635 "lazy",
1636 "builder",
1637 "default",
1638 "reader",
1639 "writer",
1640 "accessor",
1641 "predicate",
1642 "clearer",
1643 "handles",
1644 ];
1645
1646 let mut metadata = Vec::new();
1647 for key in preferred_order {
1648 if let Some(value) = option_map.get(key) {
1649 metadata.push(format!("{key}={value}"));
1650 }
1651 }
1652 metadata
1653 }
1654
1655 fn moo_accessor_doc(option_map: &HashMap<String, String>) -> String {
1664 let mut parts = Vec::new();
1665
1666 if let Some(isa) = option_map.get("isa") {
1667 parts.push(format!("isa: {isa}"));
1668 }
1669 if let Some(is) = option_map.get("is") {
1670 parts.push(is.clone());
1671 }
1672
1673 if parts.is_empty() {
1674 "Generated accessor from Moo/Moose `has`".to_string()
1675 } else {
1676 format!("Moo/Moose accessor ({})", parts.join(", "))
1677 }
1678 }
1679
1680 fn moo_accessor_names(
1682 attribute_names: &[String],
1683 option_map: &HashMap<String, String>,
1684 options_expr: &Node,
1685 ) -> Vec<String> {
1686 let mut methods = Vec::new();
1687 let mut seen = HashSet::new();
1688
1689 for key in ["accessor", "reader", "writer", "predicate", "clearer", "builder"] {
1690 for name in Self::option_method_names(options_expr, key, attribute_names) {
1691 if seen.insert(name.clone()) {
1692 methods.push(name);
1693 }
1694 }
1695 }
1696
1697 for name in Self::handles_method_names(options_expr) {
1698 if seen.insert(name.clone()) {
1699 methods.push(name);
1700 }
1701 }
1702
1703 let has_explicit_accessor = option_map.contains_key("accessor")
1705 || option_map.contains_key("reader")
1706 || option_map.contains_key("writer");
1707 if !has_explicit_accessor {
1708 for attribute_name in attribute_names {
1709 if seen.insert(attribute_name.clone()) {
1710 methods.push(attribute_name.clone());
1711 }
1712 }
1713 }
1714
1715 methods
1716 }
1717
1718 fn find_hash_option_value<'a>(options_expr: &'a Node, key: &str) -> Option<&'a Node> {
1720 let NodeKind::HashLiteral { pairs } = &options_expr.kind else {
1721 return None;
1722 };
1723
1724 for (key_node, value_node) in pairs {
1725 if Self::single_symbol_name(key_node).as_deref() == Some(key) {
1726 return Some(value_node);
1727 }
1728 }
1729
1730 None
1731 }
1732
1733 fn option_method_names(
1735 options_expr: &Node,
1736 key: &str,
1737 attribute_names: &[String],
1738 ) -> Vec<String> {
1739 let Some(value_node) = Self::find_hash_option_value(options_expr, key) else {
1740 return Vec::new();
1741 };
1742
1743 let mut names = Self::collect_symbol_names(value_node);
1744 if !names.is_empty() {
1745 names.sort();
1746 names.dedup();
1747 return names;
1748 }
1749
1750 if !Self::is_truthy_shorthand(value_node) {
1752 return Vec::new();
1753 }
1754
1755 match key {
1756 "predicate" => attribute_names.iter().map(|name| format!("has_{name}")).collect(),
1757 "clearer" => attribute_names.iter().map(|name| format!("clear_{name}")).collect(),
1758 "builder" => attribute_names.iter().map(|name| format!("_build_{name}")).collect(),
1759 _ => Vec::new(),
1760 }
1761 }
1762
1763 fn is_truthy_shorthand(node: &Node) -> bool {
1765 match &node.kind {
1766 NodeKind::Number { value } => value.trim() == "1",
1767 NodeKind::Identifier { name } => {
1768 let lower = name.trim().to_ascii_lowercase();
1769 lower == "1" || lower == "true"
1770 }
1771 NodeKind::String { value, .. } => {
1772 Self::normalize_symbol_name(value).is_some_and(|value| {
1773 let lower = value.to_ascii_lowercase();
1774 value == "1" || lower == "true"
1775 })
1776 }
1777 _ => false,
1778 }
1779 }
1780
1781 fn handles_method_names(options_expr: &Node) -> Vec<String> {
1783 let Some(handles_node) = Self::find_hash_option_value(options_expr, "handles") else {
1784 return Vec::new();
1785 };
1786
1787 let mut names = Vec::new();
1788 match &handles_node.kind {
1789 NodeKind::HashLiteral { pairs } => {
1790 for (key_node, _) in pairs {
1791 names.extend(Self::collect_symbol_names(key_node));
1792 }
1793 }
1794 _ => {
1795 names.extend(Self::collect_symbol_names(handles_node));
1796 }
1797 }
1798
1799 names.sort();
1800 names.dedup();
1801 names
1802 }
1803
1804 fn collect_symbol_names(node: &Node) -> Vec<String> {
1806 match &node.kind {
1807 NodeKind::String { value, .. } => {
1808 Self::normalize_symbol_name(value).into_iter().collect()
1809 }
1810 NodeKind::Identifier { name } => {
1811 Self::normalize_symbol_name(name).into_iter().collect()
1812 }
1813 NodeKind::ArrayLiteral { elements } => {
1814 let mut names = Vec::new();
1815 for element in elements {
1816 names.extend(Self::collect_symbol_names(element));
1817 }
1818 names
1819 }
1820 _ => Vec::new(),
1821 }
1822 }
1823
1824 fn single_symbol_name(node: &Node) -> Option<String> {
1826 Self::collect_symbol_names(node).into_iter().next()
1827 }
1828
1829 fn normalize_symbol_name(raw: &str) -> Option<String> {
1831 let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
1832 if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
1833 }
1834
1835 fn value_summary(node: &Node) -> String {
1837 match &node.kind {
1838 NodeKind::String { value, .. } => {
1839 Self::normalize_symbol_name(value).unwrap_or_else(|| value.clone())
1840 }
1841 NodeKind::Identifier { name } => name.clone(),
1842 NodeKind::Number { value } => value.clone(),
1843 NodeKind::ArrayLiteral { elements } => {
1844 let mut entries = Vec::new();
1845 for element in elements {
1846 entries.extend(Self::collect_symbol_names(element));
1847 }
1848 entries.sort();
1849 entries.dedup();
1850 if entries.is_empty() {
1851 "array".to_string()
1852 } else {
1853 format!("[{}]", entries.join(","))
1854 }
1855 }
1856 NodeKind::HashLiteral { pairs } => {
1857 let mut entries = Vec::new();
1858 for (key_node, value_node) in pairs {
1859 let Some(key_name) = Self::single_symbol_name(key_node) else {
1860 continue;
1861 };
1862 if let Some(value_name) = Self::single_symbol_name(value_node) {
1863 entries.push(format!("{key_name}->{value_name}"));
1864 } else {
1865 entries.push(key_name);
1866 }
1867 }
1868 entries.sort();
1869 entries.dedup();
1870 if entries.is_empty() {
1871 "hash".to_string()
1872 } else {
1873 format!("{{{}}}", entries.join(","))
1874 }
1875 }
1876 NodeKind::Undef => "undef".to_string(),
1877 _ => "expr".to_string(),
1878 }
1879 }
1880
1881 fn method_reference_location(
1886 &self,
1887 call_node: &Node,
1888 object: &Node,
1889 method_name: &str,
1890 ) -> SourceLocation {
1891 if self.source.is_empty() {
1892 return call_node.location;
1893 }
1894
1895 let search_start = object.location.end.min(self.source.len());
1896 let search_end = search_start.saturating_add(160).min(self.source.len());
1897 if search_start >= search_end || !self.source.is_char_boundary(search_start) {
1898 return call_node.location;
1899 }
1900
1901 let window = &self.source[search_start..search_end];
1902 let Some(arrow_idx) = window.find("->") else {
1903 return call_node.location;
1904 };
1905
1906 let mut idx = arrow_idx + 2;
1907 while idx < window.len() {
1908 let b = window.as_bytes()[idx];
1909 if b.is_ascii_whitespace() {
1910 idx += 1;
1911 } else {
1912 break;
1913 }
1914 }
1915
1916 let suffix = &window[idx..];
1917 if suffix.starts_with(method_name) {
1918 let method_start = search_start + idx;
1919 return SourceLocation { start: method_start, end: method_start + method_name.len() };
1920 }
1921
1922 if let Some(rel_idx) = suffix.find(method_name) {
1923 let method_start = search_start + idx + rel_idx;
1924 return SourceLocation { start: method_start, end: method_start + method_name.len() };
1925 }
1926
1927 call_node.location
1928 }
1929
1930 fn extract_leading_comment(&self, start: usize) -> Option<String> {
1932 if self.source.is_empty() || start == 0 {
1933 return None;
1934 }
1935 let mut end = start.min(self.source.len());
1936 let bytes = self.source.as_bytes();
1937 while end > 0 && bytes[end - 1].is_ascii_whitespace() {
1939 end -= 1;
1940 }
1941
1942 while end > 0 && !self.source.is_char_boundary(end) {
1944 end -= 1;
1945 }
1946
1947 let prefix = &self.source[..end];
1948 let mut lines = prefix.lines().rev();
1949 let mut docs = Vec::new();
1950 for line in &mut lines {
1951 let trimmed = line.trim_start();
1952 if trimmed.starts_with('#') {
1953 let content = trimmed.trim_start_matches('#').trim_start();
1955 docs.push(content);
1956 } else {
1957 break;
1959 }
1960 }
1961 if docs.is_empty() {
1962 None
1963 } else {
1964 docs.reverse();
1965 let total_len: usize =
1967 docs.iter().map(|s| s.len()).sum::<usize>() + docs.len().saturating_sub(1);
1968 let mut result = String::with_capacity(total_len);
1969 for (i, doc) in docs.iter().enumerate() {
1970 if i > 0 {
1971 result.push('\n');
1972 }
1973 result.push_str(doc);
1974 }
1975 Some(result)
1976 }
1977 }
1978
1979 fn extract_package_documentation(
1986 &self,
1987 package_name: &str,
1988 location: SourceLocation,
1989 ) -> Option<String> {
1990 let leading = self.extract_leading_comment(location.start);
1992 if leading.is_some() {
1993 return leading;
1994 }
1995
1996 if self.source.is_empty() {
1998 return None;
1999 }
2000
2001 let mut in_name_section = false;
2003 let mut name_lines: Vec<&str> = Vec::new();
2004
2005 for line in self.source.lines() {
2006 let trimmed = line.trim();
2007 if trimmed.starts_with("=head1") {
2008 if in_name_section {
2009 break;
2011 }
2012 let heading = trimmed.strip_prefix("=head1").map(|s| s.trim());
2013 if heading == Some("NAME") {
2014 in_name_section = true;
2015 continue;
2016 }
2017 } else if trimmed.starts_with("=cut") && in_name_section {
2018 break;
2019 } else if trimmed.starts_with('=') && in_name_section {
2020 break;
2022 } else if in_name_section && !trimmed.is_empty() {
2023 name_lines.push(trimmed);
2024 }
2025 }
2026
2027 if !name_lines.is_empty() {
2028 let name_doc = name_lines.join(" ");
2029 if name_doc.contains(package_name)
2031 || name_doc.contains(&package_name.replace("::", "-"))
2032 {
2033 return Some(name_doc);
2034 }
2035 }
2036
2037 None
2038 }
2039
2040 fn handle_variable_declaration(
2042 &mut self,
2043 declarator: &str,
2044 variable: &Node,
2045 attributes: &[String],
2046 location: SourceLocation,
2047 documentation: Option<String>,
2048 ) {
2049 if let NodeKind::Variable { sigil, name } = &variable.kind {
2050 let kind = match sigil.as_str() {
2051 "$" => SymbolKind::scalar(),
2052 "@" => SymbolKind::array(),
2053 "%" => SymbolKind::hash(),
2054 _ => return,
2055 };
2056
2057 let symbol = Symbol {
2058 name: name.clone(),
2059 qualified_name: if declarator == "our" {
2060 format!("{}::{}", self.table.current_package, name)
2061 } else {
2062 name.clone()
2063 },
2064 kind,
2065 location,
2066 scope_id: self.table.current_scope(),
2067 declaration: Some(declarator.to_string()),
2068 documentation,
2069 attributes: attributes.to_vec(),
2070 };
2071
2072 self.table.add_symbol(symbol);
2073 }
2074 }
2075
2076 fn mark_write_reference(&mut self, node: &Node) {
2078 if let NodeKind::Variable { .. } = &node.kind {
2081 }
2084 }
2085
2086 fn extract_vars_from_string(&mut self, value: &str, string_location: SourceLocation) {
2088 static SCALAR_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
2089
2090 let scalar_re = match SCALAR_RE
2093 .get_or_init(|| Regex::new(r"\$([a-zA-Z_]\w*|\{[a-zA-Z_]\w*\})"))
2094 .as_ref()
2095 {
2096 Ok(re) => re,
2097 Err(_) => return, };
2099
2100 let content = if value.len() >= 2 { &value[1..value.len() - 1] } else { value };
2102
2103 for cap in scalar_re.captures_iter(content) {
2104 if let Some(m) = cap.get(0) {
2105 let var_name = if m.as_str().starts_with("${") && m.as_str().ends_with("}") {
2106 &m.as_str()[2..m.as_str().len() - 1]
2108 } else {
2109 &m.as_str()[1..]
2111 };
2112
2113 let start_offset = string_location.start + 1 + m.start(); let end_offset = start_offset + m.len();
2117
2118 let reference = SymbolReference {
2119 name: var_name.to_string(),
2120 kind: SymbolKind::scalar(),
2121 location: SourceLocation { start: start_offset, end: end_offset },
2122 scope_id: self.table.current_scope(),
2123 is_write: false,
2124 };
2125
2126 self.table.add_reference(reference);
2127 }
2128 }
2129 }
2130}
2131
2132#[cfg(test)]
2133mod tests {
2134 use super::*;
2135 use crate::parser::Parser;
2136 use perl_tdd_support::must;
2137
2138 #[test]
2139 fn test_symbol_extraction() {
2140 let code = r#"
2141package Foo;
2142
2143my $x = 42;
2144our $y = "hello";
2145
2146sub bar {
2147 my $z = $x + $y;
2148 return $z;
2149}
2150"#;
2151
2152 let mut parser = Parser::new(code);
2153 let ast = must(parser.parse());
2154
2155 let extractor = SymbolExtractor::new_with_source(code);
2156 let table = extractor.extract(&ast);
2157
2158 assert!(table.symbols.contains_key("Foo"));
2160 let foo_symbols = &table.symbols["Foo"];
2161 assert_eq!(foo_symbols.len(), 1);
2162 assert_eq!(foo_symbols[0].kind, SymbolKind::Package);
2163
2164 assert!(table.symbols.contains_key("x"));
2166 assert!(table.symbols.contains_key("y"));
2167 assert!(table.symbols.contains_key("z"));
2168
2169 assert!(table.symbols.contains_key("bar"));
2171 let bar_symbols = &table.symbols["bar"];
2172 assert_eq!(bar_symbols.len(), 1);
2173 assert_eq!(bar_symbols[0].kind, SymbolKind::Subroutine);
2174 }
2175
2176 #[test]
2179 fn test_method_node_uses_symbol_kind_method() {
2180 let code = r#"
2181class MyClass {
2182 method greet {
2183 return "hello";
2184 }
2185}
2186"#;
2187 let mut parser = Parser::new(code);
2188 let ast = must(parser.parse());
2189
2190 let extractor = SymbolExtractor::new_with_source(code);
2191 let table = extractor.extract(&ast);
2192
2193 assert!(table.symbols.contains_key("greet"), "expected 'greet' in symbol table");
2194 let greet_symbols = &table.symbols["greet"];
2195 assert_eq!(greet_symbols.len(), 1);
2196 assert_eq!(
2197 greet_symbols[0].kind,
2198 SymbolKind::Method,
2199 "NodeKind::Method should produce SymbolKind::Method, not Subroutine"
2200 );
2201 assert!(
2203 greet_symbols[0].attributes.contains(&"method".to_string()),
2204 "method symbol should have 'method' attribute"
2205 );
2206 }
2207}