1use regex::Regex;
45use serde::{Deserialize, Serialize};
46use std::collections::{BTreeMap, BTreeSet};
47use std::path::Path;
48
49#[derive(Debug, Clone)]
54pub struct TextEdit {
55 pub range: (usize, usize),
57 pub new_text: String,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
63pub struct ImportAnalysis {
64 pub unused_imports: Vec<UnusedImport>,
66 pub missing_imports: Vec<MissingImport>,
68 pub duplicate_imports: Vec<DuplicateImport>,
70 pub organization_suggestions: Vec<OrganizationSuggestion>,
72 pub imports: Vec<ImportEntry>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct UnusedImport {
79 pub module: String,
81 pub symbols: Vec<String>,
83 pub line: usize,
85 pub reason: String,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MissingImport {
92 pub module: String,
94 pub symbols: Vec<String>,
96 pub suggested_location: usize,
98 pub confidence: f32,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct DuplicateImport {
105 pub module: String,
107 pub lines: Vec<usize>,
109 pub can_merge: bool,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct OrganizationSuggestion {
116 pub description: String,
118 pub priority: SuggestionPriority,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ImportEntry {
125 pub module: String,
127 pub symbols: Vec<String>,
129 pub line: usize,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135pub enum SuggestionPriority {
136 High,
138 Medium,
140 Low,
142}
143
144pub struct ImportOptimizer;
152
153fn is_pragma_module(module: &str) -> bool {
155 matches!(
156 module,
157 "strict"
158 | "warnings"
159 | "utf8"
160 | "bytes"
161 | "locale"
162 | "integer"
163 | "less"
164 | "sigtrap"
165 | "subs"
166 | "vars"
167 | "feature"
168 | "autodie"
169 | "autouse"
170 | "base"
171 | "parent"
172 | "lib"
173 | "bigint"
174 | "bignum"
175 | "bigrat"
176 )
177}
178
179fn get_known_module_exports(module: &str) -> Option<Vec<&'static str>> {
181 match module {
182 "Data::Dumper" => Some(vec!["Dumper"]),
183 "JSON" => Some(vec!["encode_json", "decode_json", "to_json", "from_json"]),
184 "YAML" => Some(vec!["Load", "Dump", "LoadFile", "DumpFile"]),
185 "Storable" => Some(vec!["store", "retrieve", "freeze", "thaw"]),
186 "List::Util" => Some(vec!["first", "max", "min", "sum", "reduce", "shuffle", "uniq"]),
187 "Scalar::Util" => Some(vec!["blessed", "reftype", "looks_like_number", "weaken"]),
188 "File::Spec" => Some(vec!["catfile", "catdir", "splitpath", "splitdir"]),
189 "File::Basename" => Some(vec!["basename", "dirname", "fileparse"]),
190 "Cwd" => Some(vec!["getcwd", "abs_path", "realpath"]),
191 "Time::HiRes" => Some(vec!["time", "sleep", "usleep", "gettimeofday"]),
192 "Digest::MD5" => Some(vec!["md5", "md5_hex", "md5_base64"]),
193 "MIME::Base64" => Some(vec!["encode_base64", "decode_base64"]),
194 "URI::Escape" => Some(vec!["uri_escape", "uri_unescape"]),
195 "LWP::Simple" => Some(vec!["get", "head", "getprint", "getstore", "mirror"]),
196 "LWP::UserAgent" => Some(vec![]),
197 "CGI" => Some(vec!["param", "header", "start_html", "end_html"]),
198 "DBI" => Some(vec![]), "strict" => Some(vec![]), "warnings" => Some(vec![]), "utf8" => Some(vec![]), _ => None,
203 }
204}
205
206impl ImportOptimizer {
207 pub fn new() -> Self {
222 Self
223 }
224
225 pub fn analyze_file(&self, file_path: &Path) -> Result<ImportAnalysis, String> {
242 let content = std::fs::read_to_string(file_path).map_err(|e| e.to_string())?;
243 self.analyze_content(&content)
244 }
245
246 pub fn analyze_content(&self, content: &str) -> Result<ImportAnalysis, String> {
264 let re_use = Regex::new(r"^\s*use\s+([A-Za-z0-9_:]+)(?:\s+qw\(([^)]*)\))?\s*;")
266 .map_err(|e| e.to_string())?;
267
268 let mut imports = Vec::new();
269 for (idx, line) in content.lines().enumerate() {
270 if let Some(caps) = re_use.captures(line) {
271 let module = caps[1].to_string();
272 let symbols_str = caps.get(2).map(|m| m.as_str()).unwrap_or("");
273 let symbols = if symbols_str.is_empty() {
274 Vec::new()
275 } else {
276 symbols_str
277 .split_whitespace()
278 .filter(|s| !s.is_empty())
279 .map(|s| s.trim_matches(|c| c == ',' || c == ';' || c == '"'))
280 .map(|s| s.to_string())
281 .collect::<Vec<_>>()
282 };
283 imports.push(ImportEntry { module, symbols, line: idx + 1 });
284 }
285 }
286
287 let mut module_to_lines: BTreeMap<String, Vec<usize>> = BTreeMap::new();
289 for imp in &imports {
290 module_to_lines.entry(imp.module.clone()).or_default().push(imp.line);
291 }
292 let duplicate_imports = module_to_lines
293 .iter()
294 .filter(|(_, lines)| lines.len() > 1)
295 .map(|(module, lines)| DuplicateImport {
296 module: module.clone(),
297 lines: lines.clone(),
298 can_merge: true,
299 })
300 .collect::<Vec<_>>();
301
302 let non_use_content = content
304 .lines()
305 .filter(
306 |line| {
307 !line.trim_start().starts_with("use ") && !line.trim_start().starts_with("#")
308 }, )
310 .collect::<Vec<_>>()
311 .join(
312 "
313",
314 );
315
316 let dumper_re = Regex::new(r"\bDumper\b").map_err(|e| e.to_string())?;
318
319 let mut unused_imports = Vec::new();
321 for imp in &imports {
322 let mut unused_symbols = Vec::new();
323
324 if !imp.symbols.is_empty() {
326 for sym in &imp.symbols {
327 let re = Regex::new(&format!(r"\b{}\b", regex::escape(sym)))
328 .map_err(|e| e.to_string())?;
329
330 if !re.is_match(&non_use_content) {
332 unused_symbols.push(sym.clone());
333 }
334 }
335 } else {
336 let is_pragma = matches!(
338 imp.module.as_str(),
339 "strict"
340 | "warnings"
341 | "utf8"
342 | "bytes"
343 | "integer"
344 | "locale"
345 | "overload"
346 | "sigtrap"
347 | "subs"
348 | "vars"
349 );
350
351 if !is_pragma {
352 let (is_known_module, known_exports) =
354 match get_known_module_exports(&imp.module) {
355 Some(exports) => (true, exports),
356 None => (false, Vec::new()),
357 };
358 let mut is_used = false;
359
360 let module_pattern = format!(r"\b{}\b", regex::escape(&imp.module));
362 let module_re = Regex::new(&module_pattern).map_err(|e| e.to_string())?;
363 if module_re.is_match(&non_use_content) {
364 is_used = true;
365 }
366
367 if !is_used {
369 let qualified_pattern = format!(r"{}::", regex::escape(&imp.module));
370 let qualified_re =
371 Regex::new(&qualified_pattern).map_err(|e| e.to_string())?;
372 if qualified_re.is_match(&non_use_content) {
373 is_used = true;
374 }
375 }
376
377 if !is_used && imp.module == "Data::Dumper" {
379 if dumper_re.is_match(&non_use_content) {
380 is_used = true;
381 }
382 }
383
384 if !is_used && !known_exports.is_empty() {
386 for export in &known_exports {
387 let export_pattern = format!(r"\b{}\b", regex::escape(export));
388 let export_re =
389 Regex::new(&export_pattern).map_err(|e| e.to_string())?;
390 if export_re.is_match(&non_use_content) {
391 is_used = true;
392 break;
393 }
394 }
395 }
396
397 if !is_used && is_known_module && known_exports.is_empty() {
401 unused_symbols.push("(bare import)".to_string());
402 }
403 }
404 }
405
406 if !unused_symbols.is_empty() {
408 unused_imports.push(UnusedImport {
409 module: imp.module.clone(),
410 symbols: unused_symbols,
411 line: imp.line,
412 reason: "Symbols not used in code".to_string(),
413 });
414 }
415 }
416
417 let imported_modules: BTreeSet<String> =
419 imports.iter().map(|imp| imp.module.clone()).collect();
420
421 let string_re = Regex::new("'[^']*'|\"[^\"]*\"").map_err(|e| e.to_string())?;
423 let stripped = string_re.replace_all(content, " ").to_string();
424 let regex_literal_re = Regex::new(r"qr/[^/]*/").map_err(|e| e.to_string())?;
425 let stripped = regex_literal_re.replace_all(&stripped, " ").to_string();
426 let comment_re = Regex::new(r"(?m)#.*$").map_err(|e| e.to_string())?;
427 let stripped = comment_re.replace_all(&stripped, " ").to_string();
428
429 let usage_re = Regex::new(
430 r"\b([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)::([A-Za-z_][A-Za-z0-9_]*)",
431 )
432 .map_err(|e| e.to_string())?;
433 let mut usage_map: BTreeMap<String, Vec<String>> = BTreeMap::new();
434 for caps in usage_re.captures_iter(&stripped) {
435 if let (Some(module_match), Some(symbol_match)) = (caps.get(1), caps.get(2)) {
437 let module = module_match.as_str().to_string();
438 let symbol = symbol_match.as_str().to_string();
439
440 if imported_modules.contains(&module) || is_pragma_module(&module) {
441 continue;
442 }
443
444 usage_map.entry(module).or_default().push(symbol);
445 }
446 }
447 let last_import_line = imports.iter().map(|i| i.line).max().unwrap_or(0);
448 let missing_imports = usage_map
449 .into_iter()
450 .map(|(module, mut symbols)| {
451 symbols.sort();
452 symbols.dedup();
453 MissingImport {
454 module,
455 symbols,
456 suggested_location: last_import_line + 1,
457 confidence: 0.8,
458 }
459 })
460 .collect::<Vec<_>>();
461
462 let mut organization_suggestions = Vec::new();
464
465 let module_order: Vec<String> = imports.iter().map(|i| i.module.clone()).collect();
467 let mut sorted_order = module_order.clone();
468 sorted_order.sort();
469 if module_order != sorted_order {
470 organization_suggestions.push(OrganizationSuggestion {
471 description: "Sort import statements alphabetically".to_string(),
472 priority: SuggestionPriority::Low,
473 });
474 }
475
476 if !duplicate_imports.is_empty() {
478 let modules =
479 duplicate_imports.iter().map(|d| d.module.clone()).collect::<Vec<_>>().join(", ");
480 organization_suggestions.push(OrganizationSuggestion {
481 description: format!("Remove duplicate imports for modules: {}", modules),
482 priority: SuggestionPriority::Medium,
483 });
484 }
485
486 let mut symbols_need_org = false;
488 for imp in &imports {
489 if imp.symbols.len() > 1 {
490 let mut sorted = imp.symbols.clone();
491 sorted.sort();
492 sorted.dedup();
493 if sorted != imp.symbols {
494 symbols_need_org = true;
495 break;
496 }
497 }
498 }
499 if symbols_need_org {
500 organization_suggestions.push(OrganizationSuggestion {
501 description: "Sort and deduplicate symbols within import statements".to_string(),
502 priority: SuggestionPriority::Low,
503 });
504 }
505
506 Ok(ImportAnalysis {
507 imports,
508 unused_imports,
509 missing_imports,
510 duplicate_imports,
511 organization_suggestions,
512 })
513 }
514
515 pub fn generate_optimized_imports(&self, analysis: &ImportAnalysis) -> String {
539 let mut optimized_imports = Vec::new();
540
541 let mut module_symbols: BTreeMap<String, Vec<String>> = BTreeMap::new();
543
544 let mut unused_by_module: BTreeMap<String, Vec<String>> = BTreeMap::new();
546 for unused in &analysis.unused_imports {
547 unused_by_module
548 .entry(unused.module.clone())
549 .or_default()
550 .extend(unused.symbols.clone());
551 }
552
553 for import in &analysis.imports {
555 let kept_symbols: Vec<String> = import
557 .symbols
558 .iter()
559 .filter(|sym| {
560 if let Some(unused_symbols) = unused_by_module.get(&import.module) {
561 !unused_symbols.contains(sym)
562 } else {
563 true }
565 })
566 .cloned()
567 .collect();
568
569 let entry = module_symbols.entry(import.module.clone()).or_default();
571 entry.extend(kept_symbols);
572
573 entry.sort();
575 entry.dedup();
576 }
577
578 for missing in &analysis.missing_imports {
580 let entry = module_symbols.entry(missing.module.clone()).or_default();
581 entry.extend(missing.symbols.clone());
582 entry.sort();
583 entry.dedup();
584 }
585
586 for (module, symbols) in &module_symbols {
589 let was_bare_import =
591 analysis.imports.iter().any(|imp| imp.module == *module && imp.symbols.is_empty());
592
593 if symbols.is_empty() && was_bare_import {
594 optimized_imports.push(format!("use {};", module));
596 } else if !symbols.is_empty() {
597 let symbol_list = symbols.join(" ");
599 optimized_imports.push(format!("use {} qw({});", module, symbol_list));
600 }
601 }
603
604 optimized_imports.sort();
606 optimized_imports.join("\n")
607 }
608
609 pub fn generate_edits(&self, content: &str, analysis: &ImportAnalysis) -> Vec<TextEdit> {
632 let optimized = self.generate_optimized_imports(analysis);
633
634 if analysis.imports.is_empty() {
635 if optimized.is_empty() {
636 return Vec::new();
637 }
638 let insert_line =
639 analysis.missing_imports.first().map(|m| m.suggested_location).unwrap_or(1);
640 let insert_offset = self.line_offset(content, insert_line);
641 return vec![TextEdit {
642 range: (insert_offset, insert_offset),
643 new_text: optimized + "\n",
644 }];
645 }
646
647 let first_line = analysis.imports.iter().map(|i| i.line).min().unwrap_or(1);
650 let last_line = analysis.imports.iter().map(|i| i.line).max().unwrap_or(1);
651
652 let start_offset = self.line_offset(content, first_line);
653 let end_offset = self.line_offset(content, last_line + 1);
654
655 vec![TextEdit {
656 range: (start_offset, end_offset),
657 new_text: if optimized.is_empty() { String::new() } else { optimized + "\n" },
658 }]
659 }
660
661 fn line_offset(&self, content: &str, line: usize) -> usize {
662 if line <= 1 {
663 return 0;
664 }
665 let mut offset = 0;
666 for (idx, l) in content.lines().enumerate() {
667 if idx + 1 >= line {
668 break;
669 }
670 offset += l.len() + 1; }
672 offset
673 }
674}
675
676impl Default for ImportOptimizer {
677 fn default() -> Self {
678 Self::new()
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use std::fs;
686 use std::path::PathBuf;
687 use tempfile::TempDir;
688
689 fn create_test_file(content: &str) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
690 let temp_dir = TempDir::new()?;
691 let file_path = temp_dir.path().join("test.pl");
692 fs::write(&file_path, content)?;
693 Ok((temp_dir, file_path))
694 }
695
696 #[test]
697 fn test_basic_import_analysis() -> Result<(), Box<dyn std::error::Error>> {
698 let optimizer = ImportOptimizer::new();
699 let content = r#"#!/usr/bin/perl
700use strict;
701use warnings;
702use Data::Dumper;
703
704print Dumper(\@ARGV);
705"#;
706
707 let (_temp_dir, file_path) = create_test_file(content)?;
708 let analysis = optimizer.analyze_file(&file_path)?;
709
710 assert_eq!(analysis.imports.len(), 3);
711 assert_eq!(analysis.imports[0].module, "strict");
712 assert_eq!(analysis.imports[1].module, "warnings");
713 assert_eq!(analysis.imports[2].module, "Data::Dumper");
714
715 assert!(analysis.unused_imports.is_empty());
717 Ok(())
718 }
719
720 #[test]
721 fn test_unused_import_detection() -> Result<(), Box<dyn std::error::Error>> {
722 let optimizer = ImportOptimizer::new();
723 let content = r#"use strict;
724use warnings;
725use Data::Dumper; # This is not used
726use JSON; # This is not used
727
728print "Hello World\n";
729"#;
730
731 let (_temp_dir, file_path) = create_test_file(content)?;
732 let analysis = optimizer.analyze_file(&file_path)?;
733
734 assert!(analysis.unused_imports.is_empty());
737 Ok(())
738 }
739
740 #[test]
741 fn test_missing_import_detection() -> Result<(), Box<dyn std::error::Error>> {
742 let optimizer = ImportOptimizer::new();
743 let content = r#"use strict;
744use warnings;
745
746# Using JSON::encode_json without importing JSON
747my $json = JSON::encode_json({key => 'value'});
748
749# Using Data::Dumper::Dumper without importing Data::Dumper
750print Data::Dumper::Dumper(\@ARGV);
751"#;
752
753 let (_temp_dir, file_path) = create_test_file(content)?;
754 let analysis = optimizer.analyze_file(&file_path)?;
755 assert_eq!(analysis.missing_imports.len(), 2);
756 assert!(analysis.missing_imports.iter().any(|m| m.module == "JSON"));
757 assert!(analysis.missing_imports.iter().any(|m| m.module == "Data::Dumper"));
758 for m in &analysis.missing_imports {
759 assert_eq!(m.suggested_location, 3);
760 }
761 Ok(())
762 }
763
764 #[test]
765 fn test_duplicate_import_detection() -> Result<(), Box<dyn std::error::Error>> {
766 let optimizer = ImportOptimizer::new();
767 let content = r#"use strict;
768use warnings;
769use Data::Dumper;
770use JSON;
771use Data::Dumper; # Duplicate
772
773print Dumper(\@ARGV);
774"#;
775
776 let (_temp_dir, file_path) = create_test_file(content)?;
777 let analysis = optimizer.analyze_file(&file_path)?;
778
779 assert_eq!(analysis.duplicate_imports.len(), 1);
780 assert_eq!(analysis.duplicate_imports[0].module, "Data::Dumper");
781 assert_eq!(analysis.duplicate_imports[0].lines.len(), 2);
782 assert!(analysis.duplicate_imports[0].can_merge);
783 Ok(())
784 }
785
786 #[test]
787 fn test_organization_suggestions() -> Result<(), Box<dyn std::error::Error>> {
788 let optimizer = ImportOptimizer::new();
789 let content = r#"use warnings;
790use strict;
791use List::Util qw(max max min);
792use Data::Dumper;
793use Data::Dumper; # duplicate
794"#;
795
796 let (_temp_dir, file_path) = create_test_file(content)?;
797 let analysis = optimizer.analyze_file(&file_path)?;
798
799 assert!(
800 analysis
801 .organization_suggestions
802 .iter()
803 .any(|s| s.description.contains("Sort import statements"))
804 );
805 assert!(
806 analysis
807 .organization_suggestions
808 .iter()
809 .any(|s| s.description.contains("Remove duplicate imports"))
810 );
811 assert!(
812 analysis
813 .organization_suggestions
814 .iter()
815 .any(|s| s.description.contains("Sort and deduplicate symbols"))
816 );
817 Ok(())
818 }
819
820 #[test]
821 fn test_qw_import_parsing() -> Result<(), Box<dyn std::error::Error>> {
822 let optimizer = ImportOptimizer::new();
823 let content = r#"use List::Util qw(first max min sum);
824use Scalar::Util qw(blessed reftype);
825
826my @nums = (1, 2, 3, 4, 5);
827print "Max: " . max(@nums) . "\n";
828print "Sum: " . sum(@nums) . "\n";
829print "First: " . first { $_ > 3 } @nums;
830"#;
831
832 let (_temp_dir, file_path) = create_test_file(content)?;
833 let analysis = optimizer.analyze_file(&file_path)?;
834
835 assert_eq!(analysis.imports.len(), 2);
836
837 let list_util = analysis
838 .imports
839 .iter()
840 .find(|i| i.module == "List::Util")
841 .ok_or("List::Util import not found")?;
842 assert_eq!(list_util.symbols, vec!["first", "max", "min", "sum"]);
843
844 let scalar_util = analysis
845 .imports
846 .iter()
847 .find(|i| i.module == "Scalar::Util")
848 .ok_or("Scalar::Util import not found")?;
849 assert_eq!(scalar_util.symbols, vec!["blessed", "reftype"]);
850
851 assert_eq!(analysis.unused_imports.len(), 2);
853
854 let list_util_unused = analysis
855 .unused_imports
856 .iter()
857 .find(|u| u.module == "List::Util")
858 .ok_or("List::Util unused imports not found")?;
859 assert_eq!(list_util_unused.symbols, vec!["min"]);
860
861 let scalar_util_unused = analysis
862 .unused_imports
863 .iter()
864 .find(|u| u.module == "Scalar::Util")
865 .ok_or("Scalar::Util unused imports not found")?;
866 assert_eq!(scalar_util_unused.symbols, vec!["blessed", "reftype"]);
867 Ok(())
868 }
869
870 #[test]
871 fn test_generate_optimized_imports() {
872 let optimizer = ImportOptimizer::new();
873
874 let analysis = ImportAnalysis {
875 imports: vec![
876 ImportEntry { module: "strict".to_string(), symbols: vec![], line: 1 },
877 ImportEntry { module: "warnings".to_string(), symbols: vec![], line: 2 },
878 ImportEntry {
879 module: "List::Util".to_string(),
880 symbols: vec!["first".to_string(), "max".to_string(), "unused".to_string()],
881 line: 3,
882 },
883 ],
884 unused_imports: vec![UnusedImport {
885 module: "List::Util".to_string(),
886 symbols: vec!["unused".to_string()],
887 line: 3,
888 reason: "Symbol not used".to_string(),
889 }],
890 missing_imports: vec![MissingImport {
891 module: "Data::Dumper".to_string(),
892 symbols: vec!["Dumper".to_string()],
893 suggested_location: 10,
894 confidence: 0.8,
895 }],
896 duplicate_imports: vec![],
897 organization_suggestions: vec![],
898 };
899
900 let optimized = optimizer.generate_optimized_imports(&analysis);
901
902 let expected_lines = [
904 "use Data::Dumper qw(Dumper);",
905 "use List::Util qw(first max);",
906 "use strict;",
907 "use warnings;",
908 ];
909
910 assert_eq!(optimized, expected_lines.join("\n"));
911 }
912
913 #[test]
914 fn test_empty_file_analysis() -> Result<(), Box<dyn std::error::Error>> {
915 let optimizer = ImportOptimizer::new();
916 let content = "";
917
918 let (_temp_dir, file_path) = create_test_file(content)?;
919 let analysis = optimizer.analyze_file(&file_path)?;
920
921 assert!(analysis.imports.is_empty());
922 assert!(analysis.unused_imports.is_empty());
923 assert!(analysis.missing_imports.is_empty());
924 assert!(analysis.duplicate_imports.is_empty());
925 Ok(())
926 }
927
928 #[test]
929 fn test_complex_perl_code_analysis() -> Result<(), Box<dyn std::error::Error>> {
930 let optimizer = ImportOptimizer::new();
931 let content = r#"#!/usr/bin/perl
932use strict;
933use warnings;
934use Data::Dumper;
935use JSON qw(encode_json decode_json);
936use LWP::UserAgent; # Unused
937use File::Spec::Functions qw(catfile catdir);
938
939# Complex code with various patterns
940my $data = { key => 'value', numbers => [1, 2, 3] };
941my $json_string = encode_json($data);
942print "JSON: $json_string\n";
943
944# Using File::Spec but not all imported functions
945my $path = catfile('/tmp', 'test.json');
946print "Path: $path\n";
947
948# Using modules without explicit imports
949my $response = HTTP::Tiny::new()->get('http://example.com');
950print Dumper($response);
951"#;
952
953 let (_temp_dir, file_path) = create_test_file(content)?;
954 let analysis = optimizer.analyze_file(&file_path)?;
955
956 assert!(analysis.unused_imports.iter().any(|u| u.module == "LWP::UserAgent"));
958
959 let file_spec_unused =
961 analysis.unused_imports.iter().find(|u| u.module == "File::Spec::Functions");
962 if let Some(unused) = file_spec_unused {
963 assert!(unused.symbols.contains(&"catdir".to_string()));
964 }
965
966 assert!(analysis.missing_imports.iter().any(|m| m.module == "HTTP::Tiny"));
968 Ok(())
969 }
970
971 #[test]
972 fn test_bare_import_with_exports_detection() -> Result<(), Box<dyn std::error::Error>> {
973 let optimizer = ImportOptimizer::new();
974 let content = r#"use strict;
975use warnings;
976use Data::Dumper; # Used
977use JSON; # Unused - has exports but none are used
978use SomeUnknownModule; # Conservative - not marked as unused
979
980print Dumper(\@ARGV);
981"#;
982
983 let (_temp_dir, file_path) = create_test_file(content)?;
984 let analysis = optimizer.analyze_file(&file_path)?;
985
986 assert!(!analysis.unused_imports.iter().any(|u| u.module == "Data::Dumper"));
988
989 assert!(analysis.unused_imports.is_empty());
992 Ok(())
993 }
994
995 #[test]
996 fn test_regex_edge_cases() -> Result<(), Box<dyn std::error::Error>> {
997 let optimizer = ImportOptimizer::new();
998 let content = r#"use strict;
999use warnings;
1000
1001# These should not be detected as module references
1002my $string = "This is not JSON::encode_json in a string";
1003my $regex = qr/Data::Dumper/;
1004print "Module::Name is just text";
1005
1006# This should be detected
1007my $result = JSON::encode_json({test => 1});
1008"#;
1009
1010 let (_temp_dir, file_path) = create_test_file(content)?;
1011 let analysis = optimizer.analyze_file(&file_path)?;
1012
1013 assert_eq!(analysis.missing_imports.len(), 1);
1015 assert_eq!(analysis.missing_imports[0].module, "JSON");
1016 Ok(())
1017 }
1018
1019 #[test]
1020 fn test_malformed_regex_capture_safety() -> Result<(), Box<dyn std::error::Error>> {
1021 let optimizer = ImportOptimizer::new();
1022 let content = r#"use strict;
1024use warnings;
1025
1026# Normal module usage
1027my $result = JSON::encode_json({test => 1});
1028
1029# Edge case patterns that might not fully match the regex
1030my $incomplete = "Something::";
1031my $partial = "::Function";
1032"#;
1033
1034 let (_temp_dir, file_path) = create_test_file(content)?;
1035 let analysis = optimizer.analyze_file(&file_path)?;
1037
1038 assert_eq!(analysis.missing_imports.len(), 1);
1040 assert_eq!(analysis.missing_imports[0].module, "JSON");
1041 Ok(())
1042 }
1043}