use std::{borrow::Cow, fmt};
use crate::SourceLocation;
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub struct Warning {
pub kind: WarningKind,
pub location: Option<SourceLocation>,
}
impl Warning {
#[must_use]
pub(crate) fn new(kind: WarningKind, location: Option<SourceLocation>) -> Self {
Self { kind, location }
}
#[must_use]
pub fn source_location(&self) -> Option<&SourceLocation> {
self.location.as_ref()
}
#[must_use]
pub fn advice(&self) -> Option<&'static str> {
match &self.kind {
WarningKind::SectionLevelOutOfSequence { .. } => Some(
"The first section after the document title must be level 1 (==). Renumber the section headings so levels increment by one.",
),
WarningKind::UnterminatedTable { .. } => Some(
"The opening delimiter was found but no matching closing delimiter was seen before end of document. Add the closing delimiter on its own line, or remove the opening delimiter if not intended.",
),
WarningKind::Other(_) => None,
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(loc) = &self.location else {
return write!(f, "{}", self.kind);
};
if let Some(name) = loc
.file
.as_ref()
.and_then(|p| p.file_name())
.and_then(|s| s.to_str())
{
write!(f, "{name}: {}: {}", loc.positioning, self.kind)
} else {
write!(f, "{}: {}", loc.positioning, self.kind)
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum WarningKind {
#[error("expected level 1 (==) as first section, got level {got} ({markers})")]
SectionLevelOutOfSequence {
got: u8,
markers: String,
},
#[error("unterminated table block (opened by `{delimiter}`)")]
UnterminatedTable {
delimiter: String,
},
#[error("{0}")]
Other(Cow<'static, str>),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Position, Positioning};
#[test]
fn display_without_location() {
let w = Warning::new(WarningKind::Other("something happened".into()), None);
assert_eq!(format!("{w}"), "something happened");
}
#[test]
fn display_with_location_no_file() {
let loc = SourceLocation {
file: None,
positioning: Positioning::Position(Position { line: 5, column: 1 }),
};
let w = Warning::new(
WarningKind::SectionLevelOutOfSequence {
got: 3,
markers: "====".into(),
},
Some(loc),
);
assert_eq!(
format!("{w}"),
"line: 5, column: 1: expected level 1 (==) as first section, got level 3 (====)",
);
}
#[test]
fn display_with_location_and_file() {
let loc = SourceLocation {
file: Some(std::path::PathBuf::from("/docs/guide.adoc")),
positioning: Positioning::Position(Position { line: 5, column: 1 }),
};
let w = Warning::new(
WarningKind::SectionLevelOutOfSequence {
got: 3,
markers: "====".into(),
},
Some(loc),
);
assert_eq!(
format!("{w}"),
"guide.adoc: line: 5, column: 1: expected level 1 (==) as first section, got level 3 (====)",
);
}
#[test]
fn equality_holds_on_kind_and_location() {
let a = Warning::new(WarningKind::Other("x".into()), None);
let b = Warning::new(WarningKind::Other("x".into()), None);
let c = Warning::new(WarningKind::Other("y".into()), None);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn unterminated_table_display_renders_original_token() {
let w = Warning::new(
WarningKind::UnterminatedTable {
delimiter: "|===".into(),
},
None,
);
assert_eq!(
format!("{w}"),
"unterminated table block (opened by `|===`)",
);
}
#[test]
fn unterminated_table_display_preserves_longer_tokens() {
let w = Warning::new(
WarningKind::UnterminatedTable {
delimiter: "!=====".into(),
},
None,
);
assert_eq!(
format!("{w}"),
"unterminated table block (opened by `!=====`)",
);
}
}