use crate::config::Scope;
use crate::linter::Span;
pub fn segment(source: &str, scope: &Scope) -> Vec<Span> {
let mut regions = Vec::new();
let mut pos = 0usize;
while let Some(start_match) = scope.start_re.find_at(source, pos) {
let end = scope
.end_re
.as_ref()
.and_then(|re| re.find_at(source, start_match.end()))
.map_or(source.len(), |m| m.start())
.max(start_match.end());
let span = Span::new(start_match.start(), end);
if !span.is_empty() {
regions.push(span);
}
pos = if start_match.is_empty() {
span.end.max(start_match.end() + 1)
} else {
span.end
};
}
regions
}
pub fn segment_all(source: &str, scope_list: &[Scope]) -> Vec<(usize, Span)> {
let mut out: Vec<(usize, Span)> = scope_list
.iter()
.enumerate()
.flat_map(|(scope_index, scope)| {
segment(source, scope)
.into_iter()
.map(move |region| (scope_index, region))
})
.collect();
out.sort_by_key(|(scope_index, region)| (region.start, region.end, *scope_index));
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
fn segment_with(yaml: &str, text: &str, scope_id: &str) -> Vec<Span> {
let config = Config::from_str(yaml).expect("config parses");
let scope = config
.scopes
.iter()
.find(|s| s.id == scope_id)
.expect("scope exists");
segment(text, scope)
}
const SHELL_FENCE: &str = "\
version: 1
scopes:
- id: sh
start: '^```sh$'
end: '^```$'
rules:
- id: r
pattern: 'x'
message: m
";
#[test]
fn fenced_regions() {
let text = "a\n```sh\nb\n```\nc\n";
let regions = segment_with(SHELL_FENCE, text, "sh");
assert_eq!(regions.len(), 1);
let inner = &text[regions[0].to_range()];
assert_eq!(inner, "```sh\nb\n");
}
#[test]
fn no_end_means_eof() {
let yaml = "\
version: 1
scopes:
- id: tail
start: '^BEGIN$'
rules:
- id: r
pattern: x
message: m
";
let text = "x\nBEGIN\nrest of file";
let regions = segment_with(yaml, text, "tail");
assert_eq!(regions.len(), 1);
assert_eq!(regions[0].start, 2);
assert_eq!(regions[0].end, text.len());
}
#[test]
fn regions_are_sequential() {
let text = "```sh\na\n```\nmid\n```sh\nb\n```\n";
let regions = segment_with(SHELL_FENCE, text, "sh");
let inner: Vec<&str> = regions.iter().map(|r| &text[r.to_range()]).collect();
assert_eq!(inner, ["```sh\na\n", "```sh\nb\n"]);
}
#[test]
fn no_matches_yields_no_regions() {
assert!(segment_with(SHELL_FENCE, "nothing here\n", "sh").is_empty());
}
#[test]
fn end_right_after_start_gives_start_only_region() {
let yaml = "\
version: 1
scopes:
- id: s
start: 'X'
end: 'X'
rules:
- id: r
pattern: x
message: m
";
let regions = segment_with(yaml, "XX\n", "s");
let ranges: Vec<_> = regions.iter().map(|r| r.to_range()).collect();
assert_eq!(ranges, [0..1, 1..3]);
}
#[test]
fn empty_start_match_cannot_stall() {
let yaml = "\
version: 1
scopes:
- id: s
start: 'x*'
end: 'y'
rules:
- id: r
pattern: x
message: m
";
let regions = segment_with(yaml, "a y b", "s");
let ranges: Vec<_> = regions.iter().map(|r| r.to_range()).collect();
assert_eq!(ranges, [0..2, 3..5]);
}
#[test]
fn different_scopes_may_overlap() {
let yaml = "\
version: 1
scopes:
- id: whole
start: '^A$'
rules:
- id: r1
pattern: x
message: m
- id: line
start: 'B'
end: '$'
rules:
- id: r2
pattern: x
message: m
";
let config = Config::from_str(yaml).unwrap();
let text = "A\nB\nC\n";
let all = segment_all(text, &config.scopes);
assert_eq!(all.len(), 2);
assert_eq!(&text[all[0].1.to_range()], "A\nB\nC\n");
assert_eq!(&text[all[1].1.to_range()], "B");
}
}