Skip to main content

perl_module_boundary/
lib.rs

1//! Boundary-aware standalone Perl module-token scanning.
2//!
3//! This crate has one responsibility: find standalone occurrences of a module
4//! token on a single source line while respecting canonical (`::`) and legacy
5//! (`'`) package separator boundaries.
6
7#![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/// Byte range for a standalone module-token match in a source line.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct ModuleTokenRange {
17    /// Inclusive byte start offset in the scanned line.
18    pub start: usize,
19    /// Exclusive byte end offset in the scanned line.
20    pub end: usize,
21}
22
23/// Iterator over standalone `module_name` matches in `line`.
24#[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/// Return an iterator of standalone `module_name` matches in `line`.
61///
62/// Matches are returned in byte-order and are non-overlapping.
63#[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/// Return `true` when `line` contains `module_name` as a standalone module
72/// token.
73#[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}