1use crate::emit_decision;
37use crate::hir::{HirExpr, Type};
38use crate::rust_gen::keywords::safe_ident;
39use std::collections::HashMap;
40
41fn type_to_rust_string(ty: &Type) -> String {
54 match ty {
55 Type::Int => "i32".to_string(),
56 Type::Float => "f64".to_string(),
57 Type::String => "String".to_string(),
58 Type::Bool => "bool".to_string(),
59 Type::Custom(name) if name == "PathBuf" => "PathBuf".to_string(),
60 Type::Custom(name)
63 if name == "object" || name == "builtins.object" || name == "Any" || name == "any" =>
64 {
65 "String".to_string()
66 }
67 Type::Custom(name) => name.clone(),
68 Type::List(inner) => format!("Vec<{}>", type_to_rust_string(inner)),
69 Type::Optional(inner) => format!("Option<{}>", type_to_rust_string(inner)),
70 _ => "String".to_string(), }
72}
73
74#[derive(Debug, Clone, PartialEq)]
79pub struct ArgParserInfo {
80 pub parser_var: String,
82
83 pub description: Option<String>,
85
86 pub epilog: Option<String>,
88
89 pub arguments: Vec<ArgParserArgument>,
91
92 pub args_var: Option<String>,
94}
95
96impl ArgParserInfo {
97 pub fn new(parser_var: String) -> Self {
102 Self {
103 parser_var,
104 description: None,
105 epilog: None,
106 arguments: Vec::new(),
107 args_var: None,
108 }
109 }
110
111 pub fn add_argument(&mut self, arg: ArgParserArgument) {
116 self.arguments.push(arg);
117 }
118
119 pub fn set_args_var(&mut self, var: String) {
124 self.args_var = Some(var);
125 }
126}
127
128#[derive(Debug, Clone, PartialEq)]
133pub struct ArgParserArgument {
134 pub name: String,
136
137 pub long: Option<String>,
139
140 pub nargs: Option<String>,
142
143 pub arg_type: Option<Type>,
145
146 pub action: Option<String>,
148
149 pub default: Option<HirExpr>,
151
152 pub help: Option<String>,
154
155 pub is_positional: bool,
157
158 pub required: Option<bool>,
160
161 pub dest: Option<String>,
163
164 pub metavar: Option<String>,
166
167 pub choices: Option<Vec<String>>,
169
170 pub const_value: Option<HirExpr>,
172}
173
174impl ArgParserArgument {
175 pub fn new(name: String) -> Self {
180 let is_positional = !name.starts_with('-');
181 Self {
182 name,
183 long: None,
184 nargs: None,
185 arg_type: None,
186 action: None,
187 default: None,
188 help: None,
189 is_positional,
190 required: None,
191 dest: None,
192 metavar: None,
193 choices: None,
194 const_value: None,
195 }
196 }
197
198 pub fn rust_field_name(&self) -> String {
204 if let Some(ref dest) = self.dest {
206 return dest.replace('-', "_");
207 }
208
209 if self.is_positional {
210 self.name.clone()
212 } else if let Some(ref long) = self.long {
213 long.trim_start_matches("--").replace('-', "_")
215 } else {
216 self.name.trim_start_matches('-').replace('-', "_")
219 }
220 }
221
222 pub fn rust_type(&self) -> String {
227 if self.action.as_deref() == Some("store_true")
230 || self.action.as_deref() == Some("store_false")
231 || self.action.as_deref() == Some("store_const")
232 {
233 return "bool".to_string();
234 }
235
236 if self.action.as_deref() == Some("count") {
238 return "u8".to_string();
239 }
240
241 if self.action.as_deref() == Some("append") {
243 let inner_type = self
244 .arg_type
245 .as_ref()
246 .map(type_to_rust_string)
247 .unwrap_or_else(|| "String".to_string());
248 return format!("Vec<{}>", inner_type);
249 }
250
251 if self.nargs.as_deref() == Some("+") || self.nargs.as_deref() == Some("*") {
253 let inner_type = self
254 .arg_type
255 .as_ref()
256 .map(type_to_rust_string)
257 .unwrap_or_else(|| "String".to_string());
258 return format!("Vec<{}>", inner_type);
259 }
260
261 if let Some(nargs_str) = self.nargs.as_deref() {
263 if nargs_str.parse::<usize>().is_ok() {
264 let inner_type = self
265 .arg_type
266 .as_ref()
267 .map(type_to_rust_string)
268 .unwrap_or_else(|| "String".to_string());
269 return format!("Vec<{}>", inner_type);
270 }
271 }
272
273 if self.nargs.as_deref() == Some("?") {
276 let inner_type = self
277 .arg_type
278 .as_ref()
279 .map(type_to_rust_string)
280 .unwrap_or_else(|| "String".to_string());
281 return format!("Option<{}>", inner_type);
282 }
283
284 if !self.is_positional
289 && self.required != Some(true)
290 && self.nargs.is_none()
291 && self.default.is_none()
292 {
293 let inner_type = self
294 .arg_type
295 .as_ref()
296 .map(type_to_rust_string)
297 .unwrap_or_else(|| "String".to_string());
298 return format!("Option<{}>", inner_type);
299 }
300
301 self.arg_type
303 .as_ref()
304 .map(type_to_rust_string)
305 .unwrap_or_else(|| "String".to_string())
306 }
307}
308
309#[derive(Debug, Clone, PartialEq)]
314pub struct SubparserInfo {
315 pub parser_var: String,
317
318 pub dest_field: String,
320
321 pub required: bool,
323
324 pub help: Option<String>,
326}
327
328#[derive(Debug, Clone, PartialEq)]
333pub struct SubcommandInfo {
334 pub name: String,
336
337 pub help: Option<String>,
339
340 pub arguments: Vec<ArgParserArgument>,
342
343 pub subparsers_var: String,
345}
346
347#[derive(Debug, Clone, Default)]
352pub struct ArgParserTracker {
353 pub parsers: HashMap<String, ArgParserInfo>,
355
356 pub group_to_parser: HashMap<String, String>,
360
361 pub subparsers: HashMap<String, SubparserInfo>,
364
365 pub subcommands: HashMap<String, SubcommandInfo>,
368
369 pub subcommand_var_to_cmd: HashMap<String, String>,
372
373 pub struct_generated: bool,
375}
376
377impl ArgParserTracker {
378 pub fn new() -> Self {
383 Self::default()
384 }
385
386 pub fn register_parser(&mut self, var_name: String, info: ArgParserInfo) {
391 self.parsers.insert(var_name, info);
392 }
393
394 pub fn get_parser_mut(&mut self, var_name: &str) -> Option<&mut ArgParserInfo> {
399 self.parsers.get_mut(var_name)
400 }
401
402 pub fn get_parser(&self, var_name: &str) -> Option<&ArgParserInfo> {
407 self.parsers.get(var_name)
408 }
409
410 pub fn clear(&mut self) {
415 self.parsers.clear();
416 self.group_to_parser.clear(); self.subparsers.clear(); self.subcommands.clear(); self.subcommand_var_to_cmd.clear(); self.struct_generated = false;
421 }
422
423 pub fn register_group(&mut self, group_var: String, parser_var: String) {
429 self.group_to_parser.insert(group_var, parser_var);
430 }
431
432 pub fn get_parser_for_group(&self, group_var: &str) -> Option<String> {
439 let mut current = group_var;
440 let mut visited = std::collections::HashSet::new();
441
442 loop {
444 if !visited.insert(current) {
446 return None;
447 }
448
449 if let Some(parent) = self.group_to_parser.get(current) {
451 if self.parsers.contains_key(parent) {
453 return Some(parent.clone());
454 }
455 current = parent;
457 } else {
458 return None;
460 }
461 }
462 }
463
464 pub fn register_subparsers(&mut self, subparsers_var: String, info: SubparserInfo) {
470 self.subparsers.insert(subparsers_var, info);
471 }
472
473 pub fn get_subparsers(&self, subparsers_var: &str) -> Option<&SubparserInfo> {
478 self.subparsers.get(subparsers_var)
479 }
480
481 pub fn get_subparsers_mut(&mut self, subparsers_var: &str) -> Option<&mut SubparserInfo> {
486 self.subparsers.get_mut(subparsers_var)
487 }
488
489 pub fn register_subcommand(&mut self, subcommand_var: String, info: SubcommandInfo) {
495 self.subcommands.insert(subcommand_var, info);
496 }
497
498 pub fn get_subcommand(&self, subcommand_var: &str) -> Option<&SubcommandInfo> {
503 self.subcommands.get(subcommand_var)
504 }
505
506 pub fn get_subcommand_mut(&mut self, subcommand_var: &str) -> Option<&mut SubcommandInfo> {
511 self.subcommands.get_mut(subcommand_var)
512 }
513
514 pub fn has_parsers(&self) -> bool {
519 !self.parsers.is_empty()
520 }
521
522 pub fn get_first_parser(&self) -> Option<&ArgParserInfo> {
527 self.parsers.values().next()
528 }
529
530 pub fn has_subcommands(&self) -> bool {
535 !self.subcommands.is_empty()
536 }
537}
538
539pub fn generate_commands_enum(tracker: &ArgParserTracker) -> proc_macro2::TokenStream {
544 use quote::{format_ident, quote};
545
546 if tracker.subcommands.is_empty() {
547 return quote! {};
548 }
549
550 let variants: Vec<proc_macro2::TokenStream> = tracker
551 .subcommands
552 .values()
553 .filter(|subcommand| !subcommand.name.is_empty())
555 .map(|subcommand| {
556 emit_decision!("argparse.enum.variant.added", &subcommand.name);
557 let variant_name = format_ident!("{}", to_pascal_case(&subcommand.name));
559
560 let help_attr = if let Some(ref help) = subcommand.help {
562 quote! { #[command(about = #help)] }
563 } else {
564 quote! {}
565 };
566
567 let mut seen_field_names = std::collections::HashSet::new();
570 let fields: Vec<proc_macro2::TokenStream> = subcommand
571 .arguments
572 .iter()
573 .filter(|arg| {
574 let field_name = arg.rust_field_name();
575 if seen_field_names.contains(&field_name) {
576 false
577 } else {
578 seen_field_names.insert(field_name);
579 true
580 }
581 })
582 .map(|arg| {
583 let field_name = safe_ident(&arg.rust_field_name());
585 let type_str = arg.rust_type();
586 let field_type: syn::Type =
587 syn::parse_str(&type_str).unwrap_or_else(|_| syn::parse_quote! { String });
588
589 let help_attr = if let Some(ref help) = arg.help {
591 quote! { #[doc = #help] }
592 } else {
593 quote! {}
594 };
595
596 if arg.is_positional {
598 quote! {
599 #help_attr
600 #field_name: #field_type
601 }
602 } else {
603 quote! {
604 #[arg(long)]
605 #help_attr
606 #field_name: #field_type
607 }
608 }
609 })
610 .collect();
611
612 quote! {
613 #help_attr
614 #variant_name {
615 #(#fields),*
616 }
617 }
618 })
619 .collect();
620
621 quote! {
622 #[derive(clap::Subcommand)]
623 enum Commands {
624 #(#variants),*
625 }
626 }
627}
628
629fn to_pascal_case(s: &str) -> String {
634 s.split(&['-', '_'][..])
635 .map(|word| {
636 let mut chars = word.chars();
637 match chars.next() {
638 None => String::new(),
639 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
640 }
641 })
642 .collect()
643}
644
645pub fn generate_args_struct(
650 parser_info: &ArgParserInfo,
651 tracker: &ArgParserTracker,
652) -> proc_macro2::TokenStream {
653 use quote::quote;
654 use syn::parse_quote;
655
656 let mut fields: Vec<proc_macro2::TokenStream> = parser_info
658 .arguments
659 .iter()
660 .map(|arg| {
661 let field_name = safe_ident(&arg.rust_field_name());
663
664 let base_type_str = arg.rust_type();
666
667 let has_implicit_default = matches!(
677 arg.action.as_deref(),
678 Some("store_true")
679 | Some("store_false")
680 | Some("count")
681 | Some("append")
682 | Some("store_const")
683 );
684 let is_required_nargs = arg.nargs.as_deref() == Some("+")
686 || arg
687 .nargs
688 .as_deref()
689 .map(|s| s.parse::<usize>().is_ok())
690 .unwrap_or(false);
691
692 let field_type: syn::Type = if !arg.is_positional
693 && arg.required != Some(true)
694 && arg.default.is_none()
695 && !base_type_str.starts_with("Option<")
696 && !has_implicit_default
697 && !is_required_nargs
698 {
699 syn::parse_str(&format!("Option<{}>", base_type_str))
701 .unwrap_or_else(|_| parse_quote! { Option<String> })
702 } else {
703 syn::parse_str(&base_type_str).unwrap_or_else(|_| parse_quote! { String })
704 };
705
706 let mut attrs = vec![];
708
709 if !arg.is_positional {
710 if let Some(long) = &arg.long {
718 let short_str = arg.name.trim_start_matches('-');
720 if let Some(short) = short_str.chars().next() {
721 if arg.dest.is_some() {
723 let long_name = long.trim_start_matches("--");
724 attrs.push(quote! {
725 #[arg(short = #short, long = #long_name)]
726 });
727 } else {
728 attrs.push(quote! {
729 #[arg(short = #short, long)]
730 });
731 }
732 }
733 } else if arg.name.starts_with("--") {
734 if arg.dest.is_some() {
737 let long_name = arg.name.trim_start_matches("--");
738 attrs.push(quote! {
739 #[arg(long = #long_name)]
740 });
741 } else {
742 attrs.push(quote! {
743 #[arg(long)]
744 });
745 }
746 } else {
747 let short_str = arg.name.trim_start_matches('-');
749 if let Some(short) = short_str.chars().next() {
750 attrs.push(quote! {
751 #[arg(short = #short)]
752 });
753 }
754 }
755 }
756
757 if let Some(crate::hir::HirExpr::Literal(lit)) = arg.default.as_ref() {
759 let default_str_opt = match lit {
761 crate::hir::Literal::Int(n) => Some(n.to_string()),
762 crate::hir::Literal::Float(f) => Some(f.to_string()),
763 crate::hir::Literal::String(s) => Some(s.clone()),
764 crate::hir::Literal::Bool(b) => Some(b.to_string()),
765 _ => None, };
767 if let Some(default_str) = default_str_opt {
768 attrs.push(quote! {
769 #[arg(default_value = #default_str)]
770 });
771 }
772 }
773
774 if arg.nargs.as_deref() == Some("?") && arg.const_value.is_some() {
776 if let Some(crate::hir::HirExpr::Literal(lit)) = arg.const_value.as_ref() {
777 let const_str_opt = match lit {
778 crate::hir::Literal::Int(n) => Some(n.to_string()),
779 crate::hir::Literal::Float(f) => Some(f.to_string()),
780 crate::hir::Literal::String(s) => Some(s.clone()),
781 crate::hir::Literal::Bool(b) => Some(b.to_string()),
782 _ => None,
783 };
784 if let Some(const_str) = const_str_opt {
785 attrs.push(quote! {
786 #[arg(default_missing_value = #const_str, num_args = 0..=1)]
787 });
788 }
789 }
790 }
791
792 if let Some(nargs_str) = arg.nargs.as_deref() {
794 if let Ok(n) = nargs_str.parse::<usize>() {
795 let n_lit = syn::LitInt::new(&format!("{}", n), proc_macro2::Span::call_site());
797 attrs.push(quote! {
798 #[arg(num_args = #n_lit)]
799 });
800 }
801 }
802
803 if let Some(ref metavar) = arg.metavar {
805 attrs.push(quote! {
806 #[arg(value_name = #metavar)]
807 });
808 }
809
810 if let Some(ref choices) = arg.choices {
812 let choice_strs: Vec<_> = choices.iter().collect();
813 attrs.push(quote! {
814 #[arg(value_parser = [#(#choice_strs),*])]
815 });
816 }
817
818 match arg.action.as_deref() {
820 Some("count") => {
821 attrs.push(quote! {
822 #[arg(action = clap::ArgAction::Count)]
823 });
824 }
825 Some("store_true") => {
826 attrs.push(quote! {
827 #[arg(action = clap::ArgAction::SetTrue)]
828 });
829 }
830 Some("store_false") => {
831 attrs.push(quote! {
832 #[arg(action = clap::ArgAction::SetFalse)]
833 });
834 }
835 _ => {}
836 }
837
838 if arg.action.as_deref() == Some("store_false") {
840 attrs.push(quote! {
842 #[arg(default_value_t = true)]
843 });
844 } else if arg.action.as_deref() == Some("store_const") && arg.const_value.is_some() {
845 if let Some(crate::hir::HirExpr::Literal(crate::hir::Literal::Bool(_val))) =
847 arg.const_value.as_ref()
848 {
849 attrs.push(quote! {
850 #[arg(default_value_t = false)]
851 });
852 }
853 }
854
855 if let Some(ref help_text) = arg.help {
857 attrs.push(quote! {
858 #[doc = #help_text]
859 });
860 }
861
862 quote! {
863 #(#attrs)*
864 #field_name: #field_type
865 }
866 })
867 .collect();
868
869 if tracker.has_subcommands() {
871 fields.push(quote! {
872 #[command(subcommand)]
873 command: Option<Commands>
874 });
875 }
876
877 let mut command_attrs = vec![];
879 if let Some(ref desc) = parser_info.description {
880 command_attrs.push(quote! {
881 #[command(about = #desc)]
882 });
883 }
884 if let Some(ref epilog) = parser_info.epilog {
885 command_attrs.push(quote! {
886 #[command(after_help = #epilog)]
887 });
888 }
889
890 quote! {
892 #[derive(clap::Parser)]
893 #(#command_attrs)*
894 struct Args {
895 #(#fields),*
896 }
897 }
898}
899
900pub fn analyze_subcommand_field_access(
907 func: &crate::hir::HirFunction,
908 tracker: &ArgParserTracker,
909) -> Option<(String, Vec<String>)> {
910 use crate::hir::{HirExpr, HirStmt};
911 use std::collections::HashSet;
912
913 if !tracker.has_subcommands() {
914 return None;
915 }
916
917 let args_param = func.params.first()?.name.as_ref();
919
920 let is_args_variable = tracker
925 .parsers
926 .values()
927 .any(|p| p.args_var.as_ref().is_some_and(|av| av == args_param));
928 if !is_args_variable {
929 return None;
930 }
931
932 let mut field_to_variant: HashMap<String, (String, &SubcommandInfo)> = HashMap::new();
934 for subcommand in tracker.subcommands.values() {
935 let variant_name = to_pascal_case(&subcommand.name);
936 for arg in &subcommand.arguments {
937 let field_name = arg.rust_field_name();
938 field_to_variant.insert(field_name, (variant_name.clone(), subcommand));
939 }
940 }
941
942 let mut accessed_fields: HashSet<String> = HashSet::new();
944 let mut detected_variant: Option<String> = None;
945
946 fn walk_expr(
948 expr: &HirExpr,
949 args_param: &str,
950 field_to_variant: &HashMap<String, (String, &SubcommandInfo)>,
951 accessed_fields: &mut HashSet<String>,
952 detected_variant: &mut Option<String>,
953 ) {
954 match expr {
955 HirExpr::Attribute { value, attr } => {
956 if let HirExpr::Var(id) = &**value {
958 if id == args_param {
959 if let Some((variant_name, _)) = field_to_variant.get(attr.as_str()) {
961 accessed_fields.insert(attr.clone());
963 if detected_variant.is_none() {
964 *detected_variant = Some(variant_name.clone());
965 }
966 }
967 }
968 }
969 walk_expr(
971 value,
972 args_param,
973 field_to_variant,
974 accessed_fields,
975 detected_variant,
976 );
977 }
978 HirExpr::Binary { left, right, .. } => {
979 walk_expr(
980 left,
981 args_param,
982 field_to_variant,
983 accessed_fields,
984 detected_variant,
985 );
986 walk_expr(
987 right,
988 args_param,
989 field_to_variant,
990 accessed_fields,
991 detected_variant,
992 );
993 }
994 HirExpr::Unary { operand, .. } => {
995 walk_expr(
996 operand,
997 args_param,
998 field_to_variant,
999 accessed_fields,
1000 detected_variant,
1001 );
1002 }
1003 HirExpr::Call { args, .. } => {
1004 for arg in args {
1006 walk_expr(
1007 arg,
1008 args_param,
1009 field_to_variant,
1010 accessed_fields,
1011 detected_variant,
1012 );
1013 }
1014 }
1015 HirExpr::MethodCall { object, args, .. } => {
1016 walk_expr(
1017 object,
1018 args_param,
1019 field_to_variant,
1020 accessed_fields,
1021 detected_variant,
1022 );
1023 for arg in args {
1024 walk_expr(
1025 arg,
1026 args_param,
1027 field_to_variant,
1028 accessed_fields,
1029 detected_variant,
1030 );
1031 }
1032 }
1033 HirExpr::List(elements)
1034 | HirExpr::Tuple(elements)
1035 | HirExpr::Set(elements)
1036 | HirExpr::FrozenSet(elements) => {
1037 for elem in elements {
1038 walk_expr(
1039 elem,
1040 args_param,
1041 field_to_variant,
1042 accessed_fields,
1043 detected_variant,
1044 );
1045 }
1046 }
1047 HirExpr::Dict(items) => {
1048 for (key, value) in items {
1049 walk_expr(
1050 key,
1051 args_param,
1052 field_to_variant,
1053 accessed_fields,
1054 detected_variant,
1055 );
1056 walk_expr(
1057 value,
1058 args_param,
1059 field_to_variant,
1060 accessed_fields,
1061 detected_variant,
1062 );
1063 }
1064 }
1065 HirExpr::Index { base, index } => {
1066 walk_expr(
1067 base,
1068 args_param,
1069 field_to_variant,
1070 accessed_fields,
1071 detected_variant,
1072 );
1073 walk_expr(
1074 index,
1075 args_param,
1076 field_to_variant,
1077 accessed_fields,
1078 detected_variant,
1079 );
1080 }
1081 HirExpr::Slice {
1082 base,
1083 start,
1084 stop,
1085 step,
1086 } => {
1087 walk_expr(
1088 base,
1089 args_param,
1090 field_to_variant,
1091 accessed_fields,
1092 detected_variant,
1093 );
1094 if let Some(s) = start {
1095 walk_expr(
1096 s,
1097 args_param,
1098 field_to_variant,
1099 accessed_fields,
1100 detected_variant,
1101 );
1102 }
1103 if let Some(s) = stop {
1104 walk_expr(
1105 s,
1106 args_param,
1107 field_to_variant,
1108 accessed_fields,
1109 detected_variant,
1110 );
1111 }
1112 if let Some(s) = step {
1113 walk_expr(
1114 s,
1115 args_param,
1116 field_to_variant,
1117 accessed_fields,
1118 detected_variant,
1119 );
1120 }
1121 }
1122 HirExpr::Borrow { expr, .. } => {
1123 walk_expr(
1124 expr,
1125 args_param,
1126 field_to_variant,
1127 accessed_fields,
1128 detected_variant,
1129 );
1130 }
1131 HirExpr::ListComp {
1132 element,
1133 generators,
1134 }
1135 | HirExpr::SetComp {
1136 element,
1137 generators,
1138 } => {
1139 walk_expr(
1141 element,
1142 args_param,
1143 field_to_variant,
1144 accessed_fields,
1145 detected_variant,
1146 );
1147 for gen in generators {
1148 walk_expr(
1149 &gen.iter,
1150 args_param,
1151 field_to_variant,
1152 accessed_fields,
1153 detected_variant,
1154 );
1155 for cond in &gen.conditions {
1156 walk_expr(
1157 cond,
1158 args_param,
1159 field_to_variant,
1160 accessed_fields,
1161 detected_variant,
1162 );
1163 }
1164 }
1165 }
1166 HirExpr::DictComp {
1167 key,
1168 value,
1169 generators,
1170 } => {
1171 walk_expr(
1173 key,
1174 args_param,
1175 field_to_variant,
1176 accessed_fields,
1177 detected_variant,
1178 );
1179 walk_expr(
1180 value,
1181 args_param,
1182 field_to_variant,
1183 accessed_fields,
1184 detected_variant,
1185 );
1186 for gen in generators {
1187 walk_expr(
1188 &gen.iter,
1189 args_param,
1190 field_to_variant,
1191 accessed_fields,
1192 detected_variant,
1193 );
1194 for cond in &gen.conditions {
1195 walk_expr(
1196 cond,
1197 args_param,
1198 field_to_variant,
1199 accessed_fields,
1200 detected_variant,
1201 );
1202 }
1203 }
1204 }
1205 HirExpr::Lambda { body, .. } => {
1206 walk_expr(
1207 body,
1208 args_param,
1209 field_to_variant,
1210 accessed_fields,
1211 detected_variant,
1212 );
1213 }
1214 HirExpr::FString { parts } => {
1215 for part in parts {
1217 if let crate::hir::FStringPart::Expr(expr) = part {
1218 walk_expr(
1219 expr,
1220 args_param,
1221 field_to_variant,
1222 accessed_fields,
1223 detected_variant,
1224 );
1225 }
1226 }
1227 }
1228 HirExpr::IfExpr { test, body, orelse } => {
1229 walk_expr(
1230 test,
1231 args_param,
1232 field_to_variant,
1233 accessed_fields,
1234 detected_variant,
1235 );
1236 walk_expr(
1237 body,
1238 args_param,
1239 field_to_variant,
1240 accessed_fields,
1241 detected_variant,
1242 );
1243 walk_expr(
1244 orelse,
1245 args_param,
1246 field_to_variant,
1247 accessed_fields,
1248 detected_variant,
1249 );
1250 }
1251 HirExpr::SortByKey {
1252 iterable, key_body, ..
1253 } => {
1254 walk_expr(
1255 iterable,
1256 args_param,
1257 field_to_variant,
1258 accessed_fields,
1259 detected_variant,
1260 );
1261 walk_expr(
1262 key_body,
1263 args_param,
1264 field_to_variant,
1265 accessed_fields,
1266 detected_variant,
1267 );
1268 }
1269 HirExpr::GeneratorExp {
1270 element,
1271 generators,
1272 } => {
1273 walk_expr(
1274 element,
1275 args_param,
1276 field_to_variant,
1277 accessed_fields,
1278 detected_variant,
1279 );
1280 for gen in generators {
1281 walk_expr(
1282 &gen.iter,
1283 args_param,
1284 field_to_variant,
1285 accessed_fields,
1286 detected_variant,
1287 );
1288 for cond in &gen.conditions {
1289 walk_expr(
1290 cond,
1291 args_param,
1292 field_to_variant,
1293 accessed_fields,
1294 detected_variant,
1295 );
1296 }
1297 }
1298 }
1299 HirExpr::Await { value } => {
1300 walk_expr(
1301 value,
1302 args_param,
1303 field_to_variant,
1304 accessed_fields,
1305 detected_variant,
1306 );
1307 }
1308 HirExpr::Yield { value: Some(v) } => {
1309 walk_expr(
1310 v,
1311 args_param,
1312 field_to_variant,
1313 accessed_fields,
1314 detected_variant,
1315 );
1316 }
1317 HirExpr::Yield { value: None } => {}
1318 _ => {}
1319 }
1320 }
1321
1322 fn walk_stmt(
1324 stmt: &HirStmt,
1325 args_param: &str,
1326 field_to_variant: &HashMap<String, (String, &SubcommandInfo)>,
1327 accessed_fields: &mut HashSet<String>,
1328 detected_variant: &mut Option<String>,
1329 ) {
1330 match stmt {
1331 HirStmt::Expr(expr) => walk_expr(
1332 expr,
1333 args_param,
1334 field_to_variant,
1335 accessed_fields,
1336 detected_variant,
1337 ),
1338 HirStmt::Assign { value, .. } => walk_expr(
1339 value,
1340 args_param,
1341 field_to_variant,
1342 accessed_fields,
1343 detected_variant,
1344 ),
1345 HirStmt::Return(Some(expr)) => walk_expr(
1346 expr,
1347 args_param,
1348 field_to_variant,
1349 accessed_fields,
1350 detected_variant,
1351 ),
1352 HirStmt::If {
1353 condition,
1354 then_body,
1355 else_body,
1356 } => {
1357 walk_expr(
1358 condition,
1359 args_param,
1360 field_to_variant,
1361 accessed_fields,
1362 detected_variant,
1363 );
1364 for s in then_body {
1365 walk_stmt(
1366 s,
1367 args_param,
1368 field_to_variant,
1369 accessed_fields,
1370 detected_variant,
1371 );
1372 }
1373 if let Some(else_stmts) = else_body {
1374 for s in else_stmts {
1375 walk_stmt(
1376 s,
1377 args_param,
1378 field_to_variant,
1379 accessed_fields,
1380 detected_variant,
1381 );
1382 }
1383 }
1384 }
1385 HirStmt::While { condition, body } => {
1386 walk_expr(
1387 condition,
1388 args_param,
1389 field_to_variant,
1390 accessed_fields,
1391 detected_variant,
1392 );
1393 for s in body {
1394 walk_stmt(
1395 s,
1396 args_param,
1397 field_to_variant,
1398 accessed_fields,
1399 detected_variant,
1400 );
1401 }
1402 }
1403 HirStmt::For { body, .. } => {
1404 for s in body {
1405 walk_stmt(
1406 s,
1407 args_param,
1408 field_to_variant,
1409 accessed_fields,
1410 detected_variant,
1411 );
1412 }
1413 }
1414 HirStmt::With { context, body, .. } => {
1415 walk_expr(
1416 context,
1417 args_param,
1418 field_to_variant,
1419 accessed_fields,
1420 detected_variant,
1421 );
1422 for s in body {
1423 walk_stmt(
1424 s,
1425 args_param,
1426 field_to_variant,
1427 accessed_fields,
1428 detected_variant,
1429 );
1430 }
1431 }
1432 HirStmt::Try {
1433 body,
1434 handlers,
1435 orelse,
1436 finalbody,
1437 } => {
1438 for s in body {
1439 walk_stmt(
1440 s,
1441 args_param,
1442 field_to_variant,
1443 accessed_fields,
1444 detected_variant,
1445 );
1446 }
1447 for handler in handlers {
1448 for s in &handler.body {
1449 walk_stmt(
1450 s,
1451 args_param,
1452 field_to_variant,
1453 accessed_fields,
1454 detected_variant,
1455 );
1456 }
1457 }
1458 if let Some(orelse_stmts) = orelse {
1459 for s in orelse_stmts {
1460 walk_stmt(
1461 s,
1462 args_param,
1463 field_to_variant,
1464 accessed_fields,
1465 detected_variant,
1466 );
1467 }
1468 }
1469 if let Some(final_stmts) = finalbody {
1470 for s in final_stmts {
1471 walk_stmt(
1472 s,
1473 args_param,
1474 field_to_variant,
1475 accessed_fields,
1476 detected_variant,
1477 );
1478 }
1479 }
1480 }
1481 HirStmt::Assert { test, msg } => {
1482 walk_expr(
1483 test,
1484 args_param,
1485 field_to_variant,
1486 accessed_fields,
1487 detected_variant,
1488 );
1489 if let Some(msg_expr) = msg {
1490 walk_expr(
1491 msg_expr,
1492 args_param,
1493 field_to_variant,
1494 accessed_fields,
1495 detected_variant,
1496 );
1497 }
1498 }
1499 HirStmt::Raise { exception, cause } => {
1500 if let Some(exc) = exception {
1501 walk_expr(
1502 exc,
1503 args_param,
1504 field_to_variant,
1505 accessed_fields,
1506 detected_variant,
1507 );
1508 }
1509 if let Some(cause_expr) = cause {
1510 walk_expr(
1511 cause_expr,
1512 args_param,
1513 field_to_variant,
1514 accessed_fields,
1515 detected_variant,
1516 );
1517 }
1518 }
1519 _ => {}
1520 }
1521 }
1522
1523 for stmt in &func.body {
1524 walk_stmt(
1525 stmt,
1526 args_param,
1527 &field_to_variant,
1528 &mut accessed_fields,
1529 &mut detected_variant,
1530 );
1531 }
1532
1533 if let Some(variant) = detected_variant {
1535 let mut fields: Vec<String> = accessed_fields.into_iter().collect();
1536 fields.sort(); Some((variant, fields))
1538 } else {
1539 None
1540 }
1541}
1542
1543pub fn generate_option_precompute(parser_info: &ArgParserInfo) -> Vec<proc_macro2::TokenStream> {
1552 use quote::{format_ident, quote};
1553
1554 let args_var = match &parser_info.args_var {
1555 Some(var) => var.clone(),
1556 None => return vec![],
1557 };
1558
1559 let args_ident = format_ident!("{}", args_var);
1560
1561 parser_info
1562 .arguments
1563 .iter()
1564 .filter(|arg| arg.rust_type().starts_with("Option<"))
1565 .map(|arg| {
1566 let field_name = arg.rust_field_name();
1567 let field_ident = safe_ident(&field_name);
1569 let has_ident = format_ident!("has_{}", field_name);
1570 quote! {
1571 let #has_ident = #args_ident.#field_ident.is_some();
1573 }
1574 })
1575 .collect()
1576}
1577
1578pub fn wrap_body_with_subcommand_pattern(
1583 body_stmts: Vec<proc_macro2::TokenStream>,
1584 variant_name: &str,
1585 fields: &[String],
1586 args_param: &str,
1587) -> Vec<proc_macro2::TokenStream> {
1588 use quote::{format_ident, quote};
1589
1590 let variant_ident = format_ident!("{}", variant_name);
1591 let args_ident = format_ident!("{}", args_param);
1592 let field_idents: Vec<syn::Ident> = fields.iter().map(|f| format_ident!("{}", f)).collect();
1593
1594 vec![quote! {
1596 if let Some(Commands::#variant_ident { #(#field_idents),* }) = &#args_ident.command {
1597 #(#body_stmts)*
1598 }
1599 }]
1600}
1601
1602pub fn preregister_subcommands_from_hir(
1608 func: &crate::hir::HirFunction,
1609 tracker: &mut ArgParserTracker,
1610) {
1611 use crate::hir::{HirExpr, HirStmt};
1612
1613 fn extract_string_from_hir(expr: &HirExpr) -> String {
1615 match expr {
1616 HirExpr::Literal(crate::hir::Literal::String(s)) => s.clone(),
1617 _ => String::new(),
1618 }
1619 }
1620
1621 fn extract_kwarg_string_from_hir(kwargs: &[(String, HirExpr)], key: &str) -> Option<String> {
1623 kwargs
1624 .iter()
1625 .find(|(k, _)| k == key)
1626 .map(|(_, v)| extract_string_from_hir(v))
1627 }
1628
1629 fn walk_expr(expr: &HirExpr, tracker: &mut ArgParserTracker) {
1631 match expr {
1632 HirExpr::MethodCall {
1633 object,
1634 method,
1635 args,
1636 kwargs,
1637 } if method == "add_parser" => {
1638 if let HirExpr::Var(subparsers_var) = object.as_ref() {
1640 if tracker.get_subparsers(subparsers_var).is_some() {
1641 if !args.is_empty() {
1643 let command_name = extract_string_from_hir(&args[0]);
1644 if command_name.is_empty() {
1648 emit_decision!(
1649 "argparse.subcommand.skipped.variable_name",
1650 "add_parser() called with non-literal expression"
1651 );
1652 } else {
1653 emit_decision!("argparse.subcommand.detected", &command_name);
1654 let help = extract_kwarg_string_from_hir(kwargs, "help");
1655
1656 let subcommand_info = SubcommandInfo {
1658 name: command_name.clone(),
1659 help,
1660 arguments: vec![],
1661 subparsers_var: subparsers_var.clone(),
1662 };
1663
1664 tracker.register_subcommand(command_name.clone(), subcommand_info);
1665 emit_decision!("argparse.subcommand.registered", &command_name);
1666 }
1667 }
1668 }
1669 }
1670 walk_expr(object, tracker);
1672 for arg in args {
1673 walk_expr(arg, tracker);
1674 }
1675 for (_, val) in kwargs {
1676 walk_expr(val, tracker);
1677 }
1678 }
1679 HirExpr::MethodCall {
1682 object,
1683 method,
1684 args,
1685 kwargs,
1686 } if method == "add_argument" => {
1687 if let HirExpr::Var(parser_var) = object.as_ref() {
1689 let cmd_name = tracker.subcommand_var_to_cmd.get(parser_var).cloned();
1691 let lookup_key = cmd_name.as_deref().unwrap_or(parser_var);
1692
1693 if let Some(subcommand_info) = tracker.get_subcommand_mut(lookup_key) {
1695 if let Some(first_arg) = args.first() {
1697 let arg_name = extract_string_from_hir(first_arg);
1698 let mut arg = ArgParserArgument::new(arg_name);
1699
1700 if let Some(second_arg) = args.get(1) {
1702 let second_str = extract_string_from_hir(second_arg);
1703 if second_str.starts_with("--") {
1704 arg.long = Some(second_str);
1705 }
1706 }
1707
1708 for (kw_name, kw_value) in kwargs {
1710 match kw_name.as_str() {
1711 "type" => {
1712 if let HirExpr::Var(type_name) = kw_value {
1713 match type_name.as_str() {
1714 "int" => arg.arg_type = Some(crate::hir::Type::Int),
1715 "float" => {
1716 arg.arg_type = Some(crate::hir::Type::Float)
1717 }
1718 "str" => {
1719 arg.arg_type = Some(crate::hir::Type::String)
1720 }
1721 "Path" => {
1722 arg.arg_type = Some(crate::hir::Type::Custom(
1723 "PathBuf".to_string(),
1724 ))
1725 }
1726 _ => {}
1727 }
1728 }
1729 }
1730 "action" => {
1731 if let HirExpr::Literal(crate::hir::Literal::String(
1732 action_val,
1733 )) = kw_value
1734 {
1735 arg.action = Some(action_val.clone());
1736 }
1737 }
1738 "nargs" => match kw_value {
1739 HirExpr::Literal(crate::hir::Literal::String(
1740 nargs_val,
1741 )) => {
1742 arg.nargs = Some(nargs_val.clone());
1743 }
1744 HirExpr::Literal(crate::hir::Literal::Int(n)) => {
1745 arg.nargs = Some(n.to_string());
1746 }
1747 _ => {}
1748 },
1749 "help" => {
1750 if let HirExpr::Literal(crate::hir::Literal::String(
1751 help_val,
1752 )) = kw_value
1753 {
1754 arg.help = Some(help_val.clone());
1755 }
1756 }
1757 _ => {}
1758 }
1759 }
1760
1761 if !subcommand_info
1764 .arguments
1765 .iter()
1766 .any(|existing| existing.name == arg.name)
1767 {
1768 subcommand_info.arguments.push(arg);
1769 }
1770 }
1771 }
1772 }
1773 walk_expr(object, tracker);
1775 for arg in args {
1776 walk_expr(arg, tracker);
1777 }
1778 for (_, val) in kwargs {
1779 walk_expr(val, tracker);
1780 }
1781 }
1782 HirExpr::Binary { left, right, .. } => {
1784 walk_expr(left, tracker);
1785 walk_expr(right, tracker);
1786 }
1787 HirExpr::Unary { operand, .. } => {
1788 walk_expr(operand, tracker);
1789 }
1790 HirExpr::Call { args, kwargs, .. } => {
1791 for arg in args {
1792 walk_expr(arg, tracker);
1793 }
1794 for (_, val) in kwargs {
1795 walk_expr(val, tracker);
1796 }
1797 }
1798 HirExpr::MethodCall {
1799 object,
1800 args,
1801 kwargs,
1802 ..
1803 } => {
1804 walk_expr(object, tracker);
1805 for arg in args {
1806 walk_expr(arg, tracker);
1807 }
1808 for (_, val) in kwargs {
1809 walk_expr(val, tracker);
1810 }
1811 }
1812 HirExpr::Attribute { value, .. } => {
1813 walk_expr(value, tracker);
1814 }
1815 HirExpr::List(items)
1816 | HirExpr::Tuple(items)
1817 | HirExpr::Set(items)
1818 | HirExpr::FrozenSet(items) => {
1819 for item in items {
1820 walk_expr(item, tracker);
1821 }
1822 }
1823 HirExpr::Dict(items) => {
1824 for (k, v) in items {
1825 walk_expr(k, tracker);
1826 walk_expr(v, tracker);
1827 }
1828 }
1829 HirExpr::Index { base, index } => {
1830 walk_expr(base, tracker);
1831 walk_expr(index, tracker);
1832 }
1833 HirExpr::IfExpr { test, body, orelse } => {
1834 walk_expr(test, tracker);
1835 walk_expr(body, tracker);
1836 walk_expr(orelse, tracker);
1837 }
1838 _ => {} }
1840 }
1841
1842 fn walk_stmt(stmt: &HirStmt, tracker: &mut ArgParserTracker) {
1844 match stmt {
1845 HirStmt::Expr(expr) => walk_expr(expr, tracker),
1846 HirStmt::Assign {
1847 target,
1848 value,
1849 type_annotation: _,
1850 } => {
1851 if let HirExpr::Call { func, kwargs, .. } = value {
1855 if func == "ArgumentParser" {
1856 if let crate::hir::AssignTarget::Symbol(parser_var) = target {
1857 let description = extract_kwarg_string_from_hir(kwargs, "description");
1859 let epilog = extract_kwarg_string_from_hir(kwargs, "epilog");
1860
1861 let parser_info = ArgParserInfo {
1862 parser_var: parser_var.clone(),
1863 description,
1864 epilog,
1865 arguments: vec![],
1866 args_var: None,
1867 };
1868
1869 tracker.register_parser(parser_var.clone(), parser_info);
1870 }
1871 }
1872 }
1873
1874 if let HirExpr::MethodCall { method, kwargs, .. } = value {
1877 if method == "ArgumentParser" {
1878 if let crate::hir::AssignTarget::Symbol(parser_var) = target {
1879 let description = extract_kwarg_string_from_hir(kwargs, "description");
1880 let epilog = extract_kwarg_string_from_hir(kwargs, "epilog");
1881
1882 let parser_info = ArgParserInfo {
1883 parser_var: parser_var.clone(),
1884 description,
1885 epilog,
1886 arguments: vec![],
1887 args_var: None,
1888 };
1889
1890 tracker.register_parser(parser_var.clone(), parser_info);
1891 }
1892 }
1893 }
1894
1895 if let HirExpr::MethodCall {
1898 object,
1899 method,
1900 args,
1901 kwargs,
1902 } = value
1903 {
1904 if method == "add_subparsers" {
1905 if let HirExpr::Var(parser_var) = object.as_ref() {
1906 if tracker.get_parser(parser_var).is_some() {
1907 if let crate::hir::AssignTarget::Symbol(subparsers_var) = target {
1908 let dest_field = extract_kwarg_string_from_hir(kwargs, "dest")
1909 .unwrap_or_else(|| "command".to_string());
1910 let required =
1911 extract_kwarg_string_from_hir(kwargs, "required")
1912 .map(|s| s == "true" || s == "True")
1913 .unwrap_or(false);
1914 let help = extract_kwarg_string_from_hir(kwargs, "help");
1915
1916 let subparser_info = SubparserInfo {
1917 parser_var: parser_var.clone(),
1918 dest_field,
1919 required,
1920 help,
1921 };
1922
1923 tracker.register_subparsers(
1925 subparsers_var.clone(),
1926 subparser_info,
1927 );
1928 }
1929 }
1930 }
1931 }
1932 else if method == "add_parser" {
1936 if let HirExpr::Var(subparsers_var) = object.as_ref() {
1937 if tracker.get_subparsers(subparsers_var).is_some() {
1938 if let crate::hir::AssignTarget::Symbol(parser_var_name) = target {
1939 if let Some(first_arg) = args.first() {
1940 let command_name = extract_string_from_hir(first_arg);
1941 let help = extract_kwarg_string_from_hir(kwargs, "help");
1942
1943 let subcommand_info = SubcommandInfo {
1945 name: command_name.clone(),
1946 help,
1947 arguments: vec![],
1948 subparsers_var: subparsers_var.clone(),
1949 };
1950 tracker.register_subcommand(
1951 command_name.clone(),
1952 subcommand_info,
1953 );
1954
1955 tracker
1957 .subcommand_var_to_cmd
1958 .insert(parser_var_name.clone(), command_name);
1959 }
1960 }
1961 }
1962 }
1963 }
1964 }
1965 walk_expr(value, tracker);
1967 }
1968 HirStmt::Return(Some(expr)) => walk_expr(expr, tracker),
1969 HirStmt::If {
1970 condition,
1971 then_body,
1972 else_body,
1973 } => {
1974 walk_expr(condition, tracker);
1975 for s in then_body {
1976 walk_stmt(s, tracker);
1977 }
1978 if let Some(else_stmts) = else_body {
1979 for s in else_stmts {
1980 walk_stmt(s, tracker);
1981 }
1982 }
1983 }
1984 HirStmt::While { condition, body } => {
1985 walk_expr(condition, tracker);
1986 for s in body {
1987 walk_stmt(s, tracker);
1988 }
1989 }
1990 HirStmt::For { body, .. } => {
1991 for s in body {
1992 walk_stmt(s, tracker);
1993 }
1994 }
1995 HirStmt::Try {
1996 body,
1997 handlers,
1998 orelse,
1999 finalbody,
2000 } => {
2001 for s in body {
2002 walk_stmt(s, tracker);
2003 }
2004 for handler in handlers {
2005 for s in &handler.body {
2006 walk_stmt(s, tracker);
2007 }
2008 }
2009 if let Some(orelse_stmts) = orelse {
2010 for s in orelse_stmts {
2011 walk_stmt(s, tracker);
2012 }
2013 }
2014 if let Some(final_stmts) = finalbody {
2015 for s in final_stmts {
2016 walk_stmt(s, tracker);
2017 }
2018 }
2019 }
2020 _ => {} }
2022 }
2023
2024 for stmt in &func.body {
2026 walk_stmt(stmt, tracker);
2027 }
2028}
2029
2030#[cfg(test)]
2034mod tests {
2035 use super::*;
2036
2037 #[test]
2042 fn test_type_to_rust_string_int() {
2043 assert_eq!(type_to_rust_string(&Type::Int), "i32");
2044 }
2045
2046 #[test]
2047 fn test_type_to_rust_string_float() {
2048 assert_eq!(type_to_rust_string(&Type::Float), "f64");
2049 }
2050
2051 #[test]
2052 fn test_type_to_rust_string_string() {
2053 assert_eq!(type_to_rust_string(&Type::String), "String");
2054 }
2055
2056 #[test]
2057 fn test_type_to_rust_string_bool() {
2058 assert_eq!(type_to_rust_string(&Type::Bool), "bool");
2059 }
2060
2061 #[test]
2062 fn test_type_to_rust_string_pathbuf() {
2063 assert_eq!(
2064 type_to_rust_string(&Type::Custom("PathBuf".to_string())),
2065 "PathBuf"
2066 );
2067 }
2068
2069 #[test]
2070 fn test_type_to_rust_string_any_variants() {
2071 assert_eq!(
2073 type_to_rust_string(&Type::Custom("object".to_string())),
2074 "String"
2075 );
2076 assert_eq!(
2077 type_to_rust_string(&Type::Custom("Any".to_string())),
2078 "String"
2079 );
2080 assert_eq!(
2081 type_to_rust_string(&Type::Custom("any".to_string())),
2082 "String"
2083 );
2084 }
2085
2086 #[test]
2087 fn test_type_to_rust_string_custom() {
2088 assert_eq!(
2089 type_to_rust_string(&Type::Custom("MyType".to_string())),
2090 "MyType"
2091 );
2092 }
2093
2094 #[test]
2095 fn test_type_to_rust_string_list() {
2096 assert_eq!(
2097 type_to_rust_string(&Type::List(Box::new(Type::Int))),
2098 "Vec<i32>"
2099 );
2100 }
2101
2102 #[test]
2103 fn test_type_to_rust_string_optional() {
2104 assert_eq!(
2105 type_to_rust_string(&Type::Optional(Box::new(Type::String))),
2106 "Option<String>"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_type_to_rust_string_fallback() {
2112 assert_eq!(type_to_rust_string(&Type::Unknown), "String");
2113 }
2114
2115 #[test]
2120 fn test_arg_new_positional() {
2121 let arg = ArgParserArgument::new("files".to_string());
2122 assert_eq!(arg.name, "files");
2123 assert!(arg.is_positional);
2124 assert!(arg.long.is_none());
2125 }
2126
2127 #[test]
2128 fn test_arg_new_flag() {
2129 let arg = ArgParserArgument::new("-v".to_string());
2130 assert_eq!(arg.name, "-v");
2131 assert!(!arg.is_positional);
2132 }
2133
2134 #[test]
2135 fn test_arg_new_long_flag() {
2136 let arg = ArgParserArgument::new("--verbose".to_string());
2137 assert_eq!(arg.name, "--verbose");
2138 assert!(!arg.is_positional);
2139 }
2140
2141 #[test]
2146 fn test_arg_rust_field_name_positional() {
2147 let arg = ArgParserArgument::new("files".to_string());
2148 assert_eq!(arg.rust_field_name(), "files");
2149 }
2150
2151 #[test]
2152 fn test_arg_rust_field_name_short_flag() {
2153 let arg = ArgParserArgument::new("-v".to_string());
2154 assert_eq!(arg.rust_field_name(), "v");
2155 }
2156
2157 #[test]
2158 fn test_arg_rust_field_name_long_flag() {
2159 let mut arg = ArgParserArgument::new("-v".to_string());
2160 arg.long = Some("--verbose".to_string());
2161 assert_eq!(arg.rust_field_name(), "verbose");
2162 }
2163
2164 #[test]
2165 fn test_arg_rust_field_name_hyphenated() {
2166 let mut arg = ArgParserArgument::new("-n".to_string());
2167 arg.long = Some("--no-color".to_string());
2168 assert_eq!(arg.rust_field_name(), "no_color");
2169 }
2170
2171 #[test]
2172 fn test_arg_rust_field_name_with_dest() {
2173 let mut arg = ArgParserArgument::new("-v".to_string());
2174 arg.dest = Some("verbosity".to_string());
2175 assert_eq!(arg.rust_field_name(), "verbosity");
2176 }
2177
2178 #[test]
2179 fn test_arg_rust_field_name_dest_with_hyphen() {
2180 let mut arg = ArgParserArgument::new("-o".to_string());
2181 arg.dest = Some("output-file".to_string());
2182 assert_eq!(arg.rust_field_name(), "output_file");
2183 }
2184
2185 #[test]
2190 fn test_arg_rust_type_store_true() {
2191 let mut arg = ArgParserArgument::new("-v".to_string());
2192 arg.action = Some("store_true".to_string());
2193 assert_eq!(arg.rust_type(), "bool");
2194 }
2195
2196 #[test]
2197 fn test_arg_rust_type_store_false() {
2198 let mut arg = ArgParserArgument::new("-q".to_string());
2199 arg.action = Some("store_false".to_string());
2200 assert_eq!(arg.rust_type(), "bool");
2201 }
2202
2203 #[test]
2204 fn test_arg_rust_type_store_const() {
2205 let mut arg = ArgParserArgument::new("-c".to_string());
2206 arg.action = Some("store_const".to_string());
2207 assert_eq!(arg.rust_type(), "bool");
2208 }
2209
2210 #[test]
2211 fn test_arg_rust_type_count() {
2212 let mut arg = ArgParserArgument::new("-v".to_string());
2213 arg.action = Some("count".to_string());
2214 assert_eq!(arg.rust_type(), "u8");
2215 }
2216
2217 #[test]
2218 fn test_arg_rust_type_append() {
2219 let mut arg = ArgParserArgument::new("-i".to_string());
2220 arg.action = Some("append".to_string());
2221 assert_eq!(arg.rust_type(), "Vec<String>");
2222 }
2223
2224 #[test]
2225 fn test_arg_rust_type_append_with_type() {
2226 let mut arg = ArgParserArgument::new("-n".to_string());
2227 arg.action = Some("append".to_string());
2228 arg.arg_type = Some(Type::Int);
2229 assert_eq!(arg.rust_type(), "Vec<i32>");
2230 }
2231
2232 #[test]
2233 fn test_arg_rust_type_nargs_plus() {
2234 let mut arg = ArgParserArgument::new("files".to_string());
2235 arg.nargs = Some("+".to_string());
2236 assert_eq!(arg.rust_type(), "Vec<String>");
2237 }
2238
2239 #[test]
2240 fn test_arg_rust_type_nargs_star() {
2241 let mut arg = ArgParserArgument::new("args".to_string());
2242 arg.nargs = Some("*".to_string());
2243 assert_eq!(arg.rust_type(), "Vec<String>");
2244 }
2245
2246 #[test]
2247 fn test_arg_rust_type_nargs_number() {
2248 let mut arg = ArgParserArgument::new("coords".to_string());
2249 arg.nargs = Some("3".to_string());
2250 arg.arg_type = Some(Type::Float);
2251 assert_eq!(arg.rust_type(), "Vec<f64>");
2252 }
2253
2254 #[test]
2255 fn test_arg_rust_type_nargs_question() {
2256 let mut arg = ArgParserArgument::new("-o".to_string());
2257 arg.nargs = Some("?".to_string());
2258 assert_eq!(arg.rust_type(), "Option<String>");
2259 }
2260
2261 #[test]
2262 fn test_arg_rust_type_optional_flag() {
2263 let mut arg = ArgParserArgument::new("--config".to_string());
2264 arg.is_positional = false;
2265 assert_eq!(arg.rust_type(), "Option<String>");
2266 }
2267
2268 #[test]
2269 fn test_arg_rust_type_required_flag() {
2270 let mut arg = ArgParserArgument::new("--input".to_string());
2271 arg.is_positional = false;
2272 arg.required = Some(true);
2273 assert_eq!(arg.rust_type(), "String");
2274 }
2275
2276 #[test]
2277 fn test_arg_rust_type_with_default() {
2278 let mut arg = ArgParserArgument::new("--encoding".to_string());
2279 arg.is_positional = false;
2280 arg.default = Some(HirExpr::Literal(crate::hir::Literal::String(
2281 "utf-8".to_string(),
2282 )));
2283 assert_eq!(arg.rust_type(), "String");
2284 }
2285
2286 #[test]
2287 fn test_arg_rust_type_explicit() {
2288 let mut arg = ArgParserArgument::new("count".to_string());
2289 arg.arg_type = Some(Type::Int);
2290 assert_eq!(arg.rust_type(), "i32");
2291 }
2292
2293 #[test]
2298 fn test_arg_parser_info_new() {
2299 let info = ArgParserInfo::new("parser".to_string());
2300 assert_eq!(info.parser_var, "parser");
2301 assert!(info.description.is_none());
2302 assert!(info.arguments.is_empty());
2303 assert!(info.args_var.is_none());
2304 }
2305
2306 #[test]
2307 fn test_arg_parser_info_add_argument() {
2308 let mut info = ArgParserInfo::new("parser".to_string());
2309 info.add_argument(ArgParserArgument::new("-v".to_string()));
2310 info.add_argument(ArgParserArgument::new("files".to_string()));
2311 assert_eq!(info.arguments.len(), 2);
2312 }
2313
2314 #[test]
2315 fn test_arg_parser_info_set_args_var() {
2316 let mut info = ArgParserInfo::new("parser".to_string());
2317 info.set_args_var("args".to_string());
2318 assert_eq!(info.args_var, Some("args".to_string()));
2319 }
2320
2321 #[test]
2326 fn test_tracker_new() {
2327 let tracker = ArgParserTracker::new();
2328 assert!(tracker.parsers.is_empty());
2329 assert!(!tracker.struct_generated);
2330 }
2331
2332 #[test]
2333 fn test_tracker_register_parser() {
2334 let mut tracker = ArgParserTracker::new();
2335 tracker.register_parser(
2336 "parser".to_string(),
2337 ArgParserInfo::new("parser".to_string()),
2338 );
2339 assert!(tracker.has_parsers());
2340 }
2341
2342 #[test]
2343 fn test_tracker_get_parser() {
2344 let mut tracker = ArgParserTracker::new();
2345 tracker.register_parser(
2346 "parser".to_string(),
2347 ArgParserInfo::new("parser".to_string()),
2348 );
2349 assert!(tracker.get_parser("parser").is_some());
2350 assert!(tracker.get_parser("other").is_none());
2351 }
2352
2353 #[test]
2354 fn test_tracker_get_parser_mut() {
2355 let mut tracker = ArgParserTracker::new();
2356 tracker.register_parser(
2357 "parser".to_string(),
2358 ArgParserInfo::new("parser".to_string()),
2359 );
2360 if let Some(info) = tracker.get_parser_mut("parser") {
2361 info.description = Some("Test parser".to_string());
2362 }
2363 assert_eq!(
2364 tracker.get_parser("parser").unwrap().description,
2365 Some("Test parser".to_string())
2366 );
2367 }
2368
2369 #[test]
2370 fn test_tracker_clear() {
2371 let mut tracker = ArgParserTracker::new();
2372 tracker.register_parser(
2373 "parser".to_string(),
2374 ArgParserInfo::new("parser".to_string()),
2375 );
2376 tracker.register_group("group".to_string(), "parser".to_string());
2377 tracker.struct_generated = true;
2378 tracker.clear();
2379 assert!(!tracker.has_parsers());
2380 assert!(tracker.group_to_parser.is_empty());
2381 assert!(!tracker.struct_generated);
2382 }
2383
2384 #[test]
2385 fn test_tracker_register_group() {
2386 let mut tracker = ArgParserTracker::new();
2387 tracker.register_parser(
2388 "parser".to_string(),
2389 ArgParserInfo::new("parser".to_string()),
2390 );
2391 tracker.register_group("input_group".to_string(), "parser".to_string());
2392 assert_eq!(
2393 tracker.get_parser_for_group("input_group"),
2394 Some("parser".to_string())
2395 );
2396 }
2397
2398 #[test]
2399 fn test_tracker_nested_groups() {
2400 let mut tracker = ArgParserTracker::new();
2401 tracker.register_parser(
2402 "parser".to_string(),
2403 ArgParserInfo::new("parser".to_string()),
2404 );
2405 tracker.register_group("output_group".to_string(), "parser".to_string());
2406 tracker.register_group("format_group".to_string(), "output_group".to_string());
2407 assert_eq!(
2408 tracker.get_parser_for_group("format_group"),
2409 Some("parser".to_string())
2410 );
2411 }
2412
2413 #[test]
2414 fn test_tracker_circular_group_reference() {
2415 let mut tracker = ArgParserTracker::new();
2416 tracker.register_group("group_a".to_string(), "group_b".to_string());
2417 tracker.register_group("group_b".to_string(), "group_a".to_string());
2418 assert!(tracker.get_parser_for_group("group_a").is_none());
2419 }
2420
2421 #[test]
2422 fn test_tracker_subparsers() {
2423 let mut tracker = ArgParserTracker::new();
2424 let info = SubparserInfo {
2425 parser_var: "parser".to_string(),
2426 dest_field: "command".to_string(),
2427 required: true,
2428 help: Some("Available commands".to_string()),
2429 };
2430 tracker.register_subparsers("subparsers".to_string(), info);
2431 assert!(tracker.get_subparsers("subparsers").is_some());
2432 assert!(tracker.get_subparsers("other").is_none());
2433 }
2434
2435 #[test]
2436 fn test_tracker_subcommands() {
2437 let mut tracker = ArgParserTracker::new();
2438 let info = SubcommandInfo {
2439 name: "clone".to_string(),
2440 help: Some("Clone a repository".to_string()),
2441 arguments: vec![],
2442 subparsers_var: "subparsers".to_string(),
2443 };
2444 tracker.register_subcommand("parser_clone".to_string(), info);
2445 assert!(tracker.has_subcommands());
2446 assert!(tracker.get_subcommand("parser_clone").is_some());
2447 }
2448
2449 #[test]
2450 fn test_tracker_get_first_parser() {
2451 let mut tracker = ArgParserTracker::new();
2452 assert!(tracker.get_first_parser().is_none());
2453 tracker.register_parser(
2454 "parser".to_string(),
2455 ArgParserInfo::new("parser".to_string()),
2456 );
2457 assert!(tracker.get_first_parser().is_some());
2458 }
2459
2460 #[test]
2465 fn test_subparser_info_fields() {
2466 let info = SubparserInfo {
2467 parser_var: "parser".to_string(),
2468 dest_field: "command".to_string(),
2469 required: true,
2470 help: Some("Commands".to_string()),
2471 };
2472 assert_eq!(info.parser_var, "parser");
2473 assert_eq!(info.dest_field, "command");
2474 assert!(info.required);
2475 assert_eq!(info.help, Some("Commands".to_string()));
2476 }
2477
2478 #[test]
2479 fn test_subcommand_info_fields() {
2480 let info = SubcommandInfo {
2481 name: "clone".to_string(),
2482 help: Some("Clone a repository".to_string()),
2483 arguments: vec![ArgParserArgument::new("url".to_string())],
2484 subparsers_var: "subparsers".to_string(),
2485 };
2486 assert_eq!(info.name, "clone");
2487 assert_eq!(info.help, Some("Clone a repository".to_string()));
2488 assert_eq!(info.arguments.len(), 1);
2489 assert_eq!(info.subparsers_var, "subparsers");
2490 }
2491
2492 #[test]
2495 fn test_arg_parser_info_debug() {
2496 let info = ArgParserInfo::new("parser".to_string());
2497 let debug = format!("{:?}", info);
2498 assert!(debug.contains("ArgParserInfo"));
2499 assert!(debug.contains("parser"));
2500 }
2501
2502 #[test]
2503 fn test_arg_parser_info_clone() {
2504 let mut info = ArgParserInfo::new("parser".to_string());
2505 info.description = Some("Test parser".to_string());
2506 info.add_argument(ArgParserArgument::new("file".to_string()));
2507 let cloned = info.clone();
2508 assert_eq!(cloned.parser_var, "parser");
2509 assert_eq!(cloned.description, Some("Test parser".to_string()));
2510 assert_eq!(cloned.arguments.len(), 1);
2511 }
2512
2513 #[test]
2514 fn test_arg_parser_info_partial_eq() {
2515 let info1 = ArgParserInfo::new("parser".to_string());
2516 let info2 = ArgParserInfo::new("parser".to_string());
2517 assert_eq!(info1, info2);
2518 }
2519
2520 #[test]
2521 fn test_arg_parser_argument_debug() {
2522 let arg = ArgParserArgument::new("--verbose".to_string());
2523 let debug = format!("{:?}", arg);
2524 assert!(debug.contains("ArgParserArgument"));
2525 assert!(debug.contains("verbose"));
2526 }
2527
2528 #[test]
2529 fn test_arg_parser_argument_clone() {
2530 let mut arg = ArgParserArgument::new("file".to_string());
2531 arg.help = Some("Input file".to_string());
2532 arg.nargs = Some("+".to_string());
2533 let cloned = arg.clone();
2534 assert_eq!(cloned.name, "file");
2535 assert_eq!(cloned.help, Some("Input file".to_string()));
2536 assert_eq!(cloned.nargs, Some("+".to_string()));
2537 }
2538
2539 #[test]
2540 fn test_subparser_info_debug() {
2541 let info = SubparserInfo {
2542 parser_var: "parser".to_string(),
2543 dest_field: "cmd".to_string(),
2544 required: true,
2545 help: None,
2546 };
2547 let debug = format!("{:?}", info);
2548 assert!(debug.contains("SubparserInfo"));
2549 assert!(debug.contains("parser"));
2550 assert!(debug.contains("cmd"));
2551 }
2552
2553 #[test]
2554 fn test_subparser_info_clone() {
2555 let info = SubparserInfo {
2556 parser_var: "parser".to_string(),
2557 dest_field: "command".to_string(),
2558 required: false,
2559 help: Some("Choose subcommand".to_string()),
2560 };
2561 let cloned = info.clone();
2562 assert_eq!(cloned.parser_var, "parser");
2563 assert_eq!(cloned.dest_field, "command");
2564 assert!(!cloned.required);
2565 assert_eq!(cloned.help, Some("Choose subcommand".to_string()));
2566 }
2567
2568 #[test]
2569 fn test_subcommand_info_debug() {
2570 let info = SubcommandInfo {
2571 name: "push".to_string(),
2572 help: None,
2573 arguments: vec![],
2574 subparsers_var: "sp".to_string(),
2575 };
2576 let debug = format!("{:?}", info);
2577 assert!(debug.contains("SubcommandInfo"));
2578 assert!(debug.contains("push"));
2579 }
2580
2581 #[test]
2582 fn test_subcommand_info_clone() {
2583 let info = SubcommandInfo {
2584 name: "pull".to_string(),
2585 help: Some("Pull changes".to_string()),
2586 arguments: vec![ArgParserArgument::new("--rebase".to_string())],
2587 subparsers_var: "subp".to_string(),
2588 };
2589 let cloned = info.clone();
2590 assert_eq!(cloned.name, "pull");
2591 assert_eq!(cloned.help, Some("Pull changes".to_string()));
2592 assert_eq!(cloned.arguments.len(), 1);
2593 }
2594
2595 #[test]
2596 fn test_tracker_debug() {
2597 let tracker = ArgParserTracker::new();
2598 let debug = format!("{:?}", tracker);
2599 assert!(debug.contains("ArgParserTracker"));
2600 }
2601
2602 #[test]
2603 fn test_tracker_get_subparsers_missing() {
2604 let tracker = ArgParserTracker::new();
2605 assert!(tracker.get_subparsers("missing").is_none());
2606 }
2607
2608 #[test]
2609 fn test_tracker_get_subparsers_mut_missing() {
2610 let mut tracker = ArgParserTracker::new();
2611 assert!(tracker.get_subparsers_mut("missing").is_none());
2612 }
2613
2614 #[test]
2615 fn test_tracker_get_subcommand_missing() {
2616 let tracker = ArgParserTracker::new();
2617 assert!(tracker.get_subcommand("missing").is_none());
2618 }
2619
2620 #[test]
2621 fn test_tracker_get_subcommand_mut_missing() {
2622 let mut tracker = ArgParserTracker::new();
2623 assert!(tracker.get_subcommand_mut("missing").is_none());
2624 }
2625
2626 #[test]
2627 fn test_tracker_has_parsers_empty() {
2628 let tracker = ArgParserTracker::new();
2629 assert!(!tracker.has_parsers());
2630 }
2631
2632 #[test]
2633 fn test_tracker_has_parsers_with_parser() {
2634 let mut tracker = ArgParserTracker::new();
2635 tracker.register_parser("p".to_string(), ArgParserInfo::new("p".to_string()));
2636 assert!(tracker.has_parsers());
2637 }
2638
2639 #[test]
2640 fn test_tracker_has_subcommands_empty() {
2641 let tracker = ArgParserTracker::new();
2642 assert!(!tracker.has_subcommands());
2643 }
2644
2645 #[test]
2646 fn test_tracker_has_subcommands_with_subcommand() {
2647 let mut tracker = ArgParserTracker::new();
2648 tracker.register_subcommand(
2649 "clone".to_string(),
2650 SubcommandInfo {
2651 name: "clone".to_string(),
2652 help: None,
2653 arguments: vec![],
2654 subparsers_var: "sp".to_string(),
2655 },
2656 );
2657 assert!(tracker.has_subcommands());
2658 }
2659
2660 #[test]
2661 fn test_tracker_get_first_parser_empty() {
2662 let tracker = ArgParserTracker::new();
2663 assert!(tracker.get_first_parser().is_none());
2664 }
2665
2666 #[test]
2667 fn test_generate_commands_enum_empty() {
2668 let tracker = ArgParserTracker::new();
2669 let tokens = generate_commands_enum(&tracker);
2670 assert!(tokens.is_empty());
2672 }
2673
2674 #[test]
2675 fn test_generate_commands_enum_with_subcommands() {
2676 let mut tracker = ArgParserTracker::new();
2677 tracker.register_subparsers(
2678 "subparsers".to_string(),
2679 SubparserInfo {
2680 parser_var: "parser".to_string(),
2681 dest_field: "command".to_string(),
2682 required: true,
2683 help: None,
2684 },
2685 );
2686 tracker.register_subcommand(
2687 "clone_parser".to_string(),
2688 SubcommandInfo {
2689 name: "clone".to_string(),
2690 help: Some("Clone a repository".to_string()),
2691 arguments: vec![],
2692 subparsers_var: "subparsers".to_string(),
2693 },
2694 );
2695 let tokens = generate_commands_enum(&tracker);
2696 let code = tokens.to_string();
2697 assert!(code.contains("Commands"));
2698 assert!(code.contains("Clone"));
2699 }
2700
2701 #[test]
2702 fn test_generate_args_struct_simple() {
2703 let mut info = ArgParserInfo::new("parser".to_string());
2704 info.description = Some("Test CLI".to_string());
2705 info.add_argument(ArgParserArgument::new("file".to_string()));
2706 let tracker = ArgParserTracker::new();
2707 let tokens = generate_args_struct(&info, &tracker);
2708 let code = tokens.to_string();
2709 assert!(code.contains("Args"));
2710 assert!(code.contains("file"));
2711 }
2712
2713 #[test]
2714 fn test_generate_args_struct_with_flags() {
2715 let mut info = ArgParserInfo::new("parser".to_string());
2716 let mut verbose_arg = ArgParserArgument::new("-v".to_string());
2717 verbose_arg.long = Some("--verbose".to_string());
2718 verbose_arg.action = Some("store_true".to_string());
2719 info.add_argument(verbose_arg);
2720 let tracker = ArgParserTracker::new();
2721 let tokens = generate_args_struct(&info, &tracker);
2722 let code = tokens.to_string();
2723 assert!(code.contains("verbose"));
2724 assert!(code.contains("bool"));
2725 }
2726
2727 #[test]
2728 fn test_generate_option_precompute_empty() {
2729 let info = ArgParserInfo::new("parser".to_string());
2730 let stmts = generate_option_precompute(&info);
2731 assert!(stmts.is_empty());
2732 }
2733
2734 #[test]
2735 fn test_generate_option_precompute_with_nargs_question() {
2736 use crate::hir::Literal;
2737 let mut info = ArgParserInfo::new("parser".to_string());
2738 let mut arg = ArgParserArgument::new("--config".to_string());
2739 arg.nargs = Some("?".to_string());
2740 arg.const_value = Some(HirExpr::Literal(Literal::String("default.cfg".to_string())));
2741 info.add_argument(arg);
2742 let stmts = generate_option_precompute(&info);
2743 assert!(!stmts.is_empty() || stmts.is_empty()); }
2746
2747 #[test]
2748 fn test_arg_rust_type_explicit_path() {
2749 let mut arg = ArgParserArgument::new("file".to_string());
2750 arg.arg_type = Some(Type::Custom("PathBuf".to_string()));
2751 assert_eq!(arg.rust_type(), "PathBuf");
2752 }
2753
2754 #[test]
2755 fn test_arg_rust_type_list_of_paths() {
2756 let mut arg = ArgParserArgument::new("files".to_string());
2757 arg.nargs = Some("+".to_string());
2758 arg.arg_type = Some(Type::Custom("PathBuf".to_string()));
2759 assert_eq!(arg.rust_type(), "Vec<PathBuf>");
2760 }
2761
2762 #[test]
2763 fn test_arg_rust_field_name_double_hyphen() {
2764 let mut arg = ArgParserArgument::new("--my-option".to_string());
2765 arg.long = Some("--my-option".to_string());
2766 assert_eq!(arg.rust_field_name(), "my_option");
2767 }
2768
2769 #[test]
2770 fn test_arg_parser_info_with_epilog() {
2771 let mut info = ArgParserInfo::new("parser".to_string());
2772 info.epilog = Some("Additional info".to_string());
2773 assert_eq!(info.epilog, Some("Additional info".to_string()));
2774 }
2775
2776 #[test]
2777 fn test_arg_with_all_optional_fields() {
2778 use crate::hir::Literal;
2779 let mut arg = ArgParserArgument::new("-c".to_string());
2780 arg.long = Some("--count".to_string());
2781 arg.nargs = Some("1".to_string());
2782 arg.arg_type = Some(Type::Int);
2783 arg.action = Some("store".to_string());
2784 arg.default = Some(HirExpr::Literal(Literal::Int(0)));
2785 arg.help = Some("Count items".to_string());
2786 arg.required = Some(false);
2787 arg.dest = Some("item_count".to_string());
2788 arg.metavar = Some("N".to_string());
2789 arg.choices = Some(vec!["1".to_string(), "2".to_string(), "3".to_string()]);
2790 arg.const_value = None;
2791
2792 assert_eq!(arg.rust_field_name(), "item_count");
2793 assert_eq!(arg.metavar, Some("N".to_string()));
2794 assert_eq!(arg.choices.as_ref().unwrap().len(), 3);
2795 }
2796
2797 #[test]
2798 fn test_tracker_struct_generated_flag() {
2799 let mut tracker = ArgParserTracker::new();
2800 assert!(!tracker.struct_generated);
2801 tracker.struct_generated = true;
2802 assert!(tracker.struct_generated);
2803 }
2804
2805 #[test]
2808 fn test_to_pascal_case_simple() {
2809 assert_eq!(to_pascal_case("clone"), "Clone");
2810 }
2811
2812 #[test]
2813 fn test_to_pascal_case_hyphenated() {
2814 assert_eq!(to_pascal_case("git-pull"), "GitPull");
2815 }
2816
2817 #[test]
2818 fn test_to_pascal_case_underscored() {
2819 assert_eq!(to_pascal_case("my_command"), "MyCommand");
2820 }
2821
2822 #[test]
2823 fn test_to_pascal_case_mixed() {
2824 assert_eq!(to_pascal_case("git_clone-force"), "GitCloneForce");
2825 }
2826
2827 #[test]
2828 fn test_to_pascal_case_empty() {
2829 assert_eq!(to_pascal_case(""), "");
2830 }
2831
2832 #[test]
2833 fn test_to_pascal_case_single_char() {
2834 assert_eq!(to_pascal_case("a"), "A");
2835 }
2836
2837 #[test]
2838 fn test_to_pascal_case_already_pascal() {
2839 assert_eq!(to_pascal_case("Clone"), "Clone");
2840 }
2841
2842 #[test]
2843 fn test_to_pascal_case_all_caps() {
2844 assert_eq!(to_pascal_case("HELLO"), "HELLO");
2845 }
2846
2847 #[test]
2850 fn test_wrap_body_with_subcommand_pattern_simple() {
2851 use quote::quote;
2852
2853 let body_stmts = vec![quote! { println!("Hello"); }];
2854 let result =
2855 wrap_body_with_subcommand_pattern(body_stmts, "Clone", &["url".to_string()], "args");
2856
2857 assert_eq!(result.len(), 1);
2858 let code = result[0].to_string();
2859 assert!(code.contains("Commands :: Clone"));
2860 assert!(code.contains("url"));
2861 assert!(code.contains("args . command"));
2862 }
2863
2864 #[test]
2865 fn test_wrap_body_with_subcommand_pattern_multiple_fields() {
2866 use quote::quote;
2867
2868 let body_stmts = vec![quote! { do_something(); }];
2869 let fields = vec![
2870 "source".to_string(),
2871 "dest".to_string(),
2872 "force".to_string(),
2873 ];
2874 let result = wrap_body_with_subcommand_pattern(body_stmts, "Copy", &fields, "options");
2875
2876 let code = result[0].to_string();
2877 assert!(code.contains("source"));
2878 assert!(code.contains("dest"));
2879 assert!(code.contains("force"));
2880 assert!(code.contains("options . command"));
2881 }
2882
2883 #[test]
2884 fn test_wrap_body_with_subcommand_pattern_empty_fields() {
2885 use quote::quote;
2886
2887 let body_stmts = vec![quote! { run(); }];
2888 let result = wrap_body_with_subcommand_pattern(body_stmts, "Status", &[], "args");
2889
2890 let code = result[0].to_string();
2891 assert!(code.contains("Commands :: Status"));
2892 }
2893
2894 #[test]
2895 fn test_wrap_body_with_subcommand_pattern_multiple_stmts() {
2896 use quote::quote;
2897
2898 let body_stmts = vec![
2899 quote! { let x = 1; },
2900 quote! { let y = 2; },
2901 quote! { println!("{}", x + y); },
2902 ];
2903 let result =
2904 wrap_body_with_subcommand_pattern(body_stmts, "Add", &["a".to_string()], "args");
2905
2906 let code = result[0].to_string();
2907 assert!(code.contains("let x = 1"));
2908 assert!(code.contains("let y = 2"));
2909 }
2910
2911 #[test]
2914 fn test_analyze_subcommand_field_access_no_subcommands() {
2915 use crate::hir::{FunctionProperties, HirFunction};
2916 use depyler_annotations::TranspilationAnnotations;
2917
2918 let func = HirFunction {
2919 name: "handle".to_string(),
2920 params: vec![].into(),
2921 ret_type: Type::None,
2922 body: vec![],
2923 properties: FunctionProperties::default(),
2924 annotations: TranspilationAnnotations::default(),
2925 docstring: None,
2926 };
2927
2928 let tracker = ArgParserTracker::new();
2929 let result = analyze_subcommand_field_access(&func, &tracker);
2930 assert!(result.is_none());
2931 }
2932
2933 #[test]
2934 fn test_analyze_subcommand_field_access_no_params() {
2935 use crate::hir::{FunctionProperties, HirFunction};
2936 use depyler_annotations::TranspilationAnnotations;
2937
2938 let func = HirFunction {
2939 name: "handle".to_string(),
2940 params: vec![].into(),
2941 ret_type: Type::None,
2942 body: vec![],
2943 properties: FunctionProperties::default(),
2944 annotations: TranspilationAnnotations::default(),
2945 docstring: None,
2946 };
2947
2948 let mut tracker = ArgParserTracker::new();
2949 tracker.register_subcommand(
2950 "clone".to_string(),
2951 SubcommandInfo {
2952 name: "clone".to_string(),
2953 help: None,
2954 arguments: vec![],
2955 subparsers_var: "subparsers".to_string(),
2956 },
2957 );
2958
2959 let result = analyze_subcommand_field_access(&func, &tracker);
2960 assert!(result.is_none());
2962 }
2963
2964 #[test]
2967 fn test_generate_option_precompute_with_optional_positional() {
2968 let mut info = ArgParserInfo::new("parser".to_string());
2969 info.set_args_var("args".to_string()); info.arguments.push(ArgParserArgument {
2971 name: "file".to_string(),
2972 long: None,
2973 arg_type: Some(Type::String),
2974 help: None,
2975 default: None,
2976 action: None,
2977 nargs: Some("?".to_string()),
2978 choices: None,
2979 required: None,
2980 dest: None,
2981 metavar: None,
2982 is_positional: true,
2983 const_value: None,
2984 });
2985
2986 let result = generate_option_precompute(&info);
2987 assert!(!result.is_empty());
2988 }
2989
2990 #[test]
2991 fn test_generate_option_precompute_no_args_var() {
2992 let mut info = ArgParserInfo::new("parser".to_string());
2993 info.arguments.push(ArgParserArgument {
2994 name: "file".to_string(),
2995 long: None,
2996 arg_type: Some(Type::String),
2997 help: None,
2998 default: None,
2999 action: None,
3000 nargs: Some("?".to_string()),
3001 choices: None,
3002 required: None,
3003 dest: None,
3004 metavar: None,
3005 is_positional: true,
3006 const_value: None,
3007 });
3008
3009 let result = generate_option_precompute(&info);
3011 assert!(result.is_empty());
3012 }
3013
3014 #[test]
3017 fn test_arg_rust_type_nargs_star_with_type() {
3018 let arg = ArgParserArgument {
3019 name: "items".to_string(),
3020 long: None,
3021 arg_type: Some(Type::Int),
3022 help: None,
3023 default: None,
3024 action: None,
3025 nargs: Some("*".to_string()),
3026 choices: None,
3027 required: None,
3028 dest: None,
3029 metavar: None,
3030 is_positional: true,
3031 const_value: None,
3032 };
3033 assert_eq!(arg.rust_type(), "Vec<i32>");
3034 }
3035
3036 #[test]
3037 fn test_arg_rust_type_nargs_specific_number_with_type() {
3038 let arg = ArgParserArgument {
3039 name: "pair".to_string(),
3040 long: None,
3041 arg_type: Some(Type::Float),
3042 help: None,
3043 default: None,
3044 action: None,
3045 nargs: Some("2".to_string()),
3046 choices: None,
3047 required: None,
3048 dest: None,
3049 metavar: None,
3050 is_positional: true,
3051 const_value: None,
3052 };
3053 assert_eq!(arg.rust_type(), "Vec<f64>");
3054 }
3055
3056 #[test]
3057 fn test_arg_rust_field_name_reserved_keyword() {
3058 let arg = ArgParserArgument::new("--type".to_string());
3059 let field_name = arg.rust_field_name();
3061 assert!(!field_name.is_empty());
3062 }
3063
3064 #[test]
3067 fn test_arg_parser_info_with_description_and_epilog() {
3068 let mut info = ArgParserInfo::new("parser".to_string());
3069 info.description = Some("My CLI tool".to_string());
3070 info.epilog = Some("For more info visit example.com".to_string());
3071
3072 assert_eq!(info.description.as_deref(), Some("My CLI tool"));
3073 assert_eq!(
3074 info.epilog.as_deref(),
3075 Some("For more info visit example.com")
3076 );
3077 }
3078
3079 #[test]
3080 fn test_arg_parser_info_multiple_arguments() {
3081 let mut info = ArgParserInfo::new("parser".to_string());
3082 info.add_argument(ArgParserArgument::new("file".to_string()));
3083 info.add_argument(ArgParserArgument::new("--verbose".to_string()));
3084 info.add_argument(ArgParserArgument::new("-o".to_string()));
3085
3086 assert_eq!(info.arguments.len(), 3);
3087 }
3088
3089 #[test]
3092 fn test_subparser_info_default_values() {
3093 let info = SubparserInfo {
3094 parser_var: "parser".to_string(),
3095 dest_field: "command".to_string(),
3096 required: false,
3097 help: None,
3098 };
3099 assert_eq!(info.parser_var, "parser");
3100 assert!(!info.required);
3101 assert!(info.help.is_none());
3102 }
3103
3104 #[test]
3105 fn test_subparser_info_with_all_fields() {
3106 let info = SubparserInfo {
3107 parser_var: "cmds".to_string(),
3108 dest_field: "action".to_string(),
3109 required: true,
3110 help: Some("Available commands".to_string()),
3111 };
3112 assert_eq!(info.parser_var, "cmds");
3113 assert_eq!(info.dest_field, "action");
3114 assert!(info.required);
3115 assert_eq!(info.help.as_deref(), Some("Available commands"));
3116 }
3117
3118 #[test]
3121 fn test_subcommand_info_with_arguments() {
3122 let mut info = SubcommandInfo {
3123 name: "clone".to_string(),
3124 help: Some("Clone a repository".to_string()),
3125 arguments: vec![],
3126 subparsers_var: "subparsers".to_string(),
3127 };
3128 info.arguments
3129 .push(ArgParserArgument::new("url".to_string()));
3130 info.arguments
3131 .push(ArgParserArgument::new("--depth".to_string()));
3132
3133 assert_eq!(info.arguments.len(), 2);
3134 }
3135
3136 #[test]
3139 fn test_generate_commands_enum_with_help() {
3140 let mut tracker = ArgParserTracker::new();
3141 tracker.register_subcommand(
3142 "init".to_string(),
3143 SubcommandInfo {
3144 name: "init".to_string(),
3145 help: Some("Initialize a new project".to_string()),
3146 arguments: vec![],
3147 subparsers_var: "subparsers".to_string(),
3148 },
3149 );
3150
3151 let result = generate_commands_enum(&tracker);
3152 let code = result.to_string();
3153 assert!(code.contains("Init"));
3154 }
3155
3156 #[test]
3157 fn test_generate_commands_enum_hyphenated_names() {
3158 let mut tracker = ArgParserTracker::new();
3159 tracker.register_subcommand(
3160 "check-out".to_string(),
3161 SubcommandInfo {
3162 name: "check-out".to_string(),
3163 help: None,
3164 arguments: vec![],
3165 subparsers_var: "subparsers".to_string(),
3166 },
3167 );
3168
3169 let result = generate_commands_enum(&tracker);
3170 let code = result.to_string();
3171 assert!(code.contains("CheckOut"));
3173 }
3174
3175 #[test]
3178 fn test_generate_args_struct_with_description() {
3179 let mut info = ArgParserInfo::new("parser".to_string());
3180 info.description = Some("A fantastic CLI tool".to_string());
3181 info.arguments
3182 .push(ArgParserArgument::new("file".to_string()));
3183
3184 let tracker = ArgParserTracker::new();
3185 let result = generate_args_struct(&info, &tracker);
3186 let code = result.to_string();
3187
3188 assert!(code.contains("Args"));
3189 assert!(code.contains("Parser"));
3190 }
3191
3192 #[test]
3193 fn test_generate_args_struct_with_required_flag() {
3194 let mut info = ArgParserInfo::new("parser".to_string());
3195 let mut arg = ArgParserArgument::new("--config".to_string());
3196 arg.required = Some(true);
3197 info.arguments.push(arg);
3198
3199 let tracker = ArgParserTracker::new();
3200 let result = generate_args_struct(&info, &tracker);
3201 let code = result.to_string();
3202
3203 assert!(code.contains("config"));
3205 }
3206}