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<Regex> =
51    LazyLock::new(|| match Regex::new(r"^\s*use\s+([A-Za-z0-9_:]+)(?:\s+qw\(([^)]*)\))?\s*;") {
52        Ok(re) => re,
53        Err(_) => unreachable!("USE_STATEMENT_RE failed to compile"),
54    });
55
56static DUMPER_SYMBOL_RE: LazyLock<Regex> = LazyLock::new(|| match Regex::new(r"\bDumper\b") {
57    Ok(re) => re,
58    Err(_) => unreachable!("DUMPER_SYMBOL_RE failed to compile"),
59});
60
61static STRING_LITERAL_RE: LazyLock<Regex> =
62    LazyLock::new(|| match Regex::new("'[^']*'|\"[^\"]*\"") {
63        Ok(re) => re,
64        Err(_) => unreachable!("STRING_LITERAL_RE failed to compile"),
65    });
66
67static REGEX_LITERAL_RE: LazyLock<Regex> = LazyLock::new(|| match Regex::new(r"qr/[^/]*/") {
68    Ok(re) => re,
69    Err(_) => unreachable!("REGEX_LITERAL_RE failed to compile"),
70});
71
72static COMMENT_RE: LazyLock<Regex> = LazyLock::new(|| match Regex::new(r"(?m)#.*$") {
73    Ok(re) => re,
74    Err(_) => unreachable!("COMMENT_RE failed to compile"),
75});
76
77static MODULE_USAGE_RE: LazyLock<Regex> = LazyLock::new(|| {
78    match Regex::new(
79        r"\b([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)::([A-Za-z_][A-Za-z0-9_]*)",
80    ) {
81        Ok(re) => re,
82        Err(_) => unreachable!("MODULE_USAGE_RE failed to compile"),
83    }
84});
85
86/// TextEdit for import optimization (local type for byte-offset ranges)
87///
88/// This is separate from LSP types which use line/character positions.
89/// Used internally for applying import optimization edits to source text.
90#[derive(Debug, Clone)]
91pub struct TextEdit {
92    /// Byte offset range (start, end) in the source text
93    pub range: (usize, usize),
94    /// Replacement text
95    pub new_text: String,
96}
97
98/// Result of import analysis containing all detected issues and suggestions
99#[derive(Debug, Serialize, Deserialize)]
100pub struct ImportAnalysis {
101    /// Import statements with unused symbols
102    pub unused_imports: Vec<UnusedImport>,
103    /// Symbols that are used but not imported
104    pub missing_imports: Vec<MissingImport>,
105    /// Modules that are imported multiple times
106    pub duplicate_imports: Vec<DuplicateImport>,
107    /// Suggestions for organizing imports
108    pub organization_suggestions: Vec<OrganizationSuggestion>,
109    /// All imports discovered in the file
110    pub imports: Vec<ImportEntry>,
111}
112
113/// An import statement containing unused symbols
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct UnusedImport {
116    /// Module name
117    pub module: String,
118    /// List of unused symbols from this import
119    pub symbols: Vec<String>,
120    /// Line number where this import statement appears (1-indexed)
121    pub line: usize,
122    /// Reason why symbols are considered unused
123    pub reason: String,
124}
125
126/// A symbol that is used but not imported
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct MissingImport {
129    /// Module name that should be imported
130    pub module: String,
131    /// List of symbols that need to be imported
132    pub symbols: Vec<String>,
133    /// Suggested line number to insert the import
134    pub suggested_location: usize,
135    /// Confidence level of the suggestion (0.0 to 1.0)
136    pub confidence: f32,
137}
138
139/// A module that is imported multiple times
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct DuplicateImport {
142    /// Module name that is duplicated
143    pub module: String,
144    /// Line numbers where this module is imported (1-indexed)
145    pub lines: Vec<usize>,
146    /// Whether these imports can be safely merged
147    pub can_merge: bool,
148}
149
150/// A suggestion for improving import organization
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct OrganizationSuggestion {
153    /// Human-readable description of the suggestion
154    pub description: String,
155    /// Priority level of this suggestion
156    pub priority: SuggestionPriority,
157}
158
159/// A single import statement discovered during analysis
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ImportEntry {
162    /// Module name
163    pub module: String,
164    /// List of imported symbols (empty for bare imports)
165    pub symbols: Vec<String>,
166    /// Line number where this import appears (1-indexed)
167    pub line: usize,
168}
169
170/// Priority level for organization suggestions
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172pub enum SuggestionPriority {
173    /// High priority - should be addressed immediately
174    High,
175    /// Medium priority - should be addressed when convenient
176    Medium,
177    /// Low priority - can be addressed later
178    Low,
179}
180
181/// Import optimizer for analyzing and optimizing Perl import statements
182///
183/// The optimizer currently supports:
184/// - Parsing basic `use Module qw(symbols)` statements
185/// - Detecting unused imported symbols
186/// - Finding duplicate imports that can be merged
187/// - Generating consolidated import statements
188pub struct ImportOptimizer;
189
190/// Check if a module is a pragma (affects compilation, no exports)
191fn is_pragma_module(module: &str) -> bool {
192    matches!(
193        module,
194        "strict"
195            | "warnings"
196            | "utf8"
197            | "bytes"
198            | "locale"
199            | "integer"
200            | "less"
201            | "sigtrap"
202            | "subs"
203            | "vars"
204            | "feature"
205            | "autodie"
206            | "autouse"
207            | "base"
208            | "parent"
209            | "lib"
210            | "bigint"
211            | "bignum"
212            | "bigrat"
213    )
214}
215
216/// Get known exports for popular Perl modules
217fn get_known_module_exports(module: &str) -> Option<Vec<&'static str>> {
218    match module {
219        "Data::Dumper" => Some(vec!["Dumper"]),
220        "JSON" => Some(vec!["encode_json", "decode_json", "to_json", "from_json"]),
221        "YAML" => Some(vec!["Load", "Dump", "LoadFile", "DumpFile"]),
222        "Storable" => Some(vec!["store", "retrieve", "freeze", "thaw"]),
223        "List::Util" => Some(vec!["first", "max", "min", "sum", "reduce", "shuffle", "uniq"]),
224        "Scalar::Util" => Some(vec!["blessed", "reftype", "looks_like_number", "weaken"]),
225        "File::Spec" => Some(vec!["catfile", "catdir", "splitpath", "splitdir"]),
226        "File::Basename" => Some(vec!["basename", "dirname", "fileparse"]),
227        "Cwd" => Some(vec!["getcwd", "abs_path", "realpath"]),
228        "Time::HiRes" => Some(vec!["time", "sleep", "usleep", "gettimeofday"]),
229        "Digest::MD5" => Some(vec!["md5", "md5_hex", "md5_base64"]),
230        "MIME::Base64" => Some(vec!["encode_base64", "decode_base64"]),
231        "URI::Escape" => Some(vec!["uri_escape", "uri_unescape"]),
232        "LWP::Simple" => Some(vec!["get", "head", "getprint", "getstore", "mirror"]),
233        "LWP::UserAgent" => Some(vec![]),
234        "CGI" => Some(vec!["param", "header", "start_html", "end_html"]),
235        "DBI" => Some(vec![]),    // DBI is object-oriented, no default exports
236        "strict" => Some(vec![]), // Pragma, no exports
237        "warnings" => Some(vec![]), // Pragma, no exports
238        "utf8" => Some(vec![]),   // Pragma, no exports
239        _ => None,
240    }
241}
242
243impl ImportOptimizer {
244    /// Create a new import optimizer for Analyze-stage refactorings.
245    ///
246    /// # Returns
247    ///
248    /// A ready-to-use `ImportOptimizer` instance.
249    ///
250    /// # Examples
251    ///
252    /// ```rust,ignore
253    /// use perl_parser::import_optimizer::ImportOptimizer;
254    ///
255    /// let optimizer = ImportOptimizer::new();
256    /// let _ = optimizer;
257    /// ```
258    pub fn new() -> Self {
259        Self
260    }
261
262    /// Analyze imports in a Perl file during the Analyze stage.
263    ///
264    /// # Arguments
265    /// * `file_path` - Path to the Perl file to analyze.
266    /// # Returns
267    /// `ImportAnalysis` with detected issues on success.
268    /// # Errors
269    /// Returns an error string if the file cannot be read or parsing fails.
270    /// # Examples
271    /// ```rust,ignore
272    /// use perl_parser::import_optimizer::ImportOptimizer;
273    ///
274    /// let optimizer = ImportOptimizer::new();
275    /// let _analysis = optimizer.analyze_file(std::path::Path::new("script.pl"))?;
276    /// # Ok::<(), String>(())
277    /// ```
278    pub fn analyze_file(&self, file_path: &Path) -> Result<ImportAnalysis, String> {
279        let content = std::fs::read_to_string(file_path).map_err(|e| e.to_string())?;
280        self.analyze_content(&content)
281    }
282
283    /// Analyze imports in Perl content during the Analyze stage.
284    ///
285    /// # Arguments
286    /// * `content` - The Perl source code content to analyze.
287    /// # Returns
288    /// `ImportAnalysis` with detected issues on success.
289    /// # Errors
290    /// Returns an error string if regex parsing or analysis fails.
291    /// # Examples
292    /// ```rust,ignore
293    /// use perl_parser::import_optimizer::ImportOptimizer;
294    ///
295    /// let optimizer = ImportOptimizer::new();
296    /// let analysis = optimizer.analyze_content("use strict;")?;
297    /// assert!(analysis.imports.len() >= 1);
298    /// # Ok::<(), String>(())
299    /// ```
300    pub fn analyze_content(&self, content: &str) -> Result<ImportAnalysis, String> {
301        let mut imports = Vec::new();
302        for (idx, line) in content.lines().enumerate() {
303            if let Some(caps) = USE_STATEMENT_RE.captures(line) {
304                let module = caps[1].to_string();
305                let symbols_str = caps.get(2).map(|m| m.as_str()).unwrap_or("");
306                let symbols = if symbols_str.is_empty() {
307                    Vec::new()
308                } else {
309                    symbols_str
310                        .split_whitespace()
311                        .filter(|s| !s.is_empty())
312                        .map(|s| s.trim_matches(|c| c == ',' || c == ';' || c == '"'))
313                        .map(|s| s.to_string())
314                        .collect::<Vec<_>>()
315                };
316                imports.push(ImportEntry { module, symbols, line: idx + 1 });
317            }
318        }
319
320        // Build map for duplicate detection
321        let mut module_to_lines: BTreeMap<String, Vec<usize>> = BTreeMap::new();
322        for imp in &imports {
323            module_to_lines.entry(imp.module.clone()).or_default().push(imp.line);
324        }
325        let duplicate_imports = module_to_lines
326            .iter()
327            .filter(|(_, lines)| lines.len() > 1)
328            .map(|(module, lines)| DuplicateImport {
329                module: module.clone(),
330                lines: lines.clone(),
331                can_merge: true,
332            })
333            .collect::<Vec<_>>();
334
335        // Build content without `use` lines for symbol usage detection
336        let non_use_content = content
337            .lines()
338            .filter(
339                |line| {
340                    !line.trim_start().starts_with("use ") && !line.trim_start().starts_with("#")
341                }, // Exclude comment lines
342            )
343            .collect::<Vec<_>>()
344            .join(
345                "
346",
347            );
348
349        // Determine unused symbols for each import entry
350        let mut unused_imports = Vec::new();
351        for imp in &imports {
352            let mut unused_symbols = Vec::new();
353
354            // If there are explicit symbols (like qw()), check each one
355            if !imp.symbols.is_empty() {
356                for sym in &imp.symbols {
357                    let re = Regex::new(&format!(r"\b{}\b", regex::escape(sym)))
358                        .map_err(|e| e.to_string())?;
359
360                    // Check if symbol is used in non-use content
361                    if !re.is_match(&non_use_content) {
362                        unused_symbols.push(sym.clone());
363                    }
364                }
365            } else {
366                // Skip pragma modules like strict, warnings, etc.
367                let is_pragma = matches!(
368                    imp.module.as_str(),
369                    "strict"
370                        | "warnings"
371                        | "utf8"
372                        | "bytes"
373                        | "integer"
374                        | "locale"
375                        | "overload"
376                        | "sigtrap"
377                        | "subs"
378                        | "vars"
379                );
380
381                if !is_pragma {
382                    // For bare imports (without qw()), check if the module or any of its known exports are used
383                    let (is_known_module, known_exports) =
384                        match get_known_module_exports(&imp.module) {
385                            Some(exports) => (true, exports),
386                            None => (false, Vec::new()),
387                        };
388                    let mut is_used = false;
389
390                    // First check if the module is directly referenced (e.g., Module::function)
391                    let module_pattern = format!(r"\b{}\b", regex::escape(&imp.module));
392                    let module_re = Regex::new(&module_pattern).map_err(|e| e.to_string())?;
393                    if module_re.is_match(&non_use_content) {
394                        is_used = true;
395                    }
396
397                    // Also check for qualified function calls like Module::function
398                    if !is_used {
399                        let qualified_pattern = format!(r"{}::", regex::escape(&imp.module));
400                        let qualified_re =
401                            Regex::new(&qualified_pattern).map_err(|e| e.to_string())?;
402                        if qualified_re.is_match(&non_use_content) {
403                            is_used = true;
404                        }
405                    }
406
407                    // Special handling for Data::Dumper - check for Dumper function usage
408                    if !is_used && imp.module == "Data::Dumper" {
409                        if DUMPER_SYMBOL_RE.is_match(&non_use_content) {
410                            is_used = true;
411                        }
412                    }
413
414                    // Then check if any known exports are used
415                    if !is_used && !known_exports.is_empty() {
416                        for export in &known_exports {
417                            let export_pattern = format!(r"\b{}\b", regex::escape(export));
418                            let export_re =
419                                Regex::new(&export_pattern).map_err(|e| e.to_string())?;
420                            if export_re.is_match(&non_use_content) {
421                                is_used = true;
422                                break;
423                            }
424                        }
425                    }
426
427                    // Conservative approach: Don't flag bare imports as unused if they have exports
428                    // Modules with exports might have side effects or implicit behavior we can't detect
429                    // But modules with no exports (like LWP::UserAgent) can still be flagged if unused
430                    if !is_used && is_known_module && known_exports.is_empty() {
431                        unused_symbols.push("(bare import)".to_string());
432                    }
433                }
434            }
435
436            // Create unused import entry if there are unused symbols
437            if !unused_symbols.is_empty() {
438                unused_imports.push(UnusedImport {
439                    module: imp.module.clone(),
440                    symbols: unused_symbols,
441                    line: imp.line,
442                    reason: "Symbols not used in code".to_string(),
443                });
444            }
445        }
446
447        // Missing import detection
448        let imported_modules: BTreeSet<String> =
449            imports.iter().map(|imp| imp.module.clone()).collect();
450
451        // Strip strings and comments before scanning for Module::symbol patterns
452        let stripped = STRING_LITERAL_RE.replace_all(content, " ").to_string();
453        let stripped = REGEX_LITERAL_RE.replace_all(&stripped, " ").to_string();
454        let stripped = COMMENT_RE.replace_all(&stripped, " ").to_string();
455        let mut usage_map: BTreeMap<String, Vec<String>> = BTreeMap::new();
456        for caps in MODULE_USAGE_RE.captures_iter(&stripped) {
457            // Only process if both capture groups matched
458            if let (Some(module_match), Some(symbol_match)) = (caps.get(1), caps.get(2)) {
459                let module = module_match.as_str().to_string();
460                let symbol = symbol_match.as_str().to_string();
461
462                if imported_modules.contains(&module) || is_pragma_module(&module) {
463                    continue;
464                }
465
466                usage_map.entry(module).or_default().push(symbol);
467            }
468        }
469        let last_import_line = imports.iter().map(|i| i.line).max().unwrap_or(0);
470        let missing_imports = usage_map
471            .into_iter()
472            .map(|(module, mut symbols)| {
473                symbols.sort();
474                symbols.dedup();
475                MissingImport {
476                    module,
477                    symbols,
478                    suggested_location: last_import_line + 1,
479                    confidence: 0.8,
480                }
481            })
482            .collect::<Vec<_>>();
483
484        // Generate organization suggestions
485        let mut organization_suggestions = Vec::new();
486
487        // Suggest sorting of import statements
488        let module_order: Vec<String> = imports.iter().map(|i| i.module.clone()).collect();
489        let mut sorted_order = module_order.clone();
490        sorted_order.sort();
491        if module_order != sorted_order {
492            organization_suggestions.push(OrganizationSuggestion {
493                description: "Sort import statements alphabetically".to_string(),
494                priority: SuggestionPriority::Low,
495            });
496        }
497
498        // Suggest removing duplicate imports
499        if !duplicate_imports.is_empty() {
500            let modules =
501                duplicate_imports.iter().map(|d| d.module.clone()).collect::<Vec<_>>().join(", ");
502            organization_suggestions.push(OrganizationSuggestion {
503                description: format!("Remove duplicate imports for modules: {}", modules),
504                priority: SuggestionPriority::Medium,
505            });
506        }
507
508        // Suggest sorting/deduplicating symbols within imports
509        let mut symbols_need_org = false;
510        for imp in &imports {
511            if imp.symbols.len() > 1 {
512                let mut sorted = imp.symbols.clone();
513                sorted.sort();
514                sorted.dedup();
515                if sorted != imp.symbols {
516                    symbols_need_org = true;
517                    break;
518                }
519            }
520        }
521        if symbols_need_org {
522            organization_suggestions.push(OrganizationSuggestion {
523                description: "Sort and deduplicate symbols within import statements".to_string(),
524                priority: SuggestionPriority::Low,
525            });
526        }
527
528        Ok(ImportAnalysis {
529            imports,
530            unused_imports,
531            missing_imports,
532            duplicate_imports,
533            organization_suggestions,
534        })
535    }
536
537    /// Generate optimized import statements from analysis results.
538    ///
539    /// Used in the Analyze stage to prepare refactoring edits for imports.
540    ///
541    /// # Arguments
542    ///
543    /// * `analysis` - The import analysis results.
544    ///
545    /// # Returns
546    ///
547    /// A string containing optimized import statements, one per line.
548    ///
549    /// # Examples
550    ///
551    /// ```rust,ignore
552    /// use perl_parser::import_optimizer::ImportOptimizer;
553    ///
554    /// let optimizer = ImportOptimizer::new();
555    /// let analysis = optimizer.analyze_content("use strict;")?;
556    /// let imports = optimizer.generate_optimized_imports(&analysis);
557    /// assert!(!imports.is_empty());
558    /// # Ok::<(), String>(())
559    /// ```
560    pub fn generate_optimized_imports(&self, analysis: &ImportAnalysis) -> String {
561        let mut optimized_imports = Vec::new();
562
563        // Create a map to track which modules we want to keep and their symbols
564        let mut module_symbols: BTreeMap<String, Vec<String>> = BTreeMap::new();
565
566        // Get a list of all unused symbols per module
567        let mut unused_by_module: BTreeMap<String, Vec<String>> = BTreeMap::new();
568        for unused in &analysis.unused_imports {
569            unused_by_module
570                .entry(unused.module.clone())
571                .or_default()
572                .extend(unused.symbols.clone());
573        }
574
575        // Process existing imports, consolidating duplicates and removing unused symbols
576        for import in &analysis.imports {
577            // Keep only symbols that are not unused
578            let kept_symbols: Vec<String> = import
579                .symbols
580                .iter()
581                .filter(|sym| {
582                    if let Some(unused_symbols) = unused_by_module.get(&import.module) {
583                        !unused_symbols.contains(sym)
584                    } else {
585                        true // Keep all symbols if no unused symbols found for this module
586                    }
587                })
588                .cloned()
589                .collect();
590
591            // Add to module_symbols map (this automatically consolidates duplicates)
592            let entry = module_symbols.entry(import.module.clone()).or_default();
593            entry.extend(kept_symbols);
594
595            // Remove duplicates and sort for consistency
596            entry.sort();
597            entry.dedup();
598        }
599
600        // Add missing imports
601        for missing in &analysis.missing_imports {
602            let entry = module_symbols.entry(missing.module.clone()).or_default();
603            entry.extend(missing.symbols.clone());
604            entry.sort();
605            entry.dedup();
606        }
607
608        // Generate import statements - only include modules that have symbols to import
609        // or are bare imports (originally had empty symbols)
610        for (module, symbols) in &module_symbols {
611            // Check if this was originally a bare import by seeing if any original import had empty symbols
612            let was_bare_import =
613                analysis.imports.iter().any(|imp| imp.module == *module && imp.symbols.is_empty());
614
615            if symbols.is_empty() && was_bare_import {
616                // Bare import (like 'use strict;')
617                optimized_imports.push(format!("use {};", module));
618            } else if !symbols.is_empty() {
619                // Import with symbols
620                let symbol_list = symbols.join(" ");
621                optimized_imports.push(format!("use {} qw({});", module, symbol_list));
622            }
623            // Skip modules with no symbols that weren't originally bare imports (all symbols were unused)
624        }
625
626        // Sort alphabetically for consistency
627        optimized_imports.sort();
628        optimized_imports.join("\n")
629    }
630
631    /// Generate text edits to apply optimized imports during Analyze workflows.
632    ///
633    /// # Arguments
634    ///
635    /// * `content` - Original Perl source content.
636    /// * `analysis` - Import analysis results.
637    ///
638    /// # Returns
639    ///
640    /// Text edits to apply to the source document.
641    ///
642    /// # Examples
643    ///
644    /// ```rust,ignore
645    /// use perl_parser::import_optimizer::ImportOptimizer;
646    ///
647    /// let optimizer = ImportOptimizer::new();
648    /// let analysis = optimizer.analyze_content("use strict;")?;
649    /// let edits = optimizer.generate_edits("use strict;", &analysis);
650    /// assert!(!edits.is_empty());
651    /// # Ok::<(), String>(())
652    /// ```
653    pub fn generate_edits(&self, content: &str, analysis: &ImportAnalysis) -> Vec<TextEdit> {
654        let optimized = self.generate_optimized_imports(analysis);
655
656        if analysis.imports.is_empty() {
657            if optimized.is_empty() {
658                return Vec::new();
659            }
660            let insert_line =
661                analysis.missing_imports.first().map(|m| m.suggested_location).unwrap_or(1);
662            let insert_offset = self.line_offset(content, insert_line);
663            return vec![TextEdit {
664                range: (insert_offset, insert_offset),
665                new_text: optimized + "\n",
666            }];
667        }
668
669        // Defensive: use unwrap_or to handle edge cases where imports is unexpectedly empty
670        // (guard at line 581 should prevent this, but defensive programming is safer)
671        let first_line = analysis.imports.iter().map(|i| i.line).min().unwrap_or(1);
672        let last_line = analysis.imports.iter().map(|i| i.line).max().unwrap_or(1);
673
674        let start_offset = self.line_offset(content, first_line);
675        let end_offset = self.line_offset(content, last_line + 1);
676
677        vec![TextEdit {
678            range: (start_offset, end_offset),
679            new_text: if optimized.is_empty() { String::new() } else { optimized + "\n" },
680        }]
681    }
682
683    fn line_offset(&self, content: &str, line: usize) -> usize {
684        if line <= 1 {
685            return 0;
686        }
687        let mut offset = 0;
688        for (idx, l) in content.lines().enumerate() {
689            if idx + 1 >= line {
690                break;
691            }
692            offset += l.len() + 1; // include newline
693        }
694        offset
695    }
696}
697
698impl Default for ImportOptimizer {
699    fn default() -> Self {
700        Self::new()
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use std::fs;
708    use std::path::PathBuf;
709    use tempfile::TempDir;
710
711    fn create_test_file(content: &str) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
712        let temp_dir = TempDir::new()?;
713        let file_path = temp_dir.path().join("test.pl");
714        fs::write(&file_path, content)?;
715        Ok((temp_dir, file_path))
716    }
717
718    #[test]
719    fn test_basic_import_analysis() -> Result<(), Box<dyn std::error::Error>> {
720        let optimizer = ImportOptimizer::new();
721        let content = r#"#!/usr/bin/perl
722use strict;
723use warnings;
724use Data::Dumper;
725
726print Dumper(\@ARGV);
727"#;
728
729        let (_temp_dir, file_path) = create_test_file(content)?;
730        let analysis = optimizer.analyze_file(&file_path)?;
731
732        assert_eq!(analysis.imports.len(), 3);
733        assert_eq!(analysis.imports[0].module, "strict");
734        assert_eq!(analysis.imports[1].module, "warnings");
735        assert_eq!(analysis.imports[2].module, "Data::Dumper");
736
737        // Data::Dumper should not be marked as unused since Dumper is used
738        assert!(analysis.unused_imports.is_empty());
739        Ok(())
740    }
741
742    #[test]
743    fn test_unused_import_detection() -> Result<(), Box<dyn std::error::Error>> {
744        let optimizer = ImportOptimizer::new();
745        let content = r#"use strict;
746use warnings;
747use Data::Dumper;  # This is not used
748use JSON;          # This is not used
749
750print "Hello World\n";
751"#;
752
753        let (_temp_dir, file_path) = create_test_file(content)?;
754        let analysis = optimizer.analyze_file(&file_path)?;
755
756        // Bare imports without explicit symbols are assumed to have side effects,
757        // so they are not reported as unused even if their exports aren't referenced.
758        assert!(analysis.unused_imports.is_empty());
759        Ok(())
760    }
761
762    #[test]
763    fn test_missing_import_detection() -> Result<(), Box<dyn std::error::Error>> {
764        let optimizer = ImportOptimizer::new();
765        let content = r#"use strict;
766use warnings;
767
768# Using JSON::encode_json without importing JSON
769my $json = JSON::encode_json({key => 'value'});
770
771# Using Data::Dumper::Dumper without importing Data::Dumper
772print Data::Dumper::Dumper(\@ARGV);
773"#;
774
775        let (_temp_dir, file_path) = create_test_file(content)?;
776        let analysis = optimizer.analyze_file(&file_path)?;
777        assert_eq!(analysis.missing_imports.len(), 2);
778        assert!(analysis.missing_imports.iter().any(|m| m.module == "JSON"));
779        assert!(analysis.missing_imports.iter().any(|m| m.module == "Data::Dumper"));
780        for m in &analysis.missing_imports {
781            assert_eq!(m.suggested_location, 3);
782        }
783        Ok(())
784    }
785
786    #[test]
787    fn test_duplicate_import_detection() -> Result<(), Box<dyn std::error::Error>> {
788        let optimizer = ImportOptimizer::new();
789        let content = r#"use strict;
790use warnings;
791use Data::Dumper;
792use JSON;
793use Data::Dumper;  # Duplicate
794
795print Dumper(\@ARGV);
796"#;
797
798        let (_temp_dir, file_path) = create_test_file(content)?;
799        let analysis = optimizer.analyze_file(&file_path)?;
800
801        assert_eq!(analysis.duplicate_imports.len(), 1);
802        assert_eq!(analysis.duplicate_imports[0].module, "Data::Dumper");
803        assert_eq!(analysis.duplicate_imports[0].lines.len(), 2);
804        assert!(analysis.duplicate_imports[0].can_merge);
805        Ok(())
806    }
807
808    #[test]
809    fn test_organization_suggestions() -> Result<(), Box<dyn std::error::Error>> {
810        let optimizer = ImportOptimizer::new();
811        let content = r#"use warnings;
812use strict;
813use List::Util qw(max max min);
814use Data::Dumper;
815use Data::Dumper;  # duplicate
816"#;
817
818        let (_temp_dir, file_path) = create_test_file(content)?;
819        let analysis = optimizer.analyze_file(&file_path)?;
820
821        assert!(
822            analysis
823                .organization_suggestions
824                .iter()
825                .any(|s| s.description.contains("Sort import statements"))
826        );
827        assert!(
828            analysis
829                .organization_suggestions
830                .iter()
831                .any(|s| s.description.contains("Remove duplicate imports"))
832        );
833        assert!(
834            analysis
835                .organization_suggestions
836                .iter()
837                .any(|s| s.description.contains("Sort and deduplicate symbols"))
838        );
839        Ok(())
840    }
841
842    #[test]
843    fn test_qw_import_parsing() -> Result<(), Box<dyn std::error::Error>> {
844        let optimizer = ImportOptimizer::new();
845        let content = r#"use List::Util qw(first max min sum);
846use Scalar::Util qw(blessed reftype);
847
848my @nums = (1, 2, 3, 4, 5);
849print "Max: " . max(@nums) . "\n";
850print "Sum: " . sum(@nums) . "\n";
851print "First: " . first { $_ > 3 } @nums;
852"#;
853
854        let (_temp_dir, file_path) = create_test_file(content)?;
855        let analysis = optimizer.analyze_file(&file_path)?;
856
857        assert_eq!(analysis.imports.len(), 2);
858
859        let list_util = analysis
860            .imports
861            .iter()
862            .find(|i| i.module == "List::Util")
863            .ok_or("List::Util import not found")?;
864        assert_eq!(list_util.symbols, vec!["first", "max", "min", "sum"]);
865
866        let scalar_util = analysis
867            .imports
868            .iter()
869            .find(|i| i.module == "Scalar::Util")
870            .ok_or("Scalar::Util import not found")?;
871        assert_eq!(scalar_util.symbols, vec!["blessed", "reftype"]);
872
873        // Should detect unused symbols in both modules
874        assert_eq!(analysis.unused_imports.len(), 2);
875
876        let list_util_unused = analysis
877            .unused_imports
878            .iter()
879            .find(|u| u.module == "List::Util")
880            .ok_or("List::Util unused imports not found")?;
881        assert_eq!(list_util_unused.symbols, vec!["min"]);
882
883        let scalar_util_unused = analysis
884            .unused_imports
885            .iter()
886            .find(|u| u.module == "Scalar::Util")
887            .ok_or("Scalar::Util unused imports not found")?;
888        assert_eq!(scalar_util_unused.symbols, vec!["blessed", "reftype"]);
889        Ok(())
890    }
891
892    #[test]
893    fn test_generate_optimized_imports() {
894        let optimizer = ImportOptimizer::new();
895
896        let analysis = ImportAnalysis {
897            imports: vec![
898                ImportEntry { module: "strict".to_string(), symbols: vec![], line: 1 },
899                ImportEntry { module: "warnings".to_string(), symbols: vec![], line: 2 },
900                ImportEntry {
901                    module: "List::Util".to_string(),
902                    symbols: vec!["first".to_string(), "max".to_string(), "unused".to_string()],
903                    line: 3,
904                },
905            ],
906            unused_imports: vec![UnusedImport {
907                module: "List::Util".to_string(),
908                symbols: vec!["unused".to_string()],
909                line: 3,
910                reason: "Symbol not used".to_string(),
911            }],
912            missing_imports: vec![MissingImport {
913                module: "Data::Dumper".to_string(),
914                symbols: vec!["Dumper".to_string()],
915                suggested_location: 10,
916                confidence: 0.8,
917            }],
918            duplicate_imports: vec![],
919            organization_suggestions: vec![],
920        };
921
922        let optimized = optimizer.generate_optimized_imports(&analysis);
923
924        // Should be sorted alphabetically
925        let expected_lines = [
926            "use Data::Dumper qw(Dumper);",
927            "use List::Util qw(first max);",
928            "use strict;",
929            "use warnings;",
930        ];
931
932        assert_eq!(optimized, expected_lines.join("\n"));
933    }
934
935    #[test]
936    fn test_empty_file_analysis() -> Result<(), Box<dyn std::error::Error>> {
937        let optimizer = ImportOptimizer::new();
938        let content = "";
939
940        let (_temp_dir, file_path) = create_test_file(content)?;
941        let analysis = optimizer.analyze_file(&file_path)?;
942
943        assert!(analysis.imports.is_empty());
944        assert!(analysis.unused_imports.is_empty());
945        assert!(analysis.missing_imports.is_empty());
946        assert!(analysis.duplicate_imports.is_empty());
947        Ok(())
948    }
949
950    #[test]
951    fn test_complex_perl_code_analysis() -> Result<(), Box<dyn std::error::Error>> {
952        let optimizer = ImportOptimizer::new();
953        let content = r#"#!/usr/bin/perl
954use strict;
955use warnings;
956use Data::Dumper;
957use JSON qw(encode_json decode_json);
958use LWP::UserAgent;  # Unused
959use File::Spec::Functions qw(catfile catdir);
960
961# Complex code with various patterns
962my $data = { key => 'value', numbers => [1, 2, 3] };
963my $json_string = encode_json($data);
964print "JSON: $json_string\n";
965
966# Using File::Spec but not all imported functions
967my $path = catfile('/tmp', 'test.json');
968print "Path: $path\n";
969
970# Using modules without explicit imports
971my $response = HTTP::Tiny::new()->get('http://example.com');
972print Dumper($response);
973"#;
974
975        let (_temp_dir, file_path) = create_test_file(content)?;
976        let analysis = optimizer.analyze_file(&file_path)?;
977
978        // Should detect unused imports
979        assert!(analysis.unused_imports.iter().any(|u| u.module == "LWP::UserAgent"));
980
981        // Should detect unused symbols from File::Spec::Functions
982        let file_spec_unused =
983            analysis.unused_imports.iter().find(|u| u.module == "File::Spec::Functions");
984        if let Some(unused) = file_spec_unused {
985            assert!(unused.symbols.contains(&"catdir".to_string()));
986        }
987
988        // Should detect missing import for HTTP::Tiny
989        assert!(analysis.missing_imports.iter().any(|m| m.module == "HTTP::Tiny"));
990        Ok(())
991    }
992
993    #[test]
994    fn test_bare_import_with_exports_detection() -> Result<(), Box<dyn std::error::Error>> {
995        let optimizer = ImportOptimizer::new();
996        let content = r#"use strict;
997use warnings;
998use Data::Dumper;  # Used
999use JSON;          # Unused - has exports but none are used
1000use SomeUnknownModule;  # Conservative - not marked as unused
1001
1002print Dumper(\@ARGV);
1003"#;
1004
1005        let (_temp_dir, file_path) = create_test_file(content)?;
1006        let analysis = optimizer.analyze_file(&file_path)?;
1007
1008        // Data::Dumper should not be unused (Dumper is used)
1009        assert!(!analysis.unused_imports.iter().any(|u| u.module == "Data::Dumper"));
1010
1011        // JSON and SomeUnknownModule are treated as having potential side effects,
1012        // so neither is flagged as unused.
1013        assert!(analysis.unused_imports.is_empty());
1014        Ok(())
1015    }
1016
1017    #[test]
1018    fn test_regex_edge_cases() -> Result<(), Box<dyn std::error::Error>> {
1019        let optimizer = ImportOptimizer::new();
1020        let content = r#"use strict;
1021use warnings;
1022
1023# These should not be detected as module references
1024my $string = "This is not JSON::encode_json in a string";
1025my $regex = qr/Data::Dumper/;
1026print "Module::Name is just text";
1027
1028# This should be detected
1029my $result = JSON::encode_json({test => 1});
1030"#;
1031
1032        let (_temp_dir, file_path) = create_test_file(content)?;
1033        let analysis = optimizer.analyze_file(&file_path)?;
1034
1035        // Should only detect the actual module usage, not the ones in strings/regex
1036        assert_eq!(analysis.missing_imports.len(), 1);
1037        assert_eq!(analysis.missing_imports[0].module, "JSON");
1038        Ok(())
1039    }
1040
1041    #[test]
1042    fn test_malformed_regex_capture_safety() -> Result<(), Box<dyn std::error::Error>> {
1043        let optimizer = ImportOptimizer::new();
1044        // Content with patterns that could potentially cause regex capture issues
1045        let content = r#"use strict;
1046use warnings;
1047
1048# Normal module usage
1049my $result = JSON::encode_json({test => 1});
1050
1051# Edge case patterns that might not fully match the regex
1052my $incomplete = "Something::";
1053my $partial = "::Function";
1054"#;
1055
1056        let (_temp_dir, file_path) = create_test_file(content)?;
1057        // Should not panic even with edge case patterns
1058        let analysis = optimizer.analyze_file(&file_path)?;
1059
1060        // Should detect JSON usage
1061        assert_eq!(analysis.missing_imports.len(), 1);
1062        assert_eq!(analysis.missing_imports[0].module, "JSON");
1063        Ok(())
1064    }
1065}