1use crate::config::Scope;
8use crate::linter::Span;
9
10pub fn segment(source: &str, scope: &Scope) -> Vec<Span> {
18 let mut regions = Vec::new();
19 let mut pos = 0usize;
20 while let Some(start_match) = scope.start_re.find_at(source, pos) {
21 let end = scope
22 .end_re
23 .as_ref()
24 .and_then(|re| re.find_at(source, start_match.end()))
25 .map_or(source.len(), |m| m.start())
26 .max(start_match.end());
27 let span = Span::new(start_match.start(), end);
28 if !span.is_empty() {
29 regions.push(span);
30 }
31 pos = if start_match.is_empty() {
34 span.end.max(start_match.end() + 1)
35 } else {
36 span.end
37 };
38 }
39 regions
40}
41
42pub fn segment_all(source: &str, scope_list: &[Scope]) -> Vec<(usize, Span)> {
46 let mut out: Vec<(usize, Span)> = scope_list
47 .iter()
48 .enumerate()
49 .flat_map(|(scope_index, scope)| {
50 segment(source, scope)
51 .into_iter()
52 .map(move |region| (scope_index, region))
53 })
54 .collect();
55 out.sort_by_key(|(scope_index, region)| (region.start, region.end, *scope_index));
56 out
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::Config;
63
64 fn segment_with(yaml: &str, text: &str, scope_id: &str) -> Vec<Span> {
65 let config = Config::from_str(yaml).expect("config parses");
66 let scope = config
67 .scopes
68 .iter()
69 .find(|s| s.id == scope_id)
70 .expect("scope exists");
71 segment(text, scope)
72 }
73
74 const SHELL_FENCE: &str = "\
75version: 1
76scopes:
77 - id: sh
78 start: '^```sh$'
79 end: '^```$'
80 rules:
81 - id: r
82 pattern: 'x'
83 message: m
84";
85
86 #[test]
87 fn fenced_regions() {
88 let text = "a\n```sh\nb\n```\nc\n";
89 let regions = segment_with(SHELL_FENCE, text, "sh");
90 assert_eq!(regions.len(), 1);
91 let inner = &text[regions[0].to_range()];
92 assert_eq!(inner, "```sh\nb\n");
93 }
94
95 #[test]
96 fn no_end_means_eof() {
97 let yaml = "\
98version: 1
99scopes:
100 - id: tail
101 start: '^BEGIN$'
102 rules:
103 - id: r
104 pattern: x
105 message: m
106";
107 let text = "x\nBEGIN\nrest of file";
108 let regions = segment_with(yaml, text, "tail");
109 assert_eq!(regions.len(), 1);
110 assert_eq!(regions[0].start, 2);
111 assert_eq!(regions[0].end, text.len());
112 }
113
114 #[test]
115 fn regions_are_sequential() {
116 let text = "```sh\na\n```\nmid\n```sh\nb\n```\n";
117 let regions = segment_with(SHELL_FENCE, text, "sh");
118 let inner: Vec<&str> = regions.iter().map(|r| &text[r.to_range()]).collect();
119 assert_eq!(inner, ["```sh\na\n", "```sh\nb\n"]);
120 }
121
122 #[test]
123 fn no_matches_yields_no_regions() {
124 assert!(segment_with(SHELL_FENCE, "nothing here\n", "sh").is_empty());
125 }
126
127 #[test]
128 fn end_right_after_start_gives_start_only_region() {
129 let yaml = "\
133version: 1
134scopes:
135 - id: s
136 start: 'X'
137 end: 'X'
138 rules:
139 - id: r
140 pattern: x
141 message: m
142";
143 let regions = segment_with(yaml, "XX\n", "s");
144 let ranges: Vec<_> = regions.iter().map(|r| r.to_range()).collect();
145 assert_eq!(ranges, [0..1, 1..3]);
148 }
149
150 #[test]
151 fn empty_start_match_cannot_stall() {
152 let yaml = "\
153version: 1
154scopes:
155 - id: s
156 start: 'x*'
157 end: 'y'
158 rules:
159 - id: r
160 pattern: x
161 message: m
162";
163 let regions = segment_with(yaml, "a y b", "s");
168 let ranges: Vec<_> = regions.iter().map(|r| r.to_range()).collect();
169 assert_eq!(ranges, [0..2, 3..5]);
170 }
171
172 #[test]
173 fn different_scopes_may_overlap() {
174 let yaml = "\
175version: 1
176scopes:
177 - id: whole
178 start: '^A$'
179 rules:
180 - id: r1
181 pattern: x
182 message: m
183 - id: line
184 start: 'B'
185 end: '$'
186 rules:
187 - id: r2
188 pattern: x
189 message: m
190";
191 let config = Config::from_str(yaml).unwrap();
192 let text = "A\nB\nC\n";
193 let all = segment_all(text, &config.scopes);
194 assert_eq!(all.len(), 2);
195 assert_eq!(&text[all[0].1.to_range()], "A\nB\nC\n");
197 assert_eq!(&text[all[1].1.to_range()], "B");
198 }
199}