1#![deny(unsafe_code)]
8#![warn(rust_2018_idioms)]
9#![warn(missing_docs)]
10#![warn(clippy::all)]
11
12use perl_module_boundary::{contains_standalone_module_token, find_standalone_module_token_ranges};
13
14pub use perl_module_name::module_variant_pairs;
34
35#[must_use]
38pub fn contains_module_token(line: &str, module_name: &str) -> bool {
39 contains_standalone_module_token(line, module_name)
40}
41
42#[must_use]
46pub fn replace_module_token(line: &str, from: &str, to: &str) -> (String, bool) {
47 if from.is_empty() || line.is_empty() {
48 return (line.to_string(), false);
49 }
50
51 let mut ranges = find_standalone_module_token_ranges(line, from).peekable();
52 if ranges.peek().is_none() {
53 return (line.to_string(), false);
54 }
55
56 let mut out = String::with_capacity(line.len());
57 let mut cursor = 0usize;
58
59 for range in ranges {
60 out.push_str(&line[cursor..range.start]);
61 out.push_str(to);
62 cursor = range.end;
63 }
64
65 out.push_str(&line[cursor..]);
66 (out, true)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::{contains_module_token, module_variant_pairs, replace_module_token};
72
73 #[test]
74 fn builds_canonical_and_legacy_variant_pairs() {
75 assert_eq!(
76 module_variant_pairs("Foo::Bar", "New::Path"),
77 vec![
78 ("Foo::Bar".to_string(), "New::Path".to_string()),
79 ("Foo'Bar".to_string(), "New'Path".to_string()),
80 ]
81 );
82 }
83
84 #[test]
85 fn canonicalizes_legacy_inputs_for_variant_pairs() {
86 assert_eq!(
87 module_variant_pairs("Foo'Bar", "New'Path"),
88 vec![
89 ("Foo::Bar".to_string(), "New::Path".to_string()),
90 ("Foo'Bar".to_string(), "New'Path".to_string()),
91 ]
92 );
93 }
94
95 #[test]
96 fn deduplicates_pair_when_no_separator_variants_exist() {
97 assert_eq!(module_variant_pairs("strict", "warnings").len(), 1);
98 }
99
100 #[test]
101 fn replaces_only_standalone_module_tokens() {
102 let (rewritten, changed) = replace_module_token("use Foo::Bar;", "Foo::Bar", "X::Y");
103 assert_eq!(rewritten, "use X::Y;");
104 assert!(changed);
105
106 let (rewritten, changed) = replace_module_token("use Foo::Barista;", "Foo::Bar", "X::Y");
107 assert_eq!(rewritten, "use Foo::Barista;");
108 assert!(!changed);
109 }
110
111 #[test]
112 fn treats_legacy_separator_as_module_character_boundary() {
113 let (rewritten, changed) = replace_module_token("use Foo'Bar'Baz;", "Foo'Bar", "X'Y");
114 assert_eq!(rewritten, "use Foo'Bar'Baz;");
115 assert!(!changed);
116 }
117
118 #[test]
119 fn contains_matches_boundary_aware_token_presence() {
120 assert!(contains_module_token("use parent 'Foo::Bar';", "Foo::Bar"));
121 assert!(!contains_module_token("use Foo::Barista;", "Foo::Bar"));
122 }
123
124 #[test]
127 fn contains_simple_bare_name_foo() {
128 assert!(contains_module_token("use Foo;", "Foo"));
129 }
130
131 #[test]
132 fn contains_simple_two_segment_foo_bar() {
133 assert!(contains_module_token("use Foo::Bar;", "Foo::Bar"));
134 }
135
136 #[test]
137 fn replace_simple_bare_name_foo() {
138 let (out, changed) = replace_module_token("use Foo;", "Foo", "Bar");
139 assert!(changed);
140 assert_eq!(out, "use Bar;");
141 }
142
143 #[test]
144 fn replace_simple_two_segment_foo_bar() {
145 let (out, changed) = replace_module_token("use Foo::Bar;", "Foo::Bar", "Baz::Qux");
146 assert!(changed);
147 assert_eq!(out, "use Baz::Qux;");
148 }
149
150 #[test]
151 fn contains_rejects_simple_name_as_substring() {
152 assert!(!contains_module_token("use Foobar;", "Foo"));
153 }
154
155 #[test]
158 fn contains_four_segment_deeply_nested() {
159 assert!(contains_module_token("use Foo::Bar::Baz::Qux;", "Foo::Bar::Baz::Qux"));
160 }
161
162 #[test]
163 fn replace_four_segment_deeply_nested() {
164 let (out, changed) = replace_module_token(
165 "use Foo::Bar::Baz::Qux;",
166 "Foo::Bar::Baz::Qux",
167 "One::Two::Three::Four",
168 );
169 assert!(changed);
170 assert_eq!(out, "use One::Two::Three::Four;");
171 }
172
173 #[test]
174 fn contains_rejects_prefix_of_deeply_nested() {
175 assert!(!contains_module_token("use Foo::Bar::Baz::Qux;", "Foo::Bar"));
177 }
178
179 #[test]
180 fn contains_rejects_suffix_of_deeply_nested() {
181 assert!(!contains_module_token("use Foo::Bar::Baz::Qux;", "Baz::Qux"));
183 }
184
185 #[test]
186 fn replace_does_not_modify_prefix_of_deeply_nested() {
187 let (out, changed) = replace_module_token("use Foo::Bar::Baz::Qux;", "Foo::Bar", "X::Y");
188 assert!(!changed);
189 assert_eq!(out, "use Foo::Bar::Baz::Qux;");
190 }
191
192 #[test]
193 fn contains_deeply_nested_in_method_call() {
194 assert!(contains_module_token("Foo::Bar::Baz::Qux->new()", "Foo::Bar::Baz::Qux"));
195 }
196
197 #[test]
198 fn replace_deeply_nested_in_method_call() {
199 let (out, changed) =
200 replace_module_token("Foo::Bar::Baz::Qux->new()", "Foo::Bar::Baz::Qux", "A::B::C::D");
201 assert!(changed);
202 assert_eq!(out, "A::B::C::D->new()");
203 }
204
205 #[test]
206 fn variant_pairs_deeply_nested_four_segments() {
207 let pairs = module_variant_pairs("Foo::Bar::Baz::Qux", "One::Two::Three::Four");
208 assert_eq!(pairs.len(), 2);
209 assert_eq!(
210 pairs[0],
211 ("Foo::Bar::Baz::Qux".to_string(), "One::Two::Three::Four".to_string())
212 );
213 assert_eq!(pairs[1], ("Foo'Bar'Baz'Qux".to_string(), "One'Two'Three'Four".to_string()));
214 }
215
216 #[test]
219 fn contains_rejects_empty_module_name() {
220 assert!(!contains_module_token("use Foo::Bar;", ""));
221 }
222
223 #[test]
224 fn contains_rejects_empty_line() {
225 assert!(!contains_module_token("", "Foo::Bar"));
226 }
227
228 #[test]
229 fn replace_empty_from_is_noop() {
230 let (out, changed) = replace_module_token("use Foo::Bar;", "", "X::Y");
231 assert!(!changed);
232 assert_eq!(out, "use Foo::Bar;");
233 }
234
235 #[test]
236 fn replace_empty_line_is_noop() {
237 let (out, changed) = replace_module_token("", "Foo::Bar", "X::Y");
238 assert!(!changed);
239 assert_eq!(out, "");
240 }
241
242 #[test]
243 fn contains_validates_boundary_before_token() {
244 assert!(!contains_module_token("use 1Foo::Bar;", "Foo::Bar"));
246 }
247
248 #[test]
249 fn contains_validates_boundary_after_token() {
250 assert!(!contains_module_token("use Foo::BarX;", "Foo::Bar"));
252 }
253
254 #[test]
257 fn rename_workflow_four_segment_canonical() {
258 let pairs = module_variant_pairs("Foo::Bar::Baz::Qux", "A::B::C::D");
259 let line = "use Foo::Bar::Baz::Qux;";
260
261 let mut result = line.to_string();
262 for (old, new) in &pairs {
263 let (candidate, changed) = replace_module_token(&result, old, new);
264 if changed {
265 result = candidate;
266 }
267 }
268 assert_eq!(result, "use A::B::C::D;");
269 }
270
271 #[test]
272 fn rename_workflow_four_segment_legacy() {
273 let pairs = module_variant_pairs("Foo::Bar::Baz::Qux", "A::B::C::D");
274 let line = "use Foo'Bar'Baz'Qux;";
275
276 let mut result = line.to_string();
277 for (old, new) in &pairs {
278 let (candidate, changed) = replace_module_token(&result, old, new);
279 if changed {
280 result = candidate;
281 }
282 }
283 assert_eq!(result, "use A'B'C'D;");
284 }
285
286 #[test]
289 fn contains_single_word_module() {
290 assert!(contains_module_token("use strict;", "strict"));
291 assert!(contains_module_token("use warnings;", "warnings"));
292 assert!(contains_module_token("use DBI;", "DBI"));
293 }
294
295 #[test]
296 fn replace_single_word_module() {
297 let (out, changed) = replace_module_token("use strict;", "strict", "warnings");
298 assert!(changed);
299 assert_eq!(out, "use warnings;");
300 }
301
302 #[test]
303 fn single_word_not_matched_as_substring() {
304 assert!(!contains_module_token("use strictures;", "strict"));
305 assert!(!contains_module_token("use astrict;", "strict"));
306 }
307
308 #[test]
309 fn variant_pairs_single_word_produces_one_pair() {
310 let pairs = module_variant_pairs("DBI", "DBIx");
311 assert_eq!(pairs.len(), 1);
312 assert_eq!(pairs[0], ("DBI".to_string(), "DBIx".to_string()));
313 }
314
315 #[test]
318 fn contains_does_not_match_when_followed_by_colon_colon() {
319 assert!(!contains_module_token("use Foo::Bar::Baz;", "Foo::Bar"));
321 }
322
323 #[test]
324 fn replace_does_not_modify_when_followed_by_colon_colon() {
325 let (out, changed) = replace_module_token("use Foo::Bar::Baz;", "Foo::Bar", "X::Y");
326 assert!(!changed);
327 assert_eq!(out, "use Foo::Bar::Baz;");
328 }
329
330 #[test]
333 fn contains_cpan_catalyst_plugin() {
334 assert!(contains_module_token(
335 "use Catalyst::Plugin::Authentication;",
336 "Catalyst::Plugin::Authentication"
337 ));
338 }
339
340 #[test]
341 fn replace_cpan_catalyst_plugin() {
342 let (out, changed) = replace_module_token(
343 "use Catalyst::Plugin::Authentication;",
344 "Catalyst::Plugin::Authentication",
345 "Catalyst::Plugin::AuthN",
346 );
347 assert!(changed);
348 assert_eq!(out, "use Catalyst::Plugin::AuthN;");
349 }
350
351 #[test]
352 fn replace_cpan_app_prove_five_segments() {
353 let (out, changed) = replace_module_token(
354 "use App::Prove::State::Result::Test;",
355 "App::Prove::State::Result::Test",
356 "TAP::Result::Test",
357 );
358 assert!(changed);
359 assert_eq!(out, "use TAP::Result::Test;");
360 }
361
362 #[test]
363 fn contains_and_replace_agree_on_deeply_nested() {
364 let line = "my $obj = Foo::Bar::Baz::Qux->new;";
365 let module = "Foo::Bar::Baz::Qux";
366 let present = contains_module_token(line, module);
367 let (_, changed) = replace_module_token(line, module, "X");
368 assert_eq!(present, changed);
369 }
370
371 #[test]
372 fn contains_and_replace_agree_on_prefix_of_deeply_nested() {
373 let line = "my $obj = Foo::Bar::Baz::Qux->new;";
374 let module = "Foo::Bar";
375 let present = contains_module_token(line, module);
376 let (_, changed) = replace_module_token(line, module, "X");
377 assert_eq!(present, changed);
378 }
379
380 #[test]
383 fn contains_underscore_prefixed_module() {
384 assert!(contains_module_token("use _Private::Module;", "_Private::Module"));
385 }
386
387 #[test]
388 fn replace_underscore_prefixed_module() {
389 let (out, changed) =
390 replace_module_token("use _Private::Module;", "_Private::Module", "Public::Module");
391 assert!(changed);
392 assert_eq!(out, "use Public::Module;");
393 }
394
395 #[test]
396 fn contains_all_underscore_segments() {
397 assert!(contains_module_token("use _::_::_;", "_::_::_"));
398 }
399
400 #[test]
403 fn replace_multiple_occurrences_deeply_nested() {
404 let line = "use Foo::Bar::Baz; my $x = Foo::Bar::Baz->new;";
405 let (out, changed) = replace_module_token(line, "Foo::Bar::Baz", "A::B::C");
406 assert!(changed);
407 assert_eq!(out, "use A::B::C; my $x = A::B::C->new;");
408 }
409}