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