1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct ModernizationSuggestion {
11 pub old_pattern: String,
13 pub new_pattern: String,
15 pub description: String,
17 pub manual_review_required: bool,
19 pub start: usize,
21 pub end: usize,
23}
24
25#[derive(Debug, Clone)]
27#[allow(dead_code)]
28struct Pattern {
29 search: &'static str,
31 replacement: &'static str,
33 description: &'static str,
35 manual_review: bool,
37}
38
39pub struct PerlModernizer {
45 _patterns: Vec<Pattern>,
47}
48
49impl PerlModernizer {
50 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 pub fn analyze(&self, code: &str) -> Vec<ModernizationSuggestion> {
99 let mut suggestions = Vec::new();
100
101 if self.looks_like_script(code) && self.missing_pragmas(code) {
103 suggestions.push(self.create_pragma_suggestion());
104 }
105
106 if let Some(suggestion) = self.check_bareword_filehandle(code) {
108 suggestions.push(suggestion);
109 }
110
111 if let Some(suggestion) = self.check_two_arg_open(code) {
113 suggestions.push(suggestion);
114 }
115
116 suggestions.extend(self.check_deprecated_patterns(code));
118
119 if let Some(suggestion) = self.check_indirect_notation(code) {
121 suggestions.push(suggestion);
122 }
123
124 suggestions.extend(self.check_risky_patterns(code));
126
127 suggestions
128 }
129
130 pub fn apply(&self, code: &str) -> String {
135 let suggestions = self.analyze(code);
136 self.apply_suggestions(code, suggestions)
137 }
138
139 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 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 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 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 if suggestion.description.contains("strict") {
292 return self.add_pragmas(code);
293 }
294
295 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}