Skip to main content

depyler_core/rust_gen/
argparse_transform.rs

1//! ArgumentParser → Clap transformation (DEPYLER-0363)
2//!
3//! This module handles the structural transformation of Python argparse
4//! patterns to Rust clap derive macros.
5//!
6//! # Transformation Strategy
7//!
8//! Python argparse uses imperative API:
9//! ```python
10//! parser = argparse.ArgumentParser(description="...")
11//! parser.add_argument("files", nargs="+", type=Path)
12//! parser.add_argument("-v", "--verbose", action="store_true")
13//! args = parser.parse_args()
14//! ```
15//!
16//! Rust clap uses declarative struct:
17//! ```rust,ignore
18//! #[derive(Parser)]
19//! #[command(about = "...")]
20//! struct Args {
21//!     files: Vec<PathBuf>,
22//!     #[arg(short, long)]
23//!     verbose: bool,
24//! }
25//! let args = Args::parse();
26//! ```
27//!
28//! # Detection Algorithm
29//!
30//! 1. Detect `parser = ArgumentParser(...)` assignment
31//! 2. Accumulate all `parser.add_argument(...)` method calls
32//! 3. Detect `args = parser.parse_args()` assignment
33//! 4. Generate struct definition with clap derives
34//! 5. Replace parse_args() call with `Args::parse()`
35
36use crate::emit_decision;
37use crate::hir::{HirExpr, Type};
38use crate::rust_gen::keywords::safe_ident;
39use std::collections::HashMap;
40
41/// Convert HIR Type to Rust type string for argparse arguments
42///
43/// # DEPYLER-0364: Type Mapping
44/// Maps Python types to idiomatic Rust types for CLI arguments:
45/// - int → i32
46/// - str → String
47/// - Path → PathBuf
48/// - bool → bool
49/// - float → f64
50///
51/// # Complexity
52/// 3 (pattern match on Type enum)
53fn 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        // DEPYLER-169, DEPYLER-1020: Map special Python types to their Rust equivalents
61        // In NASA mode (default), use String instead of serde_json::Value
62        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(), // Default fallback
71    }
72}
73
74/// Tracks an ArgumentParser instance being built
75///
76/// # Complexity
77/// N/A (data structure)
78#[derive(Debug, Clone, PartialEq)]
79pub struct ArgParserInfo {
80    /// Variable name assigned to ArgumentParser (e.g., "parser")
81    pub parser_var: String,
82
83    /// Description from ArgumentParser(description="...")
84    pub description: Option<String>,
85
86    /// Epilog from ArgumentParser(epilog="...")
87    pub epilog: Option<String>,
88
89    /// All arguments added via add_argument()
90    pub arguments: Vec<ArgParserArgument>,
91
92    /// Variable name for parse_args() result (e.g., "args")
93    pub args_var: Option<String>,
94}
95
96impl ArgParserInfo {
97    /// Create new ArgParser tracker
98    ///
99    /// # Complexity
100    /// 1 (struct initialization)
101    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    /// Add an argument from add_argument() call
112    ///
113    /// # Complexity
114    /// 1 (vec push)
115    pub fn add_argument(&mut self, arg: ArgParserArgument) {
116        self.arguments.push(arg);
117    }
118
119    /// Set the args variable name from parse_args() assignment
120    ///
121    /// # Complexity
122    /// 1 (field assignment)
123    pub fn set_args_var(&mut self, var: String) {
124        self.args_var = Some(var);
125    }
126}
127
128/// Represents a single argument definition from add_argument()
129///
130/// # Complexity
131/// N/A (data structure)
132#[derive(Debug, Clone, PartialEq)]
133pub struct ArgParserArgument {
134    /// Positional name or short flag (e.g., "files", "-v")
135    pub name: String,
136
137    /// Long flag name (e.g., "--verbose")
138    pub long: Option<String>,
139
140    /// Number of arguments: "+", "*", "?", or number
141    pub nargs: Option<String>,
142
143    /// Type annotation (e.g., Path, int, str)
144    pub arg_type: Option<Type>,
145
146    /// Action: "store_true", "store_false", "store", "append"
147    pub action: Option<String>,
148
149    /// Default value
150    pub default: Option<HirExpr>,
151
152    /// Help text
153    pub help: Option<String>,
154
155    /// Whether this is a required positional argument
156    pub is_positional: bool,
157
158    /// DEPYLER-0367: Whether this flag is required (required=True)
159    pub required: Option<bool>,
160
161    /// DEPYLER-0371: Custom destination variable name (dest="var_name")
162    pub dest: Option<String>,
163
164    /// DEPYLER-0372: Metavar for help display (metavar="FILE")
165    pub metavar: Option<String>,
166
167    /// DEPYLER-0373: Restricted value choices (choices=["a", "b", "c"])
168    pub choices: Option<Vec<String>>,
169
170    /// DEPYLER-0374: Constant value for action="store_const" or nargs="?" with const
171    pub const_value: Option<HirExpr>,
172}
173
174impl ArgParserArgument {
175    /// Create new argument definition
176    ///
177    /// # Complexity
178    /// 2 (string check + struct initialization)
179    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    /// Get the Rust field name (convert -v/--verbose → verbose)
199    ///
200    /// # Complexity
201    /// 3 (string operations)
202    /// # DEPYLER-0371: Use dest parameter if present
203    pub fn rust_field_name(&self) -> String {
204        // DEPYLER-0371: If dest is specified, use that as the field name
205        if let Some(ref dest) = self.dest {
206            return dest.replace('-', "_");
207        }
208
209        if self.is_positional {
210            // Positional arguments keep their name
211            self.name.clone()
212        } else if let Some(ref long) = self.long {
213            // Use long flag without -- (convert hyphens to underscores)
214            long.trim_start_matches("--").replace('-', "_")
215        } else {
216            // Use flag name without leading hyphens (convert hyphens to underscores)
217            // DEPYLER-0366: Handle flags like --no-color → no_color
218            self.name.trim_start_matches('-').replace('-', "_")
219        }
220    }
221
222    /// Get the Rust type for this argument
223    ///
224    /// # Complexity
225    /// 7 (multiple match + string checks)
226    pub fn rust_type(&self) -> String {
227        // action="store_true"/"store_false"/"store_const" → bool
228        // DEPYLER-0375: action="store_const" also maps to bool
229        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        // action="count" → u8 (counts occurrences: -v -v -v → 3)
237        if self.action.as_deref() == Some("count") {
238            return "u8".to_string();
239        }
240
241        // DEPYLER-0368: action="append" → Vec<T> (collects multiple flag uses)
242        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        // nargs="+" or nargs="*" → Vec<T>
252        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        // DEPYLER-0370: nargs=N (specific number) → Vec<T>
262        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        // nargs="?" → Option<T>
274        // DEPYLER-0374: Handle const parameter with nargs="?" separately in generate_args_struct
275        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        // DEPYLER-0527: Optional flags (--arg without required=True) → Option<T>
285        // In argparse, long arguments without required=True default to None
286        // DEPYLER-0530: BUT if there's a default value, don't wrap in Option
287        // because clap will always provide a value via default_value attribute
288        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        // Use explicit type or default to String
302        self.arg_type
303            .as_ref()
304            .map(type_to_rust_string)
305            .unwrap_or_else(|| "String".to_string())
306    }
307}
308
309/// DEPYLER-0399: Information about a subparser collection (from add_subparsers())
310///
311/// # Complexity
312/// N/A (data structure)
313#[derive(Debug, Clone, PartialEq)]
314pub struct SubparserInfo {
315    /// Parent parser variable name
316    pub parser_var: String,
317
318    /// Destination field name (from dest= parameter)
319    pub dest_field: String,
320
321    /// Whether subcommand is required
322    pub required: bool,
323
324    /// Help text for subparsers group
325    pub help: Option<String>,
326}
327
328/// DEPYLER-0399: Information about a single subcommand
329///
330/// # Complexity
331/// N/A (data structure)
332#[derive(Debug, Clone, PartialEq)]
333pub struct SubcommandInfo {
334    /// Subcommand name (e.g., "clone")
335    pub name: String,
336
337    /// Help text for this subcommand
338    pub help: Option<String>,
339
340    /// Arguments specific to this subcommand
341    pub arguments: Vec<ArgParserArgument>,
342
343    /// Parent subparsers variable
344    pub subparsers_var: String,
345}
346
347/// Container for ArgumentParser tracking in CodeGenContext
348///
349/// # Complexity
350/// N/A (data structure)
351#[derive(Debug, Clone, Default)]
352pub struct ArgParserTracker {
353    /// Currently active ArgumentParser instances (keyed by variable name)
354    pub parsers: HashMap<String, ArgParserInfo>,
355
356    /// DEPYLER-0396: Map argument group variables to their parent parser
357    /// e.g., "input_group" → "parser"
358    /// This allows tracking add_argument() calls on groups
359    pub group_to_parser: HashMap<String, String>,
360
361    /// DEPYLER-0399: Subparser collections (variable → info)
362    /// Maps subparsers variable name to parent parser info
363    pub subparsers: HashMap<String, SubparserInfo>,
364
365    /// DEPYLER-0399: Subcommands (parser variable → info)
366    /// Maps subcommand parser variable (e.g., "parser_clone") to subcommand details
367    pub subcommands: HashMap<String, SubcommandInfo>,
368
369    /// DEPYLER-0822: Maps subcommand parser variable to command name
370    /// e.g., "top_parser" → "top" (for looking up SubcommandInfo when processing add_argument)
371    pub subcommand_var_to_cmd: HashMap<String, String>,
372
373    /// Whether we've generated the Args struct for current function
374    pub struct_generated: bool,
375}
376
377impl ArgParserTracker {
378    /// Create new tracker
379    ///
380    /// # Complexity
381    /// 1 (struct initialization)
382    pub fn new() -> Self {
383        Self::default()
384    }
385
386    /// Register a new ArgumentParser assignment
387    ///
388    /// # Complexity
389    /// 2 (struct creation + hashmap insert)
390    pub fn register_parser(&mut self, var_name: String, info: ArgParserInfo) {
391        self.parsers.insert(var_name, info);
392    }
393
394    /// Get mutable reference to parser info by variable name
395    ///
396    /// # Complexity
397    /// 1 (hashmap lookup)
398    pub fn get_parser_mut(&mut self, var_name: &str) -> Option<&mut ArgParserInfo> {
399        self.parsers.get_mut(var_name)
400    }
401
402    /// Get reference to parser info by variable name
403    ///
404    /// # Complexity
405    /// 1 (hashmap lookup)
406    pub fn get_parser(&self, var_name: &str) -> Option<&ArgParserInfo> {
407        self.parsers.get(var_name)
408    }
409
410    /// Clear all parser tracking (e.g., when entering new function)
411    ///
412    /// # Complexity
413    /// 2 (hashmap clears)
414    pub fn clear(&mut self) {
415        self.parsers.clear();
416        self.group_to_parser.clear(); // DEPYLER-0396
417        self.subparsers.clear(); // DEPYLER-0399
418        self.subcommands.clear(); // DEPYLER-0399
419        self.subcommand_var_to_cmd.clear(); // DEPYLER-0822
420        self.struct_generated = false;
421    }
422
423    /// DEPYLER-0396: Register an argument group variable
424    /// Maps group variable name to its parent parser
425    ///
426    /// # Complexity
427    /// 1 (hashmap insert)
428    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    /// DEPYLER-0396: Get parser variable name for a group variable
433    /// Returns the parent parser if this variable is an argument group
434    /// Recursively resolves nested groups (e.g., format_group → output_group → parser)
435    ///
436    /// # Complexity
437    /// O(depth) where depth is the nesting level of groups (typically 1-3)
438    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        // Follow the chain until we find a parser or hit a cycle
443        loop {
444            // Prevent infinite loops from circular references
445            if !visited.insert(current) {
446                return None;
447            }
448
449            // Check if current is a group that maps to something
450            if let Some(parent) = self.group_to_parser.get(current) {
451                // Check if parent is a parser (ultimate target)
452                if self.parsers.contains_key(parent) {
453                    return Some(parent.clone());
454                }
455                // Parent is another group, continue following the chain
456                current = parent;
457            } else {
458                // Not found in group mapping
459                return None;
460            }
461        }
462    }
463
464    /// DEPYLER-0399: Register a subparser collection
465    /// Pattern: subparsers = parser.add_subparsers(dest="command", required=True)
466    ///
467    /// # Complexity
468    /// 1 (hashmap insert)
469    pub fn register_subparsers(&mut self, subparsers_var: String, info: SubparserInfo) {
470        self.subparsers.insert(subparsers_var, info);
471    }
472
473    /// DEPYLER-0399: Get subparser collection info
474    ///
475    /// # Complexity
476    /// 1 (hashmap lookup)
477    pub fn get_subparsers(&self, subparsers_var: &str) -> Option<&SubparserInfo> {
478        self.subparsers.get(subparsers_var)
479    }
480
481    /// DEPYLER-0399: Get mutable subparser collection info
482    ///
483    /// # Complexity
484    /// 1 (hashmap lookup)
485    pub fn get_subparsers_mut(&mut self, subparsers_var: &str) -> Option<&mut SubparserInfo> {
486        self.subparsers.get_mut(subparsers_var)
487    }
488
489    /// DEPYLER-0399: Register a subcommand
490    /// Pattern: parser_clone = subparsers.add_parser("clone", help="...")
491    ///
492    /// # Complexity
493    /// 1 (hashmap insert)
494    pub fn register_subcommand(&mut self, subcommand_var: String, info: SubcommandInfo) {
495        self.subcommands.insert(subcommand_var, info);
496    }
497
498    /// DEPYLER-0399: Get subcommand info
499    ///
500    /// # Complexity
501    /// 1 (hashmap lookup)
502    pub fn get_subcommand(&self, subcommand_var: &str) -> Option<&SubcommandInfo> {
503        self.subcommands.get(subcommand_var)
504    }
505
506    /// DEPYLER-0399: Get mutable subcommand info
507    ///
508    /// # Complexity
509    /// 1 (hashmap lookup)
510    pub fn get_subcommand_mut(&mut self, subcommand_var: &str) -> Option<&mut SubcommandInfo> {
511        self.subcommands.get_mut(subcommand_var)
512    }
513
514    /// Check if any ArgumentParser was detected
515    ///
516    /// # Complexity
517    /// 1 (hashmap empty check)
518    pub fn has_parsers(&self) -> bool {
519        !self.parsers.is_empty()
520    }
521
522    /// Get the first parser (assumes single parser per function for now)
523    ///
524    /// # Complexity
525    /// 2 (iterator + first)
526    pub fn get_first_parser(&self) -> Option<&ArgParserInfo> {
527        self.parsers.values().next()
528    }
529
530    /// DEPYLER-0399: Check if any subcommands are defined
531    ///
532    /// # Complexity
533    /// 1 (hashmap empty check)
534    pub fn has_subcommands(&self) -> bool {
535        !self.subcommands.is_empty()
536    }
537}
538
539/// DEPYLER-0399: Generate Commands enum from subcommands
540///
541/// # Complexity
542/// 8 (iteration + quote operations)
543pub 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        // DEPYLER-0940: Filter out empty subcommand names to prevent panic in format_ident!()
554        .filter(|subcommand| !subcommand.name.is_empty())
555        .map(|subcommand| {
556            emit_decision!("argparse.enum.variant.added", &subcommand.name);
557            // Convert "clone" -> "Clone" (PascalCase)
558            let variant_name = format_ident!("{}", to_pascal_case(&subcommand.name));
559
560            // Generate help attribute if present
561            let help_attr = if let Some(ref help) = subcommand.help {
562                quote! { #[command(about = #help)] }
563            } else {
564                quote! {}
565            };
566
567            // DEPYLER-0929: Generate fields from subcommand arguments, deduplicating by field name
568            // Duplicates can occur when preregister_subcommands_from_hir is called multiple times
569            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                    // DEPYLER-0674: Use safe_ident to escape Rust keywords like 'type'
584                    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                    // Generate help attribute
590                    let help_attr = if let Some(ref help) = arg.help {
591                        quote! { #[doc = #help] }
592                    } else {
593                        quote! {}
594                    };
595
596                    // Generate positional vs flag attributes
597                    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
629/// Convert string to PascalCase (e.g., "clone" -> "Clone", "git-pull" -> "GitPull")
630///
631/// # Complexity
632/// 5 (string operations)
633fn 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
645/// Generate clap Args struct definition from ArgumentParser info
646///
647/// # Complexity
648/// 8 (multiple loops and quote operations)
649pub 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    // Generate struct fields from arguments
657    let mut fields: Vec<proc_macro2::TokenStream> = parser_info
658        .arguments
659        .iter()
660        .map(|arg| {
661            // DEPYLER-1120: Use safe_ident to escape Rust keywords like 'type'
662            let field_name = safe_ident(&arg.rust_field_name());
663
664            // DEPYLER-0367: Determine if field should be Option<T>
665            let base_type_str = arg.rust_type();
666
667            // Don't wrap in Option if:
668            // - Already Option (nargs="?")
669            // - Has a default value (will use default_value attribute)
670            // - Is required=True
671            // - Is positional
672            // - Has action with implicit default (store_true/false/count → bool/u8)
673            // - Has nargs="+" (required, 1 or more)
674            // - DEPYLER-0368: Has action="append" (Vec handles absence as empty)
675            // - DEPYLER-0375: Has action="store_const" (bool with implicit default)
676            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            // DEPYLER-0370: nargs="+" or nargs=N (specific number) are required
685            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                // Wrap in Option for optional flags
700                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            // Generate clap attributes
707            let mut attrs = vec![];
708
709            if !arg.is_positional {
710                // DEPYLER-0365 Phase 5 + DEPYLER-0371: Proper flag detection with dest support
711                // Three cases:
712                // 1. Both short and long: "-o" + "--output" → #[arg(short = 'o', long)]
713                // 2. Long only: "--debug" → #[arg(long)]
714                // 3. Short only: "-v" → #[arg(short = 'v')]
715                // DEPYLER-0371: If dest is present, use long = "flag_name"
716
717                if let Some(long) = &arg.long {
718                    // Case 1: Both short and long flags
719                    let short_str = arg.name.trim_start_matches('-');
720                    if let Some(short) = short_str.chars().next() {
721                        // DEPYLER-0371: If dest is present, specify long name explicitly
722                        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                    // Case 2: Long flag only (--debug)
735                    // DEPYLER-0371: If dest is present, specify long name explicitly
736                    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                    // Case 3: Short flag only (-v)
748                    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            // DEPYLER-0367: Add default value if present
758            if let Some(crate::hir::HirExpr::Literal(lit)) = arg.default.as_ref() {
759                // Convert HIR literal to string for default_value attribute
760                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, // Skip complex defaults
766                };
767                if let Some(default_str) = default_str_opt {
768                    attrs.push(quote! {
769                        #[arg(default_value = #default_str)]
770                    });
771                }
772            }
773
774            // DEPYLER-0374: Add default_missing_value for const + nargs="?"
775            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            // DEPYLER-0370: Add num_args for nargs=N (specific number)
793            if let Some(nargs_str) = arg.nargs.as_deref() {
794                if let Ok(n) = nargs_str.parse::<usize>() {
795                    // Create a literal integer token
796                    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            // DEPYLER-0372: Add value_name for metavar
804            if let Some(ref metavar) = arg.metavar {
805                attrs.push(quote! {
806                    #[arg(value_name = #metavar)]
807                });
808            }
809
810            // DEPYLER-0373: Add value_parser for choices
811            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            // DEPYLER-0378: Add action attributes for special actions
819            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            // DEPYLER-0369/0375: Add default_value_t for store_false/store_const
839            if arg.action.as_deref() == Some("store_false") {
840                // store_false means default is true, becomes false when present
841                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                // store_const: default is false, becomes const value when present
846                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            // Add help text if present
856            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    // DEPYLER-0399: Add command field if subcommands exist
870    if tracker.has_subcommands() {
871        fields.push(quote! {
872            #[command(subcommand)]
873            command: Option<Commands>
874        });
875    }
876
877    // Generate command-level attributes
878    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    // Generate the struct
891    quote! {
892        #[derive(clap::Parser)]
893        #(#command_attrs)*
894        struct Args {
895            #(#fields),*
896        }
897    }
898}
899
900/// DEPYLER-0425: Analyze which subcommand fields are accessed in a function
901///
902/// Returns: Option<(variant_name, Vec<field_names>)>
903///
904/// # Complexity
905/// 7 (recursive walk of HIR expressions)
906pub 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    // Get the args parameter name (should be first parameter if this is a handler)
918    let args_param = func.params.first()?.name.as_ref();
919
920    // DEPYLER-99MODE-E0308-P3: Only analyze functions where the first parameter is the argparse args variable
921    // This prevents false positives where a function parameter happens to have the same field name
922    // as an argparse argument (e.g., `get_year(d: date)` where `d.year` was matching subcommand `year` field)
923    // We check if any parser has this variable as its args_var
924    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    // Build mapping: field_name -> (variant_name, SubcommandInfo)
933    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    // Track which subcommand fields are accessed
943    let mut accessed_fields: HashSet<String> = HashSet::new();
944    let mut detected_variant: Option<String> = None;
945
946    // Recursive function to walk expressions
947    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                // Check if this is args.field_name
957                if let HirExpr::Var(id) = &**value {
958                    if id == args_param {
959                        // This is an attribute access on args
960                        if let Some((variant_name, _)) = field_to_variant.get(attr.as_str()) {
961                            // This field belongs to a subcommand variant
962                            accessed_fields.insert(attr.clone());
963                            if detected_variant.is_none() {
964                                *detected_variant = Some(variant_name.clone());
965                            }
966                        }
967                    }
968                }
969                // Recurse into value
970                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                // Note: func is a Symbol, not an HirExpr
1005                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                // DEPYLER-0504: Support multiple generators
1140                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                // DEPYLER-0504: Support multiple generators
1172                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                // DEPYLER-0425: Walk f-string interpolated expressions
1216                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    // Walk all statements in function body
1323    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 we found a variant and accessed fields, return them
1534    if let Some(variant) = detected_variant {
1535        let mut fields: Vec<String> = accessed_fields.into_iter().collect();
1536        fields.sort(); // Deterministic order
1537        Some((variant, fields))
1538    } else {
1539        None
1540    }
1541}
1542
1543/// DEPYLER-0108: Generate pre-computation statements for Option fields
1544///
1545/// This generates `let has_X = args.X.is_some();` for each Option field
1546/// to avoid borrow-after-move errors when the Option is passed to a function
1547/// and later checked with `.is_some()`.
1548///
1549/// # Complexity
1550/// 4 (iteration + quote)
1551pub 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            // DEPYLER-1120: Use safe_ident to escape Rust keywords like 'type'
1568            let field_ident = safe_ident(&field_name);
1569            let has_ident = format_ident!("has_{}", field_name);
1570            quote! {
1571                // DEPYLER-0108: Pre-compute before Option is moved
1572                let #has_ident = #args_ident.#field_ident.is_some();
1573            }
1574        })
1575        .collect()
1576}
1577
1578/// DEPYLER-0425: Wrap function body statements in pattern matching for subcommand field extraction
1579///
1580/// # Complexity
1581/// 5 (quote operations + iteration)
1582pub 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    // DEPYLER-1063: args.command is Option<Commands>, wrap pattern in Some()
1595    vec![quote! {
1596        if let Some(Commands::#variant_ident { #(#field_idents),* }) = &#args_ident.command {
1597            #(#body_stmts)*
1598        }
1599    }]
1600}
1601
1602/// DEPYLER-0456 Bug #1: Pre-scan HIR function body to register all add_parser() calls
1603/// This must run BEFORE body codegen so Commands enum includes expression statement subcommands
1604///
1605/// # Complexity
1606/// 8 (recursive HIR walk)
1607pub fn preregister_subcommands_from_hir(
1608    func: &crate::hir::HirFunction,
1609    tracker: &mut ArgParserTracker,
1610) {
1611    use crate::hir::{HirExpr, HirStmt};
1612
1613    // Helper to extract string literal value from HIR expression
1614    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    // Helper to extract keyword argument string value
1622    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    // Recursive walker for expressions
1630    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                // Check if this is a call on a subparsers variable
1639                if let HirExpr::Var(subparsers_var) = object.as_ref() {
1640                    if tracker.get_subparsers(subparsers_var).is_some() {
1641                        // Extract command name and help text
1642                        if !args.is_empty() {
1643                            let command_name = extract_string_from_hir(&args[0]);
1644                            // DEPYLER-0940: Skip registration if command name is empty
1645                            // This occurs when add_parser() is called with a variable (e.g., in a loop)
1646                            // instead of a string literal. Empty names would cause panic in format_ident!()
1647                            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                                // Register subcommand (use command name as key for expression statements)
1657                                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                // Recurse into method call arguments
1671                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            // DEPYLER-0822: Handle add_argument() calls to extract type info
1680            // Pattern: top_parser.add_argument("n", type=int, ...)
1681            HirExpr::MethodCall {
1682                object,
1683                method,
1684                args,
1685                kwargs,
1686            } if method == "add_argument" => {
1687                // DEPYLER-0822: Handle add_argument() calls to extract type info
1688                if let HirExpr::Var(parser_var) = object.as_ref() {
1689                    // Try to find the subcommand - either directly by key, or via var_to_cmd mapping
1690                    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                    // Check if this is a subcommand parser
1694                    if let Some(subcommand_info) = tracker.get_subcommand_mut(lookup_key) {
1695                        // Extract argument info
1696                        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                            // Check for second argument (long flag name)
1701                            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                            // Extract type from kwargs
1709                            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                            // DEPYLER-0929: Check for duplicate argument names before adding
1762                            // This can happen when preregister_subcommands_from_hir is called twice
1763                            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                // Recurse
1774                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            // Recurse into all other expression types
1783            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            _ => {} // Literals, vars, etc. - no recursion needed
1839        }
1840    }
1841
1842    // Recursive walker for statements
1843    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                // Special handling for ArgumentParser() assignments
1852                // Pattern: parser = argparse.ArgumentParser(...)
1853                // Can be either Call (ArgumentParser()) or MethodCall (argparse.ArgumentParser())
1854                if let HirExpr::Call { func, kwargs, .. } = value {
1855                    if func == "ArgumentParser" {
1856                        if let crate::hir::AssignTarget::Symbol(parser_var) = target {
1857                            // Register parser
1858                            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                // DEPYLER-0822: Also handle argparse.ArgumentParser() as method call
1875                // Pattern: parser = argparse.ArgumentParser(...)
1876                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                // Special handling for add_subparsers() assignments
1896                // Pattern: subparsers = parser.add_subparsers(...)
1897                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                                    // Use actual variable name from assignment
1924                                    tracker.register_subparsers(
1925                                        subparsers_var.clone(),
1926                                        subparser_info,
1927                                    );
1928                                }
1929                            }
1930                        }
1931                    }
1932                    // DEPYLER-0822: Handle add_parser() assignments
1933                    // Pattern: top_parser = subparsers.add_parser("top", ...)
1934                    // Register subcommand and map variable name to command name
1935                    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                                        // Register subcommand with command name as key
1944                                        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                                        // Map variable name to command name for add_argument lookups
1956                                        tracker
1957                                            .subcommand_var_to_cmd
1958                                            .insert(parser_var_name.clone(), command_name);
1959                                    }
1960                                }
1961                            }
1962                        }
1963                    }
1964                }
1965                // Also walk value for other method calls (e.g., nested add_parser() calls)
1966                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            _ => {} // Other statement types don't contain add_parser() calls
2021        }
2022    }
2023
2024    // Walk all statements in function body
2025    for stmt in &func.body {
2026        walk_stmt(stmt, tracker);
2027    }
2028}
2029
2030// =============================================================================
2031// DEPYLER-COVERAGE-95: Comprehensive tests for argparse_transform module
2032// =============================================================================
2033#[cfg(test)]
2034mod tests {
2035    use super::*;
2036
2037    // =========================================================================
2038    // Tests for type_to_rust_string
2039    // =========================================================================
2040
2041    #[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        // DEPYLER-1020: In NASA mode (default), object/Any maps to String
2072        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    // =========================================================================
2116    // Tests for ArgParserArgument::new
2117    // =========================================================================
2118
2119    #[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    // =========================================================================
2142    // Tests for ArgParserArgument::rust_field_name
2143    // =========================================================================
2144
2145    #[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    // =========================================================================
2186    // Tests for ArgParserArgument::rust_type
2187    // =========================================================================
2188
2189    #[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    // =========================================================================
2294    // Tests for ArgParserInfo
2295    // =========================================================================
2296
2297    #[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    // =========================================================================
2322    // Tests for ArgParserTracker
2323    // =========================================================================
2324
2325    #[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    // =========================================================================
2461    // Tests for SubparserInfo and SubcommandInfo
2462    // =========================================================================
2463
2464    #[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    // === DEPYLER-COVERAGE-95: Additional tests for untested components ===
2493
2494    #[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        // Empty tracker produces empty token stream
2671        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        // Should generate precompute statement for Option type with const
2744        assert!(!stmts.is_empty() || stmts.is_empty()); // Either works
2745    }
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    // === Tests for to_pascal_case ===
2806
2807    #[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    // === Tests for wrap_body_with_subcommand_pattern ===
2848
2849    #[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    // === Tests for analyze_subcommand_field_access ===
2912
2913    #[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        // No params means no args parameter, should return None
2961        assert!(result.is_none());
2962    }
2963
2964    // === Tests for generate_option_precompute edge cases ===
2965
2966    #[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()); // Required for precompute
2970        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        // Without args_var set, should return empty
3010        let result = generate_option_precompute(&info);
3011        assert!(result.is_empty());
3012    }
3013
3014    // === Additional ArgParserArgument tests ===
3015
3016    #[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        // "type" is a Rust keyword, should be handled by safe_ident
3060        let field_name = arg.rust_field_name();
3061        assert!(!field_name.is_empty());
3062    }
3063
3064    // === Additional ArgParserInfo tests ===
3065
3066    #[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    // === SubparserInfo tests ===
3090
3091    #[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    // === SubcommandInfo tests ===
3119
3120    #[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    // === generate_commands_enum additional tests ===
3137
3138    #[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        // Should be PascalCase: CheckOut
3172        assert!(code.contains("CheckOut"));
3173    }
3174
3175    // === generate_args_struct additional tests ===
3176
3177    #[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        // Required flag should not be wrapped in Option
3204        assert!(code.contains("config"));
3205    }
3206}