1use crate::SourceLocation;
8use crate::ast::{Node, NodeKind};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Framework {
14 Moose,
16 Moo,
18 Mouse,
20 ClassAccessor,
22 ObjectPad,
24 Native,
26 NativeClass,
28 PlainOO,
30 RoleTiny,
32 ClassTiny,
34 None,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum AccessorType {
41 Ro,
43 Rw,
45 Lazy,
47 Bare,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum MethodResolutionOrder {
54 #[default]
56 Dfs,
57 C3,
59}
60
61#[derive(Debug, Clone)]
63pub struct Attribute {
64 pub name: String,
66 pub is: Option<AccessorType>,
68 pub isa: Option<String>,
70 pub default: bool,
72 pub required: bool,
74 pub accessor_name: String,
76 pub location: SourceLocation,
78 pub builder: Option<String>,
80 pub coerce: bool,
82 pub predicate: Option<String>,
84 pub clearer: Option<String>,
86 pub trigger: bool,
88}
89
90#[derive(Debug, Clone)]
92pub struct FieldInfo {
93 pub name: String,
95 pub location: SourceLocation,
97 pub attributes: Vec<String>,
99 pub param: bool,
101 pub reader: Option<String>,
103 pub writer: Option<String>,
105 pub accessor: Option<String>,
107 pub mutator: Option<String>,
109 pub default: Option<String>,
111}
112
113#[derive(Debug, Clone)]
115pub struct MethodModifier {
116 pub kind: ModifierKind,
118 pub method_name: String,
120 pub location: SourceLocation,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum ModifierKind {
127 Before,
129 After,
131 Around,
133 Override,
135 Augment,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum ClassAccessorMode {
142 Rw,
144 Ro,
146 Wo,
148}
149
150#[derive(Debug, Clone)]
152pub struct MethodInfo {
153 pub name: String,
155 pub location: SourceLocation,
157 pub synthetic: bool,
159 pub accessor_mode: Option<ClassAccessorMode>,
161}
162
163impl MethodInfo {
164 pub fn new(name: String, location: SourceLocation) -> Self {
166 Self { name, location, synthetic: false, accessor_mode: None }
167 }
168
169 pub fn synthetic(
171 name: String,
172 location: SourceLocation,
173 accessor_mode: Option<ClassAccessorMode>,
174 ) -> Self {
175 Self { name, location, synthetic: true, accessor_mode }
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct ClassModel {
182 pub name: String,
184 pub framework: Framework,
186 pub attributes: Vec<Attribute>,
188 pub fields: Vec<FieldInfo>,
190 pub methods: Vec<MethodInfo>,
192 pub adjusts: Vec<MethodInfo>,
194 pub parents: Vec<String>,
196 pub mro: MethodResolutionOrder,
198 pub roles: Vec<String>,
200 pub modifiers: Vec<MethodModifier>,
202 pub exports: Vec<String>,
204 pub export_ok: Vec<String>,
206 pub exporter_metadata: Option<ExporterMetadata>,
208}
209
210#[derive(Debug, Clone)]
212#[non_exhaustive]
213pub struct ExporterMetadata {
214 pub exports: Vec<ResolvedExport>,
216 pub export_ok: Vec<ResolvedExport>,
218 pub export_tags: HashMap<String, Vec<ResolvedExport>>,
220 pub unresolved: Vec<String>,
222}
223
224#[derive(Debug, Clone)]
226#[non_exhaustive]
227pub struct ResolvedExport {
228 pub name: String,
230 pub location: SourceLocation,
232}
233
234impl ClassModel {
235 pub fn has_framework(&self) -> bool {
237 !matches!(self.framework, Framework::None)
238 }
239
240 pub fn object_pad_param_field_names(&self) -> impl Iterator<Item = &str> {
242 self.fields.iter().filter(|field| field.param).map(|field| field.name.as_str())
243 }
244}
245
246pub struct ClassModelBuilder {
248 models: Vec<ClassModel>,
249 current_package: String,
250 current_framework: Framework,
251 current_attributes: Vec<Attribute>,
252 current_fields: Vec<FieldInfo>,
253 current_methods: Vec<MethodInfo>,
254 current_adjusts: Vec<MethodInfo>,
255 current_parents: Vec<String>,
256 current_mro: MethodResolutionOrder,
257 current_roles: Vec<String>,
258 current_modifiers: Vec<MethodModifier>,
259 current_exports: Vec<String>,
260 current_export_ok: Vec<String>,
261 current_export_tags: HashMap<String, Vec<String>>,
262 current_uses_exporter: bool,
263 current_package_aliases: HashSet<String>,
264 framework_map: HashMap<String, Framework>,
266}
267
268impl Default for ClassModelBuilder {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274impl ClassModelBuilder {
275 pub fn new() -> Self {
277 Self {
278 models: Vec::new(),
279 current_package: "main".to_string(),
280 current_framework: Framework::None,
281 current_attributes: Vec::new(),
282 current_fields: Vec::new(),
283 current_methods: Vec::new(),
284 current_adjusts: Vec::new(),
285 current_parents: Vec::new(),
286 current_mro: MethodResolutionOrder::Dfs,
287 current_roles: Vec::new(),
288 current_modifiers: Vec::new(),
289 current_exports: Vec::new(),
290 current_export_ok: Vec::new(),
291 current_export_tags: HashMap::new(),
292 current_uses_exporter: false,
293 current_package_aliases: HashSet::new(),
294 framework_map: HashMap::new(),
295 }
296 }
297
298 pub fn build(mut self, node: &Node) -> Vec<ClassModel> {
300 self.visit_node(node);
301 self.flush_current_package();
302 self.models
303 }
304
305 fn flush_current_package(&mut self) {
307 let framework = self.current_framework;
308 let has_oo_indicator = framework != Framework::None
310 || !self.current_attributes.is_empty()
311 || !self.current_fields.is_empty()
312 || !self.current_parents.is_empty()
313 || !self.current_adjusts.is_empty()
314 || !self.current_exports.is_empty()
315 || !self.current_export_ok.is_empty()
316 || !self.current_export_tags.is_empty();
317 if has_oo_indicator {
318 let exporter_metadata = self.build_exporter_metadata_for_current_package();
319 let model = ClassModel {
320 name: self.current_package.clone(),
321 framework,
322 attributes: std::mem::take(&mut self.current_attributes),
323 fields: std::mem::take(&mut self.current_fields),
324 methods: std::mem::take(&mut self.current_methods),
325 adjusts: std::mem::take(&mut self.current_adjusts),
326 parents: std::mem::take(&mut self.current_parents),
327 mro: self.current_mro,
328 roles: std::mem::take(&mut self.current_roles),
329 modifiers: std::mem::take(&mut self.current_modifiers),
330 exports: std::mem::take(&mut self.current_exports),
331 export_ok: std::mem::take(&mut self.current_export_ok),
332 exporter_metadata,
333 };
334 self.models.push(model);
335 self.current_package_aliases.clear();
336 } else {
337 self.current_attributes.clear();
339 self.current_fields.clear();
340 self.current_methods.clear();
341 self.current_adjusts.clear();
342 self.current_parents.clear();
343 self.current_mro = MethodResolutionOrder::Dfs;
344 self.current_roles.clear();
345 self.current_modifiers.clear();
346 self.current_exports.clear();
347 self.current_export_ok.clear();
348 self.current_export_tags.clear();
349 self.current_uses_exporter = false;
350 self.current_package_aliases.clear();
351 }
352 }
353
354 fn visit_node(&mut self, node: &Node) {
355 match &node.kind {
356 NodeKind::Program { statements } => {
357 self.visit_statement_list(statements);
358 }
359
360 NodeKind::Package { name, block, .. } => {
361 self.flush_current_package();
363
364 self.current_package = name.clone();
365 self.current_framework =
366 self.framework_map.get(name).copied().unwrap_or(Framework::None);
367 self.current_mro = MethodResolutionOrder::Dfs;
368 self.current_uses_exporter = false;
369
370 if let Some(block) = block {
371 self.visit_node(block);
372 }
373 }
374
375 NodeKind::Block { statements, .. } => {
376 self.visit_statement_list(statements);
377 }
378
379 NodeKind::Subroutine { name, body, .. } => {
380 if let Some(sub_name) = name {
381 self.current_methods.push(MethodInfo::new(sub_name.clone(), node.location));
382 }
383 self.visit_node(body);
384 }
385
386 NodeKind::Use { module, args, .. } => {
387 self.detect_framework(module, args);
388 }
389
390 NodeKind::No { module, .. } if module == "mro" => {
391 self.current_mro = MethodResolutionOrder::Dfs;
392 }
393
394 NodeKind::VariableDeclaration { variable, initializer, .. } => {
396 if let NodeKind::Variable { sigil, name } = &variable.kind {
397 if sigil == "$"
398 && let Some(init) = initializer
399 && self.initializer_is_current_package(init)
400 {
401 self.current_package_aliases.insert(name.clone());
402 }
403
404 if sigil == "@"
405 && let Some(init) = initializer
406 {
407 match name.as_str() {
408 "ISA" => self.extract_isa_from_node(init),
409 "EXPORT" => {
410 self.current_exports.extend(collect_symbol_names(init));
411 }
412 "EXPORT_OK" => {
413 self.current_export_ok.extend(collect_symbol_names(init));
414 }
415 _ => {}
416 }
417 }
418 if sigil == "%"
419 && name == "EXPORT_TAGS"
420 && let Some(init) = initializer
421 {
422 merge_export_tags(&mut self.current_export_tags, collect_export_tags(init));
423 }
424 }
425 }
426
427 NodeKind::Assignment { lhs, rhs, .. } => {
429 if let NodeKind::Variable { sigil, name } = &lhs.kind
430 && sigil == "@"
431 {
432 match name.as_str() {
433 "ISA" => self.extract_isa_from_node(rhs),
434 "EXPORT" => {
435 self.current_exports.extend(collect_symbol_names(rhs));
436 }
437 "EXPORT_OK" => {
438 self.current_export_ok.extend(collect_symbol_names(rhs));
439 }
440 _ => {}
441 }
442 }
443 if let NodeKind::Variable { sigil, name } = &lhs.kind
444 && sigil == "%"
445 && name == "EXPORT_TAGS"
446 {
447 merge_export_tags(&mut self.current_export_tags, collect_export_tags(rhs));
448 }
449 }
450
451 NodeKind::ExpressionStatement { expression } => {
455 if let NodeKind::FunctionCall { name, args } = &expression.kind
456 && name == "push"
457 {
458 if let Some(first_arg) = args.first() {
459 if let NodeKind::Variable { sigil, name: var_name } = &first_arg.kind
460 && sigil == "@"
461 && var_name == "ISA"
462 {
463 for arg in args.iter().skip(1) {
464 self.extract_isa_from_node(arg);
465 }
466 return;
467 }
468 }
469 }
470 self.visit_node(expression);
472 }
473
474 NodeKind::Class { name, parents, body } => {
475 self.flush_current_package();
476 self.current_package = name.clone();
477 self.current_framework = if self.current_framework == Framework::ObjectPad {
478 Framework::ObjectPad
479 } else {
480 Framework::NativeClass
481 };
482 self.current_mro = MethodResolutionOrder::Dfs;
483 self.framework_map.insert(name.clone(), self.current_framework);
484 self.current_package_aliases.clear();
485 self.current_parents.extend(parents.iter().cloned());
487 self.visit_node(body);
488 }
489
490 NodeKind::Method { name, body, .. } => {
491 self.current_methods.push(MethodInfo::new(name.clone(), node.location));
492 self.visit_node(body);
493 }
494
495 NodeKind::Error { partial, .. } => {
496 if let Some(partial) = partial {
497 self.visit_node(partial);
498 }
499 }
500
501 _ => {
502 self.visit_children(node);
504 }
505 }
506 }
507
508 fn visit_children(&mut self, node: &Node) {
509 match &node.kind {
510 NodeKind::ExpressionStatement { expression } => {
511 self.visit_node(expression);
512 }
513 NodeKind::Block { statements, .. } => {
514 self.visit_statement_list(statements);
515 }
516 NodeKind::If { condition, then_branch, else_branch, .. } => {
517 self.visit_node(condition);
518 self.visit_node(then_branch);
519 if let Some(else_node) = else_branch {
520 self.visit_node(else_node);
521 }
522 }
523 _ => {}
524 }
525 }
526
527 fn visit_statement_list(&mut self, statements: &[Node]) {
528 let mut idx = 0;
529 while idx < statements.len() {
530 if let NodeKind::Use { module, args, .. } = &statements[idx].kind {
532 self.detect_mro(module, args);
533 self.detect_framework(module, args);
534 self.extract_class_tiny_use_attrs(module, args, statements[idx].location);
535 let consumed_default_hash =
536 self.extract_class_tiny_following_default_hash(module, statements.get(idx + 1));
537 idx += 1 + usize::from(consumed_default_hash);
538 continue;
539 }
540
541 let is_framework_package = self.current_framework != Framework::None;
542
543 if is_framework_package {
544 if self.current_framework == Framework::ObjectPad
545 && let Some(consumed) = self.try_extract_object_pad_constructs(statements, idx)
546 {
547 idx += consumed;
548 continue;
549 }
550 if self.current_framework == Framework::ClassAccessor
551 && let Some(consumed) = self.try_extract_class_accessor_methods(statements, idx)
552 {
553 idx += consumed;
554 continue;
555 }
556 if let Some(consumed) = self.try_extract_has(statements, idx) {
558 idx += consumed;
559 continue;
560 }
561 if let Some(consumed) = self.try_extract_modifier(statements, idx) {
563 idx += consumed;
564 continue;
565 }
566 if let Some(consumed) = self.try_extract_extends_with(statements, idx) {
568 idx += consumed;
569 continue;
570 }
571 }
572
573 self.visit_node(&statements[idx]);
575 idx += 1;
576 }
577 }
578
579 fn detect_framework(&mut self, module: &str, args: &[String]) {
581 let framework = match module {
582 "Exporter" => {
583 self.current_uses_exporter = true;
584 return;
585 }
586 "Moose" | "Moose::Role" => Framework::Moose,
587 "Moo" | "Moo::Role" => Framework::Moo,
588 "Mouse" | "Mouse::Role" => Framework::Mouse,
589 "Role::Tiny" | "Role::Tiny::With" => Framework::RoleTiny,
590 "Class::Tiny" | "Class::Tiny::RW" => Framework::ClassTiny,
591 "Class::Accessor" => Framework::ClassAccessor,
592 "Object::Pad" => Framework::ObjectPad,
593 "base" | "parent" => {
594 let mut has_class_accessor = false;
600 let mut captured_parents: Vec<String> = Vec::new();
601
602 for arg in args {
603 let trimmed = arg.trim();
604 if trimmed.starts_with('-') || trimmed.is_empty() {
605 continue;
607 }
608 let names = expand_arg_to_names(trimmed);
610 for name in names {
611 if name == "Class::Accessor" {
612 has_class_accessor = true;
613 } else if name == "Exporter" {
614 self.current_uses_exporter = true;
615 } else {
616 captured_parents.push(name);
617 }
618 }
619 }
620
621 self.current_parents.extend(captured_parents);
622
623 if has_class_accessor {
624 Framework::ClassAccessor
625 } else if self.current_framework == Framework::None {
626 Framework::PlainOO
628 } else {
629 return;
631 }
632 }
633 _ => return,
634 };
635
636 self.current_framework = framework;
637 self.framework_map.insert(self.current_package.clone(), framework);
638 }
639
640 fn detect_mro(&mut self, module: &str, args: &[String]) {
642 if module != "mro" {
643 return;
644 }
645
646 if args.is_empty() {
647 self.current_mro = MethodResolutionOrder::Dfs;
648 return;
649 }
650
651 for arg in args {
652 let trimmed = arg.trim().trim_matches('\'').trim_matches('"');
653 match trimmed {
654 "c3" => {
655 self.current_mro = MethodResolutionOrder::C3;
656 return;
657 }
658 "dfs" => {
659 self.current_mro = MethodResolutionOrder::Dfs;
660 return;
661 }
662 _ => {}
663 }
664 }
665 }
666
667 fn extract_class_tiny_use_attrs(
676 &mut self,
677 module: &str,
678 args: &[String],
679 location: SourceLocation,
680 ) {
681 if !matches!(module, "Class::Tiny" | "Class::Tiny::RW") {
682 return;
683 }
684 let accessor_type = AccessorType::Rw;
685 for name in extract_class_tiny_attribute_names(args) {
686 self.current_attributes.push(Attribute {
687 name: name.clone(),
688 is: Some(accessor_type),
689 isa: None,
690 default: false,
691 required: false,
692 accessor_name: name,
693 location,
694 builder: None,
695 coerce: false,
696 predicate: None,
697 clearer: None,
698 trigger: false,
699 });
700 }
701 }
702
703 fn extract_class_tiny_following_default_hash(
704 &mut self,
705 module: &str,
706 statement: Option<&Node>,
707 ) -> bool {
708 if !matches!(module, "Class::Tiny" | "Class::Tiny::RW") {
709 return false;
710 }
711 let Some(statement) = statement else { return false };
712 let Some((pairs, location)) = class_tiny_default_hash_pairs(statement) else {
713 return false;
714 };
715
716 for (key_node, _) in pairs {
717 for raw_name in collect_symbol_names(key_node) {
718 let Some(attr_name) = normalize_attribute_name(&raw_name) else { continue };
719 if !is_class_tiny_attribute_name(&attr_name) {
720 continue;
721 }
722 if let Some(existing) =
723 self.current_attributes.iter_mut().find(|attr| attr.name == attr_name)
724 {
725 existing.default = true;
726 continue;
727 }
728 self.current_attributes.push(Attribute {
729 name: attr_name.clone(),
730 is: Some(AccessorType::Rw),
731 isa: None,
732 default: true,
733 required: false,
734 accessor_name: attr_name,
735 location,
736 builder: None,
737 coerce: false,
738 predicate: None,
739 clearer: None,
740 trigger: false,
741 });
742 }
743 }
744
745 true
746 }
747
748 fn try_extract_has(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
752 let first = &statements[idx];
753
754 if idx + 1 < statements.len() {
758 let second = &statements[idx + 1];
759 let is_has_marker = matches!(
760 &first.kind,
761 NodeKind::ExpressionStatement { expression }
762 if matches!(&expression.kind, NodeKind::Identifier { name } if name == "has")
763 );
764
765 if is_has_marker {
766 if let NodeKind::ExpressionStatement { expression } = &second.kind {
767 let has_location =
768 SourceLocation { start: first.location.start, end: second.location.end };
769
770 match &expression.kind {
771 NodeKind::HashLiteral { pairs } => {
772 self.extract_has_from_pairs(pairs, has_location, false);
773 return Some(2);
774 }
775 NodeKind::ArrayLiteral { elements } => {
776 if let Some(Node { kind: NodeKind::HashLiteral { pairs }, .. }) =
777 elements.last()
778 {
779 let mut names = Vec::new();
780 for el in elements.iter().take(elements.len() - 1) {
781 names.extend(collect_symbol_names(el));
782 }
783 if !names.is_empty() {
784 self.extract_has_with_names(&names, pairs, has_location);
785 return Some(2);
786 }
787 }
788 }
789 _ => {}
790 }
791 }
792 }
793 }
794
795 if let NodeKind::ExpressionStatement { expression } = &first.kind
797 && let NodeKind::HashLiteral { pairs } = &expression.kind
798 {
799 let has_embedded = pairs.iter().any(|(key_node, _)| {
800 matches!(
801 &key_node.kind,
802 NodeKind::Binary { op, left, .. }
803 if op == "[]" && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
804 )
805 });
806
807 if has_embedded {
808 self.extract_has_from_pairs(pairs, first.location, true);
809 return Some(1);
810 }
811 }
812
813 if let NodeKind::ExpressionStatement { expression } = &first.kind
816 && let NodeKind::FunctionCall { name, args } = &expression.kind
817 && name == "has"
818 && !args.is_empty()
819 {
820 let options_hash_idx =
822 args.iter().rposition(|a| matches!(a.kind, NodeKind::HashLiteral { .. }));
823 if let Some(opts_idx) = options_hash_idx {
824 if let NodeKind::HashLiteral { pairs } = &args[opts_idx].kind {
825 let names: Vec<String> =
826 args[..opts_idx].iter().flat_map(collect_symbol_names).collect();
827 if !names.is_empty() {
828 self.extract_has_with_names(&names, pairs, first.location);
829 return Some(1);
830 }
831 }
832 } else {
833 let names: Vec<String> = args.iter().flat_map(collect_symbol_names).collect();
834 if !names.is_empty() {
835 self.extract_has_with_names(&names, &[], first.location);
836 return Some(1);
837 }
838 }
839 }
840
841 None
842 }
843
844 fn extract_has_from_pairs(
846 &mut self,
847 pairs: &[(Node, Node)],
848 location: SourceLocation,
849 require_embedded: bool,
850 ) {
851 for (attr_expr, options_expr) in pairs {
852 let attr_expr = if let NodeKind::Binary { op, left, right } = &attr_expr.kind
853 && op == "[]"
854 && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
855 {
856 right.as_ref()
857 } else if require_embedded {
858 continue;
859 } else {
860 attr_expr
861 };
862
863 let names = collect_symbol_names(attr_expr);
864 if names.is_empty() {
865 continue;
866 }
867
868 if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
869 self.extract_has_with_names(&names, option_pairs, location);
870 }
871 }
872 }
873
874 fn extract_has_with_names(
876 &mut self,
877 names: &[String],
878 option_pairs: &[(Node, Node)],
879 location: SourceLocation,
880 ) {
881 let options = extract_hash_options(option_pairs);
882
883 let is = options.get("is").and_then(|v| match v.as_str() {
884 "ro" => Some(AccessorType::Ro),
885 "rw" => Some(AccessorType::Rw),
886 "lazy" => Some(AccessorType::Lazy),
887 "bare" => Some(AccessorType::Bare),
888 _ => None,
889 });
890
891 let isa = options.get("isa").cloned();
892 let default = options.contains_key("default")
893 || options.contains_key("builder")
894 || is == Some(AccessorType::Lazy);
895 let required = options.get("required").is_some_and(|v| v == "1" || v == "true");
896 let coerce = options.get("coerce").is_some_and(|v| v == "1" || v == "true");
897 let trigger = options.contains_key("trigger");
898
899 let explicit_accessor = options.get("accessor").or_else(|| options.get("reader")).cloned();
901
902 for raw_name in names {
903 let Some(name) = normalize_attribute_name(raw_name) else { continue };
904 let accessor_name = explicit_accessor.clone().unwrap_or_else(|| name.clone());
905
906 let builder = options
908 .get("builder")
909 .map(|v| if v == "1" { format!("_build_{name}") } else { v.clone() });
910
911 let predicate = options
913 .get("predicate")
914 .map(|v| if v == "1" { format!("has_{name}") } else { v.clone() });
915
916 let clearer = options
918 .get("clearer")
919 .map(|v| if v == "1" { format!("clear_{name}") } else { v.clone() });
920
921 self.current_attributes.push(Attribute {
922 name: name.clone(),
923 is,
924 isa: isa.clone(),
925 default,
926 required,
927 accessor_name,
928 location,
929 builder,
930 coerce,
931 predicate,
932 clearer,
933 trigger,
934 });
935 }
936 }
937
938 fn try_extract_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
940 let first = &statements[idx];
941
942 if let NodeKind::ExpressionStatement { expression } = &first.kind
944 && let NodeKind::FunctionCall { name, args } = &expression.kind
945 {
946 let modifier_kind = modifier_kind_from_name(name);
947 if let Some(modifier_kind) = modifier_kind {
948 let method_names: Vec<String> =
950 args.first().map(collect_symbol_names).unwrap_or_default();
951 if !method_names.is_empty() {
952 for method_name in method_names {
953 self.current_modifiers.push(MethodModifier {
954 kind: modifier_kind,
955 method_name,
956 location: first.location,
957 });
958 }
959 return Some(1);
960 }
961 }
962 }
963
964 if idx + 1 >= statements.len() {
968 return None;
969 }
970 let second = &statements[idx + 1];
971
972 let modifier_kind = match &first.kind {
973 NodeKind::ExpressionStatement { expression } => match &expression.kind {
974 NodeKind::Identifier { name } => modifier_kind_from_name(name),
975 _ => None,
976 },
977 _ => None,
978 };
979
980 let modifier_kind = modifier_kind?;
981
982 let NodeKind::ExpressionStatement { expression } = &second.kind else {
983 return None;
984 };
985 let NodeKind::HashLiteral { pairs } = &expression.kind else {
986 return None;
987 };
988
989 let location = SourceLocation { start: first.location.start, end: second.location.end };
990
991 for (key_node, _) in pairs {
992 let method_names = collect_symbol_names(key_node);
993 for method_name in method_names {
994 self.current_modifiers.push(MethodModifier {
995 kind: modifier_kind,
996 method_name,
997 location,
998 });
999 }
1000 }
1001
1002 Some(2)
1003 }
1004
1005 fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1007 let first = &statements[idx];
1008
1009 if let NodeKind::ExpressionStatement { expression } = &first.kind
1012 && let NodeKind::FunctionCall { name, args } = &expression.kind
1013 && matches!(name.as_str(), "extends" | "with")
1014 {
1015 let names: Vec<String> = args.iter().flat_map(collect_symbol_names).collect();
1016 if !names.is_empty() {
1017 if name == "extends" {
1018 self.current_parents.extend(names);
1019 } else {
1020 self.current_roles.extend(names);
1021 }
1022 return Some(1);
1023 }
1024 }
1025
1026 if idx + 1 >= statements.len() {
1030 return None;
1031 }
1032 let second = &statements[idx + 1];
1033
1034 let keyword = match &first.kind {
1035 NodeKind::ExpressionStatement { expression } => match &expression.kind {
1036 NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
1037 name.as_str()
1038 }
1039 _ => return None,
1040 },
1041 _ => return None,
1042 };
1043
1044 let NodeKind::ExpressionStatement { expression } = &second.kind else {
1045 return None;
1046 };
1047
1048 let names = collect_symbol_names(expression);
1049 if names.is_empty() {
1050 return None;
1051 }
1052
1053 if keyword == "extends" {
1054 self.current_parents.extend(names);
1055 } else {
1056 self.current_roles.extend(names);
1057 }
1058
1059 Some(2)
1060 }
1061
1062 fn try_extract_class_accessor_methods(
1064 &mut self,
1065 statements: &[Node],
1066 idx: usize,
1067 ) -> Option<usize> {
1068 let first = &statements[idx];
1069
1070 let NodeKind::ExpressionStatement { expression } = &first.kind else {
1071 return None;
1072 };
1073
1074 let NodeKind::MethodCall { object, method, args } = &expression.kind else {
1075 return None;
1076 };
1077
1078 let accessor_mode = match method.as_str() {
1079 "mk_accessors" | "mk_rw_accessors" => ClassAccessorMode::Rw,
1080 "mk_ro_accessors" => ClassAccessorMode::Ro,
1081 "mk_wo_accessors" => ClassAccessorMode::Wo,
1082 _ => return None,
1083 };
1084
1085 if !self.class_accessor_target_matches_current_package(object) {
1086 return None;
1087 }
1088
1089 let mut accessor_names = Vec::new();
1090 let mut seen = HashSet::new();
1091 for arg in args {
1092 for name in collect_accessor_names(arg) {
1093 if seen.insert(name.clone()) {
1094 accessor_names.push(name);
1095 }
1096 }
1097 }
1098
1099 if accessor_names.is_empty() {
1100 return None;
1101 }
1102
1103 for name in accessor_names {
1104 self.current_methods.push(MethodInfo::synthetic(
1105 name,
1106 first.location,
1107 Some(accessor_mode),
1108 ));
1109 }
1110
1111 Some(1)
1112 }
1113
1114 fn try_extract_object_pad_constructs(
1116 &mut self,
1117 statements: &[Node],
1118 idx: usize,
1119 ) -> Option<usize> {
1120 let statement = &statements[idx];
1121
1122 if let Some(field) = Self::object_pad_field_from_statement(statement) {
1123 let location = field.location;
1124 let field_name = field.name.clone();
1125 let traits = field.attributes.clone();
1126
1127 self.current_fields.push(field);
1128
1129 if let Some(reader) = Self::object_pad_reader_name(&field_name, &traits) {
1130 self.current_methods.push(MethodInfo::synthetic(reader, location, None));
1131 }
1132 if let Some(writer) = Self::object_pad_writer_name(&field_name, &traits) {
1133 self.current_methods.push(MethodInfo::synthetic(writer, location, None));
1134 }
1135 if let Some(accessor) = Self::object_pad_accessor_name(&field_name, &traits) {
1136 self.current_methods.push(MethodInfo::synthetic(accessor, location, None));
1137 }
1138 if let Some(mutator) = Self::object_pad_mutator_name(&field_name, &traits) {
1139 self.current_methods.push(MethodInfo::synthetic(mutator, location, None));
1140 }
1141
1142 return Some(1);
1143 }
1144
1145 match &statement.kind {
1146 NodeKind::Method { name, body, .. } if name == "ADJUST" => {
1147 self.record_object_pad_adjust(statement.location);
1148 self.visit_node(body);
1149 return Some(1);
1150 }
1151 NodeKind::Subroutine { name, body, .. } if name.as_deref() == Some("ADJUST") => {
1152 self.record_object_pad_adjust(statement.location);
1153 self.visit_node(body);
1154 return Some(1);
1155 }
1156 _ => {}
1157 }
1158
1159 None
1160 }
1161
1162 fn record_object_pad_adjust(&mut self, location: SourceLocation) {
1163 self.current_adjusts.push(MethodInfo::synthetic("ADJUST".to_string(), location, None));
1164 }
1165
1166 fn object_pad_field_from_statement(statement: &Node) -> Option<FieldInfo> {
1167 let NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } =
1168 &statement.kind
1169 else {
1170 return None;
1171 };
1172 if declarator != "field" {
1173 return None;
1174 }
1175
1176 let NodeKind::Variable { sigil, name } = &variable.kind else {
1177 return None;
1178 };
1179 if sigil != "$" {
1180 return None;
1181 }
1182
1183 let mut param = false;
1184 let mut traits = Vec::new();
1185 for attr in attributes {
1186 let attr_name = attr.trim().to_string();
1187 if attr_name == "param" {
1188 param = true;
1189 }
1190 traits.push(attr_name);
1191 }
1192
1193 let mut field = FieldInfo {
1194 name: name.clone(),
1195 location: statement.location,
1196 attributes: traits,
1197 param,
1198 reader: None,
1199 writer: None,
1200 accessor: None,
1201 mutator: None,
1202 default: initializer.as_ref().map(|node| Self::value_summary(node)),
1203 };
1204
1205 field.reader = Self::object_pad_reader_name(&field.name, &field.attributes);
1206 field.writer = Self::object_pad_writer_name(&field.name, &field.attributes);
1207 field.accessor = Self::object_pad_accessor_name(&field.name, &field.attributes);
1208 field.mutator = Self::object_pad_mutator_name(&field.name, &field.attributes);
1209
1210 Some(field)
1211 }
1212
1213 fn object_pad_reader_name(field_name: &str, traits: &[String]) -> Option<String> {
1214 if traits.iter().any(|trait_name| trait_name == "reader") {
1215 Some(Self::object_pad_public_name(field_name).to_string())
1216 } else {
1217 None
1218 }
1219 }
1220
1221 fn object_pad_writer_name(field_name: &str, traits: &[String]) -> Option<String> {
1222 if traits.iter().any(|trait_name| trait_name == "writer") {
1223 Some(format!("set_{}", Self::object_pad_public_name(field_name)))
1224 } else {
1225 None
1226 }
1227 }
1228
1229 fn object_pad_accessor_name(field_name: &str, traits: &[String]) -> Option<String> {
1230 if traits.iter().any(|trait_name| trait_name == "accessor") {
1231 Some(Self::object_pad_public_name(field_name).to_string())
1232 } else {
1233 None
1234 }
1235 }
1236
1237 fn object_pad_mutator_name(field_name: &str, traits: &[String]) -> Option<String> {
1238 if traits.iter().any(|trait_name| trait_name == "mutator") {
1239 Some(Self::object_pad_public_name(field_name).to_string())
1240 } else {
1241 None
1242 }
1243 }
1244
1245 fn object_pad_public_name(field_name: &str) -> &str {
1246 field_name.strip_prefix('_').unwrap_or(field_name)
1247 }
1248
1249 fn class_accessor_target_matches_current_package(&self, object: &Node) -> bool {
1251 match &object.kind {
1252 NodeKind::Identifier { name } => name == "__PACKAGE__" || name == &self.current_package,
1253 NodeKind::String { value, .. } => {
1254 normalize_symbol_name(value).is_some_and(|name| name == self.current_package)
1255 }
1256 NodeKind::Variable { sigil, name } if sigil == "$" => {
1257 self.current_package_aliases.contains(name)
1258 }
1259 _ => false,
1260 }
1261 }
1262
1263 fn extract_isa_from_node(&mut self, node: &Node) {
1265 let parents = collect_symbol_names(node);
1266 if !parents.is_empty() {
1267 if parents.iter().any(|parent| parent == "Exporter") {
1268 self.current_uses_exporter = true;
1269 }
1270 if self.current_framework == Framework::None {
1272 self.current_framework = Framework::PlainOO;
1273 self.framework_map.insert(self.current_package.clone(), Framework::PlainOO);
1274 }
1275 self.current_parents.extend(parents);
1276 }
1277 }
1278
1279 fn build_exporter_metadata_for_current_package(&mut self) -> Option<ExporterMetadata> {
1280 if !self.current_uses_exporter {
1281 self.current_export_tags.clear();
1282 return None;
1283 }
1284
1285 let method_map: HashMap<&str, SourceLocation> = self
1286 .current_methods
1287 .iter()
1288 .map(|method| (method.name.as_str(), method.location))
1289 .collect();
1290
1291 let (exports, unresolved_exports) = resolve_exports(&self.current_exports, &method_map);
1292 let (export_ok, unresolved_export_ok) =
1293 resolve_exports(&self.current_export_ok, &method_map);
1294
1295 let mut export_tags: HashMap<String, Vec<ResolvedExport>> = HashMap::new();
1296 let mut unresolved = unresolved_exports;
1297 unresolved.extend(unresolved_export_ok);
1298
1299 for (tag, names) in std::mem::take(&mut self.current_export_tags) {
1300 let (resolved, unresolved_names) = resolve_exports(&names, &method_map);
1301 if !resolved.is_empty() {
1302 export_tags.insert(tag, resolved);
1303 }
1304 unresolved.extend(unresolved_names);
1305 }
1306
1307 dedupe_preserve_order(&mut unresolved);
1308
1309 Some(ExporterMetadata { exports, export_ok, export_tags, unresolved })
1310 }
1311
1312 fn initializer_is_current_package(&self, node: &Node) -> bool {
1314 match &node.kind {
1315 NodeKind::Identifier { name } => name == "__PACKAGE__" || name == &self.current_package,
1316 NodeKind::FunctionCall { name, args } if name == "__PACKAGE__" && args.is_empty() => {
1317 true
1318 }
1319 NodeKind::String { value, .. } => {
1320 normalize_symbol_name(value).is_some_and(|name| name == self.current_package)
1321 }
1322 _ => false,
1323 }
1324 }
1325
1326 fn value_summary(node: &Node) -> String {
1327 match &node.kind {
1328 NodeKind::String { value, .. } => {
1329 normalize_symbol_name(value).unwrap_or_else(|| value.clone())
1330 }
1331 NodeKind::Identifier { name } => name.clone(),
1332 NodeKind::Number { value } => value.clone(),
1333 NodeKind::Undef => "undef".to_string(),
1334 NodeKind::Variable { sigil, name } => format!("{sigil}{name}"),
1335 _ => "expr".to_string(),
1336 }
1337 }
1338}
1339
1340fn class_tiny_default_hash_pairs(statement: &Node) -> Option<(&[(Node, Node)], SourceLocation)> {
1343 let expression = match &statement.kind {
1344 NodeKind::ExpressionStatement { expression } => expression.as_ref(),
1345 NodeKind::Block { statements } if statements.len() == 1 => {
1346 let NodeKind::ExpressionStatement { expression } = &statements.first()?.kind else {
1347 return None;
1348 };
1349 expression.as_ref()
1350 }
1351 _ => return None,
1352 };
1353
1354 let NodeKind::HashLiteral { pairs } = &expression.kind else {
1355 return None;
1356 };
1357 Some((pairs.as_slice(), statement.location))
1358}
1359
1360fn collect_symbol_names(node: &Node) -> Vec<String> {
1361 match &node.kind {
1362 NodeKind::String { value, .. } => normalize_symbol_name(value).into_iter().collect(),
1363 NodeKind::Identifier { name } => normalize_symbol_name(name).into_iter().collect(),
1364 NodeKind::ArrayLiteral { elements } => {
1365 elements.iter().flat_map(collect_symbol_names).collect()
1366 }
1367 _ => Vec::new(),
1368 }
1369}
1370
1371fn collect_export_tags(node: &Node) -> HashMap<String, Vec<String>> {
1372 let mut tags: HashMap<String, Vec<String>> = HashMap::new();
1373 match &node.kind {
1374 NodeKind::HashLiteral { pairs } => {
1375 for (key, value) in pairs {
1376 let Some(tag_name) = collect_single_symbol_name(key) else { continue };
1377 let Some(symbols) = collect_static_symbol_names(value) else { continue };
1378 if symbols.is_empty() {
1379 continue;
1380 }
1381 tags.entry(tag_name).or_default().extend(symbols);
1382 }
1383 }
1384 NodeKind::ArrayLiteral { elements } => {
1385 for pair in elements.chunks_exact(2) {
1386 let Some(tag_name) = collect_single_symbol_name(&pair[0]) else { continue };
1387 let Some(symbols) = collect_static_symbol_names(&pair[1]) else { continue };
1388 if symbols.is_empty() {
1389 continue;
1390 }
1391 tags.entry(tag_name).or_default().extend(symbols);
1392 }
1393 }
1394 NodeKind::Binary { op, .. } if op == "," => {
1395 let mut flattened = Vec::new();
1396 flatten_comma_expression(node, &mut flattened);
1397 for element in flattened {
1398 if let NodeKind::Binary { op, left, right } = &element.kind
1399 && op == "=>"
1400 {
1401 let Some(tag_name) = collect_single_symbol_name(left) else { continue };
1402 let Some(symbols) = collect_static_symbol_names(right) else { continue };
1403 if symbols.is_empty() {
1404 continue;
1405 }
1406 tags.entry(tag_name).or_default().extend(symbols);
1407 }
1408 }
1409 }
1410 _ => {}
1411 }
1412
1413 for symbols in tags.values_mut() {
1414 dedupe_preserve_order(symbols);
1415 }
1416 tags
1417}
1418
1419fn collect_single_symbol_name(node: &Node) -> Option<String> {
1420 let mut names = collect_static_symbol_names(node)?;
1421 dedupe_preserve_order(&mut names);
1422 names.into_iter().next()
1423}
1424
1425fn collect_static_symbol_names(node: &Node) -> Option<Vec<String>> {
1426 match &node.kind {
1427 NodeKind::String { value, .. } => Some(expand_export_name_list(value)),
1428 NodeKind::Identifier { name } => Some(expand_export_name_list(name)),
1429 NodeKind::ArrayLiteral { elements } => {
1430 let mut names = Vec::new();
1431 for element in elements {
1432 let mut element_names = collect_static_symbol_names(element)?;
1433 names.append(&mut element_names);
1434 }
1435 Some(names)
1436 }
1437 NodeKind::Binary { op, left, right } if op == "," => {
1438 let mut names = collect_static_symbol_names(left)?;
1439 let mut right_names = collect_static_symbol_names(right)?;
1440 names.append(&mut right_names);
1441 Some(names)
1442 }
1443 _ => None,
1444 }
1445}
1446
1447fn resolve_exports(
1448 names: &[String],
1449 method_map: &HashMap<&str, SourceLocation>,
1450) -> (Vec<ResolvedExport>, Vec<String>) {
1451 let mut resolved = Vec::new();
1452 let mut unresolved = Vec::new();
1453 for raw_name in names {
1454 for name in expand_export_name_list(raw_name) {
1455 if let Some(location) = method_map.get(name.as_str()) {
1456 resolved.push(ResolvedExport { name, location: *location });
1457 } else {
1458 unresolved.push(name);
1459 }
1460 }
1461 }
1462 dedupe_resolved_exports(&mut resolved);
1463 dedupe_preserve_order(&mut unresolved);
1464 (resolved, unresolved)
1465}
1466
1467fn dedupe_resolved_exports(exports: &mut Vec<ResolvedExport>) {
1468 let mut seen = HashSet::new();
1469 exports.retain(|item| seen.insert(item.name.clone()));
1470}
1471
1472fn dedupe_preserve_order(items: &mut Vec<String>) {
1473 let mut seen = HashSet::new();
1474 items.retain(|item| seen.insert(item.clone()));
1475}
1476
1477fn merge_export_tags(
1478 target: &mut HashMap<String, Vec<String>>,
1479 updates: HashMap<String, Vec<String>>,
1480) {
1481 for (tag, mut symbols) in updates {
1482 target.entry(tag).or_default().append(&mut symbols);
1483 }
1484}
1485
1486fn flatten_comma_expression<'a>(node: &'a Node, out: &mut Vec<&'a Node>) {
1487 if let NodeKind::Binary { op, left, right } = &node.kind
1488 && op == ","
1489 {
1490 flatten_comma_expression(left, out);
1491 flatten_comma_expression(right, out);
1492 } else {
1493 out.push(node);
1494 }
1495}
1496
1497fn expand_export_name_list(raw: &str) -> Vec<String> {
1498 expand_symbol_list(raw).into_iter().filter_map(|name| normalize_export_name(&name)).collect()
1499}
1500
1501fn normalize_export_name(raw: &str) -> Option<String> {
1502 let normalized = normalize_symbol_name(raw)?;
1503 let stripped = normalized
1504 .strip_prefix('&')
1505 .or_else(|| normalized.strip_prefix('$'))
1506 .or_else(|| normalized.strip_prefix('@'))
1507 .or_else(|| normalized.strip_prefix('%'))
1508 .unwrap_or(&normalized)
1509 .to_string();
1510 if stripped.is_empty() { None } else { Some(stripped) }
1511}
1512
1513fn collect_accessor_names(node: &Node) -> Vec<String> {
1514 match &node.kind {
1515 NodeKind::String { value, .. } => expand_symbol_list(value),
1516 NodeKind::Identifier { name } => expand_symbol_list(name),
1517 NodeKind::ArrayLiteral { elements } => {
1518 elements.iter().flat_map(collect_accessor_names).collect()
1519 }
1520 _ => Vec::new(),
1521 }
1522}
1523
1524fn modifier_kind_from_name(name: &str) -> Option<ModifierKind> {
1525 match name {
1526 "before" => Some(ModifierKind::Before),
1527 "after" => Some(ModifierKind::After),
1528 "around" => Some(ModifierKind::Around),
1529 "override" => Some(ModifierKind::Override),
1530 "augment" => Some(ModifierKind::Augment),
1531 _ => None,
1532 }
1533}
1534
1535fn normalize_symbol_name(raw: &str) -> Option<String> {
1536 let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
1537 if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
1538}
1539
1540fn normalize_attribute_name(raw: &str) -> Option<String> {
1541 let trimmed = raw.trim();
1542 let without_override_prefix = trimmed.strip_prefix('+').unwrap_or(trimmed);
1543 normalize_symbol_name(without_override_prefix)
1544}
1545
1546fn expand_symbol_list(raw: &str) -> Vec<String> {
1547 let raw = raw.trim();
1548
1549 if raw.starts_with("qw(") && raw.ends_with(')') {
1550 let content = &raw[3..raw.len() - 1];
1551 return content
1552 .split_whitespace()
1553 .filter(|s| !s.is_empty())
1554 .map(|s| s.to_string())
1555 .collect();
1556 }
1557
1558 if raw.starts_with("qw") && raw.len() > 2 {
1559 let open = raw.chars().nth(2).unwrap_or(' ');
1560 let close = match open {
1561 '(' => ')',
1562 '{' => '}',
1563 '[' => ']',
1564 '<' => '>',
1565 c => c,
1566 };
1567 if let (Some(start), Some(end)) = (raw.find(open), raw.rfind(close))
1568 && start < end
1569 {
1570 let content = &raw[start + 1..end];
1571 return content
1572 .split_whitespace()
1573 .filter(|s| !s.is_empty())
1574 .map(|s| s.to_string())
1575 .collect();
1576 }
1577 }
1578
1579 normalize_symbol_name(raw).into_iter().collect()
1580}
1581
1582fn expand_arg_to_names(arg: &str) -> Vec<String> {
1588 let arg = arg.trim();
1589 if arg.starts_with("qw(") && arg.ends_with(')') {
1591 let content = &arg[3..arg.len() - 1];
1592 return content
1593 .split_whitespace()
1594 .filter(|s| !s.is_empty())
1595 .map(|s| s.to_string())
1596 .collect();
1597 }
1598 if arg.starts_with("qw") && arg.len() > 2 {
1600 let open = arg.chars().nth(2).unwrap_or(' ');
1601 let close = match open {
1602 '(' => ')',
1603 '{' => '}',
1604 '[' => ']',
1605 '<' => '>',
1606 c => c,
1607 };
1608 if let (Some(start), Some(end)) = (arg.find(open), arg.rfind(close)) {
1609 if start < end {
1610 let content = &arg[start + 1..end];
1611 return content
1612 .split_whitespace()
1613 .filter(|s| !s.is_empty())
1614 .map(|s| s.to_string())
1615 .collect();
1616 }
1617 }
1618 }
1619 normalize_symbol_name(arg).into_iter().collect()
1621}
1622
1623fn extract_class_tiny_attribute_names(args: &[String]) -> Vec<String> {
1624 let mut names = Vec::new();
1625 let mut seen = HashSet::new();
1626 let mut idx = 0;
1627
1628 while idx < args.len() {
1629 let token = args[idx].trim();
1630 match token {
1631 "" | "," | "=>" | "}" => {
1632 idx += 1;
1633 }
1634 "+" if args.get(idx + 1).map(String::as_str) == Some("{") => {
1635 idx = collect_class_tiny_hash_keys(args, idx + 1, &mut names, &mut seen);
1636 }
1637 "+{" | "{" => {
1638 idx = collect_class_tiny_hash_keys(args, idx, &mut names, &mut seen);
1639 }
1640 _ => {
1641 for raw_name in expand_arg_to_names(token) {
1642 push_class_tiny_attribute_name(&raw_name, &mut names, &mut seen);
1643 }
1644 idx += 1;
1645 }
1646 }
1647 }
1648
1649 names
1650}
1651
1652fn collect_class_tiny_hash_keys(
1653 args: &[String],
1654 start_idx: usize,
1655 names: &mut Vec<String>,
1656 seen: &mut HashSet<String>,
1657) -> usize {
1658 let mut idx = start_idx;
1659 let mut depth = 0usize;
1660
1661 while idx < args.len() {
1662 let token = args[idx].trim();
1663 match token {
1664 "+{" | "{" => {
1665 depth = depth.saturating_add(1);
1666 idx += 1;
1667 }
1668 "}" => {
1669 depth = depth.saturating_sub(1);
1670 idx += 1;
1671 if depth == 0 {
1672 break;
1673 }
1674 }
1675 _ if depth == 1 && args.get(idx + 1).map(String::as_str) == Some("=>") => {
1676 push_class_tiny_attribute_name(token, names, seen);
1677 idx += 2;
1678 }
1679 _ => {
1680 idx += 1;
1681 }
1682 }
1683 }
1684
1685 idx
1686}
1687
1688fn push_class_tiny_attribute_name(
1689 raw_name: &str,
1690 names: &mut Vec<String>,
1691 seen: &mut HashSet<String>,
1692) {
1693 let Some(name) = normalize_attribute_name(raw_name) else { return };
1694 if !is_class_tiny_attribute_name(&name) || !seen.insert(name.clone()) {
1695 return;
1696 }
1697 names.push(name);
1698}
1699
1700fn is_class_tiny_attribute_name(name: &str) -> bool {
1701 let mut chars = name.chars();
1702 let Some(first) = chars.next() else { return false };
1703 (first.is_ascii_alphabetic() || first == '_')
1704 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
1705}
1706
1707fn extract_hash_options(pairs: &[(Node, Node)]) -> HashMap<String, String> {
1708 let mut options = HashMap::new();
1709 for (key_node, value_node) in pairs {
1710 let Some(key_name) = collect_symbol_names(key_node).into_iter().next() else {
1711 continue;
1712 };
1713 let value_text = value_summary(value_node);
1714 options.insert(key_name, value_text);
1715 }
1716 options
1717}
1718
1719fn value_summary(node: &Node) -> String {
1720 match &node.kind {
1721 NodeKind::String { value, .. } => {
1722 normalize_symbol_name(value).unwrap_or_else(|| value.clone())
1723 }
1724 NodeKind::Identifier { name } => name.clone(),
1725 NodeKind::Number { value } => value.clone(),
1726 _ => "expr".to_string(),
1727 }
1728}
1729
1730#[cfg(test)]
1731mod tests {
1732 use super::*;
1733 use crate::parser::Parser;
1734 use perl_tdd_support::{must, must_some};
1735 use std::collections::HashSet;
1736
1737 fn build_models(code: &str) -> Vec<ClassModel> {
1738 let mut parser = Parser::new(code);
1739 let ast = must(parser.parse());
1740 ClassModelBuilder::new().build(&ast)
1741 }
1742
1743 fn find_model<'a>(models: &'a [ClassModel], name: &str) -> Option<&'a ClassModel> {
1744 models.iter().find(|m| m.name == name)
1745 }
1746
1747 fn has_method(
1748 model: &ClassModel,
1749 name: &str,
1750 synthetic: bool,
1751 accessor_mode: Option<ClassAccessorMode>,
1752 ) -> bool {
1753 model.methods.iter().any(|method| {
1754 method.name == name
1755 && method.synthetic == synthetic
1756 && method.accessor_mode == accessor_mode
1757 })
1758 }
1759
1760 #[test]
1761 fn basic_moo_class() {
1762 let models = build_models(
1763 r#"
1764package MyApp::User;
1765use Moo;
1766
1767has 'name' => (is => 'ro', isa => 'Str');
1768has 'age' => (is => 'rw', required => 1);
1769
1770sub greet { }
1771"#,
1772 );
1773
1774 let model = find_model(&models, "MyApp::User");
1775 assert!(model.is_some(), "expected ClassModel for MyApp::User");
1776 let model = model.unwrap();
1777
1778 assert_eq!(model.framework, Framework::Moo);
1779 assert_eq!(model.attributes.len(), 2);
1780
1781 let name_attr = model.attributes.iter().find(|a| a.name == "name");
1782 assert!(name_attr.is_some());
1783 let name_attr = name_attr.unwrap();
1784 assert_eq!(name_attr.is, Some(AccessorType::Ro));
1785 assert_eq!(name_attr.isa.as_deref(), Some("Str"));
1786 assert!(!name_attr.required);
1787 assert_eq!(name_attr.accessor_name, "name");
1788
1789 let age_attr = model.attributes.iter().find(|a| a.name == "age");
1790 assert!(age_attr.is_some());
1791 let age_attr = age_attr.unwrap();
1792 assert_eq!(age_attr.is, Some(AccessorType::Rw));
1793 assert!(age_attr.required);
1794
1795 assert!(model.methods.iter().any(|m| m.name == "greet"));
1796 }
1797
1798 #[test]
1799 fn moose_extends_and_with() {
1800 let models = build_models(
1801 r#"
1802package MyApp::Admin;
1803use Moose;
1804extends 'MyApp::User';
1805with 'MyApp::Printable', 'MyApp::Serializable';
1806
1807has 'level' => (is => 'ro');
1808"#,
1809 );
1810
1811 let model = find_model(&models, "MyApp::Admin");
1812 assert!(model.is_some());
1813 let model = model.unwrap();
1814
1815 assert_eq!(model.framework, Framework::Moose);
1816 assert!(model.parents.contains(&"MyApp::User".to_string()));
1817 assert_eq!(model.roles, vec!["MyApp::Printable", "MyApp::Serializable"]);
1818 assert_eq!(model.attributes.len(), 1);
1819 }
1820
1821 #[test]
1822 fn mro_pragma_tracks_c3_and_reset() {
1823 let models = build_models(
1824 r#"
1825package Example::Child;
1826use parent 'Example::Base';
1827use mro 'c3';
1828sub greet { }
1829
1830package Example::Sibling;
1831use parent 'Example::Base';
1832no mro;
1833sub greet { }
1834"#,
1835 );
1836
1837 let child = find_model(&models, "Example::Child").expect("expected ClassModel for Child");
1838 assert_eq!(child.mro, MethodResolutionOrder::C3);
1839
1840 let sibling =
1841 find_model(&models, "Example::Sibling").expect("expected ClassModel for Sibling");
1842 assert_eq!(sibling.mro, MethodResolutionOrder::Dfs);
1843 }
1844
1845 #[test]
1846 fn method_modifiers() {
1847 let models = build_models(
1848 r#"
1849package MyApp::User;
1850use Moo;
1851before 'save' => sub { };
1852after 'save' => sub { };
1853around 'validate' => sub { };
1854override 'dispatch' => sub { super(); };
1855augment 'serialize' => sub { inner(); };
1856"#,
1857 );
1858
1859 let model = find_model(&models, "MyApp::User");
1860 assert!(model.is_some());
1861 let model = model.unwrap();
1862
1863 assert_eq!(model.modifiers.len(), 5);
1864 assert!(
1865 model
1866 .modifiers
1867 .iter()
1868 .any(|m| m.kind == ModifierKind::Before && m.method_name == "save")
1869 );
1870 assert!(
1871 model
1872 .modifiers
1873 .iter()
1874 .any(|m| m.kind == ModifierKind::After && m.method_name == "save")
1875 );
1876 assert!(
1877 model
1878 .modifiers
1879 .iter()
1880 .any(|m| m.kind == ModifierKind::Around && m.method_name == "validate")
1881 );
1882 assert!(
1883 model
1884 .modifiers
1885 .iter()
1886 .any(|m| m.kind == ModifierKind::Override && m.method_name == "dispatch")
1887 );
1888 assert!(
1889 model
1890 .modifiers
1891 .iter()
1892 .any(|m| m.kind == ModifierKind::Augment && m.method_name == "serialize")
1893 );
1894 }
1895
1896 #[test]
1897 fn class_accessor_generates_synthetic_methods_for_all_variants() {
1898 let models = build_models(
1899 r#"
1900package Example::Accessors;
1901use parent 'Class::Accessor';
1902__PACKAGE__->mk_accessors(qw(foo bar));
1903__PACKAGE__->mk_rw_accessors(qw(baz));
1904sub other_method { }
1905
1906package Example::ReadOnly;
1907use parent 'Class::Accessor';
1908my $package = __PACKAGE__;
1909$package->mk_ro_accessors('id');
1910
1911package Example::WriteOnly;
1912use parent 'Class::Accessor';
1913my $package = 'Example::WriteOnly';
1914$package->mk_wo_accessors([qw(token)]);
1915"#,
1916 );
1917
1918 let accessors = find_model(&models, "Example::Accessors")
1919 .expect("expected ClassModel for Example::Accessors");
1920 assert!(has_method(accessors, "foo", true, Some(ClassAccessorMode::Rw)));
1921 assert!(has_method(accessors, "bar", true, Some(ClassAccessorMode::Rw)));
1922 assert!(has_method(accessors, "baz", true, Some(ClassAccessorMode::Rw)));
1923 assert!(has_method(accessors, "other_method", false, None));
1924
1925 let read_only = find_model(&models, "Example::ReadOnly")
1926 .expect("expected ClassModel for Example::ReadOnly");
1927 assert!(has_method(read_only, "id", true, Some(ClassAccessorMode::Ro)));
1928
1929 let write_only = find_model(&models, "Example::WriteOnly")
1930 .expect("expected ClassModel for Example::WriteOnly");
1931 assert!(has_method(write_only, "token", true, Some(ClassAccessorMode::Wo)));
1932 }
1933
1934 #[test]
1935 fn no_model_for_plain_package() {
1936 let models = build_models(
1937 r#"
1938package MyApp::Utils;
1939sub helper { 1 }
1940"#,
1941 );
1942
1943 assert!(
1944 find_model(&models, "MyApp::Utils").is_none(),
1945 "plain package should not produce a ClassModel"
1946 );
1947 }
1948
1949 #[test]
1950 fn multiple_packages() {
1951 let models = build_models(
1952 r#"
1953package MyApp::User;
1954use Moo;
1955has 'name' => (is => 'ro');
1956
1957package MyApp::Admin;
1958use Moose;
1959extends 'MyApp::User';
1960has 'level' => (is => 'rw');
1961
1962package MyApp::Utils;
1963sub helper { 1 }
1964"#,
1965 );
1966
1967 assert_eq!(models.len(), 2, "expected 2 ClassModels (User + Admin, not Utils)");
1968 assert!(find_model(&models, "MyApp::User").is_some());
1969 assert!(find_model(&models, "MyApp::Admin").is_some());
1970 assert!(find_model(&models, "MyApp::Utils").is_none());
1971 }
1972
1973 #[test]
1974 fn qw_attribute_list() {
1975 let models = build_models(
1976 r#"
1977use Moo;
1978has [qw(first_name last_name)] => (is => 'ro');
1979"#,
1980 );
1981
1982 assert_eq!(models.len(), 1);
1983 let model = &models[0];
1984 assert_eq!(model.attributes.len(), 2);
1985
1986 let names: HashSet<_> = model.attributes.iter().map(|a| a.name.as_str()).collect();
1987 assert!(names.contains("first_name"));
1988 assert!(names.contains("last_name"));
1989 }
1990
1991 #[test]
1992 fn has_framework_false_for_none() {
1993 let model = ClassModel {
1994 name: "No::Framework".to_string(),
1995 framework: Framework::None,
1996 attributes: Vec::new(),
1997 fields: Vec::new(),
1998 methods: Vec::new(),
1999 adjusts: Vec::new(),
2000 parents: Vec::new(),
2001 mro: MethodResolutionOrder::Dfs,
2002 roles: Vec::new(),
2003 modifiers: Vec::new(),
2004 exports: Vec::new(),
2005 export_ok: Vec::new(),
2006 exporter_metadata: None,
2007 };
2008
2009 assert!(!model.has_framework());
2010 }
2011
2012 #[test]
2013 fn expand_symbol_list_supports_qw_angle_delimiters() {
2014 assert_eq!(expand_symbol_list("qw<alpha beta gamma>"), vec!["alpha", "beta", "gamma"]);
2015 }
2016
2017 #[test]
2018 fn has_framework_helper() {
2019 let models = build_models(
2020 r#"
2021package MyApp::User;
2022use Moo;
2023has 'name' => (is => 'ro');
2024"#,
2025 );
2026
2027 let model = find_model(&models, "MyApp::User").unwrap();
2028 assert!(model.has_framework());
2029 }
2030
2031 #[test]
2032 fn accessor_type_lazy() {
2033 let models = build_models(
2034 r#"
2035use Moo;
2036has 'config' => (is => 'lazy');
2037"#,
2038 );
2039
2040 let model = &models[0];
2041 assert_eq!(model.attributes[0].is, Some(AccessorType::Lazy));
2042 assert!(model.attributes[0].default, "lazy implies default");
2043 }
2044
2045 #[test]
2046 fn explicit_accessor_name() {
2047 let models = build_models(
2048 r#"
2049use Moo;
2050has 'name' => (is => 'ro', reader => 'get_name');
2051"#,
2052 );
2053
2054 let model = &models[0];
2055 assert_eq!(model.attributes[0].accessor_name, "get_name");
2056 }
2057
2058 #[test]
2059 fn inherited_attribute_override_strips_plus_prefix() {
2060 let models = build_models(
2061 r#"
2062use Moo;
2063has '+name' => (is => 'ro', builder => 1, predicate => 1, clearer => 1);
2064"#,
2065 );
2066
2067 let model = &models[0];
2068 let attr = &model.attributes[0];
2069 assert_eq!(attr.name, "name");
2070 assert_eq!(attr.accessor_name, "name");
2071 assert_eq!(attr.builder.as_deref(), Some("_build_name"));
2072 assert_eq!(attr.predicate.as_deref(), Some("has_name"));
2073 assert_eq!(attr.clearer.as_deref(), Some("clear_name"));
2074 }
2075
2076 #[test]
2077 fn default_via_builder_option() {
2078 let models = build_models(
2079 r#"
2080use Moo;
2081has 'config' => (is => 'ro', builder => 1);
2082"#,
2083 );
2084
2085 let model = &models[0];
2086 assert!(model.attributes[0].default, "builder option implies default");
2087 }
2088
2089 #[test]
2090 fn lazy_builder_with_string_name() {
2091 let models = build_models(
2092 r#"
2093use Moo;
2094has 'config' => (is => 'ro', lazy => 1, builder => '_build_config');
2095"#,
2096 );
2097
2098 let model = &models[0];
2099 let attr = &model.attributes[0];
2100 assert_eq!(
2101 attr.builder.as_deref(),
2102 Some("_build_config"),
2103 "builder string should be captured"
2104 );
2105 assert!(attr.default, "named builder implies default");
2106 }
2107
2108 #[test]
2109 fn lazy_builder_with_numeric_one_generates_default_name() {
2110 let models = build_models(
2111 r#"
2112use Moo;
2113has 'profile' => (is => 'ro', builder => 1);
2114"#,
2115 );
2116
2117 let model = &models[0];
2118 let attr = &model.attributes[0];
2119 assert_eq!(
2120 attr.builder.as_deref(),
2121 Some("_build_profile"),
2122 "builder => 1 should derive builder name as '_build_<attr>'"
2123 );
2124 }
2125
2126 #[test]
2127 fn predicate_with_string_name() {
2128 let models = build_models(
2129 r#"
2130use Moo;
2131has 'name' => (is => 'ro', predicate => 'has_name');
2132"#,
2133 );
2134
2135 let model = &models[0];
2136 let attr = &model.attributes[0];
2137 assert_eq!(
2138 attr.predicate.as_deref(),
2139 Some("has_name"),
2140 "predicate string name should be captured"
2141 );
2142 }
2143
2144 #[test]
2145 fn predicate_with_numeric_one_generates_default_name() {
2146 let models = build_models(
2147 r#"
2148use Moo;
2149has 'name' => (is => 'ro', predicate => 1);
2150"#,
2151 );
2152
2153 let model = &models[0];
2154 let attr = &model.attributes[0];
2155 assert_eq!(
2156 attr.predicate.as_deref(),
2157 Some("has_name"),
2158 "predicate => 1 should derive predicate name as 'has_<attr>'"
2159 );
2160 }
2161
2162 #[test]
2163 fn clearer_with_string_name() {
2164 let models = build_models(
2165 r#"
2166use Moo;
2167has 'name' => (is => 'rw', clearer => 'clear_name');
2168"#,
2169 );
2170
2171 let model = &models[0];
2172 let attr = &model.attributes[0];
2173 assert_eq!(
2174 attr.clearer.as_deref(),
2175 Some("clear_name"),
2176 "clearer string name should be captured"
2177 );
2178 }
2179
2180 #[test]
2181 fn clearer_with_numeric_one_generates_default_name() {
2182 let models = build_models(
2183 r#"
2184use Moo;
2185has 'name' => (is => 'rw', clearer => 1);
2186"#,
2187 );
2188
2189 let model = &models[0];
2190 let attr = &model.attributes[0];
2191 assert_eq!(
2192 attr.clearer.as_deref(),
2193 Some("clear_name"),
2194 "clearer => 1 should derive clearer name as 'clear_<attr>'"
2195 );
2196 }
2197
2198 #[test]
2199 fn coerce_flag_true() {
2200 let models = build_models(
2201 r#"
2202use Moose;
2203has 'age' => (is => 'rw', isa => 'Int', coerce => 1);
2204"#,
2205 );
2206
2207 let model = &models[0];
2208 let attr = &model.attributes[0];
2209 assert!(attr.coerce, "coerce => 1 should set coerce flag");
2210 }
2211
2212 #[test]
2213 fn coerce_flag_false_when_absent() {
2214 let models = build_models(
2215 r#"
2216use Moose;
2217has 'age' => (is => 'rw', isa => 'Int');
2218"#,
2219 );
2220
2221 let model = &models[0];
2222 let attr = &model.attributes[0];
2223 assert!(!attr.coerce, "coerce should be false when not specified");
2224 }
2225
2226 #[test]
2227 fn trigger_flag_true() {
2228 let models = build_models(
2229 r#"
2230use Moose;
2231has 'name' => (is => 'rw', trigger => \&_on_name_change);
2232"#,
2233 );
2234
2235 let model = &models[0];
2236 let attr = &model.attributes[0];
2237 assert!(attr.trigger, "trigger option should set trigger flag");
2238 }
2239
2240 #[test]
2241 fn trigger_flag_false_when_absent() {
2242 let models = build_models(
2243 r#"
2244use Moose;
2245has 'name' => (is => 'rw');
2246"#,
2247 );
2248
2249 let model = &models[0];
2250 let attr = &model.attributes[0];
2251 assert!(!attr.trigger, "trigger should be false when not specified");
2252 }
2253
2254 #[test]
2257 fn native_class_produces_model() {
2258 let models = build_models(
2259 r#"
2260class MyApp::Point {
2261 field $x :param = 0;
2262 field $y :param = 0;
2263 method get_x { return $x; }
2264 method get_y { return $y; }
2265}
2266"#,
2267 );
2268 assert_eq!(models.len(), 1, "expected one ClassModel for MyApp::Point");
2269 let model = &models[0];
2270 assert_eq!(model.name, "MyApp::Point");
2271 assert_eq!(model.framework, Framework::NativeClass);
2272 assert_eq!(model.methods.len(), 2);
2273 assert!(model.methods.iter().any(|m| m.name == "get_x"));
2274 assert!(model.methods.iter().any(|m| m.name == "get_y"));
2275 }
2276
2277 #[test]
2278 fn native_class_and_moo_class_do_not_interfere() {
2279 let models = build_models(
2280 r#"
2281class Native::Point {
2282 field $x :param = 0;
2283 method get_x { return $x; }
2284}
2285
2286package Moo::User;
2287use Moo;
2288has 'name' => (is => 'ro');
2289"#,
2290 );
2291 assert_eq!(models.len(), 2, "expected 2 ClassModels: Native::Point and Moo::User");
2292 let native = models.iter().find(|m| m.name == "Native::Point");
2293 assert!(native.is_some(), "expected Native::Point model");
2294 let native = native.unwrap();
2295 assert_eq!(native.framework, Framework::NativeClass);
2296 let moo = models.iter().find(|m| m.name == "Moo::User");
2297 assert!(moo.is_some(), "expected Moo::User model");
2298 let moo = moo.unwrap();
2299 assert_eq!(moo.framework, Framework::Moo);
2300 }
2301
2302 #[test]
2303 fn object_pad_fields_and_accessors_are_tracked() {
2304 let models = build_models(
2305 r#"
2306use Object::Pad;
2307
2308class Point {
2309 field $x :param :reader = 0;
2310 field $y :param :writer = 1;
2311
2312 method move { }
2313}
2314"#,
2315 );
2316
2317 assert!(
2318 !models.is_empty(),
2319 "expected at least one model, got {:?}",
2320 models.iter().map(|m| (&m.name, m.framework)).collect::<Vec<_>>()
2321 );
2322 let model = find_model(&models, "Point").expect("Point model");
2323 assert_eq!(model.framework, Framework::ObjectPad);
2324 assert_eq!(model.fields.len(), 2);
2325 assert!(has_method(model, "x", true, None));
2326 assert!(has_method(model, "set_y", true, None));
2327 assert!(has_method(model, "move", false, None));
2328
2329 let x = model.fields.iter().find(|field| field.name == "x").unwrap();
2330 assert!(x.param);
2331 assert_eq!(x.reader.as_deref(), Some("x"));
2332 assert_eq!(x.default.as_deref(), Some("0"));
2333
2334 let y = model.fields.iter().find(|field| field.name == "y").unwrap();
2335 assert!(y.param);
2336 assert_eq!(y.writer.as_deref(), Some("set_y"));
2337 assert_eq!(y.default.as_deref(), Some("1"));
2338
2339 let param_names: Vec<_> = model.object_pad_param_field_names().collect();
2340 assert_eq!(param_names, vec!["x", "y"]);
2341 }
2342
2343 #[test]
2344 fn object_pad_adjust_blocks_are_tracked() {
2345 let models = build_models(
2346 r#"
2347use Object::Pad;
2348
2349class Config {
2350 ADJUST {
2351 my $tmp = 1;
2352 }
2353}
2354"#,
2355 );
2356
2357 let model = find_model(&models, "Config").expect("Config model");
2358 assert_eq!(model.framework, Framework::ObjectPad);
2359 assert_eq!(model.adjusts.len(), 1, "expected one ADJUST block");
2360 assert_eq!(model.adjusts[0].name, "ADJUST");
2361 assert!(model.adjusts[0].synthetic, "ADJUST should be modeled as synthetic");
2362 }
2363
2364 #[test]
2365 fn object_pad_param_field_names_exclude_non_param_fields() {
2366 let models = build_models(
2367 r#"
2368use Object::Pad;
2369
2370class Config {
2371 field $name :param;
2372 field $cache = 1;
2373}
2374"#,
2375 );
2376
2377 let model = find_model(&models, "Config").expect("Config model");
2378 let param_names: Vec<_> = model.object_pad_param_field_names().collect();
2379 assert_eq!(param_names, vec!["name"]);
2380 }
2381
2382 #[test]
2383 fn object_pad_generated_names_follow_documented_defaults() {
2384 let models = build_models(
2385 r#"
2386use Object::Pad;
2387
2388class Defaults {
2389 field $_secret :reader :writer :accessor :mutator;
2390}
2391"#,
2392 );
2393
2394 let model = find_model(&models, "Defaults").expect("Defaults model");
2395 let field = model.fields.iter().find(|field| field.name == "_secret").unwrap();
2396
2397 assert_eq!(field.reader.as_deref(), Some("secret"));
2398 assert_eq!(field.writer.as_deref(), Some("set_secret"));
2399 assert_eq!(field.accessor.as_deref(), Some("secret"));
2400 assert_eq!(field.mutator.as_deref(), Some("secret"));
2401
2402 assert!(has_method(model, "secret", true, None));
2403 assert!(has_method(model, "set_secret", true, None));
2404 }
2405
2406 #[test]
2407 fn all_advanced_options_together() {
2408 let models = build_models(
2409 r#"
2410use Moo;
2411has 'status' => (
2412 is => 'rw',
2413 isa => 'Str',
2414 builder => '_build_status',
2415 coerce => 1,
2416 predicate => 'has_status',
2417 clearer => 'clear_status',
2418 trigger => \&_on_status_change,
2419);
2420"#,
2421 );
2422
2423 let model = &models[0];
2424 let attr = &model.attributes[0];
2425 assert_eq!(attr.builder.as_deref(), Some("_build_status"));
2426 assert!(attr.coerce);
2427 assert_eq!(attr.predicate.as_deref(), Some("has_status"));
2428 assert_eq!(attr.clearer.as_deref(), Some("clear_status"));
2429 assert!(attr.trigger);
2430 }
2431
2432 #[test]
2435 fn use_parent_plain_oo() {
2436 let code = "package Child; use parent 'Parent'; sub greet { } 1;";
2437 let models = build_models(code);
2438 let model = find_model(&models, "Child").expect("Child model");
2439 assert_eq!(model.framework, Framework::PlainOO);
2440 assert!(model.parents.contains(&"Parent".to_string()), "parents should contain 'Parent'");
2441 }
2442
2443 #[test]
2444 fn use_parent_multiple() {
2445 let code = "package Child; use parent qw(Base1 Base2); 1;";
2446 let models = build_models(code);
2447 let model = find_model(&models, "Child").expect("Child model");
2448 assert_eq!(model.framework, Framework::PlainOO);
2449 assert!(model.parents.contains(&"Base1".to_string()), "parents should contain Base1");
2450 assert!(model.parents.contains(&"Base2".to_string()), "parents should contain Base2");
2451 }
2452
2453 #[test]
2454 fn isa_array_assignment() {
2455 let code = "package Child; our @ISA = qw(Parent); sub greet { } 1;";
2456 let models = build_models(code);
2457 let model = find_model(&models, "Child").expect("Child model");
2458 assert!(
2459 model.parents.contains(&"Parent".to_string()),
2460 "parents should contain 'Parent' from @ISA"
2461 );
2462 }
2463
2464 #[test]
2465 fn use_parent_norequire() {
2466 let code = "package Child; use parent -norequire, 'Base'; 1;";
2468 let models = build_models(code);
2469 let model = find_model(&models, "Child").expect("Child model");
2470 assert!(
2471 model.parents.contains(&"Base".to_string()),
2472 "parents should contain 'Base' even with -norequire"
2473 );
2474 }
2475
2476 #[test]
2477 fn use_base_plain_oo() {
2478 let code = "package Child; use base 'Parent'; sub greet { } 1;";
2479 let models = build_models(code);
2480 let model = find_model(&models, "Child").expect("Child model");
2481 assert_eq!(model.framework, Framework::PlainOO);
2482 assert!(
2483 model.parents.contains(&"Parent".to_string()),
2484 "parents should contain 'Parent' from use base"
2485 );
2486 }
2487
2488 #[test]
2489 fn plain_oo_does_not_regress_moose_extends() {
2490 let models = build_models(
2492 r#"
2493package MyApp::Admin;
2494use Moose;
2495extends 'MyApp::User';
2496has 'level' => (is => 'ro');
2497"#,
2498 );
2499 let model = find_model(&models, "MyApp::Admin").expect("Admin model");
2500 assert_eq!(model.framework, Framework::Moose);
2501 assert!(
2502 model.parents.contains(&"MyApp::User".to_string()),
2503 "Moose extends should still populate parents"
2504 );
2505 }
2506
2507 #[test]
2510 fn export_array_captured() {
2511 let code = "package MyUtils;\nour @EXPORT = qw(foo bar);\nour @EXPORT_OK = qw(baz);\nsub foo {}\nsub bar {}\nsub baz {}\n1;";
2512 let models = build_models(code);
2513 let model = find_model(&models, "MyUtils").expect("MyUtils model");
2514 assert_eq!(model.exports, vec!["foo".to_string(), "bar".to_string()]);
2515 assert_eq!(model.export_ok, vec!["baz".to_string()]);
2516 }
2517
2518 #[test]
2519 fn export_non_oo_package_produces_model() {
2520 let code = "package MyUtils;\nour @EXPORT = qw(helper);\nsub helper { 1 }\n1;";
2521 let models = build_models(code);
2522 assert!(
2523 find_model(&models, "MyUtils").is_some(),
2524 "export-only package must produce a model"
2525 );
2526 }
2527
2528 #[test]
2529 fn export_ok_assignment_without_our() {
2530 let code = "package MyLib;\n@EXPORT_OK = qw(util_a util_b);\n1;";
2531 let models = build_models(code);
2532 let model = find_model(&models, "MyLib").expect("MyLib model");
2533 assert_eq!(model.export_ok, vec!["util_a".to_string(), "util_b".to_string()]);
2534 }
2535
2536 #[test]
2537 fn export_assignment_without_our() {
2538 let code = "package MyLib;\n@EXPORT = qw(func_a func_b);\n1;";
2540 let models = build_models(code);
2541 let model = find_model(&models, "MyLib").expect("MyLib model");
2542 assert_eq!(model.exports, vec!["func_a".to_string(), "func_b".to_string()]);
2543 }
2544
2545 #[test]
2546 fn exporter_metadata_resolves_export_and_export_ok() {
2547 let code = r#"
2548package MyUtils;
2549use Exporter 'import';
2550our @EXPORT = qw(foo missing_default);
2551our @EXPORT_OK = ('bar', "missing_ok");
2552sub foo { 1 }
2553sub bar { 1 }
25541;
2555"#;
2556 let models = build_models(code);
2557 let model = find_model(&models, "MyUtils").expect("MyUtils model");
2558 let metadata = model.exporter_metadata.as_ref().expect("exporter metadata");
2559
2560 assert_eq!(
2561 metadata.exports.iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
2562 vec!["foo"]
2563 );
2564 assert_eq!(
2565 metadata.export_ok.iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
2566 vec!["bar"]
2567 );
2568 assert!(metadata.unresolved.contains(&"missing_default".to_string()));
2569 assert!(metadata.unresolved.contains(&"missing_ok".to_string()));
2570 }
2571
2572 #[test]
2573 fn exporter_metadata_resolves_export_tags() {
2574 let code = r#"
2575package MyTags;
2576use parent 'Exporter';
2577our %EXPORT_TAGS = (
2578 util => [qw(one two missing)],
2579 misc => ['three'],
2580);
2581sub one { 1 }
2582sub two { 1 }
2583sub three { 1 }
25841;
2585"#;
2586 let models = build_models(code);
2587 let model = find_model(&models, "MyTags").expect("MyTags model");
2588 let metadata = model.exporter_metadata.as_ref().expect("exporter metadata");
2589
2590 let util_names = metadata
2591 .export_tags
2592 .get("util")
2593 .expect("util tag")
2594 .iter()
2595 .map(|item| item.name.as_str())
2596 .collect::<Vec<_>>();
2597 let misc_names = metadata
2598 .export_tags
2599 .get("misc")
2600 .expect("misc tag")
2601 .iter()
2602 .map(|item| item.name.as_str())
2603 .collect::<Vec<_>>();
2604
2605 assert_eq!(util_names, vec!["one", "two"]);
2606 assert_eq!(misc_names, vec!["three"]);
2607 assert!(metadata.unresolved.contains(&"missing".to_string()));
2608 }
2609
2610 #[test]
2611 fn export_lists_without_exporter_usage_do_not_produce_exporter_metadata() {
2612 let code = r#"
2613package NoExporter;
2614our @EXPORT = qw(foo);
2615our @EXPORT_OK = qw(bar);
2616our %EXPORT_TAGS = (all => [qw(foo bar)]);
2617sub foo { 1 }
2618sub bar { 1 }
26191;
2620"#;
2621 let models = build_models(code);
2622 let model = find_model(&models, "NoExporter").expect("NoExporter model");
2623 assert!(model.exporter_metadata.is_none());
2624 }
2625
2626 #[test]
2629 fn push_isa_single_parent() {
2630 let code = "package Child;\npush @ISA, 'Parent';\n1;";
2631 let models = build_models(code);
2632 let model = find_model(&models, "Child").expect("Child model");
2633 assert!(model.parents.contains(&"Parent".to_string()), "push @ISA must capture parent");
2634 assert_eq!(model.framework, Framework::PlainOO);
2635 }
2636
2637 #[test]
2638 fn push_isa_multiple_parents() {
2639 let code = "package Child;\npush @ISA, 'Base1', 'Base2';\n1;";
2640 let models = build_models(code);
2641 let model = find_model(&models, "Child").expect("Child model");
2642 assert!(model.parents.contains(&"Base1".to_string()));
2643 assert!(model.parents.contains(&"Base2".to_string()));
2644 }
2645
2646 #[test]
2647 fn push_isa_does_not_downgrade_moose_framework() {
2648 let code = "package Child;\nuse Moose;\nextends 'Base';\npush @ISA, 'Extra';\n1;";
2650 let models = build_models(code);
2651 let model = find_model(&models, "Child").expect("Child model");
2652 assert_eq!(model.framework, Framework::Moose, "Moose must not be downgraded to PlainOO");
2653 assert!(
2654 model.parents.contains(&"Extra".to_string()),
2655 "push @ISA parent must still be captured"
2656 );
2657 }
2658
2659 #[test]
2662 fn native_class_with_isa_has_correct_parent() {
2663 let models = build_models(
2664 r#"
2665class Point3D :isa(Point) {
2666 field $z :param = 0;
2667 method get_z { return $z; }
2668}
2669"#,
2670 );
2671 assert_eq!(models.len(), 1, "expected one ClassModel for Point3D");
2672 let model = &models[0];
2673 assert_eq!(model.name, "Point3D");
2674 assert_eq!(model.framework, Framework::NativeClass);
2675 assert!(
2676 model.parents.contains(&"Point".to_string()),
2677 "native class :isa(Point) must populate parents, got {:?}",
2678 model.parents
2679 );
2680 }
2681
2682 #[test]
2683 fn native_class_with_multiple_isa_has_all_parents() {
2684 let models = build_models(
2685 r#"
2686class Shape3D :isa(Shape) :isa(Printable) {
2687 field $z :param = 0;
2688}
2689"#,
2690 );
2691 assert_eq!(models.len(), 1, "expected one ClassModel for Shape3D");
2692 let model = &models[0];
2693 assert_eq!(model.framework, Framework::NativeClass);
2694 assert!(
2695 model.parents.contains(&"Shape".to_string()),
2696 "expected 'Shape' in parents, got {:?}",
2697 model.parents
2698 );
2699 assert!(
2700 model.parents.contains(&"Printable".to_string()),
2701 "expected 'Printable' in parents, got {:?}",
2702 model.parents
2703 );
2704 }
2705
2706 #[test]
2707 fn native_class_without_isa_has_no_parents() {
2708 let models = build_models(
2709 r#"
2710class Point {
2711 field $x :param = 0;
2712 field $y :param = 0;
2713}
2714"#,
2715 );
2716 assert_eq!(models.len(), 1);
2717 let model = &models[0];
2718 assert_eq!(model.framework, Framework::NativeClass);
2719 assert!(
2720 model.parents.is_empty(),
2721 "class without :isa must have no parents, got {:?}",
2722 model.parents
2723 );
2724 }
2725
2726 #[test]
2727 fn native_class_with_qualified_isa_has_qualified_parent() {
2728 let models = build_models(
2729 r#"
2730class MyApp::Point3D :isa(MyApp::Point) {
2731 field $z :param = 0;
2732}
2733"#,
2734 );
2735 assert_eq!(models.len(), 1);
2736 let model = &models[0];
2737 assert_eq!(model.name, "MyApp::Point3D");
2738 assert!(
2739 model.parents.contains(&"MyApp::Point".to_string()),
2740 "qualified :isa must preserve qualified name, got {:?}",
2741 model.parents
2742 );
2743 }
2744
2745 #[test]
2746 fn second_class_without_isa_does_not_inherit_first_class_parents() {
2747 let models = build_models(
2750 r#"
2751class Point3D :isa(Point) {
2752 field $z :param = 0;
2753}
2754class Standalone {
2755 field $x :param = 0;
2756}
2757"#,
2758 );
2759 let standalone = models.iter().find(|m| m.name == "Standalone").expect("Standalone model");
2760 assert!(
2761 standalone.parents.is_empty(),
2762 "Standalone class must have no parents, but got {:?}",
2763 standalone.parents
2764 );
2765 }
2766
2767 #[test]
2770 fn class_tiny_framework_detected() {
2771 let models = build_models(
2772 r#"
2773package MyApp::Point;
2774use Class::Tiny;
2775"#,
2776 );
2777 let model = must_some(find_model(&models, "MyApp::Point"));
2778 assert_eq!(model.framework, Framework::ClassTiny);
2779 }
2780
2781 #[test]
2782 fn class_tiny_rw_framework_detected() {
2783 let models = build_models(
2784 r#"
2785package MyApp::Config;
2786use Class::Tiny::RW;
2787"#,
2788 );
2789 let model = must_some(find_model(&models, "MyApp::Config"));
2790 assert_eq!(model.framework, Framework::ClassTiny);
2791 }
2792
2793 #[test]
2794 fn class_tiny_qw_use_attrs_extracted() {
2795 let models = build_models(
2796 r#"
2797package MyApp::Person;
2798use Class::Tiny qw(name age email);
2799"#,
2800 );
2801 let model = must_some(find_model(&models, "MyApp::Person"));
2802 assert_eq!(model.framework, Framework::ClassTiny);
2803 assert_eq!(model.attributes.len(), 3, "expected 3 attributes from qw list");
2804
2805 let names: HashSet<_> = model.attributes.iter().map(|a| a.name.as_str()).collect();
2806 assert!(names.contains("name"), "expected attribute 'name'");
2807 assert!(names.contains("age"), "expected attribute 'age'");
2808 assert!(names.contains("email"), "expected attribute 'email'");
2809
2810 for attr in &model.attributes {
2812 assert_eq!(
2813 attr.is,
2814 Some(AccessorType::Rw),
2815 "qw-list attribute '{}' should be rw",
2816 attr.name
2817 );
2818 }
2819 }
2820
2821 #[test]
2822 fn class_tiny_use_attrs_include_hashref_defaults() {
2823 let models = build_models(
2824 r#"
2825package MyApp::Employee;
2826use Class::Tiny qw(name ssn), {
2827 timestamp => sub { time },
2828 title => 'Peon',
2829};
2830"#,
2831 );
2832 let model = must_some(find_model(&models, "MyApp::Employee"));
2833 let names: HashSet<_> = model.attributes.iter().map(|attr| attr.name.as_str()).collect();
2834 assert_eq!(model.attributes.len(), 4);
2835 assert!(names.contains("name"));
2836 assert!(names.contains("ssn"));
2837 assert!(names.contains("timestamp"));
2838 assert!(names.contains("title"));
2839 assert!(!names.contains("time"));
2840 assert!(!names.contains("Peon"));
2841 }
2842
2843 #[test]
2844 fn class_tiny_rw_qw_use_attrs_are_rw() {
2845 let models = build_models(
2846 r#"
2847package MyApp::Widget;
2848use Class::Tiny::RW qw(width height);
2849"#,
2850 );
2851 let model = must_some(find_model(&models, "MyApp::Widget"));
2852 assert_eq!(model.attributes.len(), 2);
2853 for attr in &model.attributes {
2854 assert_eq!(attr.is, Some(AccessorType::Rw));
2855 }
2856 }
2857
2858 #[test]
2859 fn class_tiny_bare_has_declaration() {
2860 let models = build_models(
2862 r#"
2863package MyApp::Tag;
2864use Class::Tiny;
2865has 'label';
2866has 'color';
2867"#,
2868 );
2869 let model = must_some(find_model(&models, "MyApp::Tag"));
2870 assert_eq!(model.attributes.len(), 2, "expected 2 attributes from bare has");
2871
2872 let label = must_some(model.attributes.iter().find(|a| a.name == "label"));
2873 assert_eq!(label.accessor_name, "label");
2874
2875 let color = model.attributes.iter().find(|a| a.name == "color");
2876 assert!(color.is_some(), "expected attribute 'color'");
2877 }
2878
2879 #[test]
2880 fn class_tiny_has_with_options() {
2881 let models = build_models(
2883 r#"
2884package MyApp::Readonly;
2885use Class::Tiny;
2886has 'id' => (is => 'ro');
2887has 'value' => (is => 'rw');
2888"#,
2889 );
2890 let model = must_some(find_model(&models, "MyApp::Readonly"));
2891 assert_eq!(model.attributes.len(), 2);
2892
2893 let id_attr = must_some(model.attributes.iter().find(|a| a.name == "id"));
2894 assert_eq!(id_attr.is, Some(AccessorType::Ro));
2895
2896 let value_attr = must_some(model.attributes.iter().find(|a| a.name == "value"));
2897 assert_eq!(value_attr.is, Some(AccessorType::Rw));
2898 }
2899
2900 #[test]
2901 fn class_tiny_mixed_use_and_has_declarations() {
2902 let models = build_models(
2904 r#"
2905package MyApp::Mixed;
2906use Class::Tiny qw(name age);
2907has 'role' => (is => 'ro');
2908sub greet { }
2909"#,
2910 );
2911 let model = must_some(find_model(&models, "MyApp::Mixed"));
2912 assert_eq!(model.framework, Framework::ClassTiny);
2913 assert_eq!(model.attributes.len(), 3, "expected 3 total attributes");
2915
2916 let names: HashSet<_> = model.attributes.iter().map(|a| a.name.as_str()).collect();
2917 assert!(names.contains("name"));
2918 assert!(names.contains("age"));
2919 assert!(names.contains("role"));
2920
2921 let role_attr = must_some(model.attributes.iter().find(|a| a.name == "role"));
2922 assert_eq!(role_attr.is, Some(AccessorType::Ro), "explicit has ro should be preserved");
2923
2924 assert!(model.methods.iter().any(|m| m.name == "greet"), "sub greet should be tracked");
2925 }
2926
2927 #[test]
2928 fn class_tiny_has_framework_returns_true() {
2929 let models = build_models(
2930 r#"
2931package MyApp::Tiny;
2932use Class::Tiny qw(x y);
2933"#,
2934 );
2935 let model = must_some(find_model(&models, "MyApp::Tiny"));
2936 assert!(model.has_framework(), "Class::Tiny is a framework");
2937 }
2938}