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 #[arg(value_name = "VAR")]
20 pub variable: Option<String>,
21
22 #[arg(long)]
24 pub unused: bool,
25
26 #[arg(short, long)]
28 pub paths: Vec<PathBuf>,
29
30 #[arg(short = 'i', long)]
32 pub ignore: Vec<String>,
33
34 #[arg(short, long, default_value = "table")]
36 pub format: String,
37}
38
39#[derive(Subcommand)]
40pub enum DepsCommands {
41 Show {
43 variable: Option<String>,
45
46 #[arg(long)]
48 unused: bool,
49 },
50
51 Scan {
53 #[arg(default_value = ".")]
55 paths: Vec<PathBuf>,
56
57 #[arg(long)]
59 cache: bool,
60 },
61
62 Stats {
64 #[arg(long)]
66 by_usage: bool,
67 },
68}
69
70pub 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 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 let mut tracker = DependencyTracker::new();
109
110 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 for pattern in &args.ignore {
121 tracker.add_ignore_pattern(pattern.clone());
122 }
123
124 println!("š Scanning for environment variable usage...");
126 tracker.scan()?;
127
128 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 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 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 if !all_vars.contains(var_name) {
235 println!(" Note: This variable is not currently set in your environment.");
236 }
237 }
238 } else {
239 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 for path in paths {
298 tracker.add_scan_path(path.clone());
299 }
300
301 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 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 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 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 #[arg(short, long)]
388 pub force: bool,
389
390 #[arg(short = 'n', long)]
392 pub dry_run: bool,
393
394 #[arg(short = 'k', long)]
396 pub keep: Vec<String>,
397
398 #[arg(short = 'p', long)]
400 pub paths: Vec<PathBuf>,
401}
402
403pub fn handle_cleanup(args: &CleanupArgs) -> Result<()> {
413 let mut tracker = DependencyTracker::new();
415
416 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 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 let mut unused = tracker.find_unused(&all_vars);
435
436 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 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#[derive(Debug, Clone)]
516pub struct VariableUsage {
517 pub file: PathBuf,
518 pub line: usize,
519 pub context: String,
520}
521
522pub 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 pub fn add_scan_path(&mut self, path: PathBuf) {
551 self.scan_paths.push(path);
552 }
553
554 pub fn add_ignore_pattern(&mut self, pattern: String) {
556 self.ignore_patterns.push(pattern);
557 }
558
559 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 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 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 fn scan_file(&mut self, path: &Path) -> Result<()> {
605 let metadata = fs::metadata(path)?;
607 if metadata.len() > 10_000_000 {
608 return Ok(());
610 }
611
612 let Ok(content) = fs::read_to_string(path) else {
613 return Ok(()); };
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 "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 "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 _ => {
639 if filename == "Makefile" || filename.starts_with("Makefile.") {
640 self.scan_makefile(&content, path)?;
641 } else if content.starts_with("#!/") {
642 self.scan_shell(&content, path)?;
644 }
645 }
646 }
647
648 Ok(())
649 }
650
651 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 let usages = self.usages.entry(var_name).or_default();
661
662 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 fn scan_javascript(&mut self, content: &str, path: &Path) -> Result<()> {
674 let patterns = [
675 Regex::new(r"process\.env\.(\w+)")?,
677 Regex::new(r#"process\.env\[["'](\w+)["']\]"#)?,
678 Regex::new(r#"Deno\.env\.get\(["'](\w+)["']\)"#)?,
680 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 fn scan_python(&mut self, content: &str, path: &Path) -> Result<()> {
699 let patterns = [
700 Regex::new(r#"os\.environ\[["'](\w+)["']\]"#)?,
702 Regex::new(r#"os\.environ\.get\(["'](\w+)["']"#)?,
704 Regex::new(r#"os\.getenv\(["'](\w+)["']"#)?,
706 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 fn scan_rust(&mut self, content: &str, path: &Path) -> Result<()> {
725 let patterns = [
726 Regex::new(r#"env!\s*\(\s*"(\w+)"\s*\)"#)?,
728 Regex::new(r#"std::env::var\s*\(\s*"(\w+)"\s*\)"#)?,
730 Regex::new(r#"env::var\s*\(\s*"(\w+)"\s*\)"#)?,
732 Regex::new(r#"std::env::var_os\s*\(\s*"(\w+)"\s*\)"#)?,
734 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 fn scan_go(&mut self, content: &str, path: &Path) -> Result<()> {
753 let patterns = [
754 Regex::new(r#"os\.Getenv\s*\(\s*"(\w+)"\s*\)"#)?,
756 Regex::new(r#"os\.LookupEnv\s*\(\s*"(\w+)"\s*\)"#)?,
758 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 fn scan_java(&mut self, content: &str, path: &Path) -> Result<()> {
777 let patterns = [
778 Regex::new(r#"System\.getenv\s*\(\s*"(\w+)"\s*\)"#)?,
780 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 fn scan_csharp(&mut self, content: &str, path: &Path) -> Result<()> {
799 let patterns = [
800 Regex::new(r#"Environment\.GetEnvironmentVariable\s*\(\s*"(\w+)"\s*\)"#)?,
802 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 fn scan_ruby(&mut self, content: &str, path: &Path) -> Result<()> {
821 let patterns = [
822 Regex::new(r#"ENV\[["'](\w+)["']\]"#)?,
824 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 fn scan_php(&mut self, content: &str, path: &Path) -> Result<()> {
843 let patterns = [
844 Regex::new(r#"\$_ENV\[["'](\w+)["']\]"#)?,
846 Regex::new(r#"getenv\s*\(\s*["'](\w+)["']"#)?,
848 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 fn scan_c(&mut self, content: &str, path: &Path) -> Result<()> {
867 let patterns = [
868 Regex::new(r#"getenv\s*\(\s*"(\w+)"\s*\)"#)?,
870 Regex::new(r#"setenv\s*\(\s*"(\w+)"\s*,"#)?,
872 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 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 fn scan_cpp(&mut self, content: &str, path: &Path) -> Result<()> {
898 let patterns = [
899 Regex::new(r#"getenv\s*\(\s*"(\w+)"\s*\)"#)?,
901 Regex::new(r#"std::getenv\s*\(\s*"(\w+)"\s*\)"#)?,
903 Regex::new(r#"setenv\s*\(\s*"(\w+)"\s*,"#)?,
905 Regex::new(r#"GetEnvironmentVariable[AW]?\s*\(\s*"(\w+)"\s*,"#)?,
907 Regex::new(r#"SetEnvironmentVariable[AW]?\s*\(\s*"(\w+)"\s*,"#)?,
908 Regex::new(r#"boost::this_process::environment\s*\[\s*"(\w+)"\s*\]"#)?,
910 ];
911
912 for (line_num, line) in content.lines().enumerate() {
913 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 fn scan_shell(&mut self, content: &str, path: &Path) -> Result<()> {
933 let patterns = [
934 Regex::new(r"\$(\w+)")?,
936 Regex::new(r"\$\{(\w+)\}")?,
937 Regex::new(r"^\s*export\s+(\w+)")?,
939 Regex::new(r"\$\{(\w+)[:?+=\-]")?,
941 ];
942
943 for (line_num, line) in content.lines().enumerate() {
944 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 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 fn scan_powershell(&mut self, content: &str, path: &Path) -> Result<()> {
1007 let patterns = [
1008 Regex::new(r"\$env:(\w+)")?,
1010 Regex::new(r#"\[Environment\]::GetEnvironmentVariable\s*\(\s*["'](\w+)["']"#)?,
1012 Regex::new(r#"\[Environment\]::SetEnvironmentVariable\s*\(\s*["'](\w+)["']"#)?,
1014 ];
1015
1016 for (line_num, line) in content.lines().enumerate() {
1017 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 fn scan_batch(&mut self, content: &str, path: &Path) -> Result<()> {
1036 let patterns = [
1037 Regex::new(r"%(\w+)%")?,
1039 Regex::new(r"(?i)^\s*set\s+(\w+)=")?,
1041 ];
1042
1043 for (line_num, line) in content.lines().enumerate() {
1044 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 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 fn scan_makefile(&mut self, content: &str, path: &Path) -> Result<()> {
1080 let patterns = [
1081 Regex::new(r"\$\((\w+)\)")?,
1083 Regex::new(r"\$\{(\w+)\}")?,
1084 Regex::new(r"\$\$(\w+)")?,
1086 Regex::new(r"\$\$\{(\w+)\}")?,
1087 ];
1088
1089 for (line_num, line) in content.lines().enumerate() {
1090 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 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 pub fn get_usages(&self, var_name: &str) -> Option<&Vec<VariableUsage>> {
1129 self.usages.get(var_name)
1130 }
1131
1132 pub fn get_used_variables(&self) -> HashSet<String> {
1134 self.usages.keys().cloned().collect()
1135 }
1136
1137 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 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 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 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 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 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 assert!(tracker.get_usages("1").is_none());
1420 assert!(tracker.get_usages("@").is_none());
1421
1422 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 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 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_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 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 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 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 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(); 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 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 assert_eq!(usage_counts.get("API_URL"), Some(&3));
1603 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 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 let usages = tracker.get_usages("TEST_VAR").unwrap();
1641 assert_eq!(usages.len(), 1);
1642
1643 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 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 assert!(tracker.scan_file(&large_file).is_ok());
1660
1661 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 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 assert!(tracker.scan_file(&binary_file).is_ok());
1677
1678 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 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 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 create_test_file(temp_dir.path(), "app.ts", "const api = process.env.API_URL;");
1706
1707 create_test_file(
1709 temp_dir.path(),
1710 "component.jsx",
1711 "const key = process.env.REACT_APP_KEY;",
1712 );
1713
1714 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#[cfg(test)]
1756mod cli_tests {
1757 use super::*;
1758 use tempfile::TempDir;
1759
1760 fn create_test_environment() -> TempDir {
1762 let temp_dir = TempDir::new().unwrap();
1763
1764 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 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 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 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 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 let result = handle_deps_show(None, true, &args);
2048 assert!(result.is_ok());
2049
2050 let result = handle_deps_show(Some("DATABASE_URL"), false, &args);
2052 assert!(result.is_ok());
2053
2054 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 let result = handle_deps_show(None, true, &args);
2077 assert!(result.is_ok());
2078
2079 let result = handle_deps_show(Some("DATABASE_URL"), false, &args);
2081 assert!(result.is_ok());
2082
2083 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 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 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 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 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 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 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 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 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}