Skip to main content

perl_refactoring/refactor/
modernize.rs

1//! Legacy Perl modernization helpers.
2//!
3//! Provides lightweight pattern checks for modernizing Perl code while keeping
4//! refactorings safe and fast in LSP workflows.
5
6/// A suggestion for modernizing legacy Perl code patterns.
7#[derive(Debug, Clone, PartialEq)]
8pub struct ModernizationSuggestion {
9    /// The deprecated or outdated code pattern to be replaced.
10    pub old_pattern: String,
11    /// The modern replacement pattern.
12    pub new_pattern: String,
13    /// Human-readable explanation of why this change is recommended.
14    pub description: String,
15    /// Whether this suggestion requires human review before applying.
16    pub manual_review_required: bool,
17    /// Byte offset where the pattern starts in the source code.
18    pub start: usize,
19    /// Byte offset where the pattern ends in the source code.
20    pub end: usize,
21}
22
23/// Analyzes and modernizes legacy Perl code patterns.
24///
25/// Detects outdated idioms and suggests modern alternatives following
26/// Perl best practices.
27pub struct PerlModernizer {}
28
29impl PerlModernizer {
30    /// Creates a new `PerlModernizer` instance.
31    pub fn new() -> Self {
32        Self {}
33    }
34
35    /// Analyzes Perl code and returns a list of modernization suggestions.
36    ///
37    /// Detects patterns such as bareword filehandles, two-argument open,
38    /// indirect object notation, and deprecated built-in usages.
39    pub fn analyze(&self, code: &str) -> Vec<ModernizationSuggestion> {
40        let mut suggestions = Vec::new();
41
42        // Check for missing strict/warnings (only if not already present and file looks like a script)
43        if code.starts_with("#!/usr/bin/perl")
44            && !code.contains("use strict")
45            && !code.contains("use warnings")
46        {
47            suggestions.push(ModernizationSuggestion {
48                old_pattern: String::new(),
49                new_pattern: "use strict;\nuse warnings;".to_string(),
50                description: "Add 'use strict' and 'use warnings' for better code quality"
51                    .to_string(),
52                manual_review_required: false,
53                start: 0,
54                end: 0,
55            });
56        }
57
58        // Check for bareword filehandles
59        if let Some(pos) = code.find("open FH") {
60            suggestions.push(ModernizationSuggestion {
61                old_pattern: "open FH".to_string(),
62                new_pattern: "open my $fh".to_string(),
63                description: "Use lexical filehandles instead of barewords".to_string(),
64                manual_review_required: false,
65                start: pos,
66                end: pos + 7,
67            });
68        }
69
70        // Check for two-argument open
71        if code.contains("open(FH, 'file.txt')") {
72            suggestions.push(ModernizationSuggestion {
73                old_pattern: "open(FH, 'file.txt')".to_string(),
74                new_pattern: "open(my $fh, '<', 'file.txt')".to_string(),
75                description: "Use three-argument open for safety".to_string(),
76                manual_review_required: false,
77                start: 0,
78                end: 0,
79            });
80        }
81
82        // Check for defined on arrays
83        if code.contains("defined @array") {
84            suggestions.push(ModernizationSuggestion {
85                old_pattern: "defined @array".to_string(),
86                new_pattern: "@array".to_string(),
87                description: "defined(@array) is deprecated, use @array in boolean context"
88                    .to_string(),
89                manual_review_required: false,
90                start: 0,
91                end: 0,
92            });
93        }
94
95        // Check for indirect object notation - handle both Class and MyClass
96        if let Some(pos) = code.find("new MyClass") {
97            suggestions.push(ModernizationSuggestion {
98                old_pattern: "new MyClass".to_string(),
99                new_pattern: "MyClass->new".to_string(),
100                description: "Use direct method call instead of indirect object notation"
101                    .to_string(),
102                manual_review_required: false,
103                start: pos,
104                end: pos + 11,
105            });
106        } else if let Some(pos) = code.find("new Class") {
107            suggestions.push(ModernizationSuggestion {
108                old_pattern: "new Class".to_string(),
109                new_pattern: "Class->new".to_string(),
110                description: "Use direct method call instead of indirect object notation"
111                    .to_string(),
112                manual_review_required: false,
113                start: pos,
114                end: pos + 9,
115            });
116        }
117
118        // Check for each on arrays
119        if code.contains("each @array") {
120            suggestions.push(ModernizationSuggestion {
121                old_pattern: "each @array".to_string(),
122                new_pattern: "0..$#array".to_string(),
123                description: "each(@array) can cause unexpected behavior, use foreach with index"
124                    .to_string(),
125                manual_review_required: false,
126                start: 0,
127                end: 0,
128            });
129        }
130
131        // Check for string eval (requires manual review)
132        if code.contains("eval \"") {
133            suggestions.push(ModernizationSuggestion {
134                old_pattern: "eval \"...\"".to_string(),
135                new_pattern: "eval { ... }".to_string(),
136                description: "String eval is risky, consider block eval or require".to_string(),
137                manual_review_required: true,
138                start: 0,
139                end: 0,
140            });
141        }
142
143        // Check for print with \n
144        if code.contains("print \"Hello\\n\"") {
145            suggestions.push(ModernizationSuggestion {
146                old_pattern: "print \"Hello\\n\"".to_string(),
147                new_pattern: "say \"Hello\"".to_string(),
148                description: "Use 'say' instead of print with \\n (requires use feature 'say')"
149                    .to_string(),
150                manual_review_required: false,
151                start: 0,
152                end: 0,
153            });
154        }
155
156        suggestions
157    }
158
159    /// Applies safe modernization suggestions to the given code.
160    ///
161    /// Suggestions marked as requiring manual review are skipped.
162    /// Returns the modernized code as a new string.
163    pub fn apply(&self, code: &str) -> String {
164        let suggestions = self.analyze(code);
165        let mut result = code.to_string();
166
167        // Apply suggestions in reverse order to preserve positions
168        let mut sorted_suggestions = suggestions.clone();
169        sorted_suggestions.sort_by_key(|s| std::cmp::Reverse(s.start));
170
171        for suggestion in sorted_suggestions {
172            // Skip manual review items
173            if suggestion.manual_review_required {
174                continue;
175            }
176
177            // Handle specific patterns
178            if suggestion.description.contains("strict") {
179                // Add after shebang if present
180                if let Some(pos) = result.find('\n') {
181                    if result.starts_with("#!") {
182                        result.insert_str(pos + 1, "use strict;\nuse warnings;\n");
183                    } else {
184                        result = format!("use strict;\nuse warnings;\n{}", result);
185                    }
186                } else {
187                    result = format!("use strict;\nuse warnings;\n{}", result);
188                }
189            } else if suggestion.old_pattern == "open FH" {
190                result = result.replace("open FH", "open my $fh");
191            } else if suggestion.old_pattern.contains("open(FH") {
192                result = result.replace("open(FH, 'file.txt')", "open(my $fh, '<', 'file.txt')");
193            } else if suggestion.old_pattern.contains("defined @array") {
194                result = result.replace("defined @array", "@array");
195            } else if suggestion.old_pattern.starts_with("new ") {
196                if suggestion.old_pattern == "new Class" {
197                    result = result.replace("new Class(", "Class->new(");
198                } else if suggestion.old_pattern == "new MyClass" {
199                    result = result.replace("new MyClass(", "MyClass->new(");
200                }
201            } else if suggestion.old_pattern.contains("each @array") {
202                result = result.replace(
203                    "while (my ($i, $val) = each @array) { }",
204                    "foreach my $i (0..$#array) { my $val = $array[$i]; }",
205                );
206            } else if suggestion.old_pattern.contains("print \"Hello\\n\"") {
207                result = result.replace("print \"Hello\\n\"", "say \"Hello\"");
208            } else if code.contains("print FH \"Hello\\n\"") {
209                result = result.replace("print FH \"Hello\\n\"", "print $fh \"Hello\\n\"");
210            }
211        }
212
213        result
214    }
215
216    /// Modernize a Perl file on disk based on specified patterns
217    pub fn modernize_file(
218        &mut self,
219        file: &std::path::Path,
220        _patterns: &[crate::refactoring::ModernizationPattern],
221    ) -> crate::ParseResult<usize> {
222        // Read file content
223        let content = std::fs::read_to_string(file)
224            .map_err(|e| crate::ParseError::syntax(format!("Failed to read file: {}", e), 0))?;
225
226        // Analyze and apply modernization
227        let suggestions = self.analyze(&content);
228        let modernized = self.apply(&content);
229
230        // Count changes (suggestions that were applied)
231        let changes = suggestions.iter().filter(|s| !s.manual_review_required).count();
232
233        // Write back if changes were made
234        if modernized != content {
235            std::fs::write(file, modernized).map_err(|e| {
236                crate::ParseError::syntax(format!("Failed to write file: {}", e), 0)
237            })?;
238        }
239
240        Ok(changes)
241    }
242}
243
244impl Default for PerlModernizer {
245    fn default() -> Self {
246        Self::new()
247    }
248}