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