Skip to main content

perl_refactoring/refactor/
import_optimizer.rs

1//! Import optimization for Perl modules
2//!
3//! This module analyzes import statements and usage to optimize imports by:
4//! - Detecting unused imports and symbols
5//! - Finding duplicate import statements
6//! - Consolidating imports to reduce clutter
7//! - Generating optimized import statements
8//!
9//! ## LSP Workflow Integration
10//!
11//! Import optimization operates within the **Perl LSP analysis pipeline**:
12//! **Parse → Index → Navigate → Complete → Analyze**
13//!
14//! - **Parse Stage**: Identifies import statements during Perl source analysis
15//! - **Index Stage**: Builds symbol index and resolves import dependencies
16//! - **Navigate Stage**: Tracks cross-file import dependencies for refactoring
17//! - **Complete Stage**: Generates optimized import statements for code actions
18//! - **Analyze Stage**: Updates workspace symbols and reference tracking
19//!
20//! Critical for maintaining clean imports in enterprise Perl development workflows
21//! where large Perl codebases require systematic dependency management.
22//!
23//! ## Performance
24//!
25//! - **Time complexity**: O(n) over import statements with O(1) symbol lookups
26//! - **Space complexity**: O(n) for import maps and symbol sets (memory bounded)
27//! - **Optimizations**: Fast-path parsing and deduplication to keep performance stable
28//! - **Benchmarks**: Typically <5ms per file in large workspace scans
29//! - **Large file scaling**: Designed to scale across large file sets (50GB PST-style)
30//!
31//! ## Example
32//!
33//! ```rust,ignore
34//! use perl_parser::import_optimizer::ImportOptimizer;
35//! use std::path::Path;
36//!
37//! let optimizer = ImportOptimizer::new();
38//! let analysis = optimizer.analyze_file(Path::new("script.pl"))?;
39//! let optimized_imports = optimizer.generate_optimized_imports(&analysis);
40//! println!("{}", optimized_imports);
41//! # Ok::<(), String>(())
42//! ```
43
44use regex::Regex;
45use serde::{Deserialize, Serialize};
46use std::collections::{BTreeMap, BTreeSet};
47use std::path::Path;
48
49/// TextEdit for import optimization (local type for byte-offset ranges)
50///
51/// This is separate from LSP types which use line/character positions.
52/// Used internally for applying import optimization edits to source text.
53#[derive(Debug, Clone)]
54pub struct TextEdit {
55    /// Byte offset range (start, end) in the source text
56    pub range: (usize, usize),
57    /// Replacement text
58    pub new_text: String,
59}
60
61/// Result of import analysis containing all detected issues and suggestions
62#[derive(Debug, Serialize, Deserialize)]
63pub struct ImportAnalysis {
64    /// Import statements with unused symbols
65    pub unused_imports: Vec<UnusedImport>,
66    /// Symbols that are used but not imported
67    pub missing_imports: Vec<MissingImport>,
68    /// Modules that are imported multiple times
69    pub duplicate_imports: Vec<DuplicateImport>,
70    /// Suggestions for organizing imports
71    pub organization_suggestions: Vec<OrganizationSuggestion>,
72    /// All imports discovered in the file
73    pub imports: Vec<ImportEntry>,
74}
75
76/// An import statement containing unused symbols
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct UnusedImport {
79    /// Module name
80    pub module: String,
81    /// List of unused symbols from this import
82    pub symbols: Vec<String>,
83    /// Line number where this import statement appears (1-indexed)
84    pub line: usize,
85    /// Reason why symbols are considered unused
86    pub reason: String,
87}
88
89/// A symbol that is used but not imported
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MissingImport {
92    /// Module name that should be imported
93    pub module: String,
94    /// List of symbols that need to be imported
95    pub symbols: Vec<String>,
96    /// Suggested line number to insert the import
97    pub suggested_location: usize,
98    /// Confidence level of the suggestion (0.0 to 1.0)
99    pub confidence: f32,
100}
101
102/// A module that is imported multiple times
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct DuplicateImport {
105    /// Module name that is duplicated
106    pub module: String,
107    /// Line numbers where this module is imported (1-indexed)
108    pub lines: Vec<usize>,
109    /// Whether these imports can be safely merged
110    pub can_merge: bool,
111}
112
113/// A suggestion for improving import organization
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct OrganizationSuggestion {
116    /// Human-readable description of the suggestion
117    pub description: String,
118    /// Priority level of this suggestion
119    pub priority: SuggestionPriority,
120}
121
122/// A single import statement discovered during analysis
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ImportEntry {
125    /// Module name
126    pub module: String,
127    /// List of imported symbols (empty for bare imports)
128    pub symbols: Vec<String>,
129    /// Line number where this import appears (1-indexed)
130    pub line: usize,
131}
132
133/// Priority level for organization suggestions
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135pub enum SuggestionPriority {
136    /// High priority - should be addressed immediately
137    High,
138    /// Medium priority - should be addressed when convenient
139    Medium,
140    /// Low priority - can be addressed later
141    Low,
142}
143
144/// Import optimizer for analyzing and optimizing Perl import statements
145///
146/// The optimizer currently supports:
147/// - Parsing basic `use Module qw(symbols)` statements
148/// - Detecting unused imported symbols
149/// - Finding duplicate imports that can be merged
150/// - Generating consolidated import statements
151pub struct ImportOptimizer;
152
153/// Check if a module is a pragma (affects compilation, no exports)
154fn 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
179/// Get known exports for popular Perl modules
180fn 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![]),    // DBI is object-oriented, no default exports
199        "strict" => Some(vec![]), // Pragma, no exports
200        "warnings" => Some(vec![]), // Pragma, no exports
201        "utf8" => Some(vec![]),   // Pragma, no exports
202        _ => None,
203    }
204}
205
206impl ImportOptimizer {
207    /// Create a new import optimizer for Analyze-stage refactorings.
208    ///
209    /// # Returns
210    ///
211    /// A ready-to-use `ImportOptimizer` instance.
212    ///
213    /// # Examples
214    ///
215    /// ```rust,ignore
216    /// use perl_parser::import_optimizer::ImportOptimizer;
217    ///
218    /// let optimizer = ImportOptimizer::new();
219    /// let _ = optimizer;
220    /// ```
221    pub fn new() -> Self {
222        Self
223    }
224
225    /// Analyze imports in a Perl file during the Analyze stage.
226    ///
227    /// # Arguments
228    /// * `file_path` - Path to the Perl file to analyze.
229    /// # Returns
230    /// `ImportAnalysis` with detected issues on success.
231    /// # Errors
232    /// Returns an error string if the file cannot be read or parsing fails.
233    /// # Examples
234    /// ```rust,ignore
235    /// use perl_parser::import_optimizer::ImportOptimizer;
236    ///
237    /// let optimizer = ImportOptimizer::new();
238    /// let _analysis = optimizer.analyze_file(std::path::Path::new("script.pl"))?;
239    /// # Ok::<(), String>(())
240    /// ```
241    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    /// Analyze imports in Perl content during the Analyze stage.
247    ///
248    /// # Arguments
249    /// * `content` - The Perl source code content to analyze.
250    /// # Returns
251    /// `ImportAnalysis` with detected issues on success.
252    /// # Errors
253    /// Returns an error string if regex parsing or analysis fails.
254    /// # Examples
255    /// ```rust,ignore
256    /// use perl_parser::import_optimizer::ImportOptimizer;
257    ///
258    /// let optimizer = ImportOptimizer::new();
259    /// let analysis = optimizer.analyze_content("use strict;")?;
260    /// assert!(analysis.imports.len() >= 1);
261    /// # Ok::<(), String>(())
262    /// ```
263    pub fn analyze_content(&self, content: &str) -> Result<ImportAnalysis, String> {
264        // Regex for basic `use` statement parsing
265        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        // Build map for duplicate detection
288        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        // Build content without `use` lines for symbol usage detection
303        let non_use_content = content
304            .lines()
305            .filter(
306                |line| {
307                    !line.trim_start().starts_with("use ") && !line.trim_start().starts_with("#")
308                }, // Exclude comment lines
309            )
310            .collect::<Vec<_>>()
311            .join(
312                "
313",
314            );
315
316        // Pre-compile regex for special Data::Dumper case
317        let dumper_re = Regex::new(r"\bDumper\b").map_err(|e| e.to_string())?;
318
319        // Determine unused symbols for each import entry
320        let mut unused_imports = Vec::new();
321        for imp in &imports {
322            let mut unused_symbols = Vec::new();
323
324            // If there are explicit symbols (like qw()), check each one
325            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                    // Check if symbol is used in non-use content
331                    if !re.is_match(&non_use_content) {
332                        unused_symbols.push(sym.clone());
333                    }
334                }
335            } else {
336                // Skip pragma modules like strict, warnings, etc.
337                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                    // For bare imports (without qw()), check if the module or any of its known exports are used
353                    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                    // First check if the module is directly referenced (e.g., Module::function)
361                    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                    // Also check for qualified function calls like Module::function
368                    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                    // Special handling for Data::Dumper - check for Dumper function usage
378                    if !is_used && imp.module == "Data::Dumper" {
379                        if dumper_re.is_match(&non_use_content) {
380                            is_used = true;
381                        }
382                    }
383
384                    // Then check if any known exports are used
385                    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                    // Conservative approach: Don't flag bare imports as unused if they have exports
398                    // Modules with exports might have side effects or implicit behavior we can't detect
399                    // But modules with no exports (like LWP::UserAgent) can still be flagged if unused
400                    if !is_used && is_known_module && known_exports.is_empty() {
401                        unused_symbols.push("(bare import)".to_string());
402                    }
403                }
404            }
405
406            // Create unused import entry if there are unused symbols
407            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        // Missing import detection
418        let imported_modules: BTreeSet<String> =
419            imports.iter().map(|imp| imp.module.clone()).collect();
420
421        // Strip strings and comments before scanning for Module::symbol patterns
422        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            // Only process if both capture groups matched
436            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        // Generate organization suggestions
463        let mut organization_suggestions = Vec::new();
464
465        // Suggest sorting of import statements
466        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        // Suggest removing duplicate imports
477        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        // Suggest sorting/deduplicating symbols within imports
487        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    /// Generate optimized import statements from analysis results.
516    ///
517    /// Used in the Analyze stage to prepare refactoring edits for imports.
518    ///
519    /// # Arguments
520    ///
521    /// * `analysis` - The import analysis results.
522    ///
523    /// # Returns
524    ///
525    /// A string containing optimized import statements, one per line.
526    ///
527    /// # Examples
528    ///
529    /// ```rust,ignore
530    /// use perl_parser::import_optimizer::ImportOptimizer;
531    ///
532    /// let optimizer = ImportOptimizer::new();
533    /// let analysis = optimizer.analyze_content("use strict;")?;
534    /// let imports = optimizer.generate_optimized_imports(&analysis);
535    /// assert!(!imports.is_empty());
536    /// # Ok::<(), String>(())
537    /// ```
538    pub fn generate_optimized_imports(&self, analysis: &ImportAnalysis) -> String {
539        let mut optimized_imports = Vec::new();
540
541        // Create a map to track which modules we want to keep and their symbols
542        let mut module_symbols: BTreeMap<String, Vec<String>> = BTreeMap::new();
543
544        // Get a list of all unused symbols per module
545        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        // Process existing imports, consolidating duplicates and removing unused symbols
554        for import in &analysis.imports {
555            // Keep only symbols that are not unused
556            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 // Keep all symbols if no unused symbols found for this module
564                    }
565                })
566                .cloned()
567                .collect();
568
569            // Add to module_symbols map (this automatically consolidates duplicates)
570            let entry = module_symbols.entry(import.module.clone()).or_default();
571            entry.extend(kept_symbols);
572
573            // Remove duplicates and sort for consistency
574            entry.sort();
575            entry.dedup();
576        }
577
578        // Add missing imports
579        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        // Generate import statements - only include modules that have symbols to import
587        // or are bare imports (originally had empty symbols)
588        for (module, symbols) in &module_symbols {
589            // Check if this was originally a bare import by seeing if any original import had empty symbols
590            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                // Bare import (like 'use strict;')
595                optimized_imports.push(format!("use {};", module));
596            } else if !symbols.is_empty() {
597                // Import with symbols
598                let symbol_list = symbols.join(" ");
599                optimized_imports.push(format!("use {} qw({});", module, symbol_list));
600            }
601            // Skip modules with no symbols that weren't originally bare imports (all symbols were unused)
602        }
603
604        // Sort alphabetically for consistency
605        optimized_imports.sort();
606        optimized_imports.join("\n")
607    }
608
609    /// Generate text edits to apply optimized imports during Analyze workflows.
610    ///
611    /// # Arguments
612    ///
613    /// * `content` - Original Perl source content.
614    /// * `analysis` - Import analysis results.
615    ///
616    /// # Returns
617    ///
618    /// Text edits to apply to the source document.
619    ///
620    /// # Examples
621    ///
622    /// ```rust,ignore
623    /// use perl_parser::import_optimizer::ImportOptimizer;
624    ///
625    /// let optimizer = ImportOptimizer::new();
626    /// let analysis = optimizer.analyze_content("use strict;")?;
627    /// let edits = optimizer.generate_edits("use strict;", &analysis);
628    /// assert!(!edits.is_empty());
629    /// # Ok::<(), String>(())
630    /// ```
631    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        // Defensive: use unwrap_or to handle edge cases where imports is unexpectedly empty
648        // (guard at line 581 should prevent this, but defensive programming is safer)
649        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; // include newline
671        }
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        // Data::Dumper should not be marked as unused since Dumper is used
716        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        // Bare imports without explicit symbols are assumed to have side effects,
735        // so they are not reported as unused even if their exports aren't referenced.
736        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        // Should detect unused symbols in both modules
852        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        // Should be sorted alphabetically
903        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        // Should detect unused imports
957        assert!(analysis.unused_imports.iter().any(|u| u.module == "LWP::UserAgent"));
958
959        // Should detect unused symbols from File::Spec::Functions
960        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        // Should detect missing import for HTTP::Tiny
967        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        // Data::Dumper should not be unused (Dumper is used)
987        assert!(!analysis.unused_imports.iter().any(|u| u.module == "Data::Dumper"));
988
989        // JSON and SomeUnknownModule are treated as having potential side effects,
990        // so neither is flagged as unused.
991        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        // Should only detect the actual module usage, not the ones in strings/regex
1014        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        // Content with patterns that could potentially cause regex capture issues
1023        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        // Should not panic even with edge case patterns
1036        let analysis = optimizer.analyze_file(&file_path)?;
1037
1038        // Should detect JSON usage
1039        assert_eq!(analysis.missing_imports.len(), 1);
1040        assert_eq!(analysis.missing_imports[0].module, "JSON");
1041        Ok(())
1042    }
1043}