Skip to main content

perl_parser/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;
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/// TextEdit for import optimization (local type for byte-offset ranges)
73///
74/// This is separate from LSP types which use line/character positions.
75/// Used internally for applying import optimization edits to source text.
76#[derive(Debug, Clone)]
77pub struct TextEdit {
78    /// Byte offset range (start, end) in the source text
79    pub range: (usize, usize),
80    /// Replacement text
81    pub new_text: String,
82}
83
84/// Result of import analysis containing all detected issues and suggestions
85#[derive(Debug, Serialize, Deserialize)]
86pub struct ImportAnalysis {
87    /// Import statements with unused symbols
88    pub unused_imports: Vec<UnusedImport>,
89    /// Symbols that are used but not imported
90    pub missing_imports: Vec<MissingImport>,
91    /// Modules that are imported multiple times
92    pub duplicate_imports: Vec<DuplicateImport>,
93    /// Suggestions for organizing imports
94    pub organization_suggestions: Vec<OrganizationSuggestion>,
95    /// All imports discovered in the file
96    pub imports: Vec<ImportEntry>,
97}
98
99/// An import statement containing unused symbols
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct UnusedImport {
102    /// Module name
103    pub module: String,
104    /// List of unused symbols from this import
105    pub symbols: Vec<String>,
106    /// Line number where this import statement appears (1-indexed)
107    pub line: usize,
108    /// Reason why symbols are considered unused
109    pub reason: String,
110}
111
112/// A symbol that is used but not imported
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct MissingImport {
115    /// Module name that should be imported
116    pub module: String,
117    /// List of symbols that need to be imported
118    pub symbols: Vec<String>,
119    /// Suggested line number to insert the import
120    pub suggested_location: usize,
121    /// Confidence level of the suggestion (0.0 to 1.0)
122    pub confidence: f32,
123}
124
125/// A module that is imported multiple times
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct DuplicateImport {
128    /// Module name that is duplicated
129    pub module: String,
130    /// Line numbers where this module is imported (1-indexed)
131    pub lines: Vec<usize>,
132    /// Whether these imports can be safely merged
133    pub can_merge: bool,
134}
135
136/// A suggestion for improving import organization
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct OrganizationSuggestion {
139    /// Human-readable description of the suggestion
140    pub description: String,
141    /// Priority level of this suggestion
142    pub priority: SuggestionPriority,
143}
144
145/// A single import statement discovered during analysis
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ImportEntry {
148    /// Module name
149    pub module: String,
150    /// List of imported symbols (empty for bare imports)
151    pub symbols: Vec<String>,
152    /// Line number where this import appears (1-indexed)
153    pub line: usize,
154}
155
156/// Priority level for organization suggestions
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158pub enum SuggestionPriority {
159    /// High priority - should be addressed immediately
160    High,
161    /// Medium priority - should be addressed when convenient
162    Medium,
163    /// Low priority - can be addressed later
164    Low,
165}
166
167/// Import optimizer for analyzing and optimizing Perl import statements
168///
169/// The optimizer currently supports:
170/// - Parsing basic `use Module qw(symbols)` statements
171/// - Detecting unused imported symbols
172/// - Finding duplicate imports that can be merged
173/// - Generating consolidated import statements
174pub struct ImportOptimizer;
175
176/// Check if a module is a pragma (affects compilation, no exports)
177fn 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
202/// Get known exports for popular Perl modules
203fn 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![]),    // DBI is object-oriented, no default exports
222        "strict" => Some(vec![]), // Pragma, no exports
223        "warnings" => Some(vec![]), // Pragma, no exports
224        "utf8" => Some(vec![]),   // Pragma, no exports
225        _ => None,
226    }
227}
228
229impl ImportOptimizer {
230    /// Create a new import optimizer for Analyze-stage refactorings.
231    ///
232    /// # Returns
233    ///
234    /// A ready-to-use `ImportOptimizer` instance.
235    ///
236    /// # Examples
237    ///
238    /// ```rust,ignore
239    /// use perl_parser::import_optimizer::ImportOptimizer;
240    ///
241    /// let optimizer = ImportOptimizer::new();
242    /// let _ = optimizer;
243    /// ```
244    pub fn new() -> Self {
245        Self
246    }
247
248    /// Analyze imports in a Perl file during the Analyze stage.
249    ///
250    /// # Arguments
251    /// * `file_path` - Path to the Perl file to analyze.
252    /// # Returns
253    /// `ImportAnalysis` with detected issues on success.
254    /// # Errors
255    /// Returns an error string if the file cannot be read or parsing fails.
256    /// # Examples
257    /// ```rust,ignore
258    /// use perl_parser::import_optimizer::ImportOptimizer;
259    ///
260    /// let optimizer = ImportOptimizer::new();
261    /// let _analysis = optimizer.analyze_file(std::path::Path::new("script.pl"))?;
262    /// # Ok::<(), String>(())
263    /// ```
264    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    /// Analyze imports in Perl content during the Analyze stage.
270    ///
271    /// # Arguments
272    /// * `content` - The Perl source code content to analyze.
273    /// # Returns
274    /// `ImportAnalysis` with detected issues on success.
275    /// # Errors
276    /// Returns an error string if regex parsing or analysis fails.
277    /// # Examples
278    /// ```rust,ignore
279    /// use perl_parser::import_optimizer::ImportOptimizer;
280    ///
281    /// let optimizer = ImportOptimizer::new();
282    /// let analysis = optimizer.analyze_content("use strict;")?;
283    /// assert!(analysis.imports.len() >= 1);
284    /// # Ok::<(), String>(())
285    /// ```
286    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        // Build map for duplicate detection
314        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        // Build content without `use` lines for symbol usage detection
329        let non_use_content = content
330            .lines()
331            .filter(
332                |line| {
333                    !line.trim_start().starts_with("use ") && !line.trim_start().starts_with("#")
334                }, // Exclude comment lines
335            )
336            .collect::<Vec<_>>()
337            .join(
338                "
339",
340            );
341
342        // Determine unused symbols for each import entry
343        let mut unused_imports = Vec::new();
344        for imp in &imports {
345            let mut unused_symbols = Vec::new();
346
347            // If there are explicit symbols (like qw()), check each one
348            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                    // Check if symbol is used in non-use content
354                    if !re.is_match(&non_use_content) {
355                        unused_symbols.push(sym.clone());
356                    }
357                }
358            } else {
359                // Skip pragma modules like strict, warnings, etc.
360                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                    // For bare imports (without qw()), check if the module or any of its known exports are used
376                    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                    // First check if the module is directly referenced (e.g., Module::function)
384                    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                    // Also check for qualified function calls like Module::function
391                    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                    // Special handling for Data::Dumper - check for Dumper function usage
401                    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                    // Then check if any known exports are used
408                    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                    // Conservative approach: Don't flag bare imports as unused if they have exports
421                    // Modules with exports might have side effects or implicit behavior we can't detect
422                    // But modules with no exports (like LWP::UserAgent) can still be flagged if unused
423                    if !is_used && is_known_module && known_exports.is_empty() {
424                        unused_symbols.push("(bare import)".to_string());
425                    }
426                }
427            }
428
429            // Create unused import entry if there are unused symbols
430            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        // Missing import detection
441        let imported_modules: BTreeSet<String> =
442            imports.iter().map(|imp| imp.module.clone()).collect();
443
444        // Strip strings and comments before scanning for Module::symbol patterns
445        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            // Only process if both capture groups matched
451            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        // Generate organization suggestions
478        let mut organization_suggestions = Vec::new();
479
480        // Suggest sorting of import statements
481        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        // Suggest removing duplicate imports
492        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        // Suggest sorting/deduplicating symbols within imports
502        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    /// Generate optimized import statements from analysis results.
531    ///
532    /// Used in the Analyze stage to prepare refactoring edits for imports.
533    ///
534    /// # Arguments
535    ///
536    /// * `analysis` - The import analysis results.
537    ///
538    /// # Returns
539    ///
540    /// A string containing optimized import statements, one per line.
541    ///
542    /// # Examples
543    ///
544    /// ```rust,ignore
545    /// use perl_parser::import_optimizer::ImportOptimizer;
546    ///
547    /// let optimizer = ImportOptimizer::new();
548    /// let analysis = optimizer.analyze_content("use strict;")?;
549    /// let imports = optimizer.generate_optimized_imports(&analysis);
550    /// assert!(!imports.is_empty());
551    /// # Ok::<(), String>(())
552    /// ```
553    pub fn generate_optimized_imports(&self, analysis: &ImportAnalysis) -> String {
554        let mut optimized_imports = Vec::new();
555
556        // Create a map to track which modules we want to keep and their symbols
557        let mut module_symbols: BTreeMap<String, Vec<String>> = BTreeMap::new();
558
559        // Get a list of all unused symbols per module
560        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        // Process existing imports, consolidating duplicates and removing unused symbols
569        for import in &analysis.imports {
570            // Keep only symbols that are not unused
571            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 // Keep all symbols if no unused symbols found for this module
579                    }
580                })
581                .cloned()
582                .collect();
583
584            // Add to module_symbols map (this automatically consolidates duplicates)
585            let entry = module_symbols.entry(import.module.clone()).or_default();
586            entry.extend(kept_symbols);
587
588            // Remove duplicates and sort for consistency
589            entry.sort();
590            entry.dedup();
591        }
592
593        // Add missing imports
594        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        // Generate import statements - only include modules that have symbols to import
602        // or are bare imports (originally had empty symbols)
603        for (module, symbols) in &module_symbols {
604            // Check if this was originally a bare import by seeing if any original import had empty symbols
605            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                // Bare import (like 'use strict;')
610                optimized_imports.push(format!("use {};", module));
611            } else if !symbols.is_empty() {
612                // Import with symbols
613                let symbol_list = symbols.join(" ");
614                optimized_imports.push(format!("use {} qw({});", module, symbol_list));
615            }
616            // Skip modules with no symbols that weren't originally bare imports (all symbols were unused)
617        }
618
619        // Sort alphabetically for consistency
620        optimized_imports.sort();
621        optimized_imports.join("\n")
622    }
623
624    /// Generate text edits to apply optimized imports during Analyze workflows.
625    ///
626    /// # Arguments
627    ///
628    /// * `content` - Original Perl source content.
629    /// * `analysis` - Import analysis results.
630    ///
631    /// # Returns
632    ///
633    /// Text edits to apply to the source document.
634    ///
635    /// # Examples
636    ///
637    /// ```rust,ignore
638    /// use perl_parser::import_optimizer::ImportOptimizer;
639    ///
640    /// let optimizer = ImportOptimizer::new();
641    /// let analysis = optimizer.analyze_content("use strict;")?;
642    /// let edits = optimizer.generate_edits("use strict;", &analysis);
643    /// assert!(!edits.is_empty());
644    /// # Ok::<(), String>(())
645    /// ```
646    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        // Defensive: use unwrap_or to handle edge cases where imports is unexpectedly empty
663        // (guard at line 581 should prevent this, but defensive programming is safer)
664        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; // include newline
686        }
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        // Data::Dumper should not be marked as unused since Dumper is used
731        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        // Bare imports without explicit symbols are assumed to have side effects,
750        // so they are not reported as unused even if their exports aren't referenced.
751        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        // Should detect unused symbols in both modules
867        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        // Should be sorted alphabetically
918        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        // Should detect unused imports
972        assert!(analysis.unused_imports.iter().any(|u| u.module == "LWP::UserAgent"));
973
974        // Should detect unused symbols from File::Spec::Functions
975        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        // Should detect missing import for HTTP::Tiny
982        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        // Data::Dumper should not be unused (Dumper is used)
1002        assert!(!analysis.unused_imports.iter().any(|u| u.module == "Data::Dumper"));
1003
1004        // JSON and SomeUnknownModule are treated as having potential side effects,
1005        // so neither is flagged as unused.
1006        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        // Should only detect the actual module usage, not the ones in strings/regex
1029        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        // Content with patterns that could potentially cause regex capture issues
1038        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        // Should not panic even with edge case patterns
1051        let analysis = optimizer.analyze_file(&file_path)?;
1052
1053        // Should detect JSON usage
1054        assert_eq!(analysis.missing_imports.len(), 1);
1055        assert_eq!(analysis.missing_imports[0].module, "JSON");
1056        Ok(())
1057    }
1058}