1#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11use perl_module_import_match::line_references_module_import;
12use perl_module_token::{module_variant_pairs, replace_module_token};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ModuleLineEdit {
17 pub line: usize,
19 pub start_character: usize,
21 pub end_character: usize,
23 pub new_text: String,
25}
26
27#[must_use]
47pub fn plan_module_rename_edits(
48 source: &str,
49 old_module: &str,
50 new_module: &str,
51) -> Vec<ModuleLineEdit> {
52 if source.is_empty()
53 || old_module.is_empty()
54 || new_module.is_empty()
55 || old_module == new_module
56 {
57 return Vec::new();
58 }
59
60 let variants = module_variant_pairs(old_module, new_module);
61 let mut edits = Vec::new();
62
63 for (line_idx, line) in source.lines().enumerate() {
64 let mut rewritten: Option<String> = None;
65
66 for (old_variant, new_variant) in &variants {
67 {
74 let current_line = rewritten.as_deref().unwrap_or(line);
75 if line_references_module_import(current_line, old_variant) {
76 let (candidate, changed) =
77 replace_module_token(current_line, old_variant, new_variant);
78 if changed {
79 rewritten = Some(candidate);
80 }
81 }
82 }
83
84 {
87 let current_line = rewritten.as_deref().unwrap_or(line);
88 if line_references_isa_assignment(current_line, old_variant) {
89 let (candidate, changed) =
90 replace_module_token(current_line, old_variant, new_variant);
91 if changed {
92 rewritten = Some(candidate);
93 }
94 }
95 }
96
97 {
100 let current_line = rewritten.as_deref().unwrap_or(line);
101 if line_references_qualified_call(current_line, old_variant) {
102 let candidate =
103 replace_module_name_prefix(current_line, old_variant, new_variant);
104 if candidate != current_line {
105 rewritten = Some(candidate);
106 }
107 }
108 }
109 }
110
111 if let Some(new_text) = rewritten {
112 edits.push(ModuleLineEdit {
113 line: line_idx,
114 start_character: 0,
115 end_character: line.len(),
116 new_text,
117 });
118 }
119 }
120
121 edits
122}
123
124#[must_use]
132pub fn line_references_isa_assignment(line: &str, module_name: &str) -> bool {
133 if line.is_empty() || module_name.is_empty() {
134 return false;
135 }
136 if !line.contains("@ISA") {
138 return false;
139 }
140 perl_module_token::contains_module_token(line, module_name)
143}
144
145#[must_use]
157pub fn line_references_qualified_call(line: &str, module_name: &str) -> bool {
158 if line.is_empty() || module_name.is_empty() {
159 return false;
160 }
161 let needle = format!("{}::", module_name);
163 let needle_bytes = needle.as_bytes();
164 let line_bytes = line.as_bytes();
165 let needle_len = needle_bytes.len();
166
167 if line_bytes.len() < needle_len {
168 return false;
169 }
170
171 let mut start = 0usize;
172 while start + needle_len <= line_bytes.len() {
173 let Some(rel) = line[start..].find(needle.as_str()) else {
174 break;
175 };
176 let abs = start + rel;
177 let after = abs + needle_len;
178
179 let before_ok = abs == 0 || {
182 let ch = line_bytes[abs - 1] as char;
183 !ch.is_alphanumeric() && ch != '_' && ch != ':'
184 };
185
186 let after_ok = after < line_bytes.len() && {
189 let ch = line_bytes[after] as char;
190 ch.is_alphabetic() || ch == '_'
191 };
192
193 if before_ok && after_ok {
194 return true;
195 }
196 start = abs + 1;
197 }
198
199 false
200}
201
202#[must_use]
209pub fn replace_module_name_prefix(line: &str, old_module: &str, new_module: &str) -> String {
210 if old_module.is_empty() || new_module.is_empty() || line.is_empty() {
211 return line.to_string();
212 }
213
214 let needle = format!("{}::", old_module);
215 let replacement = format!("{}::", new_module);
216 let needle_bytes = needle.as_bytes();
217 let needle_len = needle_bytes.len();
218 let line_bytes = line.as_bytes();
219
220 if line_bytes.len() < needle_len {
221 return line.to_string();
222 }
223
224 let mut out = String::with_capacity(line.len());
225 let mut cursor = 0usize;
226
227 while cursor + needle_len <= line_bytes.len() {
228 let Some(rel) = line[cursor..].find(needle.as_str()) else {
229 break;
230 };
231 let abs = cursor + rel;
232 let after = abs + needle_len;
233
234 let before_ok = abs == 0 || {
235 let ch = line_bytes[abs - 1] as char;
236 !ch.is_alphanumeric() && ch != '_' && ch != ':'
237 };
238
239 let after_ok = after < line_bytes.len() && {
240 let ch = line_bytes[after] as char;
241 ch.is_alphabetic() || ch == '_'
242 };
243
244 if before_ok && after_ok {
245 out.push_str(&line[cursor..abs]);
246 out.push_str(&replacement);
247 cursor = after;
248 } else {
249 out.push_str(&line[cursor..abs + 1]);
251 cursor = abs + 1;
252 }
253 }
254
255 out.push_str(&line[cursor..]);
256 out
257}
258
259#[must_use]
261pub fn apply_module_rename_edits(source: &str, edits: &[ModuleLineEdit]) -> String {
262 if edits.is_empty() {
263 return source.to_string();
264 }
265
266 let mut lines: Vec<String> = source.split('\n').map(ToString::to_string).collect();
267
268 let mut sorted = edits.to_vec();
269 sorted.sort_by_key(|edit| edit.line);
270
271 for edit in sorted {
272 if let Some(line) = lines.get_mut(edit.line) {
273 *line = edit.new_text;
274 }
275 }
276
277 lines.join("\n")
278}
279#[cfg(test)]
280mod tests {
281 use super::{
282 ModuleLineEdit, apply_module_rename_edits, line_references_isa_assignment,
283 line_references_qualified_call, plan_module_rename_edits, replace_module_name_prefix,
284 };
285 use perl_module_token::{module_variant_pairs, replace_module_token};
286
287 #[test]
288 fn plans_basic_use_and_require_edits() {
289 let source = "use Foo::Bar;\nrequire Foo::Bar;\n";
290 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Module");
291
292 assert_eq!(
293 edits,
294 vec![
295 ModuleLineEdit {
296 line: 0,
297 start_character: 0,
298 end_character: "use Foo::Bar;".len(),
299 new_text: "use New::Module;".to_string(),
300 },
301 ModuleLineEdit {
302 line: 1,
303 start_character: 0,
304 end_character: "require Foo::Bar;".len(),
305 new_text: "require New::Module;".to_string(),
306 },
307 ]
308 );
309 }
310
311 #[test]
312 fn plans_parent_and_base_edits() {
313 let source = "use parent 'Foo::Bar';\nuse base \"Foo::Bar\";\nuse parent qw(Foo::Bar Other::Base);\n";
314 let edits = plan_module_rename_edits(source, "Foo::Bar", "Renamed::Base");
315 let rewritten = apply_module_rename_edits(source, &edits);
316
317 let expected = "use parent 'Renamed::Base';\nuse base \"Renamed::Base\";\nuse parent qw(Renamed::Base Other::Base);\n";
318 assert_eq!(rewritten, expected);
319 }
320
321 #[test]
322 fn handles_legacy_separator_variants() {
323 let source = "use Foo'Bar;\nuse parent \"Foo'Bar\";\n";
324 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Path");
325 let rewritten = apply_module_rename_edits(source, &edits);
326
327 assert_eq!(rewritten, "use New'Path;\nuse parent \"New'Path\";\n");
328 }
329
330 #[test]
331 fn does_not_touch_partial_module_names() {
332 let source = "use Foo::Barista;\n";
333 let edits = plan_module_rename_edits(source, "Foo::Bar", "Renamed::Module");
334 assert!(edits.is_empty());
335 }
336
337 #[test]
338 fn apply_edits_replaces_target_lines_only() {
339 let source = "line1\nline2\nline3\n";
340 let edits = vec![ModuleLineEdit {
341 line: 1,
342 start_character: 0,
343 end_character: 5,
344 new_text: "updated".to_string(),
345 }];
346
347 let rewritten = apply_module_rename_edits(source, &edits);
348 assert_eq!(rewritten, "line1\nupdated\nline3\n");
349 }
350
351 #[test]
352 fn module_variant_generation_deduplicates_when_not_needed() {
353 let variants = module_variant_pairs("strict", "warnings");
354 assert_eq!(variants.len(), 1);
355 }
356
357 #[test]
358 fn token_replacement_requires_boundaries() {
359 let (rewritten, changed) = replace_module_token("use Foo::Barista;", "Foo::Bar", "X::Y");
360 assert_eq!(rewritten, "use Foo::Barista;");
361 assert!(!changed);
362
363 let (rewritten, changed) = replace_module_token("use Foo::Bar;", "Foo::Bar", "X::Y");
364 assert_eq!(rewritten, "use X::Y;");
365 assert!(changed);
366 }
367
368 #[test]
369 fn plans_use_parent_simple_name_no_colons() {
370 let source = "package Child;\nuse parent 'MyBase';\n1;\n";
372 let edits = plan_module_rename_edits(source, "MyBase", "RenamedBase");
373 let rewritten = apply_module_rename_edits(source, &edits);
374 assert!(
375 rewritten.contains("use parent 'RenamedBase'"),
376 "Expected rewrite of use parent 'MyBase' to 'RenamedBase', got: {:?}",
377 rewritten
378 );
379 }
380
381 #[test]
384 fn isa_assignment_detected_single_quoted() {
385 assert!(line_references_isa_assignment("@ISA = ('Foo::Bar');", "Foo::Bar"));
386 }
387
388 #[test]
389 fn isa_assignment_detected_double_quoted() {
390 assert!(line_references_isa_assignment("@ISA = (\"Foo::Bar\");", "Foo::Bar"));
391 }
392
393 #[test]
394 fn isa_assignment_detected_qw() {
395 assert!(line_references_isa_assignment("our @ISA = qw(Foo::Bar Other::Base);", "Foo::Bar"));
396 }
397
398 #[test]
399 fn isa_push_detected() {
400 assert!(line_references_isa_assignment("push @ISA, 'Foo::Bar';", "Foo::Bar"));
401 }
402
403 #[test]
404 fn isa_assignment_rejects_absent_module() {
405 assert!(!line_references_isa_assignment("@ISA = ('Other::Base');", "Foo::Bar"));
406 }
407
408 #[test]
409 fn isa_assignment_rejects_no_isa() {
410 assert!(!line_references_isa_assignment("use Foo::Bar;", "Foo::Bar"));
411 }
412
413 #[test]
414 fn isa_assignment_rejects_partial_module_name() {
415 assert!(!line_references_isa_assignment("@ISA = ('Foo::Bar');", "Bar"));
418 }
419
420 #[test]
423 fn plans_isa_assignment_single_quoted() {
424 let source = "@ISA = ('Foo::Bar');\n";
425 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Module");
426 let rewritten = apply_module_rename_edits(source, &edits);
427 assert_eq!(rewritten, "@ISA = ('New::Module');\n");
428 }
429
430 #[test]
431 fn plans_isa_assignment_qw() {
432 let source = "our @ISA = qw(Foo::Bar Other::Base);\n";
433 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Module");
434 let rewritten = apply_module_rename_edits(source, &edits);
435 assert_eq!(rewritten, "our @ISA = qw(New::Module Other::Base);\n");
436 }
437
438 #[test]
439 fn plans_isa_push() {
440 let source = "push @ISA, 'Foo::Bar';\n";
441 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Module");
442 let rewritten = apply_module_rename_edits(source, &edits);
443 assert_eq!(rewritten, "push @ISA, 'New::Module';\n");
444 }
445
446 #[test]
449 fn qualified_call_detected_direct_function() {
450 assert!(line_references_qualified_call("Foo::Bar::baz();", "Foo::Bar"));
451 }
452
453 #[test]
454 fn qualified_call_detected_in_expression() {
455 assert!(line_references_qualified_call("my $x = Foo::Bar::create($arg);", "Foo::Bar"));
456 }
457
458 #[test]
459 fn qualified_call_rejects_standalone_module() {
460 assert!(!line_references_qualified_call("use Foo::Bar;", "Foo::Bar"));
462 }
463
464 #[test]
465 fn qualified_call_rejects_deeper_prefix() {
466 assert!(!line_references_qualified_call("Extra::Foo::Bar::baz();", "Foo::Bar"));
468 }
469
470 #[test]
471 fn qualified_call_rejects_empty_inputs() {
472 assert!(!line_references_qualified_call("", "Foo::Bar"));
473 assert!(!line_references_qualified_call("Foo::Bar::baz();", ""));
474 }
475
476 #[test]
479 fn plans_qualified_function_call() {
480 let source = "my $x = Foo::Bar::create($arg);\n";
481 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Module");
482 let rewritten = apply_module_rename_edits(source, &edits);
483 assert_eq!(rewritten, "my $x = New::Module::create($arg);\n");
484 }
485
486 #[test]
487 fn plans_qualified_call_preserves_function_name() {
488 let source = "Foo::Bar::baz();\n";
489 let edits = plan_module_rename_edits(source, "Foo::Bar", "Renamed::Pkg");
490 let rewritten = apply_module_rename_edits(source, &edits);
491 assert_eq!(rewritten, "Renamed::Pkg::baz();\n");
492 }
493
494 #[test]
495 fn plans_qualified_call_does_not_affect_deeper_prefix() {
496 let source = "Extra::Foo::Bar::baz();\n";
498 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Module");
499 assert!(edits.is_empty(), "Expected no edits, got: {:?}", edits);
501 }
502
503 #[test]
504 fn plans_multiple_qualified_calls_on_same_line() {
505 let source = "my $a = Foo::Bar::new(); my $b = Foo::Bar::clone();\n";
506 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Mod");
507 let rewritten = apply_module_rename_edits(source, &edits);
508 assert_eq!(rewritten, "my $a = New::Mod::new(); my $b = New::Mod::clone();\n");
509 }
510
511 #[test]
514 fn prefix_replace_basic() {
515 let result = replace_module_name_prefix("Foo::Bar::baz();", "Foo::Bar", "New::Mod");
516 assert_eq!(result, "New::Mod::baz();");
517 }
518
519 #[test]
520 fn prefix_replace_multiple_occurrences() {
521 let result =
522 replace_module_name_prefix("Foo::Bar::a() + Foo::Bar::b()", "Foo::Bar", "New::Mod");
523 assert_eq!(result, "New::Mod::a() + New::Mod::b()");
524 }
525
526 #[test]
527 fn prefix_replace_rejects_deeper_prefix() {
528 let result = replace_module_name_prefix("Extra::Foo::Bar::baz();", "Foo::Bar", "New::Mod");
530 assert_eq!(result, "Extra::Foo::Bar::baz();");
531 }
532
533 #[test]
534 fn prefix_replace_empty_inputs_are_noop() {
535 assert_eq!(replace_module_name_prefix("", "Foo::Bar", "New::Mod"), "");
536 assert_eq!(
537 replace_module_name_prefix("Foo::Bar::baz();", "", "New::Mod"),
538 "Foo::Bar::baz();"
539 );
540 }
541
542 #[test]
545 fn plans_full_file_with_all_patterns() {
546 let source = "use Foo::Bar;\nour @ISA = qw(Foo::Bar);\nmy $x = Foo::Bar::create();\n";
547 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Module");
548 let rewritten = apply_module_rename_edits(source, &edits);
549 assert_eq!(
550 rewritten,
551 "use New::Module;\nour @ISA = qw(New::Module);\nmy $x = New::Module::create();\n"
552 );
553 }
554
555 #[test]
556 fn plans_isa_and_qualified_call_on_same_line() {
557 let source = "@ISA = qw(Foo::Bar); Foo::Bar::init();\n";
561 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Mod");
562 let rewritten = apply_module_rename_edits(source, &edits);
563 assert_eq!(rewritten, "@ISA = qw(New::Mod); New::Mod::init();\n");
564 }
565
566 #[test]
567 fn plans_import_and_qualified_call_on_same_line() {
568 let source = "use Foo::Bar; Foo::Bar::init();\n";
572 let edits = plan_module_rename_edits(source, "Foo::Bar", "New::Mod");
573 let rewritten = apply_module_rename_edits(source, &edits);
574 assert_eq!(rewritten, "use New::Mod; New::Mod::init();\n");
575 }
576}