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