#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
use std::ops::RangeInclusive;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpanError {
StartAfterEnd,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SourceLocation {
line: usize,
column: usize,
}
impl SourceLocation {
#[must_use]
pub const fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
#[must_use]
pub const fn line(self) -> usize {
self.line
}
#[must_use]
pub const fn column(self) -> usize {
self.column
}
#[must_use]
pub const fn is_after(self, other: SourceLocation) -> bool {
self.line > other.line || (self.line == other.line && self.column > other.column)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SourceSpan {
start: SourceLocation,
end: SourceLocation,
}
impl SourceSpan {
#[must_use = "Inspect the span creation result to handle invalid ranges"]
pub fn new(start: SourceLocation, end: SourceLocation) -> Result<Self, SpanError> {
if start.is_after(end) {
Err(SpanError::StartAfterEnd)
} else {
Ok(Self { start, end })
}
}
#[must_use]
pub const fn start(self) -> SourceLocation {
self.start
}
#[must_use]
pub const fn end(self) -> SourceLocation {
self.end
}
}
#[must_use]
pub fn span_to_lines(span: SourceSpan) -> RangeInclusive<usize> {
span.start.line()..=span.end.line()
}
#[must_use]
pub fn span_line_count(span: SourceSpan) -> usize {
span.end.line() - span.start.line() + 1
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
fn span_construction_validates_order() {
let err = SourceSpan::new(SourceLocation::new(3, 1), SourceLocation::new(2, 0));
assert!(matches!(err, Err(SpanError::StartAfterEnd)));
}
#[rstest]
fn calculates_line_ranges() {
let span = SourceSpan::new(SourceLocation::new(5, 0), SourceLocation::new(7, 3))
.expect("valid span for line range test");
assert_eq!(span_to_lines(span), 5..=7);
assert_eq!(span_line_count(span), 3);
}
}