1use crate::SourceLocation;
8use crate::ast::{Node, NodeKind};
9use std::collections::HashMap;
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 None,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum AccessorType {
37 Ro,
39 Rw,
41 Lazy,
43 Bare,
45}
46
47#[derive(Debug, Clone)]
49pub struct Attribute {
50 pub name: String,
52 pub is: Option<AccessorType>,
54 pub isa: Option<String>,
56 pub default: bool,
58 pub required: bool,
60 pub accessor_name: String,
62 pub location: SourceLocation,
64 pub builder: Option<String>,
66 pub coerce: bool,
68 pub predicate: Option<String>,
70 pub clearer: Option<String>,
72 pub trigger: bool,
74}
75
76#[derive(Debug, Clone)]
78pub struct MethodModifier {
79 pub kind: ModifierKind,
81 pub method_name: String,
83 pub location: SourceLocation,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum ModifierKind {
90 Before,
92 After,
94 Around,
96}
97
98#[derive(Debug, Clone)]
100pub struct MethodInfo {
101 pub name: String,
103 pub location: SourceLocation,
105}
106
107#[derive(Debug, Clone)]
109pub struct ClassModel {
110 pub name: String,
112 pub framework: Framework,
114 pub attributes: Vec<Attribute>,
116 pub methods: Vec<MethodInfo>,
118 pub parents: Vec<String>,
120 pub roles: Vec<String>,
122 pub modifiers: Vec<MethodModifier>,
124}
125
126impl ClassModel {
127 pub fn has_framework(&self) -> bool {
129 !matches!(self.framework, Framework::None)
130 }
131}
132
133pub struct ClassModelBuilder {
135 models: Vec<ClassModel>,
136 current_package: String,
137 current_framework: Framework,
138 current_attributes: Vec<Attribute>,
139 current_methods: Vec<MethodInfo>,
140 current_parents: Vec<String>,
141 current_roles: Vec<String>,
142 current_modifiers: Vec<MethodModifier>,
143 framework_map: HashMap<String, Framework>,
145}
146
147impl Default for ClassModelBuilder {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl ClassModelBuilder {
154 pub fn new() -> Self {
156 Self {
157 models: Vec::new(),
158 current_package: "main".to_string(),
159 current_framework: Framework::None,
160 current_attributes: Vec::new(),
161 current_methods: Vec::new(),
162 current_parents: Vec::new(),
163 current_roles: Vec::new(),
164 current_modifiers: Vec::new(),
165 framework_map: HashMap::new(),
166 }
167 }
168
169 pub fn build(mut self, node: &Node) -> Vec<ClassModel> {
171 self.visit_node(node);
172 self.flush_current_package();
173 self.models
174 }
175
176 fn flush_current_package(&mut self) {
178 let framework = self.current_framework;
179 let has_oo_indicator = framework != Framework::None
181 || !self.current_attributes.is_empty()
182 || !self.current_parents.is_empty();
183 if has_oo_indicator {
184 let model = ClassModel {
185 name: self.current_package.clone(),
186 framework,
187 attributes: std::mem::take(&mut self.current_attributes),
188 methods: std::mem::take(&mut self.current_methods),
189 parents: std::mem::take(&mut self.current_parents),
190 roles: std::mem::take(&mut self.current_roles),
191 modifiers: std::mem::take(&mut self.current_modifiers),
192 };
193 self.models.push(model);
194 } else {
195 self.current_attributes.clear();
197 self.current_methods.clear();
198 self.current_parents.clear();
199 self.current_roles.clear();
200 self.current_modifiers.clear();
201 }
202 }
203
204 fn visit_node(&mut self, node: &Node) {
205 match &node.kind {
206 NodeKind::Program { statements } => {
207 self.visit_statement_list(statements);
208 }
209
210 NodeKind::Package { name, block, .. } => {
211 self.flush_current_package();
213
214 self.current_package = name.clone();
215 self.current_framework =
216 self.framework_map.get(name).copied().unwrap_or(Framework::None);
217
218 if let Some(block) = block {
219 self.visit_node(block);
220 }
221 }
222
223 NodeKind::Block { statements, .. } => {
224 self.visit_statement_list(statements);
225 }
226
227 NodeKind::Subroutine { name, body, .. } => {
228 if let Some(sub_name) = name {
229 self.current_methods
230 .push(MethodInfo { name: sub_name.clone(), location: node.location });
231 }
232 self.visit_node(body);
233 }
234
235 NodeKind::Use { module, args, .. } => {
236 self.detect_framework(module, args);
237 }
238
239 NodeKind::VariableDeclaration { variable, initializer, .. } => {
241 if let NodeKind::Variable { sigil, name } = &variable.kind
242 && sigil == "@"
243 && name == "ISA"
244 && let Some(init) = initializer
245 {
246 self.extract_isa_from_node(init);
247 }
248 }
249
250 NodeKind::Assignment { lhs, rhs, .. } => {
252 if let NodeKind::Variable { sigil, name } = &lhs.kind
253 && sigil == "@"
254 && name == "ISA"
255 {
256 self.extract_isa_from_node(rhs);
257 }
258 }
259
260 NodeKind::Class { name, body } => {
261 self.flush_current_package();
262 self.current_package = name.clone();
263 self.current_framework = Framework::NativeClass;
264 self.framework_map.insert(name.clone(), Framework::NativeClass);
265 self.visit_node(body);
266 }
267
268 NodeKind::Method { name, body, .. } => {
269 self.current_methods
270 .push(MethodInfo { name: name.clone(), location: node.location });
271 self.visit_node(body);
272 }
273
274 _ => {
275 self.visit_children(node);
277 }
278 }
279 }
280
281 fn visit_children(&mut self, node: &Node) {
282 match &node.kind {
283 NodeKind::ExpressionStatement { expression } => {
284 self.visit_node(expression);
285 }
286 NodeKind::Block { statements, .. } => {
287 self.visit_statement_list(statements);
288 }
289 NodeKind::If { condition, then_branch, else_branch, .. } => {
290 self.visit_node(condition);
291 self.visit_node(then_branch);
292 if let Some(else_node) = else_branch {
293 self.visit_node(else_node);
294 }
295 }
296 _ => {}
297 }
298 }
299
300 fn visit_statement_list(&mut self, statements: &[Node]) {
301 let mut idx = 0;
302 while idx < statements.len() {
303 if let NodeKind::Use { module, args, .. } = &statements[idx].kind {
305 self.detect_framework(module, args);
306 idx += 1;
307 continue;
308 }
309
310 let is_framework_package = self.current_framework != Framework::None;
311
312 if is_framework_package {
313 if let Some(consumed) = self.try_extract_has(statements, idx) {
315 idx += consumed;
316 continue;
317 }
318 if let Some(consumed) = self.try_extract_modifier(statements, idx) {
320 idx += consumed;
321 continue;
322 }
323 if let Some(consumed) = self.try_extract_extends_with(statements, idx) {
325 idx += consumed;
326 continue;
327 }
328 }
329
330 self.visit_node(&statements[idx]);
332 idx += 1;
333 }
334 }
335
336 fn detect_framework(&mut self, module: &str, args: &[String]) {
338 let framework = match module {
339 "Moose" | "Moose::Role" => Framework::Moose,
340 "Moo" | "Moo::Role" => Framework::Moo,
341 "Mouse" | "Mouse::Role" => Framework::Mouse,
342 "Class::Accessor" => Framework::ClassAccessor,
343 "Object::Pad" => Framework::ObjectPad,
344 "base" | "parent" => {
345 let mut has_class_accessor = false;
351 let mut captured_parents: Vec<String> = Vec::new();
352
353 for arg in args {
354 let trimmed = arg.trim();
355 if trimmed.starts_with('-') || trimmed.is_empty() {
356 continue;
358 }
359 let names = expand_arg_to_names(trimmed);
361 for name in names {
362 if name == "Class::Accessor" {
363 has_class_accessor = true;
364 } else {
365 captured_parents.push(name);
366 }
367 }
368 }
369
370 self.current_parents.extend(captured_parents);
371
372 if has_class_accessor {
373 Framework::ClassAccessor
374 } else if self.current_framework == Framework::None {
375 Framework::PlainOO
377 } else {
378 return;
380 }
381 }
382 _ => return,
383 };
384
385 self.current_framework = framework;
386 self.framework_map.insert(self.current_package.clone(), framework);
387 }
388
389 fn try_extract_has(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
393 let first = &statements[idx];
394
395 if idx + 1 < statements.len() {
399 let second = &statements[idx + 1];
400 let is_has_marker = matches!(
401 &first.kind,
402 NodeKind::ExpressionStatement { expression }
403 if matches!(&expression.kind, NodeKind::Identifier { name } if name == "has")
404 );
405
406 if is_has_marker {
407 if let NodeKind::ExpressionStatement { expression } = &second.kind {
408 let has_location =
409 SourceLocation { start: first.location.start, end: second.location.end };
410
411 match &expression.kind {
412 NodeKind::HashLiteral { pairs } => {
413 self.extract_has_from_pairs(pairs, has_location, false);
414 return Some(2);
415 }
416 NodeKind::ArrayLiteral { elements } => {
417 if let Some(Node { kind: NodeKind::HashLiteral { pairs }, .. }) =
418 elements.last()
419 {
420 let mut names = Vec::new();
421 for el in elements.iter().take(elements.len() - 1) {
422 names.extend(collect_symbol_names(el));
423 }
424 if !names.is_empty() {
425 self.extract_has_with_names(&names, pairs, has_location);
426 return Some(2);
427 }
428 }
429 }
430 _ => {}
431 }
432 }
433 }
434 }
435
436 if let NodeKind::ExpressionStatement { expression } = &first.kind
438 && let NodeKind::HashLiteral { pairs } = &expression.kind
439 {
440 let has_embedded = pairs.iter().any(|(key_node, _)| {
441 matches!(
442 &key_node.kind,
443 NodeKind::Binary { op, left, .. }
444 if op == "[]" && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
445 )
446 });
447
448 if has_embedded {
449 self.extract_has_from_pairs(pairs, first.location, true);
450 return Some(1);
451 }
452 }
453
454 if let NodeKind::ExpressionStatement { expression } = &first.kind
457 && let NodeKind::FunctionCall { name, args } = &expression.kind
458 && name == "has"
459 && !args.is_empty()
460 {
461 let options_hash_idx =
463 args.iter().rposition(|a| matches!(a.kind, NodeKind::HashLiteral { .. }));
464 if let Some(opts_idx) = options_hash_idx {
465 if let NodeKind::HashLiteral { pairs } = &args[opts_idx].kind {
466 let names: Vec<String> =
467 args[..opts_idx].iter().flat_map(collect_symbol_names).collect();
468 if !names.is_empty() {
469 self.extract_has_with_names(&names, pairs, first.location);
470 return Some(1);
471 }
472 }
473 }
474 }
475
476 None
477 }
478
479 fn extract_has_from_pairs(
481 &mut self,
482 pairs: &[(Node, Node)],
483 location: SourceLocation,
484 require_embedded: bool,
485 ) {
486 for (attr_expr, options_expr) in pairs {
487 let attr_expr = if let NodeKind::Binary { op, left, right } = &attr_expr.kind
488 && op == "[]"
489 && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
490 {
491 right.as_ref()
492 } else if require_embedded {
493 continue;
494 } else {
495 attr_expr
496 };
497
498 let names = collect_symbol_names(attr_expr);
499 if names.is_empty() {
500 continue;
501 }
502
503 if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
504 self.extract_has_with_names(&names, option_pairs, location);
505 }
506 }
507 }
508
509 fn extract_has_with_names(
511 &mut self,
512 names: &[String],
513 option_pairs: &[(Node, Node)],
514 location: SourceLocation,
515 ) {
516 let options = extract_hash_options(option_pairs);
517
518 let is = options.get("is").and_then(|v| match v.as_str() {
519 "ro" => Some(AccessorType::Ro),
520 "rw" => Some(AccessorType::Rw),
521 "lazy" => Some(AccessorType::Lazy),
522 "bare" => Some(AccessorType::Bare),
523 _ => None,
524 });
525
526 let isa = options.get("isa").cloned();
527 let default = options.contains_key("default")
528 || options.contains_key("builder")
529 || is == Some(AccessorType::Lazy);
530 let required = options.get("required").is_some_and(|v| v == "1" || v == "true");
531 let coerce = options.get("coerce").is_some_and(|v| v == "1" || v == "true");
532 let trigger = options.contains_key("trigger");
533
534 let explicit_accessor = options.get("accessor").or_else(|| options.get("reader")).cloned();
536
537 for name in names {
538 let accessor_name = explicit_accessor.clone().unwrap_or_else(|| name.clone());
539
540 let builder = options
542 .get("builder")
543 .map(|v| if v == "1" { format!("_build_{name}") } else { v.clone() });
544
545 let predicate = options
547 .get("predicate")
548 .map(|v| if v == "1" { format!("has_{name}") } else { v.clone() });
549
550 let clearer = options
552 .get("clearer")
553 .map(|v| if v == "1" { format!("clear_{name}") } else { v.clone() });
554
555 self.current_attributes.push(Attribute {
556 name: name.clone(),
557 is,
558 isa: isa.clone(),
559 default,
560 required,
561 accessor_name,
562 location,
563 builder,
564 coerce,
565 predicate,
566 clearer,
567 trigger,
568 });
569 }
570 }
571
572 fn try_extract_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
574 let first = &statements[idx];
575
576 if let NodeKind::ExpressionStatement { expression } = &first.kind
578 && let NodeKind::FunctionCall { name, args } = &expression.kind
579 {
580 let modifier_kind = match name.as_str() {
581 "before" => Some(ModifierKind::Before),
582 "after" => Some(ModifierKind::After),
583 "around" => Some(ModifierKind::Around),
584 _ => None,
585 };
586 if let Some(modifier_kind) = modifier_kind {
587 let method_names: Vec<String> =
589 args.first().map(collect_symbol_names).unwrap_or_default();
590 if !method_names.is_empty() {
591 for method_name in method_names {
592 self.current_modifiers.push(MethodModifier {
593 kind: modifier_kind,
594 method_name,
595 location: first.location,
596 });
597 }
598 return Some(1);
599 }
600 }
601 }
602
603 if idx + 1 >= statements.len() {
607 return None;
608 }
609 let second = &statements[idx + 1];
610
611 let modifier_kind = match &first.kind {
612 NodeKind::ExpressionStatement { expression } => match &expression.kind {
613 NodeKind::Identifier { name } => match name.as_str() {
614 "before" => Some(ModifierKind::Before),
615 "after" => Some(ModifierKind::After),
616 "around" => Some(ModifierKind::Around),
617 _ => None,
618 },
619 _ => None,
620 },
621 _ => None,
622 };
623
624 let modifier_kind = modifier_kind?;
625
626 let NodeKind::ExpressionStatement { expression } = &second.kind else {
627 return None;
628 };
629 let NodeKind::HashLiteral { pairs } = &expression.kind else {
630 return None;
631 };
632
633 let location = SourceLocation { start: first.location.start, end: second.location.end };
634
635 for (key_node, _) in pairs {
636 let method_names = collect_symbol_names(key_node);
637 for method_name in method_names {
638 self.current_modifiers.push(MethodModifier {
639 kind: modifier_kind,
640 method_name,
641 location,
642 });
643 }
644 }
645
646 Some(2)
647 }
648
649 fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
651 let first = &statements[idx];
652
653 if let NodeKind::ExpressionStatement { expression } = &first.kind
656 && let NodeKind::FunctionCall { name, args } = &expression.kind
657 && matches!(name.as_str(), "extends" | "with")
658 {
659 let names: Vec<String> = args.iter().flat_map(collect_symbol_names).collect();
660 if !names.is_empty() {
661 if name == "extends" {
662 self.current_parents.extend(names);
663 } else {
664 self.current_roles.extend(names);
665 }
666 return Some(1);
667 }
668 }
669
670 if idx + 1 >= statements.len() {
674 return None;
675 }
676 let second = &statements[idx + 1];
677
678 let keyword = match &first.kind {
679 NodeKind::ExpressionStatement { expression } => match &expression.kind {
680 NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
681 name.as_str()
682 }
683 _ => return None,
684 },
685 _ => return None,
686 };
687
688 let NodeKind::ExpressionStatement { expression } = &second.kind else {
689 return None;
690 };
691
692 let names = collect_symbol_names(expression);
693 if names.is_empty() {
694 return None;
695 }
696
697 if keyword == "extends" {
698 self.current_parents.extend(names);
699 } else {
700 self.current_roles.extend(names);
701 }
702
703 Some(2)
704 }
705
706 fn extract_isa_from_node(&mut self, node: &Node) {
708 let parents = collect_symbol_names(node);
709 if !parents.is_empty() {
710 if self.current_framework == Framework::None {
712 self.current_framework = Framework::PlainOO;
713 self.framework_map.insert(self.current_package.clone(), Framework::PlainOO);
714 }
715 self.current_parents.extend(parents);
716 }
717 }
718}
719
720fn collect_symbol_names(node: &Node) -> Vec<String> {
723 match &node.kind {
724 NodeKind::String { value, .. } => normalize_symbol_name(value).into_iter().collect(),
725 NodeKind::Identifier { name } => normalize_symbol_name(name).into_iter().collect(),
726 NodeKind::ArrayLiteral { elements } => {
727 elements.iter().flat_map(collect_symbol_names).collect()
728 }
729 _ => Vec::new(),
730 }
731}
732
733fn normalize_symbol_name(raw: &str) -> Option<String> {
734 let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
735 if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
736}
737
738fn expand_arg_to_names(arg: &str) -> Vec<String> {
744 let arg = arg.trim();
745 if arg.starts_with("qw(") && arg.ends_with(')') {
747 let content = &arg[3..arg.len() - 1];
748 return content
749 .split_whitespace()
750 .filter(|s| !s.is_empty())
751 .map(|s| s.to_string())
752 .collect();
753 }
754 if arg.starts_with("qw") && arg.len() > 2 {
756 let open = arg.chars().nth(2).unwrap_or(' ');
757 let close = match open {
758 '(' => ')',
759 '{' => '}',
760 '[' => ']',
761 '<' => '>',
762 c => c,
763 };
764 if let (Some(start), Some(end)) = (arg.find(open), arg.rfind(close)) {
765 if start < end {
766 let content = &arg[start + 1..end];
767 return content
768 .split_whitespace()
769 .filter(|s| !s.is_empty())
770 .map(|s| s.to_string())
771 .collect();
772 }
773 }
774 }
775 normalize_symbol_name(arg).into_iter().collect()
777}
778
779fn extract_hash_options(pairs: &[(Node, Node)]) -> HashMap<String, String> {
780 let mut options = HashMap::new();
781 for (key_node, value_node) in pairs {
782 let Some(key_name) = collect_symbol_names(key_node).into_iter().next() else {
783 continue;
784 };
785 let value_text = value_summary(value_node);
786 options.insert(key_name, value_text);
787 }
788 options
789}
790
791fn value_summary(node: &Node) -> String {
792 match &node.kind {
793 NodeKind::String { value, .. } => {
794 normalize_symbol_name(value).unwrap_or_else(|| value.clone())
795 }
796 NodeKind::Identifier { name } => name.clone(),
797 NodeKind::Number { value } => value.clone(),
798 _ => "expr".to_string(),
799 }
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805 use crate::parser::Parser;
806 use perl_tdd_support::must;
807 use std::collections::HashSet;
808
809 fn build_models(code: &str) -> Vec<ClassModel> {
810 let mut parser = Parser::new(code);
811 let ast = must(parser.parse());
812 ClassModelBuilder::new().build(&ast)
813 }
814
815 fn find_model<'a>(models: &'a [ClassModel], name: &str) -> Option<&'a ClassModel> {
816 models.iter().find(|m| m.name == name)
817 }
818
819 #[test]
820 fn basic_moo_class() {
821 let models = build_models(
822 r#"
823package MyApp::User;
824use Moo;
825
826has 'name' => (is => 'ro', isa => 'Str');
827has 'age' => (is => 'rw', required => 1);
828
829sub greet { }
830"#,
831 );
832
833 let model = find_model(&models, "MyApp::User");
834 assert!(model.is_some(), "expected ClassModel for MyApp::User");
835 let model = model.unwrap();
836
837 assert_eq!(model.framework, Framework::Moo);
838 assert_eq!(model.attributes.len(), 2);
839
840 let name_attr = model.attributes.iter().find(|a| a.name == "name");
841 assert!(name_attr.is_some());
842 let name_attr = name_attr.unwrap();
843 assert_eq!(name_attr.is, Some(AccessorType::Ro));
844 assert_eq!(name_attr.isa.as_deref(), Some("Str"));
845 assert!(!name_attr.required);
846 assert_eq!(name_attr.accessor_name, "name");
847
848 let age_attr = model.attributes.iter().find(|a| a.name == "age");
849 assert!(age_attr.is_some());
850 let age_attr = age_attr.unwrap();
851 assert_eq!(age_attr.is, Some(AccessorType::Rw));
852 assert!(age_attr.required);
853
854 assert!(model.methods.iter().any(|m| m.name == "greet"));
855 }
856
857 #[test]
858 fn moose_extends_and_with() {
859 let models = build_models(
860 r#"
861package MyApp::Admin;
862use Moose;
863extends 'MyApp::User';
864with 'MyApp::Printable', 'MyApp::Serializable';
865
866has 'level' => (is => 'ro');
867"#,
868 );
869
870 let model = find_model(&models, "MyApp::Admin");
871 assert!(model.is_some());
872 let model = model.unwrap();
873
874 assert_eq!(model.framework, Framework::Moose);
875 assert!(model.parents.contains(&"MyApp::User".to_string()));
876 assert_eq!(model.roles, vec!["MyApp::Printable", "MyApp::Serializable"]);
877 assert_eq!(model.attributes.len(), 1);
878 }
879
880 #[test]
881 fn method_modifiers() {
882 let models = build_models(
883 r#"
884package MyApp::User;
885use Moo;
886before 'save' => sub { };
887after 'save' => sub { };
888around 'validate' => sub { };
889"#,
890 );
891
892 let model = find_model(&models, "MyApp::User");
893 assert!(model.is_some());
894 let model = model.unwrap();
895
896 assert_eq!(model.modifiers.len(), 3);
897 assert!(
898 model
899 .modifiers
900 .iter()
901 .any(|m| m.kind == ModifierKind::Before && m.method_name == "save")
902 );
903 assert!(
904 model
905 .modifiers
906 .iter()
907 .any(|m| m.kind == ModifierKind::After && m.method_name == "save")
908 );
909 assert!(
910 model
911 .modifiers
912 .iter()
913 .any(|m| m.kind == ModifierKind::Around && m.method_name == "validate")
914 );
915 }
916
917 #[test]
918 fn no_model_for_plain_package() {
919 let models = build_models(
920 r#"
921package MyApp::Utils;
922sub helper { 1 }
923"#,
924 );
925
926 assert!(
927 find_model(&models, "MyApp::Utils").is_none(),
928 "plain package should not produce a ClassModel"
929 );
930 }
931
932 #[test]
933 fn multiple_packages() {
934 let models = build_models(
935 r#"
936package MyApp::User;
937use Moo;
938has 'name' => (is => 'ro');
939
940package MyApp::Admin;
941use Moose;
942extends 'MyApp::User';
943has 'level' => (is => 'rw');
944
945package MyApp::Utils;
946sub helper { 1 }
947"#,
948 );
949
950 assert_eq!(models.len(), 2, "expected 2 ClassModels (User + Admin, not Utils)");
951 assert!(find_model(&models, "MyApp::User").is_some());
952 assert!(find_model(&models, "MyApp::Admin").is_some());
953 assert!(find_model(&models, "MyApp::Utils").is_none());
954 }
955
956 #[test]
957 fn qw_attribute_list() {
958 let models = build_models(
959 r#"
960use Moo;
961has [qw(first_name last_name)] => (is => 'ro');
962"#,
963 );
964
965 assert_eq!(models.len(), 1);
966 let model = &models[0];
967 assert_eq!(model.attributes.len(), 2);
968
969 let names: HashSet<_> = model.attributes.iter().map(|a| a.name.as_str()).collect();
970 assert!(names.contains("first_name"));
971 assert!(names.contains("last_name"));
972 }
973
974 #[test]
975 fn has_framework_helper() {
976 let models = build_models(
977 r#"
978package MyApp::User;
979use Moo;
980has 'name' => (is => 'ro');
981"#,
982 );
983
984 let model = find_model(&models, "MyApp::User").unwrap();
985 assert!(model.has_framework());
986 }
987
988 #[test]
989 fn accessor_type_lazy() {
990 let models = build_models(
991 r#"
992use Moo;
993has 'config' => (is => 'lazy');
994"#,
995 );
996
997 let model = &models[0];
998 assert_eq!(model.attributes[0].is, Some(AccessorType::Lazy));
999 assert!(model.attributes[0].default, "lazy implies default");
1000 }
1001
1002 #[test]
1003 fn explicit_accessor_name() {
1004 let models = build_models(
1005 r#"
1006use Moo;
1007has 'name' => (is => 'ro', reader => 'get_name');
1008"#,
1009 );
1010
1011 let model = &models[0];
1012 assert_eq!(model.attributes[0].accessor_name, "get_name");
1013 }
1014
1015 #[test]
1016 fn default_via_builder_option() {
1017 let models = build_models(
1018 r#"
1019use Moo;
1020has 'config' => (is => 'ro', builder => 1);
1021"#,
1022 );
1023
1024 let model = &models[0];
1025 assert!(model.attributes[0].default, "builder option implies default");
1026 }
1027
1028 #[test]
1029 fn lazy_builder_with_string_name() {
1030 let models = build_models(
1031 r#"
1032use Moo;
1033has 'config' => (is => 'ro', lazy => 1, builder => '_build_config');
1034"#,
1035 );
1036
1037 let model = &models[0];
1038 let attr = &model.attributes[0];
1039 assert_eq!(
1040 attr.builder.as_deref(),
1041 Some("_build_config"),
1042 "builder string should be captured"
1043 );
1044 assert!(attr.default, "named builder implies default");
1045 }
1046
1047 #[test]
1048 fn lazy_builder_with_numeric_one_generates_default_name() {
1049 let models = build_models(
1050 r#"
1051use Moo;
1052has 'profile' => (is => 'ro', builder => 1);
1053"#,
1054 );
1055
1056 let model = &models[0];
1057 let attr = &model.attributes[0];
1058 assert_eq!(
1059 attr.builder.as_deref(),
1060 Some("_build_profile"),
1061 "builder => 1 should derive builder name as '_build_<attr>'"
1062 );
1063 }
1064
1065 #[test]
1066 fn predicate_with_string_name() {
1067 let models = build_models(
1068 r#"
1069use Moo;
1070has 'name' => (is => 'ro', predicate => 'has_name');
1071"#,
1072 );
1073
1074 let model = &models[0];
1075 let attr = &model.attributes[0];
1076 assert_eq!(
1077 attr.predicate.as_deref(),
1078 Some("has_name"),
1079 "predicate string name should be captured"
1080 );
1081 }
1082
1083 #[test]
1084 fn predicate_with_numeric_one_generates_default_name() {
1085 let models = build_models(
1086 r#"
1087use Moo;
1088has 'name' => (is => 'ro', predicate => 1);
1089"#,
1090 );
1091
1092 let model = &models[0];
1093 let attr = &model.attributes[0];
1094 assert_eq!(
1095 attr.predicate.as_deref(),
1096 Some("has_name"),
1097 "predicate => 1 should derive predicate name as 'has_<attr>'"
1098 );
1099 }
1100
1101 #[test]
1102 fn clearer_with_string_name() {
1103 let models = build_models(
1104 r#"
1105use Moo;
1106has 'name' => (is => 'rw', clearer => 'clear_name');
1107"#,
1108 );
1109
1110 let model = &models[0];
1111 let attr = &model.attributes[0];
1112 assert_eq!(
1113 attr.clearer.as_deref(),
1114 Some("clear_name"),
1115 "clearer string name should be captured"
1116 );
1117 }
1118
1119 #[test]
1120 fn clearer_with_numeric_one_generates_default_name() {
1121 let models = build_models(
1122 r#"
1123use Moo;
1124has 'name' => (is => 'rw', clearer => 1);
1125"#,
1126 );
1127
1128 let model = &models[0];
1129 let attr = &model.attributes[0];
1130 assert_eq!(
1131 attr.clearer.as_deref(),
1132 Some("clear_name"),
1133 "clearer => 1 should derive clearer name as 'clear_<attr>'"
1134 );
1135 }
1136
1137 #[test]
1138 fn coerce_flag_true() {
1139 let models = build_models(
1140 r#"
1141use Moose;
1142has 'age' => (is => 'rw', isa => 'Int', coerce => 1);
1143"#,
1144 );
1145
1146 let model = &models[0];
1147 let attr = &model.attributes[0];
1148 assert!(attr.coerce, "coerce => 1 should set coerce flag");
1149 }
1150
1151 #[test]
1152 fn coerce_flag_false_when_absent() {
1153 let models = build_models(
1154 r#"
1155use Moose;
1156has 'age' => (is => 'rw', isa => 'Int');
1157"#,
1158 );
1159
1160 let model = &models[0];
1161 let attr = &model.attributes[0];
1162 assert!(!attr.coerce, "coerce should be false when not specified");
1163 }
1164
1165 #[test]
1166 fn trigger_flag_true() {
1167 let models = build_models(
1168 r#"
1169use Moose;
1170has 'name' => (is => 'rw', trigger => \&_on_name_change);
1171"#,
1172 );
1173
1174 let model = &models[0];
1175 let attr = &model.attributes[0];
1176 assert!(attr.trigger, "trigger option should set trigger flag");
1177 }
1178
1179 #[test]
1180 fn trigger_flag_false_when_absent() {
1181 let models = build_models(
1182 r#"
1183use Moose;
1184has 'name' => (is => 'rw');
1185"#,
1186 );
1187
1188 let model = &models[0];
1189 let attr = &model.attributes[0];
1190 assert!(!attr.trigger, "trigger should be false when not specified");
1191 }
1192
1193 #[test]
1196 fn native_class_produces_model() {
1197 let models = build_models(
1198 r#"
1199class MyApp::Point {
1200 field $x :param = 0;
1201 field $y :param = 0;
1202 method get_x { return $x; }
1203 method get_y { return $y; }
1204}
1205"#,
1206 );
1207 assert_eq!(models.len(), 1, "expected one ClassModel for MyApp::Point");
1208 let model = &models[0];
1209 assert_eq!(model.name, "MyApp::Point");
1210 assert_eq!(model.framework, Framework::NativeClass);
1211 assert_eq!(model.methods.len(), 2);
1212 assert!(model.methods.iter().any(|m| m.name == "get_x"));
1213 assert!(model.methods.iter().any(|m| m.name == "get_y"));
1214 }
1215
1216 #[test]
1217 fn native_class_and_moo_class_do_not_interfere() {
1218 let models = build_models(
1219 r#"
1220class Native::Point {
1221 field $x :param = 0;
1222 method get_x { return $x; }
1223}
1224
1225package Moo::User;
1226use Moo;
1227has 'name' => (is => 'ro');
1228"#,
1229 );
1230 assert_eq!(models.len(), 2, "expected 2 ClassModels: Native::Point and Moo::User");
1231 let native = models.iter().find(|m| m.name == "Native::Point");
1232 assert!(native.is_some(), "expected Native::Point model");
1233 let native = native.unwrap();
1234 assert_eq!(native.framework, Framework::NativeClass);
1235 let moo = models.iter().find(|m| m.name == "Moo::User");
1236 assert!(moo.is_some(), "expected Moo::User model");
1237 let moo = moo.unwrap();
1238 assert_eq!(moo.framework, Framework::Moo);
1239 }
1240
1241 #[test]
1242 fn all_advanced_options_together() {
1243 let models = build_models(
1244 r#"
1245use Moo;
1246has 'status' => (
1247 is => 'rw',
1248 isa => 'Str',
1249 builder => '_build_status',
1250 coerce => 1,
1251 predicate => 'has_status',
1252 clearer => 'clear_status',
1253 trigger => \&_on_status_change,
1254);
1255"#,
1256 );
1257
1258 let model = &models[0];
1259 let attr = &model.attributes[0];
1260 assert_eq!(attr.builder.as_deref(), Some("_build_status"));
1261 assert!(attr.coerce);
1262 assert_eq!(attr.predicate.as_deref(), Some("has_status"));
1263 assert_eq!(attr.clearer.as_deref(), Some("clear_status"));
1264 assert!(attr.trigger);
1265 }
1266
1267 #[test]
1270 fn use_parent_plain_oo() {
1271 let code = "package Child; use parent 'Parent'; sub greet { } 1;";
1272 let models = build_models(code);
1273 let model = find_model(&models, "Child").expect("Child model");
1274 assert_eq!(model.framework, Framework::PlainOO);
1275 assert!(model.parents.contains(&"Parent".to_string()), "parents should contain 'Parent'");
1276 }
1277
1278 #[test]
1279 fn use_parent_multiple() {
1280 let code = "package Child; use parent qw(Base1 Base2); 1;";
1281 let models = build_models(code);
1282 let model = find_model(&models, "Child").expect("Child model");
1283 assert_eq!(model.framework, Framework::PlainOO);
1284 assert!(model.parents.contains(&"Base1".to_string()), "parents should contain Base1");
1285 assert!(model.parents.contains(&"Base2".to_string()), "parents should contain Base2");
1286 }
1287
1288 #[test]
1289 fn isa_array_assignment() {
1290 let code = "package Child; our @ISA = qw(Parent); sub greet { } 1;";
1291 let models = build_models(code);
1292 let model = find_model(&models, "Child").expect("Child model");
1293 assert!(
1294 model.parents.contains(&"Parent".to_string()),
1295 "parents should contain 'Parent' from @ISA"
1296 );
1297 }
1298
1299 #[test]
1300 fn use_parent_norequire() {
1301 let code = "package Child; use parent -norequire, 'Base'; 1;";
1303 let models = build_models(code);
1304 let model = find_model(&models, "Child").expect("Child model");
1305 assert!(
1306 model.parents.contains(&"Base".to_string()),
1307 "parents should contain 'Base' even with -norequire"
1308 );
1309 }
1310
1311 #[test]
1312 fn use_base_plain_oo() {
1313 let code = "package Child; use base 'Parent'; sub greet { } 1;";
1314 let models = build_models(code);
1315 let model = find_model(&models, "Child").expect("Child model");
1316 assert_eq!(model.framework, Framework::PlainOO);
1317 assert!(
1318 model.parents.contains(&"Parent".to_string()),
1319 "parents should contain 'Parent' from use base"
1320 );
1321 }
1322
1323 #[test]
1324 fn plain_oo_does_not_regress_moose_extends() {
1325 let models = build_models(
1327 r#"
1328package MyApp::Admin;
1329use Moose;
1330extends 'MyApp::User';
1331has 'level' => (is => 'ro');
1332"#,
1333 );
1334 let model = find_model(&models, "MyApp::Admin").expect("Admin model");
1335 assert_eq!(model.framework, Framework::Moose);
1336 assert!(
1337 model.parents.contains(&"MyApp::User".to_string()),
1338 "Moose extends should still populate parents"
1339 );
1340 }
1341}