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 }
746 }
747
748 None
749 }
750
751 fn extract_has_from_pairs(
753 &mut self,
754 pairs: &[(Node, Node)],
755 location: SourceLocation,
756 require_embedded: bool,
757 ) {
758 for (attr_expr, options_expr) in pairs {
759 let attr_expr = if let NodeKind::Binary { op, left, right } = &attr_expr.kind
760 && op == "[]"
761 && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
762 {
763 right.as_ref()
764 } else if require_embedded {
765 continue;
766 } else {
767 attr_expr
768 };
769
770 let names = collect_symbol_names(attr_expr);
771 if names.is_empty() {
772 continue;
773 }
774
775 if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
776 self.extract_has_with_names(&names, option_pairs, location);
777 }
778 }
779 }
780
781 fn extract_has_with_names(
783 &mut self,
784 names: &[String],
785 option_pairs: &[(Node, Node)],
786 location: SourceLocation,
787 ) {
788 let options = extract_hash_options(option_pairs);
789
790 let is = options.get("is").and_then(|v| match v.as_str() {
791 "ro" => Some(AccessorType::Ro),
792 "rw" => Some(AccessorType::Rw),
793 "lazy" => Some(AccessorType::Lazy),
794 "bare" => Some(AccessorType::Bare),
795 _ => None,
796 });
797
798 let isa = options.get("isa").cloned();
799 let default = options.contains_key("default")
800 || options.contains_key("builder")
801 || is == Some(AccessorType::Lazy);
802 let required = options.get("required").is_some_and(|v| v == "1" || v == "true");
803 let coerce = options.get("coerce").is_some_and(|v| v == "1" || v == "true");
804 let trigger = options.contains_key("trigger");
805
806 let explicit_accessor = options.get("accessor").or_else(|| options.get("reader")).cloned();
808
809 for raw_name in names {
810 let Some(name) = normalize_attribute_name(raw_name) else { continue };
811 let accessor_name = explicit_accessor.clone().unwrap_or_else(|| name.clone());
812
813 let builder = options
815 .get("builder")
816 .map(|v| if v == "1" { format!("_build_{name}") } else { v.clone() });
817
818 let predicate = options
820 .get("predicate")
821 .map(|v| if v == "1" { format!("has_{name}") } else { v.clone() });
822
823 let clearer = options
825 .get("clearer")
826 .map(|v| if v == "1" { format!("clear_{name}") } else { v.clone() });
827
828 self.current_attributes.push(Attribute {
829 name: name.clone(),
830 is,
831 isa: isa.clone(),
832 default,
833 required,
834 accessor_name,
835 location,
836 builder,
837 coerce,
838 predicate,
839 clearer,
840 trigger,
841 });
842 }
843 }
844
845 fn try_extract_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
847 let first = &statements[idx];
848
849 if let NodeKind::ExpressionStatement { expression } = &first.kind
851 && let NodeKind::FunctionCall { name, args } = &expression.kind
852 {
853 let modifier_kind = modifier_kind_from_name(name);
854 if let Some(modifier_kind) = modifier_kind {
855 let method_names: Vec<String> =
857 args.first().map(collect_symbol_names).unwrap_or_default();
858 if !method_names.is_empty() {
859 for method_name in method_names {
860 self.current_modifiers.push(MethodModifier {
861 kind: modifier_kind,
862 method_name,
863 location: first.location,
864 });
865 }
866 return Some(1);
867 }
868 }
869 }
870
871 if idx + 1 >= statements.len() {
875 return None;
876 }
877 let second = &statements[idx + 1];
878
879 let modifier_kind = match &first.kind {
880 NodeKind::ExpressionStatement { expression } => match &expression.kind {
881 NodeKind::Identifier { name } => modifier_kind_from_name(name),
882 _ => None,
883 },
884 _ => None,
885 };
886
887 let modifier_kind = modifier_kind?;
888
889 let NodeKind::ExpressionStatement { expression } = &second.kind else {
890 return None;
891 };
892 let NodeKind::HashLiteral { pairs } = &expression.kind else {
893 return None;
894 };
895
896 let location = SourceLocation { start: first.location.start, end: second.location.end };
897
898 for (key_node, _) in pairs {
899 let method_names = collect_symbol_names(key_node);
900 for method_name in method_names {
901 self.current_modifiers.push(MethodModifier {
902 kind: modifier_kind,
903 method_name,
904 location,
905 });
906 }
907 }
908
909 Some(2)
910 }
911
912 fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
914 let first = &statements[idx];
915
916 if let NodeKind::ExpressionStatement { expression } = &first.kind
919 && let NodeKind::FunctionCall { name, args } = &expression.kind
920 && matches!(name.as_str(), "extends" | "with")
921 {
922 let names: Vec<String> = args.iter().flat_map(collect_symbol_names).collect();
923 if !names.is_empty() {
924 if name == "extends" {
925 self.current_parents.extend(names);
926 } else {
927 self.current_roles.extend(names);
928 }
929 return Some(1);
930 }
931 }
932
933 if idx + 1 >= statements.len() {
937 return None;
938 }
939 let second = &statements[idx + 1];
940
941 let keyword = match &first.kind {
942 NodeKind::ExpressionStatement { expression } => match &expression.kind {
943 NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
944 name.as_str()
945 }
946 _ => return None,
947 },
948 _ => return None,
949 };
950
951 let NodeKind::ExpressionStatement { expression } = &second.kind else {
952 return None;
953 };
954
955 let names = collect_symbol_names(expression);
956 if names.is_empty() {
957 return None;
958 }
959
960 if keyword == "extends" {
961 self.current_parents.extend(names);
962 } else {
963 self.current_roles.extend(names);
964 }
965
966 Some(2)
967 }
968
969 fn try_extract_class_accessor_methods(
971 &mut self,
972 statements: &[Node],
973 idx: usize,
974 ) -> Option<usize> {
975 let first = &statements[idx];
976
977 let NodeKind::ExpressionStatement { expression } = &first.kind else {
978 return None;
979 };
980
981 let NodeKind::MethodCall { object, method, args } = &expression.kind else {
982 return None;
983 };
984
985 let accessor_mode = match method.as_str() {
986 "mk_accessors" => ClassAccessorMode::Rw,
987 "mk_ro_accessors" => ClassAccessorMode::Ro,
988 "mk_wo_accessors" => ClassAccessorMode::Wo,
989 _ => return None,
990 };
991
992 if !self.class_accessor_target_matches_current_package(object) {
993 return None;
994 }
995
996 let mut accessor_names = Vec::new();
997 let mut seen = HashSet::new();
998 for arg in args {
999 for name in collect_accessor_names(arg) {
1000 if seen.insert(name.clone()) {
1001 accessor_names.push(name);
1002 }
1003 }
1004 }
1005
1006 if accessor_names.is_empty() {
1007 return None;
1008 }
1009
1010 for name in accessor_names {
1011 self.current_methods.push(MethodInfo::synthetic(
1012 name,
1013 first.location,
1014 Some(accessor_mode),
1015 ));
1016 }
1017
1018 Some(1)
1019 }
1020
1021 fn try_extract_object_pad_constructs(
1023 &mut self,
1024 statements: &[Node],
1025 idx: usize,
1026 ) -> Option<usize> {
1027 let statement = &statements[idx];
1028
1029 if let Some(field) = Self::object_pad_field_from_statement(statement) {
1030 let location = field.location;
1031 let field_name = field.name.clone();
1032 let traits = field.attributes.clone();
1033
1034 self.current_fields.push(field);
1035
1036 if let Some(reader) = Self::object_pad_reader_name(&field_name, &traits) {
1037 self.current_methods.push(MethodInfo::synthetic(reader, location, None));
1038 }
1039 if let Some(writer) = Self::object_pad_writer_name(&field_name, &traits) {
1040 self.current_methods.push(MethodInfo::synthetic(writer, location, None));
1041 }
1042 if let Some(accessor) = Self::object_pad_accessor_name(&field_name, &traits) {
1043 self.current_methods.push(MethodInfo::synthetic(accessor, location, None));
1044 }
1045 if let Some(mutator) = Self::object_pad_mutator_name(&field_name, &traits) {
1046 self.current_methods.push(MethodInfo::synthetic(mutator, location, None));
1047 }
1048
1049 return Some(1);
1050 }
1051
1052 match &statement.kind {
1053 NodeKind::Method { name, body, .. } if name == "ADJUST" => {
1054 self.record_object_pad_adjust(statement.location);
1055 self.visit_node(body);
1056 return Some(1);
1057 }
1058 NodeKind::Subroutine { name, body, .. } if name.as_deref() == Some("ADJUST") => {
1059 self.record_object_pad_adjust(statement.location);
1060 self.visit_node(body);
1061 return Some(1);
1062 }
1063 _ => {}
1064 }
1065
1066 None
1067 }
1068
1069 fn record_object_pad_adjust(&mut self, location: SourceLocation) {
1070 self.current_adjusts.push(MethodInfo::synthetic("ADJUST".to_string(), location, None));
1071 }
1072
1073 fn object_pad_field_from_statement(statement: &Node) -> Option<FieldInfo> {
1074 let NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } =
1075 &statement.kind
1076 else {
1077 return None;
1078 };
1079 if declarator != "field" {
1080 return None;
1081 }
1082
1083 let NodeKind::Variable { sigil, name } = &variable.kind else {
1084 return None;
1085 };
1086 if sigil != "$" {
1087 return None;
1088 }
1089
1090 let mut param = false;
1091 let mut traits = Vec::new();
1092 for attr in attributes {
1093 let attr_name = attr.trim().to_string();
1094 if attr_name == "param" {
1095 param = true;
1096 }
1097 traits.push(attr_name);
1098 }
1099
1100 let mut field = FieldInfo {
1101 name: name.clone(),
1102 location: statement.location,
1103 attributes: traits,
1104 param,
1105 reader: None,
1106 writer: None,
1107 accessor: None,
1108 mutator: None,
1109 default: initializer.as_ref().map(|node| Self::value_summary(node)),
1110 };
1111
1112 field.reader = Self::object_pad_reader_name(&field.name, &field.attributes);
1113 field.writer = Self::object_pad_writer_name(&field.name, &field.attributes);
1114 field.accessor = Self::object_pad_accessor_name(&field.name, &field.attributes);
1115 field.mutator = Self::object_pad_mutator_name(&field.name, &field.attributes);
1116
1117 Some(field)
1118 }
1119
1120 fn object_pad_reader_name(field_name: &str, traits: &[String]) -> Option<String> {
1121 if traits.iter().any(|trait_name| trait_name == "reader") {
1122 Some(Self::object_pad_public_name(field_name).to_string())
1123 } else {
1124 None
1125 }
1126 }
1127
1128 fn object_pad_writer_name(field_name: &str, traits: &[String]) -> Option<String> {
1129 if traits.iter().any(|trait_name| trait_name == "writer") {
1130 Some(format!("set_{}", Self::object_pad_public_name(field_name)))
1131 } else {
1132 None
1133 }
1134 }
1135
1136 fn object_pad_accessor_name(field_name: &str, traits: &[String]) -> Option<String> {
1137 if traits.iter().any(|trait_name| trait_name == "accessor") {
1138 Some(Self::object_pad_public_name(field_name).to_string())
1139 } else {
1140 None
1141 }
1142 }
1143
1144 fn object_pad_mutator_name(field_name: &str, traits: &[String]) -> Option<String> {
1145 if traits.iter().any(|trait_name| trait_name == "mutator") {
1146 Some(Self::object_pad_public_name(field_name).to_string())
1147 } else {
1148 None
1149 }
1150 }
1151
1152 fn object_pad_public_name(field_name: &str) -> &str {
1153 field_name.strip_prefix('_').unwrap_or(field_name)
1154 }
1155
1156 fn class_accessor_target_matches_current_package(&self, object: &Node) -> bool {
1158 match &object.kind {
1159 NodeKind::Identifier { name } => name == "__PACKAGE__" || name == &self.current_package,
1160 NodeKind::String { value, .. } => {
1161 normalize_symbol_name(value).is_some_and(|name| name == self.current_package)
1162 }
1163 NodeKind::Variable { sigil, name } if sigil == "$" => {
1164 self.current_package_aliases.contains(name)
1165 }
1166 _ => false,
1167 }
1168 }
1169
1170 fn extract_isa_from_node(&mut self, node: &Node) {
1172 let parents = collect_symbol_names(node);
1173 if !parents.is_empty() {
1174 if parents.iter().any(|parent| parent == "Exporter") {
1175 self.current_uses_exporter = true;
1176 }
1177 if self.current_framework == Framework::None {
1179 self.current_framework = Framework::PlainOO;
1180 self.framework_map.insert(self.current_package.clone(), Framework::PlainOO);
1181 }
1182 self.current_parents.extend(parents);
1183 }
1184 }
1185
1186 fn build_exporter_metadata_for_current_package(&mut self) -> Option<ExporterMetadata> {
1187 if !self.current_uses_exporter {
1188 self.current_export_tags.clear();
1189 return None;
1190 }
1191
1192 let method_map: HashMap<&str, SourceLocation> = self
1193 .current_methods
1194 .iter()
1195 .map(|method| (method.name.as_str(), method.location))
1196 .collect();
1197
1198 let (exports, unresolved_exports) = resolve_exports(&self.current_exports, &method_map);
1199 let (export_ok, unresolved_export_ok) =
1200 resolve_exports(&self.current_export_ok, &method_map);
1201
1202 let mut export_tags: HashMap<String, Vec<ResolvedExport>> = HashMap::new();
1203 let mut unresolved = unresolved_exports;
1204 unresolved.extend(unresolved_export_ok);
1205
1206 for (tag, names) in std::mem::take(&mut self.current_export_tags) {
1207 let (resolved, unresolved_names) = resolve_exports(&names, &method_map);
1208 if !resolved.is_empty() {
1209 export_tags.insert(tag, resolved);
1210 }
1211 unresolved.extend(unresolved_names);
1212 }
1213
1214 dedupe_preserve_order(&mut unresolved);
1215
1216 Some(ExporterMetadata { exports, export_ok, export_tags, unresolved })
1217 }
1218
1219 fn initializer_is_current_package(&self, node: &Node) -> bool {
1221 match &node.kind {
1222 NodeKind::Identifier { name } => name == "__PACKAGE__" || name == &self.current_package,
1223 NodeKind::FunctionCall { name, args } if name == "__PACKAGE__" && args.is_empty() => {
1224 true
1225 }
1226 NodeKind::String { value, .. } => {
1227 normalize_symbol_name(value).is_some_and(|name| name == self.current_package)
1228 }
1229 _ => false,
1230 }
1231 }
1232
1233 fn value_summary(node: &Node) -> String {
1234 match &node.kind {
1235 NodeKind::String { value, .. } => {
1236 normalize_symbol_name(value).unwrap_or_else(|| value.clone())
1237 }
1238 NodeKind::Identifier { name } => name.clone(),
1239 NodeKind::Number { value } => value.clone(),
1240 NodeKind::Undef => "undef".to_string(),
1241 NodeKind::Variable { sigil, name } => format!("{sigil}{name}"),
1242 _ => "expr".to_string(),
1243 }
1244 }
1245}
1246
1247fn collect_symbol_names(node: &Node) -> Vec<String> {
1250 match &node.kind {
1251 NodeKind::String { value, .. } => normalize_symbol_name(value).into_iter().collect(),
1252 NodeKind::Identifier { name } => normalize_symbol_name(name).into_iter().collect(),
1253 NodeKind::ArrayLiteral { elements } => {
1254 elements.iter().flat_map(collect_symbol_names).collect()
1255 }
1256 _ => Vec::new(),
1257 }
1258}
1259
1260fn collect_export_tags(node: &Node) -> HashMap<String, Vec<String>> {
1261 let mut tags: HashMap<String, Vec<String>> = HashMap::new();
1262 match &node.kind {
1263 NodeKind::HashLiteral { pairs } => {
1264 for (key, value) in pairs {
1265 let Some(tag_name) = collect_single_symbol_name(key) else { continue };
1266 let Some(symbols) = collect_static_symbol_names(value) else { continue };
1267 if symbols.is_empty() {
1268 continue;
1269 }
1270 tags.entry(tag_name).or_default().extend(symbols);
1271 }
1272 }
1273 NodeKind::ArrayLiteral { elements } => {
1274 for pair in elements.chunks_exact(2) {
1275 let Some(tag_name) = collect_single_symbol_name(&pair[0]) else { continue };
1276 let Some(symbols) = collect_static_symbol_names(&pair[1]) else { continue };
1277 if symbols.is_empty() {
1278 continue;
1279 }
1280 tags.entry(tag_name).or_default().extend(symbols);
1281 }
1282 }
1283 NodeKind::Binary { op, .. } if op == "," => {
1284 let mut flattened = Vec::new();
1285 flatten_comma_expression(node, &mut flattened);
1286 for element in flattened {
1287 if let NodeKind::Binary { op, left, right } = &element.kind
1288 && op == "=>"
1289 {
1290 let Some(tag_name) = collect_single_symbol_name(left) else { continue };
1291 let Some(symbols) = collect_static_symbol_names(right) else { continue };
1292 if symbols.is_empty() {
1293 continue;
1294 }
1295 tags.entry(tag_name).or_default().extend(symbols);
1296 }
1297 }
1298 }
1299 _ => {}
1300 }
1301
1302 for symbols in tags.values_mut() {
1303 dedupe_preserve_order(symbols);
1304 }
1305 tags
1306}
1307
1308fn collect_single_symbol_name(node: &Node) -> Option<String> {
1309 let mut names = collect_static_symbol_names(node)?;
1310 dedupe_preserve_order(&mut names);
1311 names.into_iter().next()
1312}
1313
1314fn collect_static_symbol_names(node: &Node) -> Option<Vec<String>> {
1315 match &node.kind {
1316 NodeKind::String { value, .. } => Some(expand_export_name_list(value)),
1317 NodeKind::Identifier { name } => Some(expand_export_name_list(name)),
1318 NodeKind::ArrayLiteral { elements } => {
1319 let mut names = Vec::new();
1320 for element in elements {
1321 let mut element_names = collect_static_symbol_names(element)?;
1322 names.append(&mut element_names);
1323 }
1324 Some(names)
1325 }
1326 NodeKind::Binary { op, left, right } if op == "," => {
1327 let mut names = collect_static_symbol_names(left)?;
1328 let mut right_names = collect_static_symbol_names(right)?;
1329 names.append(&mut right_names);
1330 Some(names)
1331 }
1332 _ => None,
1333 }
1334}
1335
1336fn resolve_exports(
1337 names: &[String],
1338 method_map: &HashMap<&str, SourceLocation>,
1339) -> (Vec<ResolvedExport>, Vec<String>) {
1340 let mut resolved = Vec::new();
1341 let mut unresolved = Vec::new();
1342 for raw_name in names {
1343 for name in expand_export_name_list(raw_name) {
1344 if let Some(location) = method_map.get(name.as_str()) {
1345 resolved.push(ResolvedExport { name, location: *location });
1346 } else {
1347 unresolved.push(name);
1348 }
1349 }
1350 }
1351 dedupe_resolved_exports(&mut resolved);
1352 dedupe_preserve_order(&mut unresolved);
1353 (resolved, unresolved)
1354}
1355
1356fn dedupe_resolved_exports(exports: &mut Vec<ResolvedExport>) {
1357 let mut seen = HashSet::new();
1358 exports.retain(|item| seen.insert(item.name.clone()));
1359}
1360
1361fn dedupe_preserve_order(items: &mut Vec<String>) {
1362 let mut seen = HashSet::new();
1363 items.retain(|item| seen.insert(item.clone()));
1364}
1365
1366fn merge_export_tags(
1367 target: &mut HashMap<String, Vec<String>>,
1368 updates: HashMap<String, Vec<String>>,
1369) {
1370 for (tag, mut symbols) in updates {
1371 target.entry(tag).or_default().append(&mut symbols);
1372 }
1373}
1374
1375fn flatten_comma_expression<'a>(node: &'a Node, out: &mut Vec<&'a Node>) {
1376 if let NodeKind::Binary { op, left, right } = &node.kind
1377 && op == ","
1378 {
1379 flatten_comma_expression(left, out);
1380 flatten_comma_expression(right, out);
1381 } else {
1382 out.push(node);
1383 }
1384}
1385
1386fn expand_export_name_list(raw: &str) -> Vec<String> {
1387 expand_symbol_list(raw).into_iter().filter_map(|name| normalize_export_name(&name)).collect()
1388}
1389
1390fn normalize_export_name(raw: &str) -> Option<String> {
1391 let normalized = normalize_symbol_name(raw)?;
1392 let stripped = normalized
1393 .strip_prefix('&')
1394 .or_else(|| normalized.strip_prefix('$'))
1395 .or_else(|| normalized.strip_prefix('@'))
1396 .or_else(|| normalized.strip_prefix('%'))
1397 .unwrap_or(&normalized)
1398 .to_string();
1399 if stripped.is_empty() { None } else { Some(stripped) }
1400}
1401
1402fn collect_accessor_names(node: &Node) -> Vec<String> {
1403 match &node.kind {
1404 NodeKind::String { value, .. } => expand_symbol_list(value),
1405 NodeKind::Identifier { name } => expand_symbol_list(name),
1406 NodeKind::ArrayLiteral { elements } => {
1407 elements.iter().flat_map(collect_accessor_names).collect()
1408 }
1409 _ => Vec::new(),
1410 }
1411}
1412
1413fn modifier_kind_from_name(name: &str) -> Option<ModifierKind> {
1414 match name {
1415 "before" => Some(ModifierKind::Before),
1416 "after" => Some(ModifierKind::After),
1417 "around" => Some(ModifierKind::Around),
1418 "override" => Some(ModifierKind::Override),
1419 "augment" => Some(ModifierKind::Augment),
1420 _ => None,
1421 }
1422}
1423
1424fn normalize_symbol_name(raw: &str) -> Option<String> {
1425 let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
1426 if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
1427}
1428
1429fn normalize_attribute_name(raw: &str) -> Option<String> {
1430 let trimmed = raw.trim();
1431 let without_override_prefix = trimmed.strip_prefix('+').unwrap_or(trimmed);
1432 normalize_symbol_name(without_override_prefix)
1433}
1434
1435fn expand_symbol_list(raw: &str) -> Vec<String> {
1436 let raw = raw.trim();
1437
1438 if raw.starts_with("qw(") && raw.ends_with(')') {
1439 let content = &raw[3..raw.len() - 1];
1440 return content
1441 .split_whitespace()
1442 .filter(|s| !s.is_empty())
1443 .map(|s| s.to_string())
1444 .collect();
1445 }
1446
1447 if raw.starts_with("qw") && raw.len() > 2 {
1448 let open = raw.chars().nth(2).unwrap_or(' ');
1449 let close = match open {
1450 '(' => ')',
1451 '{' => '}',
1452 '[' => ']',
1453 '<' => '>',
1454 c => c,
1455 };
1456 if let (Some(start), Some(end)) = (raw.find(open), raw.rfind(close))
1457 && start < end
1458 {
1459 let content = &raw[start + 1..end];
1460 return content
1461 .split_whitespace()
1462 .filter(|s| !s.is_empty())
1463 .map(|s| s.to_string())
1464 .collect();
1465 }
1466 }
1467
1468 normalize_symbol_name(raw).into_iter().collect()
1469}
1470
1471fn expand_arg_to_names(arg: &str) -> Vec<String> {
1477 let arg = arg.trim();
1478 if arg.starts_with("qw(") && arg.ends_with(')') {
1480 let content = &arg[3..arg.len() - 1];
1481 return content
1482 .split_whitespace()
1483 .filter(|s| !s.is_empty())
1484 .map(|s| s.to_string())
1485 .collect();
1486 }
1487 if arg.starts_with("qw") && arg.len() > 2 {
1489 let open = arg.chars().nth(2).unwrap_or(' ');
1490 let close = match open {
1491 '(' => ')',
1492 '{' => '}',
1493 '[' => ']',
1494 '<' => '>',
1495 c => c,
1496 };
1497 if let (Some(start), Some(end)) = (arg.find(open), arg.rfind(close)) {
1498 if start < end {
1499 let content = &arg[start + 1..end];
1500 return content
1501 .split_whitespace()
1502 .filter(|s| !s.is_empty())
1503 .map(|s| s.to_string())
1504 .collect();
1505 }
1506 }
1507 }
1508 normalize_symbol_name(arg).into_iter().collect()
1510}
1511
1512fn extract_hash_options(pairs: &[(Node, Node)]) -> HashMap<String, String> {
1513 let mut options = HashMap::new();
1514 for (key_node, value_node) in pairs {
1515 let Some(key_name) = collect_symbol_names(key_node).into_iter().next() else {
1516 continue;
1517 };
1518 let value_text = value_summary(value_node);
1519 options.insert(key_name, value_text);
1520 }
1521 options
1522}
1523
1524fn value_summary(node: &Node) -> String {
1525 match &node.kind {
1526 NodeKind::String { value, .. } => {
1527 normalize_symbol_name(value).unwrap_or_else(|| value.clone())
1528 }
1529 NodeKind::Identifier { name } => name.clone(),
1530 NodeKind::Number { value } => value.clone(),
1531 _ => "expr".to_string(),
1532 }
1533}
1534
1535#[cfg(test)]
1536mod tests {
1537 use super::*;
1538 use crate::parser::Parser;
1539 use perl_tdd_support::must;
1540 use std::collections::HashSet;
1541
1542 fn build_models(code: &str) -> Vec<ClassModel> {
1543 let mut parser = Parser::new(code);
1544 let ast = must(parser.parse());
1545 ClassModelBuilder::new().build(&ast)
1546 }
1547
1548 fn find_model<'a>(models: &'a [ClassModel], name: &str) -> Option<&'a ClassModel> {
1549 models.iter().find(|m| m.name == name)
1550 }
1551
1552 fn has_method(
1553 model: &ClassModel,
1554 name: &str,
1555 synthetic: bool,
1556 accessor_mode: Option<ClassAccessorMode>,
1557 ) -> bool {
1558 model.methods.iter().any(|method| {
1559 method.name == name
1560 && method.synthetic == synthetic
1561 && method.accessor_mode == accessor_mode
1562 })
1563 }
1564
1565 #[test]
1566 fn basic_moo_class() {
1567 let models = build_models(
1568 r#"
1569package MyApp::User;
1570use Moo;
1571
1572has 'name' => (is => 'ro', isa => 'Str');
1573has 'age' => (is => 'rw', required => 1);
1574
1575sub greet { }
1576"#,
1577 );
1578
1579 let model = find_model(&models, "MyApp::User");
1580 assert!(model.is_some(), "expected ClassModel for MyApp::User");
1581 let model = model.unwrap();
1582
1583 assert_eq!(model.framework, Framework::Moo);
1584 assert_eq!(model.attributes.len(), 2);
1585
1586 let name_attr = model.attributes.iter().find(|a| a.name == "name");
1587 assert!(name_attr.is_some());
1588 let name_attr = name_attr.unwrap();
1589 assert_eq!(name_attr.is, Some(AccessorType::Ro));
1590 assert_eq!(name_attr.isa.as_deref(), Some("Str"));
1591 assert!(!name_attr.required);
1592 assert_eq!(name_attr.accessor_name, "name");
1593
1594 let age_attr = model.attributes.iter().find(|a| a.name == "age");
1595 assert!(age_attr.is_some());
1596 let age_attr = age_attr.unwrap();
1597 assert_eq!(age_attr.is, Some(AccessorType::Rw));
1598 assert!(age_attr.required);
1599
1600 assert!(model.methods.iter().any(|m| m.name == "greet"));
1601 }
1602
1603 #[test]
1604 fn moose_extends_and_with() {
1605 let models = build_models(
1606 r#"
1607package MyApp::Admin;
1608use Moose;
1609extends 'MyApp::User';
1610with 'MyApp::Printable', 'MyApp::Serializable';
1611
1612has 'level' => (is => 'ro');
1613"#,
1614 );
1615
1616 let model = find_model(&models, "MyApp::Admin");
1617 assert!(model.is_some());
1618 let model = model.unwrap();
1619
1620 assert_eq!(model.framework, Framework::Moose);
1621 assert!(model.parents.contains(&"MyApp::User".to_string()));
1622 assert_eq!(model.roles, vec!["MyApp::Printable", "MyApp::Serializable"]);
1623 assert_eq!(model.attributes.len(), 1);
1624 }
1625
1626 #[test]
1627 fn mro_pragma_tracks_c3_and_reset() {
1628 let models = build_models(
1629 r#"
1630package Example::Child;
1631use parent 'Example::Base';
1632use mro 'c3';
1633sub greet { }
1634
1635package Example::Sibling;
1636use parent 'Example::Base';
1637no mro;
1638sub greet { }
1639"#,
1640 );
1641
1642 let child = find_model(&models, "Example::Child").expect("expected ClassModel for Child");
1643 assert_eq!(child.mro, MethodResolutionOrder::C3);
1644
1645 let sibling =
1646 find_model(&models, "Example::Sibling").expect("expected ClassModel for Sibling");
1647 assert_eq!(sibling.mro, MethodResolutionOrder::Dfs);
1648 }
1649
1650 #[test]
1651 fn method_modifiers() {
1652 let models = build_models(
1653 r#"
1654package MyApp::User;
1655use Moo;
1656before 'save' => sub { };
1657after 'save' => sub { };
1658around 'validate' => sub { };
1659override 'dispatch' => sub { super(); };
1660augment 'serialize' => sub { inner(); };
1661"#,
1662 );
1663
1664 let model = find_model(&models, "MyApp::User");
1665 assert!(model.is_some());
1666 let model = model.unwrap();
1667
1668 assert_eq!(model.modifiers.len(), 5);
1669 assert!(
1670 model
1671 .modifiers
1672 .iter()
1673 .any(|m| m.kind == ModifierKind::Before && m.method_name == "save")
1674 );
1675 assert!(
1676 model
1677 .modifiers
1678 .iter()
1679 .any(|m| m.kind == ModifierKind::After && m.method_name == "save")
1680 );
1681 assert!(
1682 model
1683 .modifiers
1684 .iter()
1685 .any(|m| m.kind == ModifierKind::Around && m.method_name == "validate")
1686 );
1687 assert!(
1688 model
1689 .modifiers
1690 .iter()
1691 .any(|m| m.kind == ModifierKind::Override && m.method_name == "dispatch")
1692 );
1693 assert!(
1694 model
1695 .modifiers
1696 .iter()
1697 .any(|m| m.kind == ModifierKind::Augment && m.method_name == "serialize")
1698 );
1699 }
1700
1701 #[test]
1702 fn class_accessor_generates_synthetic_methods_for_all_variants() {
1703 let models = build_models(
1704 r#"
1705package Example::Accessors;
1706use parent 'Class::Accessor';
1707__PACKAGE__->mk_accessors(qw(foo bar));
1708sub other_method { }
1709
1710package Example::ReadOnly;
1711use parent 'Class::Accessor';
1712my $package = __PACKAGE__;
1713$package->mk_ro_accessors('id');
1714
1715package Example::WriteOnly;
1716use parent 'Class::Accessor';
1717my $package = 'Example::WriteOnly';
1718$package->mk_wo_accessors([qw(token)]);
1719"#,
1720 );
1721
1722 let accessors = find_model(&models, "Example::Accessors")
1723 .expect("expected ClassModel for Example::Accessors");
1724 assert!(has_method(accessors, "foo", true, Some(ClassAccessorMode::Rw)));
1725 assert!(has_method(accessors, "bar", true, Some(ClassAccessorMode::Rw)));
1726 assert!(has_method(accessors, "other_method", false, None));
1727
1728 let read_only = find_model(&models, "Example::ReadOnly")
1729 .expect("expected ClassModel for Example::ReadOnly");
1730 assert!(has_method(read_only, "id", true, Some(ClassAccessorMode::Ro)));
1731
1732 let write_only = find_model(&models, "Example::WriteOnly")
1733 .expect("expected ClassModel for Example::WriteOnly");
1734 assert!(has_method(write_only, "token", true, Some(ClassAccessorMode::Wo)));
1735 }
1736
1737 #[test]
1738 fn no_model_for_plain_package() {
1739 let models = build_models(
1740 r#"
1741package MyApp::Utils;
1742sub helper { 1 }
1743"#,
1744 );
1745
1746 assert!(
1747 find_model(&models, "MyApp::Utils").is_none(),
1748 "plain package should not produce a ClassModel"
1749 );
1750 }
1751
1752 #[test]
1753 fn multiple_packages() {
1754 let models = build_models(
1755 r#"
1756package MyApp::User;
1757use Moo;
1758has 'name' => (is => 'ro');
1759
1760package MyApp::Admin;
1761use Moose;
1762extends 'MyApp::User';
1763has 'level' => (is => 'rw');
1764
1765package MyApp::Utils;
1766sub helper { 1 }
1767"#,
1768 );
1769
1770 assert_eq!(models.len(), 2, "expected 2 ClassModels (User + Admin, not Utils)");
1771 assert!(find_model(&models, "MyApp::User").is_some());
1772 assert!(find_model(&models, "MyApp::Admin").is_some());
1773 assert!(find_model(&models, "MyApp::Utils").is_none());
1774 }
1775
1776 #[test]
1777 fn qw_attribute_list() {
1778 let models = build_models(
1779 r#"
1780use Moo;
1781has [qw(first_name last_name)] => (is => 'ro');
1782"#,
1783 );
1784
1785 assert_eq!(models.len(), 1);
1786 let model = &models[0];
1787 assert_eq!(model.attributes.len(), 2);
1788
1789 let names: HashSet<_> = model.attributes.iter().map(|a| a.name.as_str()).collect();
1790 assert!(names.contains("first_name"));
1791 assert!(names.contains("last_name"));
1792 }
1793
1794 #[test]
1795 fn has_framework_helper() {
1796 let models = build_models(
1797 r#"
1798package MyApp::User;
1799use Moo;
1800has 'name' => (is => 'ro');
1801"#,
1802 );
1803
1804 let model = find_model(&models, "MyApp::User").unwrap();
1805 assert!(model.has_framework());
1806 }
1807
1808 #[test]
1809 fn accessor_type_lazy() {
1810 let models = build_models(
1811 r#"
1812use Moo;
1813has 'config' => (is => 'lazy');
1814"#,
1815 );
1816
1817 let model = &models[0];
1818 assert_eq!(model.attributes[0].is, Some(AccessorType::Lazy));
1819 assert!(model.attributes[0].default, "lazy implies default");
1820 }
1821
1822 #[test]
1823 fn explicit_accessor_name() {
1824 let models = build_models(
1825 r#"
1826use Moo;
1827has 'name' => (is => 'ro', reader => 'get_name');
1828"#,
1829 );
1830
1831 let model = &models[0];
1832 assert_eq!(model.attributes[0].accessor_name, "get_name");
1833 }
1834
1835 #[test]
1836 fn inherited_attribute_override_strips_plus_prefix() {
1837 let models = build_models(
1838 r#"
1839use Moo;
1840has '+name' => (is => 'ro', builder => 1, predicate => 1, clearer => 1);
1841"#,
1842 );
1843
1844 let model = &models[0];
1845 let attr = &model.attributes[0];
1846 assert_eq!(attr.name, "name");
1847 assert_eq!(attr.accessor_name, "name");
1848 assert_eq!(attr.builder.as_deref(), Some("_build_name"));
1849 assert_eq!(attr.predicate.as_deref(), Some("has_name"));
1850 assert_eq!(attr.clearer.as_deref(), Some("clear_name"));
1851 }
1852
1853 #[test]
1854 fn default_via_builder_option() {
1855 let models = build_models(
1856 r#"
1857use Moo;
1858has 'config' => (is => 'ro', builder => 1);
1859"#,
1860 );
1861
1862 let model = &models[0];
1863 assert!(model.attributes[0].default, "builder option implies default");
1864 }
1865
1866 #[test]
1867 fn lazy_builder_with_string_name() {
1868 let models = build_models(
1869 r#"
1870use Moo;
1871has 'config' => (is => 'ro', lazy => 1, builder => '_build_config');
1872"#,
1873 );
1874
1875 let model = &models[0];
1876 let attr = &model.attributes[0];
1877 assert_eq!(
1878 attr.builder.as_deref(),
1879 Some("_build_config"),
1880 "builder string should be captured"
1881 );
1882 assert!(attr.default, "named builder implies default");
1883 }
1884
1885 #[test]
1886 fn lazy_builder_with_numeric_one_generates_default_name() {
1887 let models = build_models(
1888 r#"
1889use Moo;
1890has 'profile' => (is => 'ro', builder => 1);
1891"#,
1892 );
1893
1894 let model = &models[0];
1895 let attr = &model.attributes[0];
1896 assert_eq!(
1897 attr.builder.as_deref(),
1898 Some("_build_profile"),
1899 "builder => 1 should derive builder name as '_build_<attr>'"
1900 );
1901 }
1902
1903 #[test]
1904 fn predicate_with_string_name() {
1905 let models = build_models(
1906 r#"
1907use Moo;
1908has 'name' => (is => 'ro', predicate => 'has_name');
1909"#,
1910 );
1911
1912 let model = &models[0];
1913 let attr = &model.attributes[0];
1914 assert_eq!(
1915 attr.predicate.as_deref(),
1916 Some("has_name"),
1917 "predicate string name should be captured"
1918 );
1919 }
1920
1921 #[test]
1922 fn predicate_with_numeric_one_generates_default_name() {
1923 let models = build_models(
1924 r#"
1925use Moo;
1926has 'name' => (is => 'ro', predicate => 1);
1927"#,
1928 );
1929
1930 let model = &models[0];
1931 let attr = &model.attributes[0];
1932 assert_eq!(
1933 attr.predicate.as_deref(),
1934 Some("has_name"),
1935 "predicate => 1 should derive predicate name as 'has_<attr>'"
1936 );
1937 }
1938
1939 #[test]
1940 fn clearer_with_string_name() {
1941 let models = build_models(
1942 r#"
1943use Moo;
1944has 'name' => (is => 'rw', clearer => 'clear_name');
1945"#,
1946 );
1947
1948 let model = &models[0];
1949 let attr = &model.attributes[0];
1950 assert_eq!(
1951 attr.clearer.as_deref(),
1952 Some("clear_name"),
1953 "clearer string name should be captured"
1954 );
1955 }
1956
1957 #[test]
1958 fn clearer_with_numeric_one_generates_default_name() {
1959 let models = build_models(
1960 r#"
1961use Moo;
1962has 'name' => (is => 'rw', clearer => 1);
1963"#,
1964 );
1965
1966 let model = &models[0];
1967 let attr = &model.attributes[0];
1968 assert_eq!(
1969 attr.clearer.as_deref(),
1970 Some("clear_name"),
1971 "clearer => 1 should derive clearer name as 'clear_<attr>'"
1972 );
1973 }
1974
1975 #[test]
1976 fn coerce_flag_true() {
1977 let models = build_models(
1978 r#"
1979use Moose;
1980has 'age' => (is => 'rw', isa => 'Int', coerce => 1);
1981"#,
1982 );
1983
1984 let model = &models[0];
1985 let attr = &model.attributes[0];
1986 assert!(attr.coerce, "coerce => 1 should set coerce flag");
1987 }
1988
1989 #[test]
1990 fn coerce_flag_false_when_absent() {
1991 let models = build_models(
1992 r#"
1993use Moose;
1994has 'age' => (is => 'rw', isa => 'Int');
1995"#,
1996 );
1997
1998 let model = &models[0];
1999 let attr = &model.attributes[0];
2000 assert!(!attr.coerce, "coerce should be false when not specified");
2001 }
2002
2003 #[test]
2004 fn trigger_flag_true() {
2005 let models = build_models(
2006 r#"
2007use Moose;
2008has 'name' => (is => 'rw', trigger => \&_on_name_change);
2009"#,
2010 );
2011
2012 let model = &models[0];
2013 let attr = &model.attributes[0];
2014 assert!(attr.trigger, "trigger option should set trigger flag");
2015 }
2016
2017 #[test]
2018 fn trigger_flag_false_when_absent() {
2019 let models = build_models(
2020 r#"
2021use Moose;
2022has 'name' => (is => 'rw');
2023"#,
2024 );
2025
2026 let model = &models[0];
2027 let attr = &model.attributes[0];
2028 assert!(!attr.trigger, "trigger should be false when not specified");
2029 }
2030
2031 #[test]
2034 fn native_class_produces_model() {
2035 let models = build_models(
2036 r#"
2037class MyApp::Point {
2038 field $x :param = 0;
2039 field $y :param = 0;
2040 method get_x { return $x; }
2041 method get_y { return $y; }
2042}
2043"#,
2044 );
2045 assert_eq!(models.len(), 1, "expected one ClassModel for MyApp::Point");
2046 let model = &models[0];
2047 assert_eq!(model.name, "MyApp::Point");
2048 assert_eq!(model.framework, Framework::NativeClass);
2049 assert_eq!(model.methods.len(), 2);
2050 assert!(model.methods.iter().any(|m| m.name == "get_x"));
2051 assert!(model.methods.iter().any(|m| m.name == "get_y"));
2052 }
2053
2054 #[test]
2055 fn native_class_and_moo_class_do_not_interfere() {
2056 let models = build_models(
2057 r#"
2058class Native::Point {
2059 field $x :param = 0;
2060 method get_x { return $x; }
2061}
2062
2063package Moo::User;
2064use Moo;
2065has 'name' => (is => 'ro');
2066"#,
2067 );
2068 assert_eq!(models.len(), 2, "expected 2 ClassModels: Native::Point and Moo::User");
2069 let native = models.iter().find(|m| m.name == "Native::Point");
2070 assert!(native.is_some(), "expected Native::Point model");
2071 let native = native.unwrap();
2072 assert_eq!(native.framework, Framework::NativeClass);
2073 let moo = models.iter().find(|m| m.name == "Moo::User");
2074 assert!(moo.is_some(), "expected Moo::User model");
2075 let moo = moo.unwrap();
2076 assert_eq!(moo.framework, Framework::Moo);
2077 }
2078
2079 #[test]
2080 fn object_pad_fields_and_accessors_are_tracked() {
2081 let models = build_models(
2082 r#"
2083use Object::Pad;
2084
2085class Point {
2086 field $x :param :reader = 0;
2087 field $y :param :writer = 1;
2088
2089 method move { }
2090}
2091"#,
2092 );
2093
2094 assert!(
2095 !models.is_empty(),
2096 "expected at least one model, got {:?}",
2097 models.iter().map(|m| (&m.name, m.framework)).collect::<Vec<_>>()
2098 );
2099 let model = find_model(&models, "Point").expect("Point model");
2100 assert_eq!(model.framework, Framework::ObjectPad);
2101 assert_eq!(model.fields.len(), 2);
2102 assert!(has_method(model, "x", true, None));
2103 assert!(has_method(model, "set_y", true, None));
2104 assert!(has_method(model, "move", false, None));
2105
2106 let x = model.fields.iter().find(|field| field.name == "x").unwrap();
2107 assert!(x.param);
2108 assert_eq!(x.reader.as_deref(), Some("x"));
2109 assert_eq!(x.default.as_deref(), Some("0"));
2110
2111 let y = model.fields.iter().find(|field| field.name == "y").unwrap();
2112 assert!(y.param);
2113 assert_eq!(y.writer.as_deref(), Some("set_y"));
2114 assert_eq!(y.default.as_deref(), Some("1"));
2115
2116 let param_names: Vec<_> = model.object_pad_param_field_names().collect();
2117 assert_eq!(param_names, vec!["x", "y"]);
2118 }
2119
2120 #[test]
2121 fn object_pad_adjust_blocks_are_tracked() {
2122 let models = build_models(
2123 r#"
2124use Object::Pad;
2125
2126class Config {
2127 ADJUST {
2128 my $tmp = 1;
2129 }
2130}
2131"#,
2132 );
2133
2134 let model = find_model(&models, "Config").expect("Config model");
2135 assert_eq!(model.framework, Framework::ObjectPad);
2136 assert_eq!(model.adjusts.len(), 1, "expected one ADJUST block");
2137 assert_eq!(model.adjusts[0].name, "ADJUST");
2138 assert!(model.adjusts[0].synthetic, "ADJUST should be modeled as synthetic");
2139 }
2140
2141 #[test]
2142 fn object_pad_param_field_names_exclude_non_param_fields() {
2143 let models = build_models(
2144 r#"
2145use Object::Pad;
2146
2147class Config {
2148 field $name :param;
2149 field $cache = 1;
2150}
2151"#,
2152 );
2153
2154 let model = find_model(&models, "Config").expect("Config model");
2155 let param_names: Vec<_> = model.object_pad_param_field_names().collect();
2156 assert_eq!(param_names, vec!["name"]);
2157 }
2158
2159 #[test]
2160 fn object_pad_generated_names_follow_documented_defaults() {
2161 let models = build_models(
2162 r#"
2163use Object::Pad;
2164
2165class Defaults {
2166 field $_secret :reader :writer :accessor :mutator;
2167}
2168"#,
2169 );
2170
2171 let model = find_model(&models, "Defaults").expect("Defaults model");
2172 let field = model.fields.iter().find(|field| field.name == "_secret").unwrap();
2173
2174 assert_eq!(field.reader.as_deref(), Some("secret"));
2175 assert_eq!(field.writer.as_deref(), Some("set_secret"));
2176 assert_eq!(field.accessor.as_deref(), Some("secret"));
2177 assert_eq!(field.mutator.as_deref(), Some("secret"));
2178
2179 assert!(has_method(model, "secret", true, None));
2180 assert!(has_method(model, "set_secret", true, None));
2181 }
2182
2183 #[test]
2184 fn all_advanced_options_together() {
2185 let models = build_models(
2186 r#"
2187use Moo;
2188has 'status' => (
2189 is => 'rw',
2190 isa => 'Str',
2191 builder => '_build_status',
2192 coerce => 1,
2193 predicate => 'has_status',
2194 clearer => 'clear_status',
2195 trigger => \&_on_status_change,
2196);
2197"#,
2198 );
2199
2200 let model = &models[0];
2201 let attr = &model.attributes[0];
2202 assert_eq!(attr.builder.as_deref(), Some("_build_status"));
2203 assert!(attr.coerce);
2204 assert_eq!(attr.predicate.as_deref(), Some("has_status"));
2205 assert_eq!(attr.clearer.as_deref(), Some("clear_status"));
2206 assert!(attr.trigger);
2207 }
2208
2209 #[test]
2212 fn use_parent_plain_oo() {
2213 let code = "package Child; use parent 'Parent'; sub greet { } 1;";
2214 let models = build_models(code);
2215 let model = find_model(&models, "Child").expect("Child model");
2216 assert_eq!(model.framework, Framework::PlainOO);
2217 assert!(model.parents.contains(&"Parent".to_string()), "parents should contain 'Parent'");
2218 }
2219
2220 #[test]
2221 fn use_parent_multiple() {
2222 let code = "package Child; use parent qw(Base1 Base2); 1;";
2223 let models = build_models(code);
2224 let model = find_model(&models, "Child").expect("Child model");
2225 assert_eq!(model.framework, Framework::PlainOO);
2226 assert!(model.parents.contains(&"Base1".to_string()), "parents should contain Base1");
2227 assert!(model.parents.contains(&"Base2".to_string()), "parents should contain Base2");
2228 }
2229
2230 #[test]
2231 fn isa_array_assignment() {
2232 let code = "package Child; our @ISA = qw(Parent); sub greet { } 1;";
2233 let models = build_models(code);
2234 let model = find_model(&models, "Child").expect("Child model");
2235 assert!(
2236 model.parents.contains(&"Parent".to_string()),
2237 "parents should contain 'Parent' from @ISA"
2238 );
2239 }
2240
2241 #[test]
2242 fn use_parent_norequire() {
2243 let code = "package Child; use parent -norequire, 'Base'; 1;";
2245 let models = build_models(code);
2246 let model = find_model(&models, "Child").expect("Child model");
2247 assert!(
2248 model.parents.contains(&"Base".to_string()),
2249 "parents should contain 'Base' even with -norequire"
2250 );
2251 }
2252
2253 #[test]
2254 fn use_base_plain_oo() {
2255 let code = "package Child; use base 'Parent'; sub greet { } 1;";
2256 let models = build_models(code);
2257 let model = find_model(&models, "Child").expect("Child model");
2258 assert_eq!(model.framework, Framework::PlainOO);
2259 assert!(
2260 model.parents.contains(&"Parent".to_string()),
2261 "parents should contain 'Parent' from use base"
2262 );
2263 }
2264
2265 #[test]
2266 fn plain_oo_does_not_regress_moose_extends() {
2267 let models = build_models(
2269 r#"
2270package MyApp::Admin;
2271use Moose;
2272extends 'MyApp::User';
2273has 'level' => (is => 'ro');
2274"#,
2275 );
2276 let model = find_model(&models, "MyApp::Admin").expect("Admin model");
2277 assert_eq!(model.framework, Framework::Moose);
2278 assert!(
2279 model.parents.contains(&"MyApp::User".to_string()),
2280 "Moose extends should still populate parents"
2281 );
2282 }
2283
2284 #[test]
2287 fn export_array_captured() {
2288 let code = "package MyUtils;\nour @EXPORT = qw(foo bar);\nour @EXPORT_OK = qw(baz);\nsub foo {}\nsub bar {}\nsub baz {}\n1;";
2289 let models = build_models(code);
2290 let model = find_model(&models, "MyUtils").expect("MyUtils model");
2291 assert_eq!(model.exports, vec!["foo".to_string(), "bar".to_string()]);
2292 assert_eq!(model.export_ok, vec!["baz".to_string()]);
2293 }
2294
2295 #[test]
2296 fn export_non_oo_package_produces_model() {
2297 let code = "package MyUtils;\nour @EXPORT = qw(helper);\nsub helper { 1 }\n1;";
2298 let models = build_models(code);
2299 assert!(
2300 find_model(&models, "MyUtils").is_some(),
2301 "export-only package must produce a model"
2302 );
2303 }
2304
2305 #[test]
2306 fn export_ok_assignment_without_our() {
2307 let code = "package MyLib;\n@EXPORT_OK = qw(util_a util_b);\n1;";
2308 let models = build_models(code);
2309 let model = find_model(&models, "MyLib").expect("MyLib model");
2310 assert_eq!(model.export_ok, vec!["util_a".to_string(), "util_b".to_string()]);
2311 }
2312
2313 #[test]
2314 fn export_assignment_without_our() {
2315 let code = "package MyLib;\n@EXPORT = qw(func_a func_b);\n1;";
2317 let models = build_models(code);
2318 let model = find_model(&models, "MyLib").expect("MyLib model");
2319 assert_eq!(model.exports, vec!["func_a".to_string(), "func_b".to_string()]);
2320 }
2321
2322 #[test]
2323 fn exporter_metadata_resolves_export_and_export_ok() {
2324 let code = r#"
2325package MyUtils;
2326use Exporter 'import';
2327our @EXPORT = qw(foo missing_default);
2328our @EXPORT_OK = ('bar', "missing_ok");
2329sub foo { 1 }
2330sub bar { 1 }
23311;
2332"#;
2333 let models = build_models(code);
2334 let model = find_model(&models, "MyUtils").expect("MyUtils model");
2335 let metadata = model.exporter_metadata.as_ref().expect("exporter metadata");
2336
2337 assert_eq!(
2338 metadata.exports.iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
2339 vec!["foo"]
2340 );
2341 assert_eq!(
2342 metadata.export_ok.iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
2343 vec!["bar"]
2344 );
2345 assert!(metadata.unresolved.contains(&"missing_default".to_string()));
2346 assert!(metadata.unresolved.contains(&"missing_ok".to_string()));
2347 }
2348
2349 #[test]
2350 fn exporter_metadata_resolves_export_tags() {
2351 let code = r#"
2352package MyTags;
2353use parent 'Exporter';
2354our %EXPORT_TAGS = (
2355 util => [qw(one two missing)],
2356 misc => ['three'],
2357);
2358sub one { 1 }
2359sub two { 1 }
2360sub three { 1 }
23611;
2362"#;
2363 let models = build_models(code);
2364 let model = find_model(&models, "MyTags").expect("MyTags model");
2365 let metadata = model.exporter_metadata.as_ref().expect("exporter metadata");
2366
2367 let util_names = metadata
2368 .export_tags
2369 .get("util")
2370 .expect("util tag")
2371 .iter()
2372 .map(|item| item.name.as_str())
2373 .collect::<Vec<_>>();
2374 let misc_names = metadata
2375 .export_tags
2376 .get("misc")
2377 .expect("misc tag")
2378 .iter()
2379 .map(|item| item.name.as_str())
2380 .collect::<Vec<_>>();
2381
2382 assert_eq!(util_names, vec!["one", "two"]);
2383 assert_eq!(misc_names, vec!["three"]);
2384 assert!(metadata.unresolved.contains(&"missing".to_string()));
2385 }
2386
2387 #[test]
2388 fn export_lists_without_exporter_usage_do_not_produce_exporter_metadata() {
2389 let code = r#"
2390package NoExporter;
2391our @EXPORT = qw(foo);
2392our @EXPORT_OK = qw(bar);
2393our %EXPORT_TAGS = (all => [qw(foo bar)]);
2394sub foo { 1 }
2395sub bar { 1 }
23961;
2397"#;
2398 let models = build_models(code);
2399 let model = find_model(&models, "NoExporter").expect("NoExporter model");
2400 assert!(model.exporter_metadata.is_none());
2401 }
2402
2403 #[test]
2406 fn push_isa_single_parent() {
2407 let code = "package Child;\npush @ISA, 'Parent';\n1;";
2408 let models = build_models(code);
2409 let model = find_model(&models, "Child").expect("Child model");
2410 assert!(model.parents.contains(&"Parent".to_string()), "push @ISA must capture parent");
2411 assert_eq!(model.framework, Framework::PlainOO);
2412 }
2413
2414 #[test]
2415 fn push_isa_multiple_parents() {
2416 let code = "package Child;\npush @ISA, 'Base1', 'Base2';\n1;";
2417 let models = build_models(code);
2418 let model = find_model(&models, "Child").expect("Child model");
2419 assert!(model.parents.contains(&"Base1".to_string()));
2420 assert!(model.parents.contains(&"Base2".to_string()));
2421 }
2422
2423 #[test]
2424 fn push_isa_does_not_downgrade_moose_framework() {
2425 let code = "package Child;\nuse Moose;\nextends 'Base';\npush @ISA, 'Extra';\n1;";
2427 let models = build_models(code);
2428 let model = find_model(&models, "Child").expect("Child model");
2429 assert_eq!(model.framework, Framework::Moose, "Moose must not be downgraded to PlainOO");
2430 assert!(
2431 model.parents.contains(&"Extra".to_string()),
2432 "push @ISA parent must still be captured"
2433 );
2434 }
2435
2436 #[test]
2439 fn native_class_with_isa_has_correct_parent() {
2440 let models = build_models(
2441 r#"
2442class Point3D :isa(Point) {
2443 field $z :param = 0;
2444 method get_z { return $z; }
2445}
2446"#,
2447 );
2448 assert_eq!(models.len(), 1, "expected one ClassModel for Point3D");
2449 let model = &models[0];
2450 assert_eq!(model.name, "Point3D");
2451 assert_eq!(model.framework, Framework::NativeClass);
2452 assert!(
2453 model.parents.contains(&"Point".to_string()),
2454 "native class :isa(Point) must populate parents, got {:?}",
2455 model.parents
2456 );
2457 }
2458
2459 #[test]
2460 fn native_class_with_multiple_isa_has_all_parents() {
2461 let models = build_models(
2462 r#"
2463class Shape3D :isa(Shape) :isa(Printable) {
2464 field $z :param = 0;
2465}
2466"#,
2467 );
2468 assert_eq!(models.len(), 1, "expected one ClassModel for Shape3D");
2469 let model = &models[0];
2470 assert_eq!(model.framework, Framework::NativeClass);
2471 assert!(
2472 model.parents.contains(&"Shape".to_string()),
2473 "expected 'Shape' in parents, got {:?}",
2474 model.parents
2475 );
2476 assert!(
2477 model.parents.contains(&"Printable".to_string()),
2478 "expected 'Printable' in parents, got {:?}",
2479 model.parents
2480 );
2481 }
2482
2483 #[test]
2484 fn native_class_without_isa_has_no_parents() {
2485 let models = build_models(
2486 r#"
2487class Point {
2488 field $x :param = 0;
2489 field $y :param = 0;
2490}
2491"#,
2492 );
2493 assert_eq!(models.len(), 1);
2494 let model = &models[0];
2495 assert_eq!(model.framework, Framework::NativeClass);
2496 assert!(
2497 model.parents.is_empty(),
2498 "class without :isa must have no parents, got {:?}",
2499 model.parents
2500 );
2501 }
2502
2503 #[test]
2504 fn native_class_with_qualified_isa_has_qualified_parent() {
2505 let models = build_models(
2506 r#"
2507class MyApp::Point3D :isa(MyApp::Point) {
2508 field $z :param = 0;
2509}
2510"#,
2511 );
2512 assert_eq!(models.len(), 1);
2513 let model = &models[0];
2514 assert_eq!(model.name, "MyApp::Point3D");
2515 assert!(
2516 model.parents.contains(&"MyApp::Point".to_string()),
2517 "qualified :isa must preserve qualified name, got {:?}",
2518 model.parents
2519 );
2520 }
2521
2522 #[test]
2523 fn second_class_without_isa_does_not_inherit_first_class_parents() {
2524 let models = build_models(
2527 r#"
2528class Point3D :isa(Point) {
2529 field $z :param = 0;
2530}
2531class Standalone {
2532 field $x :param = 0;
2533}
2534"#,
2535 );
2536 let standalone = models.iter().find(|m| m.name == "Standalone").expect("Standalone model");
2537 assert!(
2538 standalone.parents.is_empty(),
2539 "Standalone class must have no parents, but got {:?}",
2540 standalone.parents
2541 );
2542 }
2543}