envx_cli/
deps.rs

1use ahash::AHashMap as HashMap;
2use clap::{Args, Subcommand};
3use color_eyre::Result;
4use comfy_table::{Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL};
5use envx_core::EnvVarManager;
6use regex::Regex;
7use std::collections::HashSet;
8use std::fs;
9use std::path::Path;
10use std::path::PathBuf;
11use walkdir::WalkDir;
12
13#[derive(Args)]
14pub struct DepsArgs {
15    #[command(subcommand)]
16    pub command: Option<DepsCommands>,
17
18    /// Variable name to show dependencies for (shows all if not specified)
19    #[arg(value_name = "VAR")]
20    pub variable: Option<String>,
21
22    /// Show only unused variables
23    #[arg(long)]
24    pub unused: bool,
25
26    /// Paths to scan (defaults to current directory)
27    #[arg(short, long)]
28    pub paths: Vec<PathBuf>,
29
30    /// Additional patterns to ignore during scanning
31    #[arg(short = 'i', long)]
32    pub ignore: Vec<String>,
33
34    /// Output format (table, json, simple)
35    #[arg(short, long, default_value = "table")]
36    pub format: String,
37}
38
39#[derive(Subcommand)]
40pub enum DepsCommands {
41    /// Show dependencies for variables
42    Show {
43        /// Variable name to show dependencies for
44        variable: Option<String>,
45
46        /// Show only unused variables
47        #[arg(long)]
48        unused: bool,
49    },
50
51    /// Scan for environment variable usage
52    Scan {
53        /// Paths to scan
54        #[arg(default_value = ".")]
55        paths: Vec<PathBuf>,
56
57        /// Save scan results to cache
58        #[arg(long)]
59        cache: bool,
60    },
61
62    /// Show usage statistics
63    Stats {
64        /// Sort by usage count
65        #[arg(long)]
66        by_usage: bool,
67    },
68}
69
70/// Handle environment variable dependency operations.
71///
72/// # Errors
73///
74/// Returns an error if:
75/// - File scanning fails
76/// - Environment variable loading fails
77/// - I/O operations fail (reading files, writing output)
78/// - JSON serialization fails (when using JSON format)
79/// - Directory traversal fails
80pub fn handle_deps(args: &DepsArgs) -> Result<()> {
81    match args.command {
82        Some(DepsCommands::Show { ref variable, unused }) => {
83            let var_ref = variable.as_deref();
84            handle_deps_show(var_ref, unused, args)?;
85        }
86        Some(DepsCommands::Scan { ref paths, cache }) => {
87            handle_deps_scan(paths, cache, args)?;
88        }
89        Some(DepsCommands::Stats { by_usage }) => {
90            handle_deps_stats(by_usage, args)?;
91        }
92        None => {
93            // Default behavior: show dependencies for specified variable or all
94            if args.unused {
95                handle_deps_show(None, true, args)?;
96            } else {
97                handle_deps_show(args.variable.as_deref(), false, args)?;
98            }
99        }
100    }
101
102    Ok(())
103}
104
105#[allow(clippy::too_many_lines)]
106fn handle_deps_show(variable: Option<&str>, show_unused: bool, args: &DepsArgs) -> Result<()> {
107    // Initialize dependency tracker
108    let mut tracker = DependencyTracker::new();
109
110    // Add scan paths
111    if args.paths.is_empty() {
112        tracker.add_scan_path(PathBuf::from("."));
113    } else {
114        for path in &args.paths {
115            tracker.add_scan_path(path.clone());
116        }
117    }
118
119    // Add ignore patterns
120    for pattern in &args.ignore {
121        tracker.add_ignore_pattern(pattern.clone());
122    }
123
124    // Scan for dependencies
125    println!("šŸ” Scanning for environment variable usage...");
126    tracker.scan()?;
127
128    // Load current environment variables
129    let mut manager = EnvVarManager::new();
130    manager.load_all()?;
131    let all_vars: HashSet<String> = manager.list().iter().map(|v| v.name.clone()).collect();
132
133    if show_unused {
134        // Show unused variables
135        let unused = tracker.find_unused(&all_vars);
136
137        if unused.is_empty() {
138            println!("āœ… No unused environment variables found!");
139        } else {
140            println!("\nāš ļø  Found {} unused environment variables:", unused.len());
141
142            match args.format.as_str() {
143                "json" => {
144                    let json = serde_json::json!({
145                        "unused_variables": unused,
146                        "count": unused.len()
147                    });
148                    println!("{}", serde_json::to_string_pretty(&json)?);
149                }
150                "simple" => {
151                    for var in unused {
152                        println!("{var}");
153                    }
154                }
155                _ => {
156                    let mut table = Table::new();
157                    table
158                        .load_preset(UTF8_FULL)
159                        .apply_modifier(UTF8_ROUND_CORNERS)
160                        .set_header(vec!["Variable", "Value", "Source"]);
161
162                    let mut sorted_vars: Vec<_> = unused.into_iter().collect();
163                    sorted_vars.sort();
164
165                    for var_name in sorted_vars {
166                        if let Some(var) = manager.get(&var_name) {
167                            table.add_row(vec![
168                                var.name.clone(),
169                                if var.value.len() > 50 {
170                                    format!("{}...", &var.value[..47])
171                                } else {
172                                    var.value.clone()
173                                },
174                                format!("{:?}", var.source),
175                            ]);
176                        }
177                    }
178
179                    println!("{table}");
180                }
181            }
182        }
183    } else if let Some(var_name) = variable {
184        // Show dependencies for specific variable
185        if let Some(usages) = tracker.get_usages(var_name) {
186            println!("\nšŸ“Š Dependencies for '{var_name}':");
187            println!("Found {} usage(s):\n", usages.len());
188
189            match args.format.as_str() {
190                "json" => {
191                    let json = serde_json::json!({
192                        "variable": var_name,
193                        "usages": usages.iter().map(|u| {
194                            serde_json::json!({
195                                "file": u.file.display().to_string(),
196                                "line": u.line,
197                                "context": u.context
198                            })
199                        }).collect::<Vec<_>>()
200                    });
201                    println!("{}", serde_json::to_string_pretty(&json)?);
202                }
203                "simple" => {
204                    for usage in usages {
205                        println!("{}:{} - {}", usage.file.display(), usage.line, usage.context);
206                    }
207                }
208                _ => {
209                    let mut table = Table::new();
210                    table
211                        .load_preset(UTF8_FULL)
212                        .apply_modifier(UTF8_ROUND_CORNERS)
213                        .set_header(vec!["File", "Line", "Context"]);
214
215                    for usage in usages {
216                        table.add_row(vec![
217                            usage.file.display().to_string(),
218                            usage.line.to_string(),
219                            if usage.context.len() > 60 {
220                                format!("{}...", &usage.context[..57])
221                            } else {
222                                usage.context.clone()
223                            },
224                        ]);
225                    }
226
227                    println!("{table}");
228                }
229            }
230        } else {
231            println!("āŒ No usages found for variable '{var_name}'");
232
233            // Check if the variable exists
234            if !all_vars.contains(var_name) {
235                println!("   Note: This variable is not currently set in your environment.");
236            }
237        }
238    } else {
239        // Show all dependencies
240        let usage_counts = tracker.get_usage_counts();
241        let used_vars = tracker.get_used_variables();
242
243        println!("\nšŸ“Š Environment Variable Dependencies:");
244        println!("Found {} variables used in codebase\n", used_vars.len());
245
246        match args.format.as_str() {
247            "json" => {
248                let json = serde_json::json!({
249                    "total_variables": all_vars.len(),
250                    "used_variables": used_vars.len(),
251                    "unused_variables": all_vars.len() - used_vars.len(),
252                    "usage_counts": usage_counts
253                });
254                println!("{}", serde_json::to_string_pretty(&json)?);
255            }
256            "simple" => {
257                let mut sorted_vars: Vec<_> = usage_counts.into_iter().collect();
258                sorted_vars.sort_by_key(|(name, _)| name.clone());
259
260                for (var, count) in sorted_vars {
261                    println!("{var}: {count} usage(s)");
262                }
263            }
264            _ => {
265                let mut table = Table::new();
266                table
267                    .load_preset(UTF8_FULL)
268                    .apply_modifier(UTF8_ROUND_CORNERS)
269                    .set_header(vec!["Variable", "Usage Count", "Status"]);
270
271                let mut sorted_vars: Vec<_> = all_vars.iter().collect();
272                sorted_vars.sort();
273
274                for var_name in sorted_vars {
275                    let usage_count = usage_counts.get(var_name).copied().unwrap_or(0);
276                    let status = if usage_count > 0 {
277                        "āœ… Used".to_string()
278                    } else {
279                        "āš ļø  Unused".to_string()
280                    };
281
282                    table.add_row(vec![var_name.clone(), usage_count.to_string(), status]);
283                }
284
285                println!("{table}");
286            }
287        }
288    }
289
290    Ok(())
291}
292
293fn handle_deps_scan(paths: &[PathBuf], cache: bool, args: &DepsArgs) -> Result<()> {
294    let mut tracker = DependencyTracker::new();
295
296    // Add scan paths
297    for path in paths {
298        tracker.add_scan_path(path.clone());
299    }
300
301    // Add ignore patterns
302    for pattern in &args.ignore {
303        tracker.add_ignore_pattern(pattern.clone());
304    }
305
306    println!("šŸ” Scanning paths:");
307    for path in paths {
308        println!("   - {}", path.display());
309    }
310
311    tracker.scan()?;
312
313    let used_vars = tracker.get_used_variables();
314    println!("\nāœ… Scan complete!");
315    println!("Found {} unique environment variables", used_vars.len());
316
317    if cache {
318        // TODO: Implement caching mechanism
319        println!("šŸ“¦ Caching scan results... (not yet implemented)");
320    }
321
322    Ok(())
323}
324
325fn handle_deps_stats(by_usage: bool, args: &DepsArgs) -> Result<()> {
326    let mut tracker = DependencyTracker::new();
327
328    // Add scan paths
329    if args.paths.is_empty() {
330        tracker.add_scan_path(PathBuf::from("."));
331    } else {
332        for path in &args.paths {
333            tracker.add_scan_path(path.clone());
334        }
335    }
336
337    println!("šŸ” Analyzing environment variable usage...");
338    tracker.scan()?;
339
340    let usage_counts = tracker.get_usage_counts();
341    let mut stats: Vec<_> = usage_counts.into_iter().collect();
342
343    if by_usage {
344        stats.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
345    } else {
346        stats.sort_by_key(|(name, _)| name.clone());
347    }
348
349    println!("\nšŸ“Š Environment Variable Usage Statistics:\n");
350
351    let mut table = Table::new();
352    table
353        .load_preset(UTF8_FULL)
354        .apply_modifier(UTF8_ROUND_CORNERS)
355        .set_header(vec!["Rank", "Variable", "Usage Count", "Frequency"]);
356
357    let total_usages: usize = stats.iter().map(|(_, count)| count).sum();
358
359    for (rank, (var, count)) in stats.iter().enumerate() {
360        #[allow(clippy::cast_precision_loss)]
361        let frequency = if total_usages > 0 {
362            format!("{:.1}%", (*count as f64 / total_usages as f64) * 100.0)
363        } else {
364            "0.0%".to_string()
365        };
366
367        table.add_row(vec![(rank + 1).to_string(), var.clone(), count.to_string(), frequency]);
368
369        if rank >= 19 {
370            // Show top 20
371            break;
372        }
373    }
374
375    println!("{table}");
376
377    if stats.len() > 20 {
378        println!("\n... and {} more variables", stats.len() - 20);
379    }
380
381    Ok(())
382}
383
384#[derive(Args)]
385pub struct CleanupArgs {
386    /// Force cleanup without confirmation
387    #[arg(short, long)]
388    pub force: bool,
389
390    /// Dry run - show what would be removed without making changes
391    #[arg(short = 'n', long)]
392    pub dry_run: bool,
393
394    /// Keep variables matching these patterns
395    #[arg(short = 'k', long)]
396    pub keep: Vec<String>,
397
398    /// Additional paths to scan for usage
399    #[arg(short = 'p', long)]
400    pub paths: Vec<PathBuf>,
401}
402
403/// Handle cleanup of unused environment variables.
404///
405/// # Errors
406///
407/// Returns an error if:
408/// - File scanning fails
409/// - Environment variable loading fails
410/// - Environment variable deletion fails
411/// - I/O operations fail (reading user input, writing output)
412pub fn handle_cleanup(args: &CleanupArgs) -> Result<()> {
413    // Initialize dependency tracker
414    let mut tracker = DependencyTracker::new();
415
416    // Add scan paths
417    if args.paths.is_empty() {
418        tracker.add_scan_path(PathBuf::from("."));
419    } else {
420        for path in &args.paths {
421            tracker.add_scan_path(path.clone());
422        }
423    }
424
425    println!("šŸ” Scanning for environment variable usage...");
426    tracker.scan()?;
427
428    // Load current environment variables
429    let mut manager = EnvVarManager::new();
430    manager.load_all()?;
431    let all_vars: HashSet<String> = manager.list().iter().map(|v| v.name.clone()).collect();
432
433    // Find unused variables
434    let mut unused = tracker.find_unused(&all_vars);
435
436    // Filter out variables that should be kept
437    if !args.keep.is_empty() {
438        unused.retain(|var| {
439            !args.keep.iter().any(|pattern| {
440                var.contains(pattern) || glob::Pattern::new(pattern).map(|p| p.matches(var)).unwrap_or(false)
441            })
442        });
443    }
444
445    if unused.is_empty() {
446        println!("āœ… No unused environment variables found!");
447        return Ok(());
448    }
449
450    println!("\nāš ļø  Found {} unused environment variables:", unused.len());
451
452    let mut sorted_unused: Vec<_> = unused.into_iter().collect();
453    sorted_unused.sort();
454
455    for var in &sorted_unused {
456        if let Some(env_var) = manager.get(var) {
457            println!(
458                "   - {} = {} [{:?}]",
459                var,
460                if env_var.value.len() > 50 {
461                    format!("{}...", &env_var.value[..47])
462                } else {
463                    env_var.value.clone()
464                },
465                env_var.source
466            );
467        }
468    }
469
470    if args.dry_run {
471        println!("\n(Dry run - no changes made)");
472        return Ok(());
473    }
474
475    if !args.force {
476        print!("\nRemove these unused variables? [y/N]: ");
477        std::io::Write::flush(&mut std::io::stdout())?;
478
479        let mut input = String::new();
480        std::io::stdin().read_line(&mut input)?;
481
482        if !input.trim().eq_ignore_ascii_case("y") {
483            println!("Cleanup cancelled.");
484            return Ok(());
485        }
486    }
487
488    // Remove unused variables
489    let mut removed = 0;
490    let mut failed = 0;
491
492    for var in sorted_unused {
493        match manager.delete(&var) {
494            Ok(()) => {
495                removed += 1;
496                println!("āœ… Removed: {var}");
497            }
498            Err(e) => {
499                failed += 1;
500                eprintln!("āŒ Failed to remove {var}: {e}");
501            }
502        }
503    }
504
505    println!("\nšŸ“Š Cleanup complete:");
506    println!("   - Removed: {removed} variables");
507    if failed > 0 {
508        println!("   - Failed: {failed} variables");
509    }
510
511    Ok(())
512}
513
514/// Represents a location where an environment variable is used
515#[derive(Debug, Clone)]
516pub struct VariableUsage {
517    pub file: PathBuf,
518    pub line: usize,
519    pub context: String,
520}
521
522/// Tracks dependencies for environment variables
523pub struct DependencyTracker {
524    usages: HashMap<String, Vec<VariableUsage>>,
525    scan_paths: Vec<PathBuf>,
526    ignore_patterns: Vec<String>,
527}
528
529impl DependencyTracker {
530    pub fn new() -> Self {
531        Self {
532            usages: HashMap::new(),
533            scan_paths: vec![PathBuf::from(".")],
534            ignore_patterns: vec![
535                ".git".to_string(),
536                "node_modules".to_string(),
537                "target".to_string(),
538                ".venv".to_string(),
539                "__pycache__".to_string(),
540                "dist".to_string(),
541                "build".to_string(),
542                ".envx".to_string(),
543                "vendor".to_string(),
544                ".cargo".to_string(),
545            ],
546        }
547    }
548
549    /// Add a path to scan for dependencies
550    pub fn add_scan_path(&mut self, path: PathBuf) {
551        self.scan_paths.push(path);
552    }
553
554    /// Add patterns to ignore during scanning
555    pub fn add_ignore_pattern(&mut self, pattern: String) {
556        self.ignore_patterns.push(pattern);
557    }
558
559    /// Scan all configured paths for environment variable usage
560    pub fn scan(&mut self) -> Result<()> {
561        self.usages.clear();
562
563        for path in &self.scan_paths.clone() {
564            if path.is_file() {
565                self.scan_file(path)?;
566            } else if path.is_dir() {
567                self.scan_directory(path)?;
568            }
569        }
570
571        Ok(())
572    }
573
574    /// Scan a directory recursively
575    fn scan_directory(&mut self, dir: &Path) -> Result<()> {
576        let ignore_patterns = self.ignore_patterns.clone();
577
578        for entry in WalkDir::new(dir)
579            .follow_links(false)
580            .into_iter()
581            .filter_entry(|e| !Self::should_ignore_with_patterns(e.path(), &ignore_patterns))
582        {
583            let entry = entry?;
584            if entry.file_type().is_file() {
585                self.scan_file(entry.path())?;
586            }
587        }
588        Ok(())
589    }
590
591    /// Check if a path should be ignored using provided patterns
592    fn should_ignore_with_patterns(path: &Path, ignore_patterns: &[String]) -> bool {
593        for component in path.components() {
594            if let Some(name) = component.as_os_str().to_str() {
595                if ignore_patterns.iter().any(|p| name.contains(p)) {
596                    return true;
597                }
598            }
599        }
600        false
601    }
602
603    /// Scan a single file for environment variable usage
604    fn scan_file(&mut self, path: &Path) -> Result<()> {
605        // Skip binary files and very large files
606        let metadata = fs::metadata(path)?;
607        if metadata.len() > 10_000_000 {
608            // Skip files larger than 10MB
609            return Ok(());
610        }
611
612        let Ok(content) = fs::read_to_string(path) else {
613            return Ok(()); // Skip binary files
614        };
615
616        let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
617        let filename = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
618
619        match extension {
620            // Source code files
621            "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => self.scan_javascript(&content, path)?,
622            "py" | "pyw" => self.scan_python(&content, path)?,
623            "rs" => self.scan_rust(&content, path)?,
624            "go" => self.scan_go(&content, path)?,
625            "java" => self.scan_java(&content, path)?,
626            "cs" => self.scan_csharp(&content, path)?,
627            "rb" => self.scan_ruby(&content, path)?,
628            "php" => self.scan_php(&content, path)?,
629            "c" | "h" => self.scan_c(&content, path)?,
630            "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "h++" => self.scan_cpp(&content, path)?,
631
632            // Shell scripts
633            "sh" | "bash" | "zsh" | "fish" => self.scan_shell(&content, path)?,
634            "ps1" | "psm1" => self.scan_powershell(&content, path)?,
635            "bat" | "cmd" => self.scan_batch(&content, path)?,
636
637            // Check by filename or content
638            _ => {
639                if filename == "Makefile" || filename.starts_with("Makefile.") {
640                    self.scan_makefile(&content, path)?;
641                } else if content.starts_with("#!/") {
642                    // Shebang script - likely a shell script
643                    self.scan_shell(&content, path)?;
644                }
645            }
646        }
647
648        Ok(())
649    }
650
651    /// Record a usage of an environment variable
652    fn record_usage(&mut self, var_name: String, file: &Path, line: usize, context: String) {
653        let usage = VariableUsage {
654            file: file.to_path_buf(),
655            line,
656            context,
657        };
658
659        // Check if this exact usage already exists
660        let usages = self.usages.entry(var_name).or_default();
661
662        // Avoid duplicate entries for the same file and line
663        let already_exists = usages
664            .iter()
665            .any(|u| u.file == usage.file && u.line == usage.line && u.context == usage.context);
666
667        if !already_exists {
668            usages.push(usage);
669        }
670    }
671
672    /// Scan JavaScript/TypeScript files
673    fn scan_javascript(&mut self, content: &str, path: &Path) -> Result<()> {
674        let patterns = [
675            // process.env.VAR or process.env["VAR"] or process.env['VAR']
676            Regex::new(r"process\.env\.(\w+)")?,
677            Regex::new(r#"process\.env\[["'](\w+)["']\]"#)?,
678            // Deno.env.get("VAR")
679            Regex::new(r#"Deno\.env\.get\(["'](\w+)["']\)"#)?,
680            // import.meta.env.VAR
681            Regex::new(r"import\.meta\.env\.(\w+)")?,
682        ];
683
684        for (line_num, line) in content.lines().enumerate() {
685            for pattern in &patterns {
686                for cap in pattern.captures_iter(line) {
687                    if let Some(var) = cap.get(1) {
688                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
689                    }
690                }
691            }
692        }
693
694        Ok(())
695    }
696
697    /// Scan Python files
698    fn scan_python(&mut self, content: &str, path: &Path) -> Result<()> {
699        let patterns = [
700            // os.environ["VAR"] or os.environ['VAR']
701            Regex::new(r#"os\.environ\[["'](\w+)["']\]"#)?,
702            // os.environ.get("VAR") or os.environ.get('VAR')
703            Regex::new(r#"os\.environ\.get\(["'](\w+)["']"#)?,
704            // os.getenv("VAR") or os.getenv('VAR')
705            Regex::new(r#"os\.getenv\(["'](\w+)["']"#)?,
706            // environ["VAR"] after from os import environ
707            Regex::new(r#"environ\[["'](\w+)["']\]"#)?,
708        ];
709
710        for (line_num, line) in content.lines().enumerate() {
711            for pattern in &patterns {
712                for cap in pattern.captures_iter(line) {
713                    if let Some(var) = cap.get(1) {
714                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
715                    }
716                }
717            }
718        }
719
720        Ok(())
721    }
722
723    /// Scan Rust files
724    fn scan_rust(&mut self, content: &str, path: &Path) -> Result<()> {
725        let patterns = [
726            // env!("VAR")
727            Regex::new(r#"env!\s*\(\s*"(\w+)"\s*\)"#)?,
728            // std::env::var("VAR")
729            Regex::new(r#"std::env::var\s*\(\s*"(\w+)"\s*\)"#)?,
730            // env::var("VAR")
731            Regex::new(r#"env::var\s*\(\s*"(\w+)"\s*\)"#)?,
732            // std::env::var_os("VAR")
733            Regex::new(r#"std::env::var_os\s*\(\s*"(\w+)"\s*\)"#)?,
734            // env::var_os("VAR")
735            Regex::new(r#"env::var_os\s*\(\s*"(\w+)"\s*\)"#)?,
736        ];
737
738        for (line_num, line) in content.lines().enumerate() {
739            for pattern in &patterns {
740                for cap in pattern.captures_iter(line) {
741                    if let Some(var) = cap.get(1) {
742                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
743                    }
744                }
745            }
746        }
747
748        Ok(())
749    }
750
751    /// Scan Go files
752    fn scan_go(&mut self, content: &str, path: &Path) -> Result<()> {
753        let patterns = [
754            // os.Getenv("VAR")
755            Regex::new(r#"os\.Getenv\s*\(\s*"(\w+)"\s*\)"#)?,
756            // os.LookupEnv("VAR")
757            Regex::new(r#"os\.LookupEnv\s*\(\s*"(\w+)"\s*\)"#)?,
758            // os.Setenv("VAR", ...)
759            Regex::new(r#"os\.Setenv\s*\(\s*"(\w+)"\s*,"#)?,
760        ];
761
762        for (line_num, line) in content.lines().enumerate() {
763            for pattern in &patterns {
764                for cap in pattern.captures_iter(line) {
765                    if let Some(var) = cap.get(1) {
766                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
767                    }
768                }
769            }
770        }
771
772        Ok(())
773    }
774
775    /// Scan Java files
776    fn scan_java(&mut self, content: &str, path: &Path) -> Result<()> {
777        let patterns = [
778            // System.getenv("VAR")
779            Regex::new(r#"System\.getenv\s*\(\s*"(\w+)"\s*\)"#)?,
780            // System.getenv().get("VAR")
781            Regex::new(r#"getenv\s*\(\s*\)\.get\s*\(\s*"(\w+)"\s*\)"#)?,
782        ];
783
784        for (line_num, line) in content.lines().enumerate() {
785            for pattern in &patterns {
786                for cap in pattern.captures_iter(line) {
787                    if let Some(var) = cap.get(1) {
788                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
789                    }
790                }
791            }
792        }
793
794        Ok(())
795    }
796
797    /// Scan C# files
798    fn scan_csharp(&mut self, content: &str, path: &Path) -> Result<()> {
799        let patterns = [
800            // Environment.GetEnvironmentVariable("VAR")
801            Regex::new(r#"Environment\.GetEnvironmentVariable\s*\(\s*"(\w+)"\s*\)"#)?,
802            // Environment.SetEnvironmentVariable("VAR", ...)
803            Regex::new(r#"Environment\.SetEnvironmentVariable\s*\(\s*"(\w+)"\s*,"#)?,
804        ];
805
806        for (line_num, line) in content.lines().enumerate() {
807            for pattern in &patterns {
808                for cap in pattern.captures_iter(line) {
809                    if let Some(var) = cap.get(1) {
810                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
811                    }
812                }
813            }
814        }
815
816        Ok(())
817    }
818
819    /// Scan Ruby files
820    fn scan_ruby(&mut self, content: &str, path: &Path) -> Result<()> {
821        let patterns = [
822            // ENV["VAR"] or ENV['VAR']
823            Regex::new(r#"ENV\[["'](\w+)["']\]"#)?,
824            // ENV.fetch("VAR") or ENV.fetch('VAR')
825            Regex::new(r#"ENV\.fetch\s*\(\s*["'](\w+)["']"#)?,
826        ];
827
828        for (line_num, line) in content.lines().enumerate() {
829            for pattern in &patterns {
830                for cap in pattern.captures_iter(line) {
831                    if let Some(var) = cap.get(1) {
832                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
833                    }
834                }
835            }
836        }
837
838        Ok(())
839    }
840
841    /// Scan PHP files
842    fn scan_php(&mut self, content: &str, path: &Path) -> Result<()> {
843        let patterns = [
844            // $_ENV["VAR"] or $_ENV['VAR']
845            Regex::new(r#"\$_ENV\[["'](\w+)["']\]"#)?,
846            // getenv("VAR") or getenv('VAR')
847            Regex::new(r#"getenv\s*\(\s*["'](\w+)["']"#)?,
848            // $_SERVER["VAR"] or $_SERVER['VAR'] (often contains env vars)
849            Regex::new(r#"\$_SERVER\[["'](\w+)["']\]"#)?,
850        ];
851
852        for (line_num, line) in content.lines().enumerate() {
853            for pattern in &patterns {
854                for cap in pattern.captures_iter(line) {
855                    if let Some(var) = cap.get(1) {
856                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
857                    }
858                }
859            }
860        }
861
862        Ok(())
863    }
864
865    /// Scan C files
866    fn scan_c(&mut self, content: &str, path: &Path) -> Result<()> {
867        let patterns = [
868            // getenv("VAR")
869            Regex::new(r#"getenv\s*\(\s*"(\w+)"\s*\)"#)?,
870            // setenv("VAR", ...) or putenv("VAR=...")
871            Regex::new(r#"setenv\s*\(\s*"(\w+)"\s*,"#)?,
872            // Common Windows variants
873            Regex::new(r#"GetEnvironmentVariable[AW]?\s*\(\s*"(\w+)"\s*,"#)?,
874            Regex::new(r#"SetEnvironmentVariable[AW]?\s*\(\s*"(\w+)"\s*,"#)?,
875        ];
876
877        for (line_num, line) in content.lines().enumerate() {
878            // Skip comments
879            let trimmed = line.trim();
880            if trimmed.starts_with("//") || (trimmed.starts_with("/*") && trimmed.ends_with("*/")) {
881                continue;
882            }
883
884            for pattern in &patterns {
885                for cap in pattern.captures_iter(line) {
886                    if let Some(var) = cap.get(1) {
887                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
888                    }
889                }
890            }
891        }
892
893        Ok(())
894    }
895
896    /// Scan C++ files
897    fn scan_cpp(&mut self, content: &str, path: &Path) -> Result<()> {
898        let patterns = [
899            // getenv("VAR") - C-style
900            Regex::new(r#"getenv\s*\(\s*"(\w+)"\s*\)"#)?,
901            // std::getenv("VAR")
902            Regex::new(r#"std::getenv\s*\(\s*"(\w+)"\s*\)"#)?,
903            // setenv/putenv variants
904            Regex::new(r#"setenv\s*\(\s*"(\w+)"\s*,"#)?,
905            // Windows API
906            Regex::new(r#"GetEnvironmentVariable[AW]?\s*\(\s*"(\w+)"\s*,"#)?,
907            Regex::new(r#"SetEnvironmentVariable[AW]?\s*\(\s*"(\w+)"\s*,"#)?,
908            // Boost
909            Regex::new(r#"boost::this_process::environment\s*\[\s*"(\w+)"\s*\]"#)?,
910        ];
911
912        for (line_num, line) in content.lines().enumerate() {
913            // Skip comments
914            let trimmed = line.trim();
915            if trimmed.starts_with("//") || (trimmed.starts_with("/*") && trimmed.ends_with("*/")) {
916                continue;
917            }
918
919            for pattern in &patterns {
920                for cap in pattern.captures_iter(line) {
921                    if let Some(var) = cap.get(1) {
922                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
923                    }
924                }
925            }
926        }
927
928        Ok(())
929    }
930
931    /// Scan shell scripts (bash, sh, zsh, fish)
932    fn scan_shell(&mut self, content: &str, path: &Path) -> Result<()> {
933        let patterns = [
934            // $VAR or ${VAR}
935            Regex::new(r"\$(\w+)")?,
936            Regex::new(r"\$\{(\w+)\}")?,
937            // export VAR=... or export VAR
938            Regex::new(r"^\s*export\s+(\w+)")?,
939            // : ${VAR:=default} or similar parameter expansion
940            Regex::new(r"\$\{(\w+)[:?+=\-]")?,
941        ];
942
943        for (line_num, line) in content.lines().enumerate() {
944            // Skip comments
945            if line.trim().starts_with('#') {
946                continue;
947            }
948
949            for pattern in &patterns {
950                for cap in pattern.captures_iter(line) {
951                    if let Some(var) = cap.get(1) {
952                        // Skip common shell built-in variables
953                        let var_name = var.as_str();
954                        if ![
955                            "1",
956                            "2",
957                            "3",
958                            "4",
959                            "5",
960                            "6",
961                            "7",
962                            "8",
963                            "9",
964                            "0",
965                            "@",
966                            "*",
967                            "#",
968                            "?",
969                            "-",
970                            "$",
971                            "!",
972                            "_",
973                            "PPID",
974                            "PWD",
975                            "OLDPWD",
976                            "REPLY",
977                            "UID",
978                            "EUID",
979                            "GROUPS",
980                            "BASH",
981                            "BASH_VERSION",
982                            "BASH_VERSINFO",
983                            "SHLVL",
984                            "RANDOM",
985                            "SECONDS",
986                            "LINENO",
987                            "HISTCMD",
988                            "FUNCNAME",
989                            "PIPESTATUS",
990                            "IFS",
991                        ]
992                        .contains(&var_name)
993                            && !var_name.starts_with("BASH_")
994                        {
995                            self.record_usage(var_name.to_string(), path, line_num + 1, line.trim().to_string());
996                        }
997                    }
998                }
999            }
1000        }
1001
1002        Ok(())
1003    }
1004
1005    /// Scan `PowerShell` scripts
1006    fn scan_powershell(&mut self, content: &str, path: &Path) -> Result<()> {
1007        let patterns = [
1008            // $env:VAR
1009            Regex::new(r"\$env:(\w+)")?,
1010            // [Environment]::GetEnvironmentVariable("VAR")
1011            Regex::new(r#"\[Environment\]::GetEnvironmentVariable\s*\(\s*["'](\w+)["']"#)?,
1012            // [Environment]::SetEnvironmentVariable("VAR", ...)
1013            Regex::new(r#"\[Environment\]::SetEnvironmentVariable\s*\(\s*["'](\w+)["']"#)?,
1014        ];
1015
1016        for (line_num, line) in content.lines().enumerate() {
1017            // Skip comments
1018            if line.trim().starts_with('#') {
1019                continue;
1020            }
1021
1022            for pattern in &patterns {
1023                for cap in pattern.captures_iter(line) {
1024                    if let Some(var) = cap.get(1) {
1025                        self.record_usage(var.as_str().to_string(), path, line_num + 1, line.trim().to_string());
1026                    }
1027                }
1028            }
1029        }
1030
1031        Ok(())
1032    }
1033
1034    /// Scan batch files
1035    fn scan_batch(&mut self, content: &str, path: &Path) -> Result<()> {
1036        let patterns = [
1037            // %VAR%
1038            Regex::new(r"%(\w+)%")?,
1039            // set VAR=...
1040            Regex::new(r"(?i)^\s*set\s+(\w+)=")?,
1041        ];
1042
1043        for (line_num, line) in content.lines().enumerate() {
1044            // Skip comments
1045            if line.trim().starts_with("REM") || line.trim().starts_with("::") {
1046                continue;
1047            }
1048
1049            for pattern in &patterns {
1050                for cap in pattern.captures_iter(line) {
1051                    if let Some(var) = cap.get(1) {
1052                        // Skip common Windows built-in variables
1053                        let var_name = var.as_str();
1054                        if ![
1055                            "errorlevel",
1056                            "cd",
1057                            "date",
1058                            "time",
1059                            "random",
1060                            "CD",
1061                            "DATE",
1062                            "TIME",
1063                            "RANDOM",
1064                            "ERRORLEVEL",
1065                        ]
1066                        .contains(&var_name)
1067                        {
1068                            self.record_usage(var_name.to_string(), path, line_num + 1, line.trim().to_string());
1069                        }
1070                    }
1071                }
1072            }
1073        }
1074
1075        Ok(())
1076    }
1077
1078    /// Scan Makefiles
1079    fn scan_makefile(&mut self, content: &str, path: &Path) -> Result<()> {
1080        let patterns = [
1081            // $(VAR) or ${VAR}
1082            Regex::new(r"\$\((\w+)\)")?,
1083            Regex::new(r"\$\{(\w+)\}")?,
1084            // Environment variable references in recipes
1085            Regex::new(r"\$\$(\w+)")?,
1086            Regex::new(r"\$\$\{(\w+)\}")?,
1087        ];
1088
1089        for (line_num, line) in content.lines().enumerate() {
1090            // Skip comments
1091            if line.trim().starts_with('#') {
1092                continue;
1093            }
1094
1095            for pattern in &patterns {
1096                for cap in pattern.captures_iter(line) {
1097                    if let Some(var) = cap.get(1) {
1098                        // Skip common Make built-in variables
1099                        let var_name = var.as_str();
1100                        if ![
1101                            "MAKE",
1102                            "MAKEFLAGS",
1103                            "MAKECMDGOALS",
1104                            "CURDIR",
1105                            "SHELL",
1106                            "MAKEFILE_LIST",
1107                            "MAKEFILES",
1108                            "VPATH",
1109                            "SUFFIXES",
1110                            ".DEFAULT_GOAL",
1111                            ".VARIABLES",
1112                            ".FEATURES",
1113                        ]
1114                        .contains(&var_name)
1115                            && !var_name.starts_with('.')
1116                        {
1117                            self.record_usage(var_name.to_string(), path, line_num + 1, line.trim().to_string());
1118                        }
1119                    }
1120                }
1121            }
1122        }
1123
1124        Ok(())
1125    }
1126
1127    /// Get all found usages for a specific variable
1128    pub fn get_usages(&self, var_name: &str) -> Option<&Vec<VariableUsage>> {
1129        self.usages.get(var_name)
1130    }
1131
1132    /// Get all variables that have been found in the codebase
1133    pub fn get_used_variables(&self) -> HashSet<String> {
1134        self.usages.keys().cloned().collect()
1135    }
1136
1137    /// Get all variables and their usage counts
1138    pub fn get_usage_counts(&self) -> HashMap<String, usize> {
1139        self.usages
1140            .iter()
1141            .map(|(name, usages)| (name.clone(), usages.len()))
1142            .collect()
1143    }
1144
1145    /// Find unused variables from a given set
1146    pub fn find_unused(&self, all_vars: &HashSet<String>) -> HashSet<String> {
1147        let used_vars = self.get_used_variables();
1148        all_vars.difference(&used_vars).cloned().collect()
1149    }
1150}
1151
1152impl Default for DependencyTracker {
1153    fn default() -> Self {
1154        Self::new()
1155    }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160    use super::*;
1161    use std::fs;
1162    use tempfile::TempDir;
1163
1164    /// Helper function to create a test file with content
1165    fn create_test_file(dir: &Path, filename: &str, content: &str) -> PathBuf {
1166        let file_path = dir.join(filename);
1167        fs::write(&file_path, content).unwrap();
1168        file_path
1169    }
1170
1171    /// Helper function to create a test directory structure
1172    fn create_test_dir() -> TempDir {
1173        TempDir::new().unwrap()
1174    }
1175
1176    #[test]
1177    fn test_new_tracker() {
1178        let tracker = DependencyTracker::new();
1179        assert_eq!(tracker.scan_paths.len(), 1);
1180        assert_eq!(tracker.scan_paths[0], PathBuf::from("."));
1181        assert!(!tracker.ignore_patterns.is_empty());
1182        assert!(tracker.usages.is_empty());
1183    }
1184
1185    #[test]
1186    fn test_add_scan_path() {
1187        let mut tracker = DependencyTracker::new();
1188        let path = PathBuf::from("/test/path");
1189        tracker.add_scan_path(path.clone());
1190        assert_eq!(tracker.scan_paths.len(), 2);
1191        assert_eq!(tracker.scan_paths[1], path);
1192    }
1193
1194    #[test]
1195    fn test_add_ignore_pattern() {
1196        let mut tracker = DependencyTracker::new();
1197        tracker.add_ignore_pattern("test_pattern".to_string());
1198        assert!(tracker.ignore_patterns.contains(&"test_pattern".to_string()));
1199    }
1200
1201    #[test]
1202    fn test_scan_javascript_files() {
1203        let temp_dir = create_test_dir();
1204        let js_content = r#"
1205const dbUrl = process.env.DATABASE_URL;
1206const apiKey = process.env["API_KEY"];
1207const secret = process.env['SECRET_KEY'];
1208const port = process.env.PORT || 3000;
1209
1210// Deno style
1211const denoVar = Deno.env.get("DENO_VAR");
1212
1213// Vite/import.meta style
1214const viteVar = import.meta.env.VITE_API_URL;
1215"#;
1216
1217        let js_file = create_test_file(temp_dir.path(), "test.js", js_content);
1218
1219        let mut tracker = DependencyTracker::new();
1220        tracker.scan_file(&js_file).unwrap();
1221
1222        assert!(tracker.get_usages("DATABASE_URL").is_some());
1223        assert!(tracker.get_usages("API_KEY").is_some());
1224        assert!(tracker.get_usages("SECRET_KEY").is_some());
1225        assert!(tracker.get_usages("PORT").is_some());
1226        assert!(tracker.get_usages("DENO_VAR").is_some());
1227        assert!(tracker.get_usages("VITE_API_URL").is_some());
1228
1229        let used_vars = tracker.get_used_variables();
1230        assert_eq!(used_vars.len(), 6);
1231    }
1232
1233    #[test]
1234    fn test_scan_python_files() {
1235        let temp_dir = create_test_dir();
1236        let py_content = r#"
1237import os
1238from os import environ
1239
1240# Different ways to access env vars
1241db_url = os.environ["DATABASE_URL"]
1242api_key = os.environ.get("API_KEY", "default")
1243secret = os.getenv("SECRET_KEY")
1244home = environ["HOME"]
1245
1246# This should not create duplicates
1247node_env = os.environ.get("NODE_ENV", "development")
1248"#;
1249
1250        let py_file = create_test_file(temp_dir.path(), "test.py", py_content);
1251
1252        let mut tracker = DependencyTracker::new();
1253        tracker.scan_file(&py_file).unwrap();
1254
1255        assert!(tracker.get_usages("DATABASE_URL").is_some());
1256        assert!(tracker.get_usages("API_KEY").is_some());
1257        assert!(tracker.get_usages("SECRET_KEY").is_some());
1258        assert!(tracker.get_usages("HOME").is_some());
1259        assert!(tracker.get_usages("NODE_ENV").is_some());
1260
1261        // Check that NODE_ENV is only recorded once
1262        let node_env_usages = tracker.get_usages("NODE_ENV").unwrap();
1263        assert_eq!(node_env_usages.len(), 1);
1264    }
1265
1266    #[test]
1267    fn test_scan_rust_files() {
1268        let temp_dir = create_test_dir();
1269        let rs_content = r#"
1270use std::env;
1271
1272fn main() {
1273    let db_url = env::var("DATABASE_URL").unwrap();
1274    let api_key = std::env::var("API_KEY").unwrap_or_default();
1275    let home = env::var_os("HOME");
1276    let compile_time = env!("CARGO_PKG_VERSION");
1277}
1278"#;
1279
1280        let rs_file = create_test_file(temp_dir.path(), "test.rs", rs_content);
1281
1282        let mut tracker = DependencyTracker::new();
1283        tracker.scan_file(&rs_file).unwrap();
1284
1285        assert!(tracker.get_usages("DATABASE_URL").is_some());
1286        assert!(tracker.get_usages("API_KEY").is_some());
1287        assert!(tracker.get_usages("HOME").is_some());
1288        assert!(tracker.get_usages("CARGO_PKG_VERSION").is_some());
1289    }
1290
1291    #[test]
1292    fn test_scan_go_files() {
1293        let temp_dir = create_test_dir();
1294        let go_content = r#"
1295package main
1296
1297import "os"
1298
1299func main() {
1300    dbUrl := os.Getenv("DATABASE_URL")
1301    apiKey, exists := os.LookupEnv("API_KEY")
1302    os.Setenv("NEW_VAR", "value")
1303}
1304"#;
1305
1306        let go_file = create_test_file(temp_dir.path(), "test.go", go_content);
1307
1308        let mut tracker = DependencyTracker::new();
1309        tracker.scan_file(&go_file).unwrap();
1310
1311        assert!(tracker.get_usages("DATABASE_URL").is_some());
1312        assert!(tracker.get_usages("API_KEY").is_some());
1313        assert!(tracker.get_usages("NEW_VAR").is_some());
1314    }
1315
1316    #[test]
1317    fn test_scan_c_files() {
1318        let temp_dir = create_test_dir();
1319        let c_content = r#"
1320#include <stdlib.h>
1321
1322int main() {
1323    char* db_url = getenv("DATABASE_URL");
1324    setenv("API_KEY", "secret", 1);
1325    
1326    // Windows style
1327    GetEnvironmentVariable("WINDOWS_VAR", buffer, size);
1328    SetEnvironmentVariableA("WIN_API_KEY", "value");
1329    
1330    // This is a comment: getenv("COMMENTED_VAR")
1331    /* Also commented: getenv("BLOCK_COMMENT_VAR") */
1332}
1333"#;
1334
1335        let c_file = create_test_file(temp_dir.path(), "test.c", c_content);
1336
1337        let mut tracker = DependencyTracker::new();
1338        tracker.scan_file(&c_file).unwrap();
1339
1340        assert!(tracker.get_usages("DATABASE_URL").is_some());
1341        assert!(tracker.get_usages("API_KEY").is_some());
1342        assert!(tracker.get_usages("WINDOWS_VAR").is_some());
1343        assert!(tracker.get_usages("WIN_API_KEY").is_some());
1344
1345        // Comments should be ignored
1346        assert!(tracker.get_usages("COMMENTED_VAR").is_none());
1347        assert!(tracker.get_usages("BLOCK_COMMENT_VAR").is_none());
1348    }
1349
1350    #[test]
1351    fn test_scan_cpp_files() {
1352        let temp_dir = create_test_dir();
1353        let cpp_content = r#"
1354#include <cstdlib>
1355#include <iostream>
1356
1357int main() {
1358    // C-style
1359    const char* db_url = getenv("DATABASE_URL");
1360    
1361    // C++ style
1362    const char* api_key = std::getenv("API_KEY");
1363    
1364    // Boost style
1365    auto value = boost::this_process::environment["BOOST_VAR"];
1366    
1367    // Comment should be ignored
1368    // std::getenv("COMMENTED_VAR");
1369}
1370"#;
1371
1372        let cpp_file = create_test_file(temp_dir.path(), "test.cpp", cpp_content);
1373
1374        let mut tracker = DependencyTracker::new();
1375        tracker.scan_file(&cpp_file).unwrap();
1376
1377        assert!(tracker.get_usages("DATABASE_URL").is_some());
1378        assert!(tracker.get_usages("API_KEY").is_some());
1379        assert!(tracker.get_usages("BOOST_VAR").is_some());
1380        assert!(tracker.get_usages("COMMENTED_VAR").is_none());
1381    }
1382
1383    #[test]
1384    fn test_scan_shell_scripts() {
1385        let temp_dir = create_test_dir();
1386        let sh_content = r#"
1387#!/bin/bash
1388
1389# Variable references
1390echo $DATABASE_URL
1391echo ${API_KEY}
1392
1393# Export statements
1394export NEW_VAR="value"
1395export ANOTHER_VAR
1396
1397# Parameter expansion
1398: ${DEFAULT_VAR:=default_value}
1399
1400# Common shell variables should be ignored
1401echo $1 $2 $@ $* $# $? $$ $!
1402
1403# Comments should be ignored
1404# echo $COMMENTED_VAR
1405"#;
1406
1407        let sh_file = create_test_file(temp_dir.path(), "test.sh", sh_content);
1408
1409        let mut tracker = DependencyTracker::new();
1410        tracker.scan_file(&sh_file).unwrap();
1411
1412        assert!(tracker.get_usages("DATABASE_URL").is_some());
1413        assert!(tracker.get_usages("API_KEY").is_some());
1414        assert!(tracker.get_usages("NEW_VAR").is_some());
1415        assert!(tracker.get_usages("ANOTHER_VAR").is_some());
1416        assert!(tracker.get_usages("DEFAULT_VAR").is_some());
1417
1418        // Shell built-ins should be ignored
1419        assert!(tracker.get_usages("1").is_none());
1420        assert!(tracker.get_usages("@").is_none());
1421
1422        // Comments should be ignored
1423        assert!(tracker.get_usages("COMMENTED_VAR").is_none());
1424    }
1425
1426    #[test]
1427    fn test_scan_powershell_scripts() {
1428        let temp_dir = create_test_dir();
1429        let ps1_content = r#"
1430# PowerShell environment variables
1431$dbUrl = $env:DATABASE_URL
1432$apiKey = [Environment]::GetEnvironmentVariable("API_KEY")
1433[Environment]::SetEnvironmentVariable("NEW_VAR", "value")
1434
1435# Comment should be ignored
1436# $env:COMMENTED_VAR
1437"#;
1438
1439        let ps1_file = create_test_file(temp_dir.path(), "test.ps1", ps1_content);
1440
1441        let mut tracker = DependencyTracker::new();
1442        tracker.scan_file(&ps1_file).unwrap();
1443
1444        assert!(tracker.get_usages("DATABASE_URL").is_some());
1445        assert!(tracker.get_usages("API_KEY").is_some());
1446        assert!(tracker.get_usages("NEW_VAR").is_some());
1447        assert!(tracker.get_usages("COMMENTED_VAR").is_none());
1448    }
1449
1450    #[test]
1451    fn test_scan_batch_files() {
1452        let temp_dir = create_test_dir();
1453        let bat_content = r"
1454@echo off
1455REM Batch file environment variables
1456
1457echo %DATABASE_URL%
1458set API_KEY=secret
1459
1460REM This is a comment: %COMMENTED_VAR%
1461:: Another comment style: %ALSO_COMMENTED%
1462
1463REM Built-in variables should be ignored
1464echo %DATE% %TIME% %ERRORLEVEL%
1465";
1466
1467        let bat_file = create_test_file(temp_dir.path(), "test.bat", bat_content);
1468
1469        let mut tracker = DependencyTracker::new();
1470        tracker.scan_file(&bat_file).unwrap();
1471
1472        assert!(tracker.get_usages("DATABASE_URL").is_some());
1473        assert!(tracker.get_usages("API_KEY").is_some());
1474
1475        // Comments and built-ins should be ignored
1476        assert!(tracker.get_usages("COMMENTED_VAR").is_none());
1477        assert!(tracker.get_usages("ALSO_COMMENTED").is_none());
1478        assert!(tracker.get_usages("DATE").is_none());
1479        assert!(tracker.get_usages("ERRORLEVEL").is_none());
1480    }
1481
1482    #[test]
1483    fn test_scan_makefile() {
1484        let temp_dir = create_test_dir();
1485        let makefile_content = r"
1486# Makefile variables
1487DB_URL = $(DATABASE_URL)
1488API_KEY = ${API_KEY}
1489
1490# Environment variables in recipes
1491build:
1492    echo $$HOME
1493    echo $${USER}
1494
1495# Built-in variables should be ignored
1496    echo $(MAKE) $(SHELL) $(CURDIR)
1497
1498# Comments should be ignored
1499# $(COMMENTED_VAR)
1500";
1501
1502        let makefile = create_test_file(temp_dir.path(), "Makefile", makefile_content);
1503
1504        let mut tracker = DependencyTracker::new();
1505        tracker.scan_file(&makefile).unwrap();
1506
1507        assert!(tracker.get_usages("DATABASE_URL").is_some());
1508        assert!(tracker.get_usages("API_KEY").is_some());
1509        assert!(tracker.get_usages("HOME").is_some());
1510        assert!(tracker.get_usages("USER").is_some());
1511
1512        // Built-ins and comments should be ignored
1513        assert!(tracker.get_usages("MAKE").is_none());
1514        assert!(tracker.get_usages("SHELL").is_none());
1515        assert!(tracker.get_usages("COMMENTED_VAR").is_none());
1516    }
1517
1518    #[test]
1519    fn test_scan_directory() {
1520        let temp_dir = create_test_dir();
1521
1522        // Create multiple files
1523        create_test_file(temp_dir.path(), "app.js", "const url = process.env.API_URL;");
1524        create_test_file(
1525            temp_dir.path(),
1526            "config.py",
1527            "import os\ndb = os.getenv('DATABASE_URL')",
1528        );
1529        create_test_file(
1530            temp_dir.path(),
1531            "main.rs",
1532            "let key = env::var(\"SECRET_KEY\").unwrap();",
1533        );
1534
1535        // Create a subdirectory
1536        let sub_dir = temp_dir.path().join("scripts");
1537        fs::create_dir(&sub_dir).unwrap();
1538        create_test_file(&sub_dir, "deploy.sh", "echo $DEPLOY_KEY");
1539
1540        // Create an ignored directory
1541        let ignored_dir = temp_dir.path().join("node_modules");
1542        fs::create_dir(&ignored_dir).unwrap();
1543        create_test_file(&ignored_dir, "package.js", "process.env.IGNORED_VAR");
1544
1545        let mut tracker = DependencyTracker::new();
1546        tracker.scan_directory(temp_dir.path()).unwrap();
1547
1548        // Check that all non-ignored files were scanned
1549        assert!(tracker.get_usages("API_URL").is_some());
1550        assert!(tracker.get_usages("DATABASE_URL").is_some());
1551        assert!(tracker.get_usages("SECRET_KEY").is_some());
1552        assert!(tracker.get_usages("DEPLOY_KEY").is_some());
1553
1554        // Ignored directory should not be scanned
1555        assert!(tracker.get_usages("IGNORED_VAR").is_none());
1556    }
1557
1558    #[test]
1559    fn test_scan_with_multiple_paths() {
1560        let temp_dir1 = create_test_dir();
1561        let temp_dir2 = create_test_dir();
1562
1563        create_test_file(temp_dir1.path(), "app1.js", "process.env.VAR1");
1564        create_test_file(temp_dir2.path(), "app2.js", "process.env.VAR2");
1565
1566        let mut tracker = DependencyTracker::new();
1567        tracker.scan_paths.clear(); // Remove default path
1568        tracker.add_scan_path(temp_dir1.path().to_path_buf());
1569        tracker.add_scan_path(temp_dir2.path().to_path_buf());
1570
1571        tracker.scan().unwrap();
1572
1573        assert!(tracker.get_usages("VAR1").is_some());
1574        assert!(tracker.get_usages("VAR2").is_some());
1575    }
1576
1577    #[test]
1578    fn test_get_usage_counts() {
1579        let temp_dir = create_test_dir();
1580
1581        // Create files with multiple usages of the same variable
1582        let js_content = r"
1583const url1 = process.env.API_URL;
1584const url2 = process.env.API_URL;
1585const db = process.env.DATABASE_URL;
1586";
1587
1588        let py_content = r#"
1589import os
1590api = os.getenv("API_URL")
1591"#;
1592
1593        create_test_file(temp_dir.path(), "app.js", js_content);
1594        create_test_file(temp_dir.path(), "config.py", py_content);
1595
1596        let mut tracker = DependencyTracker::new();
1597        tracker.scan_directory(temp_dir.path()).unwrap();
1598
1599        let usage_counts = tracker.get_usage_counts();
1600
1601        // API_URL appears 3 times (2 in JS, 1 in Python)
1602        assert_eq!(usage_counts.get("API_URL"), Some(&3));
1603        // DATABASE_URL appears once
1604        assert_eq!(usage_counts.get("DATABASE_URL"), Some(&1));
1605    }
1606
1607    #[test]
1608    fn test_find_unused_variables() {
1609        let temp_dir = create_test_dir();
1610        create_test_file(temp_dir.path(), "app.js", "process.env.USED_VAR");
1611
1612        let mut tracker = DependencyTracker::new();
1613        tracker.scan_directory(temp_dir.path()).unwrap();
1614
1615        let all_vars = HashSet::from([
1616            "USED_VAR".to_string(),
1617            "UNUSED_VAR1".to_string(),
1618            "UNUSED_VAR2".to_string(),
1619        ]);
1620
1621        let unused = tracker.find_unused(&all_vars);
1622
1623        assert_eq!(unused.len(), 2);
1624        assert!(unused.contains("UNUSED_VAR1"));
1625        assert!(unused.contains("UNUSED_VAR2"));
1626        assert!(!unused.contains("USED_VAR"));
1627    }
1628
1629    #[test]
1630    fn test_record_usage_deduplication() {
1631        let mut tracker = DependencyTracker::new();
1632        let path = PathBuf::from("test.js");
1633
1634        // Record the same usage multiple times
1635        tracker.record_usage("TEST_VAR".to_string(), &path, 10, "context".to_string());
1636        tracker.record_usage("TEST_VAR".to_string(), &path, 10, "context".to_string());
1637        tracker.record_usage("TEST_VAR".to_string(), &path, 10, "context".to_string());
1638
1639        // Should only have one usage recorded
1640        let usages = tracker.get_usages("TEST_VAR").unwrap();
1641        assert_eq!(usages.len(), 1);
1642
1643        // Different line should create a new usage
1644        tracker.record_usage("TEST_VAR".to_string(), &path, 20, "different context".to_string());
1645        let usages = tracker.get_usages("TEST_VAR").unwrap();
1646        assert_eq!(usages.len(), 2);
1647    }
1648
1649    #[test]
1650    fn test_skip_large_files() {
1651        let temp_dir = create_test_dir();
1652
1653        // Create a large file (> 10MB)
1654        let large_content = "x".repeat(11_000_000);
1655        let large_file = create_test_file(temp_dir.path(), "large.js", &large_content);
1656
1657        let mut tracker = DependencyTracker::new();
1658        // Should not panic and should skip the file
1659        assert!(tracker.scan_file(&large_file).is_ok());
1660
1661        // No variables should be found
1662        assert!(tracker.get_used_variables().is_empty());
1663    }
1664
1665    #[test]
1666    fn test_skip_binary_files() {
1667        let temp_dir = create_test_dir();
1668
1669        // Create a binary file
1670        let binary_content = vec![0u8, 1, 2, 3, 255, 254, 253];
1671        let binary_file = temp_dir.path().join("binary.exe");
1672        fs::write(&binary_file, binary_content).unwrap();
1673
1674        let mut tracker = DependencyTracker::new();
1675        // Should not panic and should skip the file
1676        assert!(tracker.scan_file(&binary_file).is_ok());
1677
1678        // No variables should be found
1679        assert!(tracker.get_used_variables().is_empty());
1680    }
1681
1682    #[test]
1683    fn test_shebang_detection() {
1684        let temp_dir = create_test_dir();
1685
1686        // File without extension but with shebang
1687        let script_content = r"#!/bin/bash
1688echo $DATABASE_URL
1689";
1690
1691        let script_file = create_test_file(temp_dir.path(), "deploy_script", script_content);
1692
1693        let mut tracker = DependencyTracker::new();
1694        tracker.scan_file(&script_file).unwrap();
1695
1696        // Should detect as shell script and find the variable
1697        assert!(tracker.get_usages("DATABASE_URL").is_some());
1698    }
1699
1700    #[test]
1701    fn test_multiple_language_support() {
1702        let temp_dir = create_test_dir();
1703
1704        // Test TypeScript
1705        create_test_file(temp_dir.path(), "app.ts", "const api = process.env.API_URL;");
1706
1707        // Test JSX
1708        create_test_file(
1709            temp_dir.path(),
1710            "component.jsx",
1711            "const key = process.env.REACT_APP_KEY;",
1712        );
1713
1714        // Test different extensions
1715        create_test_file(temp_dir.path(), "server.mjs", "const db = process.env.DATABASE_URL;");
1716        create_test_file(temp_dir.path(), "old.cjs", "const port = process.env.PORT;");
1717
1718        let mut tracker = DependencyTracker::new();
1719        tracker.scan_directory(temp_dir.path()).unwrap();
1720
1721        assert!(tracker.get_usages("API_URL").is_some());
1722        assert!(tracker.get_usages("REACT_APP_KEY").is_some());
1723        assert!(tracker.get_usages("DATABASE_URL").is_some());
1724        assert!(tracker.get_usages("PORT").is_some());
1725    }
1726
1727    #[test]
1728    fn test_usage_context_preservation() {
1729        let temp_dir = create_test_dir();
1730        let content = r"
1731const dbUrl = process.env.DATABASE_URL;
1732    const apiKey = process.env.API_KEY; // Indented line
1733";
1734
1735        create_test_file(temp_dir.path(), "test.js", content);
1736
1737        let mut tracker = DependencyTracker::new();
1738        tracker.scan_directory(temp_dir.path()).unwrap();
1739
1740        let db_usage = tracker.get_usages("DATABASE_URL").unwrap();
1741        assert_eq!(db_usage[0].context, "const dbUrl = process.env.DATABASE_URL;");
1742        assert_eq!(db_usage[0].line, 2);
1743
1744        let api_usage = tracker.get_usages("API_KEY").unwrap();
1745        assert_eq!(
1746            api_usage[0].context,
1747            "const apiKey = process.env.API_KEY; // Indented line"
1748        );
1749        assert_eq!(api_usage[0].line, 3);
1750    }
1751}
1752
1753// ...existing code...
1754
1755#[cfg(test)]
1756mod cli_tests {
1757    use super::*;
1758    use tempfile::TempDir;
1759
1760    /// Helper to create test environment with files
1761    fn create_test_environment() -> TempDir {
1762        let temp_dir = TempDir::new().unwrap();
1763
1764        // Create test files with environment variable usage
1765        fs::write(
1766            temp_dir.path().join("app.js"),
1767            r"
1768const db = process.env.DATABASE_URL;
1769const api = process.env.API_KEY;
1770const port = process.env.PORT || 3000;
1771",
1772        )
1773        .unwrap();
1774
1775        fs::write(
1776            temp_dir.path().join("config.py"),
1777            r#"
1778import os
1779db_url = os.environ.get("DATABASE_URL")
1780debug = os.getenv("DEBUG", "false")
1781"#,
1782        )
1783        .unwrap();
1784
1785        fs::write(
1786            temp_dir.path().join("unused.rs"),
1787            r#"
1788// This file doesn't use UNUSED_VAR
1789let api = env::var("API_KEY").unwrap();
1790"#,
1791        )
1792        .unwrap();
1793
1794        // Create subdirectory with more files
1795        let scripts_dir = temp_dir.path().join("scripts");
1796        fs::create_dir(&scripts_dir).unwrap();
1797
1798        fs::write(
1799            scripts_dir.join("deploy.sh"),
1800            r"
1801#!/bin/bash
1802echo $DATABASE_URL
1803export DEPLOY_ENV=production
1804",
1805        )
1806        .unwrap();
1807
1808        temp_dir
1809    }
1810
1811    /// Helper to set up environment variables for testing
1812    fn setup_test_env_vars() {
1813        unsafe { std::env::set_var("DATABASE_URL", "postgres://localhost:5432/test") };
1814        unsafe { std::env::set_var("API_KEY", "test-api-key-123") };
1815        unsafe { std::env::set_var("PORT", "3000") };
1816        unsafe { std::env::set_var("DEBUG", "true") };
1817        unsafe { std::env::set_var("UNUSED_VAR", "this-is-not-used") };
1818        unsafe { std::env::set_var("DEPLOY_ENV", "staging") };
1819    }
1820
1821    /// Helper to clean up environment variables after testing
1822    fn cleanup_test_env_vars() {
1823        unsafe { std::env::remove_var("DATABASE_URL") };
1824        unsafe { std::env::remove_var("API_KEY") };
1825        unsafe { std::env::remove_var("PORT") };
1826        unsafe { std::env::remove_var("DEBUG") };
1827        unsafe { std::env::remove_var("UNUSED_VAR") };
1828        unsafe { std::env::remove_var("DEPLOY_ENV") };
1829    }
1830
1831    #[test]
1832    fn test_handle_deps_default_behavior() {
1833        let temp_dir = create_test_environment();
1834        setup_test_env_vars();
1835
1836        // Test default behavior (show all dependencies)
1837        let args = DepsArgs {
1838            command: None,
1839            variable: None,
1840            unused: false,
1841            paths: vec![temp_dir.path().to_path_buf()],
1842            ignore: vec![],
1843            format: "table".to_string(),
1844        };
1845
1846        let result = handle_deps(&args);
1847        assert!(result.is_ok());
1848
1849        cleanup_test_env_vars();
1850    }
1851
1852    #[test]
1853    fn test_handle_deps_with_specific_variable() {
1854        let temp_dir = create_test_environment();
1855        setup_test_env_vars();
1856
1857        let args = DepsArgs {
1858            command: None,
1859            variable: Some("DATABASE_URL".to_string()),
1860            unused: false,
1861            paths: vec![temp_dir.path().to_path_buf()],
1862            ignore: vec![],
1863            format: "table".to_string(),
1864        };
1865
1866        let result = handle_deps(&args);
1867        assert!(result.is_ok());
1868
1869        cleanup_test_env_vars();
1870    }
1871
1872    #[test]
1873    fn test_handle_deps_show_unused() {
1874        let temp_dir = create_test_environment();
1875        setup_test_env_vars();
1876
1877        let args = DepsArgs {
1878            command: None,
1879            variable: None,
1880            unused: true,
1881            paths: vec![temp_dir.path().to_path_buf()],
1882            ignore: vec![],
1883            format: "table".to_string(),
1884        };
1885
1886        let result = handle_deps(&args);
1887        assert!(result.is_ok());
1888
1889        cleanup_test_env_vars();
1890    }
1891
1892    #[test]
1893    fn test_handle_deps_show_command() {
1894        let temp_dir = create_test_environment();
1895        setup_test_env_vars();
1896
1897        let args = DepsArgs {
1898            command: Some(DepsCommands::Show {
1899                variable: Some("API_KEY".to_string()),
1900                unused: false,
1901            }),
1902            variable: None,
1903            unused: false,
1904            paths: vec![temp_dir.path().to_path_buf()],
1905            ignore: vec![],
1906            format: "simple".to_string(),
1907        };
1908
1909        let result = handle_deps(&args);
1910        assert!(result.is_ok());
1911
1912        cleanup_test_env_vars();
1913    }
1914
1915    #[test]
1916    fn test_handle_deps_scan_command() {
1917        let temp_dir = create_test_environment();
1918
1919        let args = DepsArgs {
1920            command: Some(DepsCommands::Scan {
1921                paths: vec![temp_dir.path().to_path_buf()],
1922                cache: false,
1923            }),
1924            variable: None,
1925            unused: false,
1926            paths: vec![],
1927            ignore: vec![],
1928            format: "table".to_string(),
1929        };
1930
1931        let result = handle_deps(&args);
1932        assert!(result.is_ok());
1933    }
1934
1935    #[test]
1936    fn test_handle_deps_stats_command() {
1937        let temp_dir = create_test_environment();
1938
1939        let args = DepsArgs {
1940            command: Some(DepsCommands::Stats { by_usage: true }),
1941            variable: None,
1942            unused: false,
1943            paths: vec![temp_dir.path().to_path_buf()],
1944            ignore: vec![],
1945            format: "table".to_string(),
1946        };
1947
1948        let result = handle_deps(&args);
1949        assert!(result.is_ok());
1950    }
1951
1952    #[test]
1953    fn test_handle_deps_show_specific_variable_found() {
1954        let temp_dir = create_test_environment();
1955        setup_test_env_vars();
1956
1957        let args = DepsArgs {
1958            command: None,
1959            variable: None,
1960            unused: false,
1961            paths: vec![temp_dir.path().to_path_buf()],
1962            ignore: vec![],
1963            format: "table".to_string(),
1964        };
1965
1966        let result = handle_deps_show(Some("DATABASE_URL"), false, &args);
1967        assert!(result.is_ok());
1968
1969        cleanup_test_env_vars();
1970    }
1971
1972    #[test]
1973    fn test_handle_deps_show_specific_variable_not_found() {
1974        let temp_dir = create_test_environment();
1975        setup_test_env_vars();
1976
1977        let args = DepsArgs {
1978            command: None,
1979            variable: None,
1980            unused: false,
1981            paths: vec![temp_dir.path().to_path_buf()],
1982            ignore: vec![],
1983            format: "table".to_string(),
1984        };
1985
1986        let result = handle_deps_show(Some("NONEXISTENT_VAR"), false, &args);
1987        assert!(result.is_ok());
1988
1989        cleanup_test_env_vars();
1990    }
1991
1992    #[test]
1993    fn test_handle_deps_show_unused_variables() {
1994        let temp_dir = create_test_environment();
1995        setup_test_env_vars();
1996
1997        let args = DepsArgs {
1998            command: None,
1999            variable: None,
2000            unused: false,
2001            paths: vec![temp_dir.path().to_path_buf()],
2002            ignore: vec![],
2003            format: "table".to_string(),
2004        };
2005
2006        let result = handle_deps_show(None, true, &args);
2007        assert!(result.is_ok());
2008
2009        cleanup_test_env_vars();
2010    }
2011
2012    #[test]
2013    fn test_handle_deps_show_all_dependencies() {
2014        let temp_dir = create_test_environment();
2015        setup_test_env_vars();
2016
2017        let args = DepsArgs {
2018            command: None,
2019            variable: None,
2020            unused: false,
2021            paths: vec![temp_dir.path().to_path_buf()],
2022            ignore: vec![],
2023            format: "table".to_string(),
2024        };
2025
2026        let result = handle_deps_show(None, false, &args);
2027        assert!(result.is_ok());
2028
2029        cleanup_test_env_vars();
2030    }
2031
2032    #[test]
2033    fn test_handle_deps_show_json_format() {
2034        let temp_dir = create_test_environment();
2035        setup_test_env_vars();
2036
2037        let args = DepsArgs {
2038            command: None,
2039            variable: None,
2040            unused: false,
2041            paths: vec![temp_dir.path().to_path_buf()],
2042            ignore: vec![],
2043            format: "json".to_string(),
2044        };
2045
2046        // Test unused variables in JSON format
2047        let result = handle_deps_show(None, true, &args);
2048        assert!(result.is_ok());
2049
2050        // Test specific variable in JSON format
2051        let result = handle_deps_show(Some("DATABASE_URL"), false, &args);
2052        assert!(result.is_ok());
2053
2054        // Test all dependencies in JSON format
2055        let result = handle_deps_show(None, false, &args);
2056        assert!(result.is_ok());
2057
2058        cleanup_test_env_vars();
2059    }
2060
2061    #[test]
2062    fn test_handle_deps_show_simple_format() {
2063        let temp_dir = create_test_environment();
2064        setup_test_env_vars();
2065
2066        let args = DepsArgs {
2067            command: None,
2068            variable: None,
2069            unused: false,
2070            paths: vec![temp_dir.path().to_path_buf()],
2071            ignore: vec![],
2072            format: "simple".to_string(),
2073        };
2074
2075        // Test unused variables in simple format
2076        let result = handle_deps_show(None, true, &args);
2077        assert!(result.is_ok());
2078
2079        // Test specific variable in simple format
2080        let result = handle_deps_show(Some("DATABASE_URL"), false, &args);
2081        assert!(result.is_ok());
2082
2083        // Test all dependencies in simple format
2084        let result = handle_deps_show(None, false, &args);
2085        assert!(result.is_ok());
2086
2087        cleanup_test_env_vars();
2088    }
2089
2090    #[test]
2091    fn test_handle_deps_show_with_ignore_patterns() {
2092        let temp_dir = create_test_environment();
2093        setup_test_env_vars();
2094
2095        let args = DepsArgs {
2096            command: None,
2097            variable: None,
2098            unused: false,
2099            paths: vec![temp_dir.path().to_path_buf()],
2100            ignore: vec!["scripts".to_string()],
2101            format: "table".to_string(),
2102        };
2103
2104        let result = handle_deps_show(None, false, &args);
2105        assert!(result.is_ok());
2106
2107        cleanup_test_env_vars();
2108    }
2109
2110    #[test]
2111    fn test_handle_deps_show_no_env_vars_set() {
2112        let temp_dir = create_test_environment();
2113        // Don't set up environment variables
2114
2115        let args = DepsArgs {
2116            command: None,
2117            variable: None,
2118            unused: false,
2119            paths: vec![temp_dir.path().to_path_buf()],
2120            ignore: vec![],
2121            format: "table".to_string(),
2122        };
2123
2124        let result = handle_deps_show(None, true, &args);
2125        assert!(result.is_ok());
2126    }
2127
2128    #[test]
2129    fn test_handle_deps_scan_single_path() {
2130        let temp_dir = create_test_environment();
2131
2132        let args = DepsArgs {
2133            command: None,
2134            variable: None,
2135            unused: false,
2136            paths: vec![],
2137            ignore: vec![],
2138            format: "table".to_string(),
2139        };
2140
2141        let result = handle_deps_scan(&[temp_dir.path().to_path_buf()], false, &args);
2142        assert!(result.is_ok());
2143    }
2144
2145    #[test]
2146    fn test_handle_deps_scan_multiple_paths() {
2147        let temp_dir1 = create_test_environment();
2148        let temp_dir2 = create_test_environment();
2149
2150        let args = DepsArgs {
2151            command: None,
2152            variable: None,
2153            unused: false,
2154            paths: vec![],
2155            ignore: vec![],
2156            format: "table".to_string(),
2157        };
2158
2159        let result = handle_deps_scan(
2160            &[temp_dir1.path().to_path_buf(), temp_dir2.path().to_path_buf()],
2161            false,
2162            &args,
2163        );
2164        assert!(result.is_ok());
2165    }
2166
2167    #[test]
2168    fn test_handle_deps_scan_with_cache() {
2169        let temp_dir = create_test_environment();
2170
2171        let args = DepsArgs {
2172            command: None,
2173            variable: None,
2174            unused: false,
2175            paths: vec![],
2176            ignore: vec![],
2177            format: "table".to_string(),
2178        };
2179
2180        let result = handle_deps_scan(&[temp_dir.path().to_path_buf()], true, &args);
2181        assert!(result.is_ok());
2182    }
2183
2184    #[test]
2185    fn test_handle_deps_scan_with_ignore_patterns() {
2186        let temp_dir = create_test_environment();
2187
2188        let args = DepsArgs {
2189            command: None,
2190            variable: None,
2191            unused: false,
2192            paths: vec![],
2193            ignore: vec!["scripts".to_string(), "*.py".to_string()],
2194            format: "table".to_string(),
2195        };
2196
2197        let result = handle_deps_scan(&[temp_dir.path().to_path_buf()], false, &args);
2198        assert!(result.is_ok());
2199    }
2200
2201    #[test]
2202    fn test_handle_deps_stats_default_sorting() {
2203        let temp_dir = create_test_environment();
2204
2205        let args = DepsArgs {
2206            command: None,
2207            variable: None,
2208            unused: false,
2209            paths: vec![temp_dir.path().to_path_buf()],
2210            ignore: vec![],
2211            format: "table".to_string(),
2212        };
2213
2214        let result = handle_deps_stats(false, &args);
2215        assert!(result.is_ok());
2216    }
2217
2218    #[test]
2219    fn test_handle_deps_stats_sort_by_usage() {
2220        let temp_dir = create_test_environment();
2221
2222        let args = DepsArgs {
2223            command: None,
2224            variable: None,
2225            unused: false,
2226            paths: vec![temp_dir.path().to_path_buf()],
2227            ignore: vec![],
2228            format: "table".to_string(),
2229        };
2230
2231        let result = handle_deps_stats(true, &args);
2232        assert!(result.is_ok());
2233    }
2234
2235    #[test]
2236    fn test_handle_deps_stats_empty_directory() {
2237        let temp_dir = TempDir::new().unwrap();
2238
2239        let args = DepsArgs {
2240            command: None,
2241            variable: None,
2242            unused: false,
2243            paths: vec![temp_dir.path().to_path_buf()],
2244            ignore: vec![],
2245            format: "table".to_string(),
2246        };
2247
2248        let result = handle_deps_stats(false, &args);
2249        assert!(result.is_ok());
2250    }
2251
2252    #[test]
2253    fn test_handle_deps_stats_no_paths_specified() {
2254        // Should use current directory by default
2255        let args = DepsArgs {
2256            command: None,
2257            variable: None,
2258            unused: false,
2259            paths: vec![],
2260            ignore: vec![],
2261            format: "table".to_string(),
2262        };
2263
2264        let result = handle_deps_stats(false, &args);
2265        assert!(result.is_ok());
2266    }
2267
2268    #[test]
2269    fn test_handle_deps_with_nonexistent_path() {
2270        let args = DepsArgs {
2271            command: None,
2272            variable: None,
2273            unused: false,
2274            paths: vec![PathBuf::from("/nonexistent/path")],
2275            ignore: vec![],
2276            format: "table".to_string(),
2277        };
2278
2279        let result = handle_deps_show(None, false, &args);
2280        // Should handle gracefully
2281        assert!(result.is_err() || result.is_ok());
2282    }
2283
2284    #[test]
2285    fn test_handle_deps_show_variable_with_long_value() {
2286        setup_test_env_vars();
2287        unsafe { std::env::set_var("LONG_VAR", "a".repeat(100)) };
2288
2289        let temp_dir = create_test_environment();
2290        let args = DepsArgs {
2291            command: None,
2292            variable: None,
2293            unused: false,
2294            paths: vec![temp_dir.path().to_path_buf()],
2295            ignore: vec![],
2296            format: "table".to_string(),
2297        };
2298
2299        let result = handle_deps_show(None, true, &args);
2300        assert!(result.is_ok());
2301
2302        unsafe { std::env::remove_var("LONG_VAR") };
2303        cleanup_test_env_vars();
2304    }
2305
2306    #[test]
2307    fn test_handle_deps_stats_with_many_variables() {
2308        let temp_dir = TempDir::new().unwrap();
2309
2310        // Create a file with many different environment variables
2311        let content = (0..30)
2312            .map(|i| format!("const var{i} = process.env.VAR_{i};"))
2313            .collect::<Vec<_>>()
2314            .join("\n");
2315
2316        fs::write(temp_dir.path().join("many_vars.js"), content).unwrap();
2317
2318        let args = DepsArgs {
2319            command: None,
2320            variable: None,
2321            unused: false,
2322            paths: vec![temp_dir.path().to_path_buf()],
2323            ignore: vec![],
2324            format: "table".to_string(),
2325        };
2326
2327        let result = handle_deps_stats(true, &args);
2328        assert!(result.is_ok());
2329    }
2330
2331    #[test]
2332    fn test_integration_full_workflow() {
2333        let temp_dir = create_test_environment();
2334        setup_test_env_vars();
2335
2336        // First scan
2337        let scan_args = DepsArgs {
2338            command: Some(DepsCommands::Scan {
2339                paths: vec![temp_dir.path().to_path_buf()],
2340                cache: false,
2341            }),
2342            variable: None,
2343            unused: false,
2344            paths: vec![],
2345            ignore: vec![],
2346            format: "table".to_string(),
2347        };
2348        assert!(handle_deps(&scan_args).is_ok());
2349
2350        // Then show stats
2351        let stats_args = DepsArgs {
2352            command: Some(DepsCommands::Stats { by_usage: true }),
2353            variable: None,
2354            unused: false,
2355            paths: vec![temp_dir.path().to_path_buf()],
2356            ignore: vec![],
2357            format: "table".to_string(),
2358        };
2359        assert!(handle_deps(&stats_args).is_ok());
2360
2361        // Show specific variable
2362        let show_args = DepsArgs {
2363            command: Some(DepsCommands::Show {
2364                variable: Some("DATABASE_URL".to_string()),
2365                unused: false,
2366            }),
2367            variable: None,
2368            unused: false,
2369            paths: vec![temp_dir.path().to_path_buf()],
2370            ignore: vec![],
2371            format: "json".to_string(),
2372        };
2373        assert!(handle_deps(&show_args).is_ok());
2374
2375        // Show unused
2376        let unused_args = DepsArgs {
2377            command: Some(DepsCommands::Show {
2378                variable: None,
2379                unused: true,
2380            }),
2381            variable: None,
2382            unused: false,
2383            paths: vec![temp_dir.path().to_path_buf()],
2384            ignore: vec![],
2385            format: "simple".to_string(),
2386        };
2387        assert!(handle_deps(&unused_args).is_ok());
2388
2389        cleanup_test_env_vars();
2390    }
2391}