perl_module/rename/
mod.rs1use crate::import_match::line_references_module_import;
6use crate::token::{module_variant_pairs, replace_module_token};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ModuleLineEdit {
11 pub line: usize,
13 pub start_character: usize,
15 pub end_character: usize,
17 pub new_text: String,
19}
20
21#[must_use]
45pub fn plan_module_rename_edits(
46 source: &str,
47 old_module: &str,
48 new_module: &str,
49) -> Vec<ModuleLineEdit> {
50 if source.is_empty()
51 || old_module.is_empty()
52 || new_module.is_empty()
53 || old_module == new_module
54 {
55 return Vec::new();
56 }
57
58 let variants = module_variant_pairs(old_module, new_module);
59 let mut edits = Vec::new();
60
61 for (line_idx, line) in source.lines().enumerate() {
62 let mut rewritten: Option<String> = None;
63
64 for (old_variant, new_variant) in &variants {
65 {
67 let current_line = rewritten.as_deref().unwrap_or(line);
68 if line_references_module_import(current_line, old_variant) {
69 let (candidate, changed) =
70 replace_module_token(current_line, old_variant, new_variant);
71 if changed {
72 rewritten = Some(candidate);
73 }
74 }
75 }
76
77 {
79 let current_line = rewritten.as_deref().unwrap_or(line);
80 if line_references_moose_moo_dsl(current_line, old_variant) {
81 let (candidate, changed) =
82 replace_module_token(current_line, old_variant, new_variant);
83 if changed {
84 rewritten = Some(candidate);
85 }
86 }
87 }
88
89 {
91 let current_line = rewritten.as_deref().unwrap_or(line);
92 if line_references_isa_assignment(current_line, old_variant) {
93 let (candidate, changed) =
94 replace_module_token(current_line, old_variant, new_variant);
95 if changed {
96 rewritten = Some(candidate);
97 }
98 }
99 }
100
101 {
103 let current_line = rewritten.as_deref().unwrap_or(line);
104 if line_references_qualified_call(current_line, old_variant) {
105 let candidate =
106 replace_module_name_prefix(current_line, old_variant, new_variant);
107 if candidate != current_line {
108 rewritten = Some(candidate);
109 }
110 }
111 }
112
113 {
115 let current_line = rewritten.as_deref().unwrap_or(line);
116 if line_references_package_declaration(current_line, old_variant) {
117 let (candidate, changed) =
118 replace_module_token(current_line, old_variant, new_variant);
119 if changed {
120 rewritten = Some(candidate);
121 }
122 }
123 }
124 }
125
126 if let Some(new_text) = rewritten {
127 edits.push(ModuleLineEdit {
128 line: line_idx,
129 start_character: 0,
130 end_character: line.len(),
131 new_text,
132 });
133 }
134 }
135
136 edits
137}
138
139fn line_references_moose_moo_dsl(line: &str, module_name: &str) -> bool {
140 if line.is_empty() || module_name.is_empty() {
141 return false;
142 }
143 let trimmed = line.trim_start();
144 let is_extends =
145 trimmed == "extends" || trimmed.starts_with("extends ") || trimmed.starts_with("extends(");
146 let is_with = trimmed == "with" || trimmed.starts_with("with ") || trimmed.starts_with("with(");
147 if !is_extends && !is_with {
148 return false;
149 }
150 crate::token::contains_module_token(line, module_name)
151}
152
153#[must_use]
156pub fn line_references_isa_assignment(line: &str, module_name: &str) -> bool {
157 if line.is_empty() || module_name.is_empty() {
158 return false;
159 }
160 if !line.contains("@ISA") {
161 return false;
162 }
163 crate::token::contains_module_token(line, module_name)
164}
165
166#[must_use]
169pub fn line_references_qualified_call(line: &str, module_name: &str) -> bool {
170 if line.is_empty() || module_name.is_empty() {
171 return false;
172 }
173 let trimmed = line.trim_start();
174 if trimmed.starts_with("package ")
175 || trimmed.starts_with("use ")
176 || trimmed.starts_with("require ")
177 || trimmed.starts_with("no ")
178 {
179 return false;
180 }
181 for separator in ["::", "'"] {
182 let needle = format!("{module_name}{separator}");
183 let needle_bytes = needle.as_bytes();
184 let line_bytes = line.as_bytes();
185 let needle_len = needle_bytes.len();
186
187 if line_bytes.len() < needle_len {
188 continue;
189 }
190
191 let mut start = 0usize;
192 while start + needle_len <= line_bytes.len() {
193 let Some(rel) = line[start..].find(needle.as_str()) else {
194 break;
195 };
196 let abs = start + rel;
197 let after = abs + needle_len;
198
199 let before_ok = abs == 0 || {
200 let ch = line_bytes[abs - 1] as char;
201 !ch.is_alphanumeric() && ch != '_' && ch != ':'
202 };
203
204 let after_ok = after < line_bytes.len() && {
205 let ch = line_bytes[after] as char;
206 ch.is_alphabetic() || ch == '_'
207 };
208
209 if before_ok && after_ok && !index_is_in_quote_or_comment(line, abs) {
210 return true;
211 }
212 start = abs + 1;
213 }
214 }
215
216 false
217}
218
219#[must_use]
222pub fn line_references_package_declaration(line: &str, module_name: &str) -> bool {
223 if line.is_empty() || module_name.is_empty() {
224 return false;
225 }
226 if !line.trim_start().starts_with("package ") {
227 return false;
228 }
229 crate::token::contains_module_token(line, module_name)
230}
231
232#[must_use]
234pub fn replace_module_name_prefix(line: &str, old_module: &str, new_module: &str) -> String {
235 if old_module.is_empty() || new_module.is_empty() || line.is_empty() {
236 return line.to_string();
237 }
238 let trimmed = line.trim_start();
239 if trimmed.starts_with("package ")
240 || trimmed.starts_with("use ")
241 || trimmed.starts_with("require ")
242 || trimmed.starts_with("no ")
243 {
244 return line.to_string();
245 }
246
247 let mut out = line.to_string();
248
249 for separator in ["::", "'"] {
250 let needle = format!("{old_module}{separator}");
251 let replacement = format!("{new_module}{separator}");
252 let needle_bytes = needle.as_bytes();
253 let needle_len = needle_bytes.len();
254 let line_bytes = out.as_bytes();
255
256 if line_bytes.len() < needle_len {
257 continue;
258 }
259
260 let mut replaced = String::with_capacity(out.len());
261 let mut cursor = 0usize;
262
263 while cursor + needle_len <= line_bytes.len() {
264 let Some(rel) = out[cursor..].find(needle.as_str()) else {
265 break;
266 };
267 let abs = cursor + rel;
268 let after = abs + needle_len;
269
270 let before_ok = abs == 0 || {
271 let ch = line_bytes[abs - 1] as char;
272 !ch.is_alphanumeric() && ch != '_' && ch != ':'
273 };
274
275 let after_ok = after < line_bytes.len() && {
276 let ch = line_bytes[after] as char;
277 ch.is_alphabetic() || ch == '_'
278 };
279
280 if before_ok && after_ok && !index_is_in_quote_or_comment(&out, abs) {
281 replaced.push_str(&out[cursor..abs]);
282 replaced.push_str(&replacement);
283 cursor = after;
284 } else {
285 replaced.push_str(&out[cursor..abs + 1]);
286 cursor = abs + 1;
287 }
288 }
289
290 replaced.push_str(&out[cursor..]);
291 out = replaced;
292 }
293
294 out
295}
296
297fn index_is_in_quote_or_comment(line: &str, index: usize) -> bool {
298 let bytes = line.as_bytes();
299 if index >= bytes.len() {
300 return false;
301 }
302
303 let mut in_single = false;
304 let mut in_double = false;
305 let mut escaped = false;
306
307 for (i, &byte) in bytes.iter().enumerate() {
308 if i == index {
309 return in_single || in_double;
310 }
311
312 let ch = byte as char;
313 if escaped {
314 escaped = false;
315 continue;
316 }
317
318 if in_single {
319 if ch == '\\' {
320 escaped = true;
321 continue;
322 }
323 if ch == '\'' {
324 in_single = false;
325 }
326 continue;
327 }
328
329 if in_double {
330 if ch == '\\' {
331 escaped = true;
332 continue;
333 }
334 if ch == '"' {
335 in_double = false;
336 }
337 continue;
338 }
339
340 if ch == '#' {
341 return i < index;
342 }
343
344 if ch == '\'' {
345 in_single = true;
346 continue;
347 }
348
349 if ch == '"' {
350 in_double = true;
351 }
352 }
353
354 false
355}
356
357#[must_use]
359pub fn apply_module_rename_edits(source: &str, edits: &[ModuleLineEdit]) -> String {
360 if edits.is_empty() {
361 return source.to_string();
362 }
363
364 let mut lines: Vec<String> = source.split('\n').map(ToString::to_string).collect();
365
366 let mut sorted = edits.to_vec();
367 sorted.sort_by_key(|edit| edit.line);
368
369 for edit in sorted {
370 if let Some(line) = lines.get_mut(edit.line) {
371 *line = edit.new_text;
372 }
373 }
374
375 lines.join("\n")
376}