perl_module_boundary/
lib.rs1#![deny(unsafe_code)]
8#![warn(rust_2018_idioms)]
9#![warn(missing_docs)]
10#![warn(clippy::all)]
11
12use perl_module_token_core::has_standalone_module_token_boundaries;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct ModuleTokenRange {
17 pub start: usize,
19 pub end: usize,
21}
22
23#[derive(Debug, Clone)]
25pub struct ModuleTokenRangeIter<'a> {
26 line: &'a str,
27 module_name: &'a str,
28 search_start: usize,
29 done: bool,
30}
31
32impl<'a> Iterator for ModuleTokenRangeIter<'a> {
33 type Item = ModuleTokenRange;
34
35 fn next(&mut self) -> Option<Self::Item> {
36 if self.done || self.module_name.is_empty() || self.line.is_empty() {
37 self.done = true;
38 return None;
39 }
40
41 while self.search_start <= self.line.len() {
42 let Some(rel_pos) = self.line[self.search_start..].find(self.module_name) else {
43 break;
44 };
45
46 let start = self.search_start + rel_pos;
47 let end = start + self.module_name.len();
48 self.search_start = end;
49
50 if has_standalone_module_token_boundaries(self.line, start, end) {
51 return Some(ModuleTokenRange { start, end });
52 }
53 }
54
55 self.done = true;
56 None
57 }
58}
59
60#[must_use]
64pub fn find_standalone_module_token_ranges<'a>(
65 line: &'a str,
66 module_name: &'a str,
67) -> ModuleTokenRangeIter<'a> {
68 ModuleTokenRangeIter { line, module_name, search_start: 0, done: false }
69}
70
71#[must_use]
74pub fn contains_standalone_module_token(line: &str, module_name: &str) -> bool {
75 find_standalone_module_token_ranges(line, module_name).next().is_some()
76}
77
78#[cfg(test)]
79mod tests {
80 use super::{contains_standalone_module_token, find_standalone_module_token_ranges};
81
82 #[test]
83 fn finds_standalone_canonical_module_token() {
84 let ranges =
85 find_standalone_module_token_ranges("use Foo::Bar;", "Foo::Bar").collect::<Vec<_>>();
86
87 assert_eq!(ranges.len(), 1);
88 assert_eq!(ranges[0].start, 4);
89 assert_eq!(ranges[0].end, 12);
90 assert!(contains_standalone_module_token("use Foo::Bar;", "Foo::Bar"));
91 }
92
93 #[test]
94 fn rejects_partial_module_name_matches() {
95 let ranges = find_standalone_module_token_ranges("use Foo::Barista;", "Foo::Bar")
96 .collect::<Vec<_>>();
97 assert!(ranges.is_empty());
98 assert!(!contains_standalone_module_token("use Foo::Barista;", "Foo::Bar"));
99 }
100
101 #[test]
102 fn treats_legacy_separator_as_module_boundary() {
103 let ranges =
104 find_standalone_module_token_ranges("use Foo'Bar'Baz;", "Foo'Bar").collect::<Vec<_>>();
105 assert!(ranges.is_empty());
106 assert!(!contains_standalone_module_token("use Foo'Bar'Baz;", "Foo'Bar"));
107 }
108
109 #[test]
110 fn empty_inputs_never_match() {
111 assert!(!contains_standalone_module_token("", "Foo::Bar"));
112 assert!(!contains_standalone_module_token("use Foo::Bar;", ""));
113 assert!(
114 find_standalone_module_token_ranges("use Foo::Bar;", "").collect::<Vec<_>>().is_empty()
115 );
116 }
117}