use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SourceSpan {
pub start: usize,
pub end: usize,
pub source: Option<String>,
}
impl SourceSpan {
pub fn new(start: usize, end: usize) -> Self {
Self {
start,
end,
source: None,
}
}
pub fn with_source(start: usize, end: usize, source: impl Into<String>) -> Self {
Self {
start,
end,
source: Some(source.into()),
}
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.end <= self.start
}
}
impl fmt::Display for SourceSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.source {
Some(src) => write!(f, "{}:{}..{}", src, self.start, self.end),
None => write!(f, "{}..{}", self.start, self.end),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Severity {
Info,
Warning,
Error,
Fatal,
}
impl Severity {
pub fn is_blocking(self) -> bool {
matches!(self, Severity::Error | Severity::Fatal)
}
pub fn label(self) -> &'static str {
match self {
Severity::Info => "info",
Severity::Warning => "warning",
Severity::Error => "error",
Severity::Fatal => "fatal",
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
pub location: Option<SourceSpan>,
pub expression_index: Option<usize>,
}
impl Diagnostic {
pub fn new(severity: Severity, message: impl Into<String>) -> Self {
Self {
severity,
message: message.into(),
location: None,
expression_index: None,
}
}
pub fn info(message: impl Into<String>) -> Self {
Self::new(Severity::Info, message)
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new(Severity::Warning, message)
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(Severity::Error, message)
}
pub fn fatal(message: impl Into<String>) -> Self {
Self::new(Severity::Fatal, message)
}
pub fn with_expression_index(mut self, idx: usize) -> Self {
self.expression_index = Some(idx);
self
}
pub fn with_location(mut self, span: SourceSpan) -> Self {
self.location = Some(span);
self
}
pub fn is_blocking(&self) -> bool {
self.severity.is_blocking()
}
pub fn is_fatal(&self) -> bool {
self.severity == Severity::Fatal
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}]", self.severity)?;
if let Some(idx) = self.expression_index {
write!(f, "[expr#{}]", idx)?;
}
if let Some(span) = &self.location {
write!(f, " ({})", span)?;
}
write!(f, " {}", self.message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
assert!(Severity::Error < Severity::Fatal);
}
#[test]
fn severity_is_blocking() {
assert!(!Severity::Info.is_blocking());
assert!(!Severity::Warning.is_blocking());
assert!(Severity::Error.is_blocking());
assert!(Severity::Fatal.is_blocking());
}
#[test]
fn diagnostic_builders() {
let d = Diagnostic::error("boom").with_expression_index(7);
assert_eq!(d.severity, Severity::Error);
assert_eq!(d.message, "boom");
assert_eq!(d.expression_index, Some(7));
assert!(d.is_blocking());
assert!(!d.is_fatal());
}
#[test]
fn diagnostic_display_includes_index_and_span() {
let d = Diagnostic::warning("possible issue")
.with_expression_index(3)
.with_location(SourceSpan::with_source(10, 20, "main.tl"));
let s = format!("{}", d);
assert!(s.contains("warning"));
assert!(s.contains("expr#3"));
assert!(s.contains("main.tl:10..20"));
assert!(s.contains("possible issue"));
}
#[test]
fn source_span_basics() {
let sp = SourceSpan::new(3, 7);
assert_eq!(sp.len(), 4);
assert!(!sp.is_empty());
let empty = SourceSpan::new(5, 5);
assert!(empty.is_empty());
}
}