Skip to main content

perl_refactoring/refactor/
modernize_refactored.rs

1//! Refactored modernization engine with structured pattern definitions.
2//!
3//! Provides deterministic modernization checks with explicit pattern metadata,
4//! suitable for Analyze-stage code actions.
5
6use std::collections::HashMap;
7
8/// A suggestion for modernizing legacy Perl code patterns.
9#[derive(Debug, Clone, PartialEq)]
10pub struct ModernizationSuggestion {
11    /// The original code pattern that should be replaced.
12    pub old_pattern: String,
13    /// The modern replacement pattern.
14    pub new_pattern: String,
15    /// Human-readable explanation of why this change is recommended.
16    pub description: String,
17    /// Whether this suggestion requires manual review before applying.
18    pub manual_review_required: bool,
19    /// Start byte offset of the pattern in the source code.
20    pub start: usize,
21    /// End byte offset of the pattern in the source code.
22    pub end: usize,
23}
24
25/// A pattern definition for detecting legacy Perl idioms.
26#[derive(Debug, Clone)]
27#[allow(dead_code)]
28struct Pattern {
29    /// The pattern to search for in source code.
30    search: &'static str,
31    /// The recommended modern replacement.
32    replacement: &'static str,
33    /// Explanation of the modernization benefit.
34    description: &'static str,
35    /// Whether this pattern requires manual review when applying.
36    manual_review: bool,
37}
38
39/// Analyzer and transformer for modernizing Perl code.
40///
41/// Detects legacy Perl idioms and suggests modern replacements,
42/// such as replacing bareword filehandles with lexical variables
43/// or adding missing `use strict` and `use warnings` pragmas.
44pub struct PerlModernizer {
45    /// Collection of patterns to check against source code.
46    _patterns: Vec<Pattern>,
47}
48
49impl PerlModernizer {
50    /// Creates a new `PerlModernizer` with the default set of patterns.
51    pub fn new() -> Self {
52        let patterns = vec![
53            Pattern {
54                search: "open FH",
55                replacement: "open my $fh",
56                description: "Use lexical filehandles instead of barewords",
57                manual_review: false,
58            },
59            Pattern {
60                search: "open(FH, 'file.txt')",
61                replacement: "open(my $fh, '<', 'file.txt')",
62                description: "Use three-argument open for safety",
63                manual_review: false,
64            },
65            Pattern {
66                search: "defined @array",
67                replacement: "@array",
68                description: "defined(@array) is deprecated, use @array in boolean context",
69                manual_review: false,
70            },
71            Pattern {
72                search: "each @array",
73                replacement: "foreach with index",
74                description: "each(@array) can cause unexpected behavior, use foreach with index",
75                manual_review: false,
76            },
77            Pattern {
78                search: "eval \"",
79                replacement: "eval { }",
80                description: "String eval is risky, consider block eval or require",
81                manual_review: true,
82            },
83            Pattern {
84                search: "print \"Hello\\n\"",
85                replacement: "say \"Hello\"",
86                description: "Use 'say' instead of print with \\n (requires use feature 'say')",
87                manual_review: false,
88            },
89        ];
90
91        Self { _patterns: patterns }
92    }
93
94    /// Analyzes Perl source code and returns modernization suggestions.
95    ///
96    /// Checks for missing pragmas, bareword filehandles, deprecated patterns,
97    /// indirect object notation, and other legacy idioms.
98    pub fn analyze(&self, code: &str) -> Vec<ModernizationSuggestion> {
99        let mut suggestions = Vec::new();
100
101        // Check for missing pragmas in scripts
102        if self.looks_like_script(code) && self.missing_pragmas(code) {
103            suggestions.push(self.create_pragma_suggestion());
104        }
105
106        // Check for bareword filehandles
107        if let Some(suggestion) = self.check_bareword_filehandle(code) {
108            suggestions.push(suggestion);
109        }
110
111        // Check for two-arg open
112        if let Some(suggestion) = self.check_two_arg_open(code) {
113            suggestions.push(suggestion);
114        }
115
116        // Check for deprecated patterns
117        suggestions.extend(self.check_deprecated_patterns(code));
118
119        // Check for indirect object notation
120        if let Some(suggestion) = self.check_indirect_notation(code) {
121            suggestions.push(suggestion);
122        }
123
124        // Check for risky patterns
125        suggestions.extend(self.check_risky_patterns(code));
126
127        suggestions
128    }
129
130    /// Applies automatic modernization suggestions to the given code.
131    ///
132    /// Suggestions marked as requiring manual review are skipped.
133    /// Returns the transformed source code.
134    pub fn apply(&self, code: &str) -> String {
135        let suggestions = self.analyze(code);
136        self.apply_suggestions(code, suggestions)
137    }
138
139    // Helper methods
140    fn looks_like_script(&self, code: &str) -> bool {
141        code.starts_with("#!/usr/bin/perl")
142    }
143
144    fn missing_pragmas(&self, code: &str) -> bool {
145        !code.contains("use strict") && !code.contains("use warnings")
146    }
147
148    fn create_pragma_suggestion(&self) -> ModernizationSuggestion {
149        ModernizationSuggestion {
150            old_pattern: String::new(),
151            new_pattern: "use strict;\nuse warnings;".to_string(),
152            description: "Add 'use strict' and 'use warnings' for better code quality".to_string(),
153            manual_review_required: false,
154            start: 0,
155            end: 0,
156        }
157    }
158
159    fn check_bareword_filehandle(&self, code: &str) -> Option<ModernizationSuggestion> {
160        code.find("open FH").map(|pos| ModernizationSuggestion {
161            old_pattern: "open FH".to_string(),
162            new_pattern: "open my $fh".to_string(),
163            description: "Use lexical filehandles instead of barewords".to_string(),
164            manual_review_required: false,
165            start: pos,
166            end: pos + 7,
167        })
168    }
169
170    fn check_two_arg_open(&self, code: &str) -> Option<ModernizationSuggestion> {
171        if code.contains("open(FH, 'file.txt')") {
172            Some(ModernizationSuggestion {
173                old_pattern: "open(FH, 'file.txt')".to_string(),
174                new_pattern: "open(my $fh, '<', 'file.txt')".to_string(),
175                description: "Use three-argument open for safety".to_string(),
176                manual_review_required: false,
177                start: 0,
178                end: 0,
179            })
180        } else {
181            None
182        }
183    }
184
185    fn check_deprecated_patterns(&self, code: &str) -> Vec<ModernizationSuggestion> {
186        let mut suggestions = Vec::new();
187
188        if code.contains("defined @array") {
189            suggestions.push(ModernizationSuggestion {
190                old_pattern: "defined @array".to_string(),
191                new_pattern: "@array".to_string(),
192                description: "defined(@array) is deprecated, use @array in boolean context"
193                    .to_string(),
194                manual_review_required: false,
195                start: 0,
196                end: 0,
197            });
198        }
199
200        if code.contains("each @array") {
201            suggestions.push(ModernizationSuggestion {
202                old_pattern: "each @array".to_string(),
203                new_pattern: "0..$#array".to_string(),
204                description: "each(@array) can cause unexpected behavior, use foreach with index"
205                    .to_string(),
206                manual_review_required: false,
207                start: 0,
208                end: 0,
209            });
210        }
211
212        if code.contains("print \"Hello\\n\"") {
213            suggestions.push(ModernizationSuggestion {
214                old_pattern: "print \"Hello\\n\"".to_string(),
215                new_pattern: "say \"Hello\"".to_string(),
216                description: "Use 'say' instead of print with \\n (requires use feature 'say')"
217                    .to_string(),
218                manual_review_required: false,
219                start: 0,
220                end: 0,
221            });
222        }
223
224        suggestions
225    }
226
227    fn check_indirect_notation(&self, code: &str) -> Option<ModernizationSuggestion> {
228        // Check for common indirect object notation patterns
229        let indirect_patterns =
230            [("new MyClass", "MyClass->new", 11), ("new Class", "Class->new", 9)];
231
232        for (pattern, replacement, len) in &indirect_patterns {
233            if let Some(pos) = code.find(pattern) {
234                return Some(ModernizationSuggestion {
235                    old_pattern: pattern.to_string(),
236                    new_pattern: replacement.to_string(),
237                    description: "Use direct method call instead of indirect object notation"
238                        .to_string(),
239                    manual_review_required: false,
240                    start: pos,
241                    end: pos + len,
242                });
243            }
244        }
245
246        None
247    }
248
249    fn check_risky_patterns(&self, code: &str) -> Vec<ModernizationSuggestion> {
250        let mut suggestions = Vec::new();
251
252        if code.contains("eval \"") {
253            suggestions.push(ModernizationSuggestion {
254                old_pattern: "eval \"...\"".to_string(),
255                new_pattern: "eval { ... }".to_string(),
256                description: "String eval is risky, consider block eval or require".to_string(),
257                manual_review_required: true,
258                start: 0,
259                end: 0,
260            });
261        }
262
263        suggestions
264    }
265
266    fn apply_suggestions(&self, code: &str, suggestions: Vec<ModernizationSuggestion>) -> String {
267        let mut result = code.to_string();
268
269        // Sort suggestions by position (reverse) to maintain string positions
270        let mut sorted_suggestions = suggestions.clone();
271        sorted_suggestions.sort_by_key(|s| std::cmp::Reverse(s.start));
272
273        for suggestion in sorted_suggestions {
274            // Skip manual review items
275            if suggestion.manual_review_required {
276                continue;
277            }
278
279            result = self.apply_single_suggestion(result, &suggestion);
280        }
281
282        result
283    }
284
285    fn apply_single_suggestion(
286        &self,
287        mut code: String,
288        suggestion: &ModernizationSuggestion,
289    ) -> String {
290        // Handle pragma additions
291        if suggestion.description.contains("strict") {
292            return self.add_pragmas(code);
293        }
294
295        // Handle specific replacements
296        let replacements: HashMap<&str, (&str, &str)> = [
297            ("open FH", ("open FH", "open my $fh")),
298            ("open(FH, 'file.txt')", ("open(FH, 'file.txt')", "open(my $fh, '<', 'file.txt')")),
299            ("defined @array", ("defined @array", "@array")),
300            ("new Class", ("new Class(", "Class->new(")),
301            ("new MyClass", ("new MyClass(", "MyClass->new(")),
302            (
303                "each @array",
304                (
305                    "while (my ($i, $val) = each @array) { }",
306                    "foreach my $i (0..$#array) { my $val = $array[$i]; }",
307                ),
308            ),
309            ("print \"Hello\\n\"", ("print \"Hello\\n\"", "say \"Hello\"")),
310        ]
311        .into_iter()
312        .collect();
313
314        for (key, (from, to)) in replacements {
315            if suggestion.old_pattern.contains(key) {
316                code = code.replace(from, to);
317                break;
318            }
319        }
320
321        code
322    }
323
324    fn add_pragmas(&self, code: String) -> String {
325        if let Some(pos) = code.find('\n') {
326            if code.starts_with("#!") {
327                format!("{}\nuse strict;\nuse warnings;{}", &code[..pos], &code[pos..])
328            } else {
329                format!("use strict;\nuse warnings;\n{}", code)
330            }
331        } else {
332            format!("use strict;\nuse warnings;\n{}", code)
333        }
334    }
335}
336
337impl Default for PerlModernizer {
338    fn default() -> Self {
339        Self::new()
340    }
341}