1use regex::Regex;
45use serde::{Deserialize, Serialize};
46use std::collections::{BTreeMap, BTreeSet};
47use std::path::Path;
48use std::sync::LazyLock;
49
50static USE_STATEMENT_RE: LazyLock<Result<Regex, String>> = LazyLock::new(|| {
51 Regex::new(r"^\s*use\s+([A-Za-z0-9_:]+)(?:\s+qw\(([^)]*)\))?\s*;")
52 .map_err(|err| err.to_string())
53});
54
55static DUMPER_SYMBOL_RE: LazyLock<Result<Regex, String>> =
56 LazyLock::new(|| Regex::new(r"\bDumper\b").map_err(|err| err.to_string()));
57
58static STRING_LITERAL_RE: LazyLock<Result<Regex, String>> =
59 LazyLock::new(|| Regex::new("'[^']*'|\"[^\"]*\"").map_err(|err| err.to_string()));
60
61static REGEX_LITERAL_RE: LazyLock<Result<Regex, String>> =
62 LazyLock::new(|| Regex::new(r"qr/[^/]*/").map_err(|err| err.to_string()));
63
64static COMMENT_RE: LazyLock<Result<Regex, String>> =
65 LazyLock::new(|| Regex::new(r"(?m)#.*$").map_err(|err| err.to_string()));
66
67static MODULE_USAGE_RE: LazyLock<Result<Regex, String>> = LazyLock::new(|| {
68 Regex::new(r"\b([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)::([A-Za-z_][A-Za-z0-9_]*)")
69 .map_err(|err| err.to_string())
70});
71
72#[derive(Debug, Clone)]
77pub struct TextEdit {
78 pub range: (usize, usize),
80 pub new_text: String,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
86pub struct ImportAnalysis {
87 pub unused_imports: Vec<UnusedImport>,
89 pub missing_imports: Vec<MissingImport>,
91 pub duplicate_imports: Vec<DuplicateImport>,
93 pub organization_suggestions: Vec<OrganizationSuggestion>,
95 pub imports: Vec<ImportEntry>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct UnusedImport {
102 pub module: String,
104 pub symbols: Vec<String>,
106 pub line: usize,
108 pub reason: String,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct MissingImport {
115 pub module: String,
117 pub symbols: Vec<String>,
119 pub suggested_location: usize,
121 pub confidence: f32,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct DuplicateImport {
128 pub module: String,
130 pub lines: Vec<usize>,
132 pub can_merge: bool,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct OrganizationSuggestion {
139 pub description: String,
141 pub priority: SuggestionPriority,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ImportEntry {
148 pub module: String,
150 pub symbols: Vec<String>,
152 pub line: usize,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158pub enum SuggestionPriority {
159 High,
161 Medium,
163 Low,
165}
166
167pub struct ImportOptimizer;
175
176fn is_pragma_module(module: &str) -> bool {
178 matches!(
179 module,
180 "strict"
181 | "warnings"
182 | "utf8"
183 | "bytes"
184 | "locale"
185 | "integer"
186 | "less"
187 | "sigtrap"
188 | "subs"
189 | "vars"
190 | "feature"
191 | "autodie"
192 | "autouse"
193 | "base"
194 | "parent"
195 | "lib"
196 | "bigint"
197 | "bignum"
198 | "bigrat"
199 )
200}
201
202fn get_known_module_exports(module: &str) -> Option<Vec<&'static str>> {
204 match module {
205 "Data::Dumper" => Some(vec!["Dumper"]),
206 "JSON" => Some(vec!["encode_json", "decode_json", "to_json", "from_json"]),
207 "YAML" => Some(vec!["Load", "Dump", "LoadFile", "DumpFile"]),
208 "Storable" => Some(vec!["store", "retrieve", "freeze", "thaw"]),
209 "List::Util" => Some(vec!["first", "max", "min", "sum", "reduce", "shuffle", "uniq"]),
210 "Scalar::Util" => Some(vec!["blessed", "reftype", "looks_like_number", "weaken"]),
211 "File::Spec" => Some(vec!["catfile", "catdir", "splitpath", "splitdir"]),
212 "File::Basename" => Some(vec!["basename", "dirname", "fileparse"]),
213 "Cwd" => Some(vec!["getcwd", "abs_path", "realpath"]),
214 "Time::HiRes" => Some(vec!["time", "sleep", "usleep", "gettimeofday"]),
215 "Digest::MD5" => Some(vec!["md5", "md5_hex", "md5_base64"]),
216 "MIME::Base64" => Some(vec!["encode_base64", "decode_base64"]),
217 "URI::Escape" => Some(vec!["uri_escape", "uri_unescape"]),
218 "LWP::Simple" => Some(vec!["get", "head", "getprint", "getstore", "mirror"]),
219 "LWP::UserAgent" => Some(vec![]),
220 "CGI" => Some(vec!["param", "header", "start_html", "end_html"]),
221 "DBI" => Some(vec![]), "strict" => Some(vec![]), "warnings" => Some(vec![]), "utf8" => Some(vec![]), _ => None,
226 }
227}
228
229impl ImportOptimizer {
230 pub fn new() -> Self {
245 Self
246 }
247
248 pub fn analyze_file(&self, file_path: &Path) -> Result<ImportAnalysis, String> {
265 let content = std::fs::read_to_string(file_path).map_err(|e| e.to_string())?;
266 self.analyze_content(&content)
267 }
268
269 pub fn analyze_content(&self, content: &str) -> Result<ImportAnalysis, String> {
287 let use_statement_re = USE_STATEMENT_RE.as_ref().map_err(|err| err.clone())?;
288 let dumper_symbol_re = DUMPER_SYMBOL_RE.as_ref().map_err(|err| err.clone())?;
289 let string_literal_re = STRING_LITERAL_RE.as_ref().map_err(|err| err.clone())?;
290 let regex_literal_re = REGEX_LITERAL_RE.as_ref().map_err(|err| err.clone())?;
291 let comment_re = COMMENT_RE.as_ref().map_err(|err| err.clone())?;
292 let module_usage_re = MODULE_USAGE_RE.as_ref().map_err(|err| err.clone())?;
293
294 let mut imports = Vec::new();
295 for (idx, line) in content.lines().enumerate() {
296 if let Some(caps) = use_statement_re.captures(line) {
297 let module = caps[1].to_string();
298 let symbols_str = caps.get(2).map(|m| m.as_str()).unwrap_or("");
299 let symbols = if symbols_str.is_empty() {
300 Vec::new()
301 } else {
302 symbols_str
303 .split_whitespace()
304 .filter(|s| !s.is_empty())
305 .map(|s| s.trim_matches(|c| c == ',' || c == ';' || c == '"'))
306 .map(|s| s.to_string())
307 .collect::<Vec<_>>()
308 };
309 imports.push(ImportEntry { module, symbols, line: idx + 1 });
310 }
311 }
312
313 let mut module_to_lines: BTreeMap<String, Vec<usize>> = BTreeMap::new();
315 for imp in &imports {
316 module_to_lines.entry(imp.module.clone()).or_default().push(imp.line);
317 }
318 let duplicate_imports = module_to_lines
319 .iter()
320 .filter(|(_, lines)| lines.len() > 1)
321 .map(|(module, lines)| DuplicateImport {
322 module: module.clone(),
323 lines: lines.clone(),
324 can_merge: true,
325 })
326 .collect::<Vec<_>>();
327
328 let non_use_content = content
330 .lines()
331 .filter(
332 |line| {
333 !line.trim_start().starts_with("use ") && !line.trim_start().starts_with("#")
334 }, )
336 .collect::<Vec<_>>()
337 .join(
338 "
339",
340 );
341
342 let mut unused_imports = Vec::new();
344 for imp in &imports {
345 let mut unused_symbols = Vec::new();
346
347 if !imp.symbols.is_empty() {
349 for sym in &imp.symbols {
350 let re = Regex::new(&format!(r"\b{}\b", regex::escape(sym)))
351 .map_err(|e| e.to_string())?;
352
353 if !re.is_match(&non_use_content) {
355 unused_symbols.push(sym.clone());
356 }
357 }
358 } else {
359 let is_pragma = matches!(
361 imp.module.as_str(),
362 "strict"
363 | "warnings"
364 | "utf8"
365 | "bytes"
366 | "integer"
367 | "locale"
368 | "overload"
369 | "sigtrap"
370 | "subs"
371 | "vars"
372 );
373
374 if !is_pragma {
375 let (is_known_module, known_exports) =
377 match get_known_module_exports(&imp.module) {
378 Some(exports) => (true, exports),
379 None => (false, Vec::new()),
380 };
381 let mut is_used = false;
382
383 let module_pattern = format!(r"\b{}\b", regex::escape(&imp.module));
385 let module_re = Regex::new(&module_pattern).map_err(|e| e.to_string())?;
386 if module_re.is_match(&non_use_content) {
387 is_used = true;
388 }
389
390 if !is_used {
392 let qualified_pattern = format!(r"{}::", regex::escape(&imp.module));
393 let qualified_re =
394 Regex::new(&qualified_pattern).map_err(|e| e.to_string())?;
395 if qualified_re.is_match(&non_use_content) {
396 is_used = true;
397 }
398 }
399
400 if !is_used && imp.module == "Data::Dumper" {
402 if dumper_symbol_re.is_match(&non_use_content) {
403 is_used = true;
404 }
405 }
406
407 if !is_used && !known_exports.is_empty() {
409 for export in &known_exports {
410 let export_pattern = format!(r"\b{}\b", regex::escape(export));
411 let export_re =
412 Regex::new(&export_pattern).map_err(|e| e.to_string())?;
413 if export_re.is_match(&non_use_content) {
414 is_used = true;
415 break;
416 }
417 }
418 }
419
420 if !is_used && is_known_module && known_exports.is_empty() {
424 unused_symbols.push("(bare import)".to_string());
425 }
426 }
427 }
428
429 if !unused_symbols.is_empty() {
431 unused_imports.push(UnusedImport {
432 module: imp.module.clone(),
433 symbols: unused_symbols,
434 line: imp.line,
435 reason: "Symbols not used in code".to_string(),
436 });
437 }
438 }
439
440 let imported_modules: BTreeSet<String> =
442 imports.iter().map(|imp| imp.module.clone()).collect();
443
444 let stripped = string_literal_re.replace_all(content, " ").to_string();
446 let stripped = regex_literal_re.replace_all(&stripped, " ").to_string();
447 let stripped = comment_re.replace_all(&stripped, " ").to_string();
448 let mut usage_map: BTreeMap<String, Vec<String>> = BTreeMap::new();
449 for caps in module_usage_re.captures_iter(&stripped) {
450 if let (Some(module_match), Some(symbol_match)) = (caps.get(1), caps.get(2)) {
452 let module = module_match.as_str().to_string();
453 let symbol = symbol_match.as_str().to_string();
454
455 if imported_modules.contains(&module) || is_pragma_module(&module) {
456 continue;
457 }
458
459 usage_map.entry(module).or_default().push(symbol);
460 }
461 }
462 let last_import_line = imports.iter().map(|i| i.line).max().unwrap_or(0);
463 let missing_imports = usage_map
464 .into_iter()
465 .map(|(module, mut symbols)| {
466 symbols.sort();
467 symbols.dedup();
468 MissingImport {
469 module,
470 symbols,
471 suggested_location: last_import_line + 1,
472 confidence: 0.8,
473 }
474 })
475 .collect::<Vec<_>>();
476
477 let mut organization_suggestions = Vec::new();
479
480 let module_order: Vec<String> = imports.iter().map(|i| i.module.clone()).collect();
482 let mut sorted_order = module_order.clone();
483 sorted_order.sort();
484 if module_order != sorted_order {
485 organization_suggestions.push(OrganizationSuggestion {
486 description: "Sort import statements alphabetically".to_string(),
487 priority: SuggestionPriority::Low,
488 });
489 }
490
491 if !duplicate_imports.is_empty() {
493 let modules =
494 duplicate_imports.iter().map(|d| d.module.clone()).collect::<Vec<_>>().join(", ");
495 organization_suggestions.push(OrganizationSuggestion {
496 description: format!("Remove duplicate imports for modules: {}", modules),
497 priority: SuggestionPriority::Medium,
498 });
499 }
500
501 let mut symbols_need_org = false;
503 for imp in &imports {
504 if imp.symbols.len() > 1 {
505 let mut sorted = imp.symbols.clone();
506 sorted.sort();
507 sorted.dedup();
508 if sorted != imp.symbols {
509 symbols_need_org = true;
510 break;
511 }
512 }
513 }
514 if symbols_need_org {
515 organization_suggestions.push(OrganizationSuggestion {
516 description: "Sort and deduplicate symbols within import statements".to_string(),
517 priority: SuggestionPriority::Low,
518 });
519 }
520
521 Ok(ImportAnalysis {
522 imports,
523 unused_imports,
524 missing_imports,
525 duplicate_imports,
526 organization_suggestions,
527 })
528 }
529
530 pub fn generate_optimized_imports(&self, analysis: &ImportAnalysis) -> String {
554 let mut optimized_imports = Vec::new();
555
556 let mut module_symbols: BTreeMap<String, Vec<String>> = BTreeMap::new();
558
559 let mut unused_by_module: BTreeMap<String, Vec<String>> = BTreeMap::new();
561 for unused in &analysis.unused_imports {
562 unused_by_module
563 .entry(unused.module.clone())
564 .or_default()
565 .extend(unused.symbols.clone());
566 }
567
568 for import in &analysis.imports {
570 let kept_symbols: Vec<String> = import
572 .symbols
573 .iter()
574 .filter(|sym| {
575 if let Some(unused_symbols) = unused_by_module.get(&import.module) {
576 !unused_symbols.contains(sym)
577 } else {
578 true }
580 })
581 .cloned()
582 .collect();
583
584 let entry = module_symbols.entry(import.module.clone()).or_default();
586 entry.extend(kept_symbols);
587
588 entry.sort();
590 entry.dedup();
591 }
592
593 for missing in &analysis.missing_imports {
595 let entry = module_symbols.entry(missing.module.clone()).or_default();
596 entry.extend(missing.symbols.clone());
597 entry.sort();
598 entry.dedup();
599 }
600
601 for (module, symbols) in &module_symbols {
604 let was_bare_import =
606 analysis.imports.iter().any(|imp| imp.module == *module && imp.symbols.is_empty());
607
608 if symbols.is_empty() && was_bare_import {
609 optimized_imports.push(format!("use {};", module));
611 } else if !symbols.is_empty() {
612 let symbol_list = symbols.join(" ");
614 optimized_imports.push(format!("use {} qw({});", module, symbol_list));
615 }
616 }
618
619 optimized_imports.sort();
621 optimized_imports.join("\n")
622 }
623
624 pub fn generate_edits(&self, content: &str, analysis: &ImportAnalysis) -> Vec<TextEdit> {
647 let optimized = self.generate_optimized_imports(analysis);
648
649 if analysis.imports.is_empty() {
650 if optimized.is_empty() {
651 return Vec::new();
652 }
653 let insert_line =
654 analysis.missing_imports.first().map(|m| m.suggested_location).unwrap_or(1);
655 let insert_offset = self.line_offset(content, insert_line);
656 return vec![TextEdit {
657 range: (insert_offset, insert_offset),
658 new_text: optimized + "\n",
659 }];
660 }
661
662 let first_line = analysis.imports.iter().map(|i| i.line).min().unwrap_or(1);
665 let last_line = analysis.imports.iter().map(|i| i.line).max().unwrap_or(1);
666
667 let start_offset = self.line_offset(content, first_line);
668 let end_offset = self.line_offset(content, last_line + 1);
669
670 vec![TextEdit {
671 range: (start_offset, end_offset),
672 new_text: if optimized.is_empty() { String::new() } else { optimized + "\n" },
673 }]
674 }
675
676 fn line_offset(&self, content: &str, line: usize) -> usize {
677 if line <= 1 {
678 return 0;
679 }
680 let mut offset = 0;
681 for (idx, l) in content.lines().enumerate() {
682 if idx + 1 >= line {
683 break;
684 }
685 offset += l.len() + 1; }
687 offset
688 }
689}
690
691impl Default for ImportOptimizer {
692 fn default() -> Self {
693 Self::new()
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use std::fs;
701 use std::path::PathBuf;
702 use tempfile::TempDir;
703
704 fn create_test_file(content: &str) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
705 let temp_dir = TempDir::new()?;
706 let file_path = temp_dir.path().join("test.pl");
707 fs::write(&file_path, content)?;
708 Ok((temp_dir, file_path))
709 }
710
711 #[test]
712 fn test_basic_import_analysis() -> Result<(), Box<dyn std::error::Error>> {
713 let optimizer = ImportOptimizer::new();
714 let content = r#"#!/usr/bin/perl
715use strict;
716use warnings;
717use Data::Dumper;
718
719print Dumper(\@ARGV);
720"#;
721
722 let (_temp_dir, file_path) = create_test_file(content)?;
723 let analysis = optimizer.analyze_file(&file_path)?;
724
725 assert_eq!(analysis.imports.len(), 3);
726 assert_eq!(analysis.imports[0].module, "strict");
727 assert_eq!(analysis.imports[1].module, "warnings");
728 assert_eq!(analysis.imports[2].module, "Data::Dumper");
729
730 assert!(analysis.unused_imports.is_empty());
732 Ok(())
733 }
734
735 #[test]
736 fn test_unused_import_detection() -> Result<(), Box<dyn std::error::Error>> {
737 let optimizer = ImportOptimizer::new();
738 let content = r#"use strict;
739use warnings;
740use Data::Dumper; # This is not used
741use JSON; # This is not used
742
743print "Hello World\n";
744"#;
745
746 let (_temp_dir, file_path) = create_test_file(content)?;
747 let analysis = optimizer.analyze_file(&file_path)?;
748
749 assert!(analysis.unused_imports.is_empty());
752 Ok(())
753 }
754
755 #[test]
756 fn test_missing_import_detection() -> Result<(), Box<dyn std::error::Error>> {
757 let optimizer = ImportOptimizer::new();
758 let content = r#"use strict;
759use warnings;
760
761# Using JSON::encode_json without importing JSON
762my $json = JSON::encode_json({key => 'value'});
763
764# Using Data::Dumper::Dumper without importing Data::Dumper
765print Data::Dumper::Dumper(\@ARGV);
766"#;
767
768 let (_temp_dir, file_path) = create_test_file(content)?;
769 let analysis = optimizer.analyze_file(&file_path)?;
770 assert_eq!(analysis.missing_imports.len(), 2);
771 assert!(analysis.missing_imports.iter().any(|m| m.module == "JSON"));
772 assert!(analysis.missing_imports.iter().any(|m| m.module == "Data::Dumper"));
773 for m in &analysis.missing_imports {
774 assert_eq!(m.suggested_location, 3);
775 }
776 Ok(())
777 }
778
779 #[test]
780 fn test_duplicate_import_detection() -> Result<(), Box<dyn std::error::Error>> {
781 let optimizer = ImportOptimizer::new();
782 let content = r#"use strict;
783use warnings;
784use Data::Dumper;
785use JSON;
786use Data::Dumper; # Duplicate
787
788print Dumper(\@ARGV);
789"#;
790
791 let (_temp_dir, file_path) = create_test_file(content)?;
792 let analysis = optimizer.analyze_file(&file_path)?;
793
794 assert_eq!(analysis.duplicate_imports.len(), 1);
795 assert_eq!(analysis.duplicate_imports[0].module, "Data::Dumper");
796 assert_eq!(analysis.duplicate_imports[0].lines.len(), 2);
797 assert!(analysis.duplicate_imports[0].can_merge);
798 Ok(())
799 }
800
801 #[test]
802 fn test_organization_suggestions() -> Result<(), Box<dyn std::error::Error>> {
803 let optimizer = ImportOptimizer::new();
804 let content = r#"use warnings;
805use strict;
806use List::Util qw(max max min);
807use Data::Dumper;
808use Data::Dumper; # duplicate
809"#;
810
811 let (_temp_dir, file_path) = create_test_file(content)?;
812 let analysis = optimizer.analyze_file(&file_path)?;
813
814 assert!(
815 analysis
816 .organization_suggestions
817 .iter()
818 .any(|s| s.description.contains("Sort import statements"))
819 );
820 assert!(
821 analysis
822 .organization_suggestions
823 .iter()
824 .any(|s| s.description.contains("Remove duplicate imports"))
825 );
826 assert!(
827 analysis
828 .organization_suggestions
829 .iter()
830 .any(|s| s.description.contains("Sort and deduplicate symbols"))
831 );
832 Ok(())
833 }
834
835 #[test]
836 fn test_qw_import_parsing() -> Result<(), Box<dyn std::error::Error>> {
837 let optimizer = ImportOptimizer::new();
838 let content = r#"use List::Util qw(first max min sum);
839use Scalar::Util qw(blessed reftype);
840
841my @nums = (1, 2, 3, 4, 5);
842print "Max: " . max(@nums) . "\n";
843print "Sum: " . sum(@nums) . "\n";
844print "First: " . first { $_ > 3 } @nums;
845"#;
846
847 let (_temp_dir, file_path) = create_test_file(content)?;
848 let analysis = optimizer.analyze_file(&file_path)?;
849
850 assert_eq!(analysis.imports.len(), 2);
851
852 let list_util = analysis
853 .imports
854 .iter()
855 .find(|i| i.module == "List::Util")
856 .ok_or("List::Util import not found")?;
857 assert_eq!(list_util.symbols, vec!["first", "max", "min", "sum"]);
858
859 let scalar_util = analysis
860 .imports
861 .iter()
862 .find(|i| i.module == "Scalar::Util")
863 .ok_or("Scalar::Util import not found")?;
864 assert_eq!(scalar_util.symbols, vec!["blessed", "reftype"]);
865
866 assert_eq!(analysis.unused_imports.len(), 2);
868
869 let list_util_unused = analysis
870 .unused_imports
871 .iter()
872 .find(|u| u.module == "List::Util")
873 .ok_or("List::Util unused imports not found")?;
874 assert_eq!(list_util_unused.symbols, vec!["min"]);
875
876 let scalar_util_unused = analysis
877 .unused_imports
878 .iter()
879 .find(|u| u.module == "Scalar::Util")
880 .ok_or("Scalar::Util unused imports not found")?;
881 assert_eq!(scalar_util_unused.symbols, vec!["blessed", "reftype"]);
882 Ok(())
883 }
884
885 #[test]
886 fn test_generate_optimized_imports() {
887 let optimizer = ImportOptimizer::new();
888
889 let analysis = ImportAnalysis {
890 imports: vec![
891 ImportEntry { module: "strict".to_string(), symbols: vec![], line: 1 },
892 ImportEntry { module: "warnings".to_string(), symbols: vec![], line: 2 },
893 ImportEntry {
894 module: "List::Util".to_string(),
895 symbols: vec!["first".to_string(), "max".to_string(), "unused".to_string()],
896 line: 3,
897 },
898 ],
899 unused_imports: vec![UnusedImport {
900 module: "List::Util".to_string(),
901 symbols: vec!["unused".to_string()],
902 line: 3,
903 reason: "Symbol not used".to_string(),
904 }],
905 missing_imports: vec![MissingImport {
906 module: "Data::Dumper".to_string(),
907 symbols: vec!["Dumper".to_string()],
908 suggested_location: 10,
909 confidence: 0.8,
910 }],
911 duplicate_imports: vec![],
912 organization_suggestions: vec![],
913 };
914
915 let optimized = optimizer.generate_optimized_imports(&analysis);
916
917 let expected_lines = [
919 "use Data::Dumper qw(Dumper);",
920 "use List::Util qw(first max);",
921 "use strict;",
922 "use warnings;",
923 ];
924
925 assert_eq!(optimized, expected_lines.join("\n"));
926 }
927
928 #[test]
929 fn test_empty_file_analysis() -> Result<(), Box<dyn std::error::Error>> {
930 let optimizer = ImportOptimizer::new();
931 let content = "";
932
933 let (_temp_dir, file_path) = create_test_file(content)?;
934 let analysis = optimizer.analyze_file(&file_path)?;
935
936 assert!(analysis.imports.is_empty());
937 assert!(analysis.unused_imports.is_empty());
938 assert!(analysis.missing_imports.is_empty());
939 assert!(analysis.duplicate_imports.is_empty());
940 Ok(())
941 }
942
943 #[test]
944 fn test_complex_perl_code_analysis() -> Result<(), Box<dyn std::error::Error>> {
945 let optimizer = ImportOptimizer::new();
946 let content = r#"#!/usr/bin/perl
947use strict;
948use warnings;
949use Data::Dumper;
950use JSON qw(encode_json decode_json);
951use LWP::UserAgent; # Unused
952use File::Spec::Functions qw(catfile catdir);
953
954# Complex code with various patterns
955my $data = { key => 'value', numbers => [1, 2, 3] };
956my $json_string = encode_json($data);
957print "JSON: $json_string\n";
958
959# Using File::Spec but not all imported functions
960my $path = catfile('/tmp', 'test.json');
961print "Path: $path\n";
962
963# Using modules without explicit imports
964my $response = HTTP::Tiny::new()->get('http://example.com');
965print Dumper($response);
966"#;
967
968 let (_temp_dir, file_path) = create_test_file(content)?;
969 let analysis = optimizer.analyze_file(&file_path)?;
970
971 assert!(analysis.unused_imports.iter().any(|u| u.module == "LWP::UserAgent"));
973
974 let file_spec_unused =
976 analysis.unused_imports.iter().find(|u| u.module == "File::Spec::Functions");
977 if let Some(unused) = file_spec_unused {
978 assert!(unused.symbols.contains(&"catdir".to_string()));
979 }
980
981 assert!(analysis.missing_imports.iter().any(|m| m.module == "HTTP::Tiny"));
983 Ok(())
984 }
985
986 #[test]
987 fn test_bare_import_with_exports_detection() -> Result<(), Box<dyn std::error::Error>> {
988 let optimizer = ImportOptimizer::new();
989 let content = r#"use strict;
990use warnings;
991use Data::Dumper; # Used
992use JSON; # Unused - has exports but none are used
993use SomeUnknownModule; # Conservative - not marked as unused
994
995print Dumper(\@ARGV);
996"#;
997
998 let (_temp_dir, file_path) = create_test_file(content)?;
999 let analysis = optimizer.analyze_file(&file_path)?;
1000
1001 assert!(!analysis.unused_imports.iter().any(|u| u.module == "Data::Dumper"));
1003
1004 assert!(analysis.unused_imports.is_empty());
1007 Ok(())
1008 }
1009
1010 #[test]
1011 fn test_regex_edge_cases() -> Result<(), Box<dyn std::error::Error>> {
1012 let optimizer = ImportOptimizer::new();
1013 let content = r#"use strict;
1014use warnings;
1015
1016# These should not be detected as module references
1017my $string = "This is not JSON::encode_json in a string";
1018my $regex = qr/Data::Dumper/;
1019print "Module::Name is just text";
1020
1021# This should be detected
1022my $result = JSON::encode_json({test => 1});
1023"#;
1024
1025 let (_temp_dir, file_path) = create_test_file(content)?;
1026 let analysis = optimizer.analyze_file(&file_path)?;
1027
1028 assert_eq!(analysis.missing_imports.len(), 1);
1030 assert_eq!(analysis.missing_imports[0].module, "JSON");
1031 Ok(())
1032 }
1033
1034 #[test]
1035 fn test_malformed_regex_capture_safety() -> Result<(), Box<dyn std::error::Error>> {
1036 let optimizer = ImportOptimizer::new();
1037 let content = r#"use strict;
1039use warnings;
1040
1041# Normal module usage
1042my $result = JSON::encode_json({test => 1});
1043
1044# Edge case patterns that might not fully match the regex
1045my $incomplete = "Something::";
1046my $partial = "::Function";
1047"#;
1048
1049 let (_temp_dir, file_path) = create_test_file(content)?;
1050 let analysis = optimizer.analyze_file(&file_path)?;
1052
1053 assert_eq!(analysis.missing_imports.len(), 1);
1055 assert_eq!(analysis.missing_imports[0].module, "JSON");
1056 Ok(())
1057 }
1058}