1use std::{borrow::Cow, fmt};
10
11use crate::SourceLocation;
12
13#[derive(Debug, PartialEq)]
19#[non_exhaustive]
20pub struct Warning {
21 pub kind: WarningKind,
23 pub location: Option<SourceLocation>,
26}
27
28impl Warning {
29 #[must_use]
31 pub(crate) fn new(kind: WarningKind, location: Option<SourceLocation>) -> Self {
32 Self { kind, location }
33 }
34
35 #[must_use]
37 pub fn source_location(&self) -> Option<&SourceLocation> {
38 self.location.as_ref()
39 }
40
41 #[must_use]
44 pub fn advice(&self) -> Option<&'static str> {
45 match &self.kind {
46 WarningKind::SectionLevelOutOfSequence { .. } => Some(
47 "The first section after the document title must be level 1 (==). Renumber the section headings so levels increment by one.",
48 ),
49 WarningKind::UnterminatedTable { .. } => Some(
50 "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.",
51 ),
52 WarningKind::Other(_) => None,
53 }
54 }
55}
56
57impl fmt::Display for Warning {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 let Some(loc) = &self.location else {
60 return write!(f, "{}", self.kind);
61 };
62 if let Some(name) = loc
63 .file
64 .as_ref()
65 .and_then(|p| p.file_name())
66 .and_then(|s| s.to_str())
67 {
68 write!(f, "{name}: {}: {}", loc.positioning, self.kind)
69 } else {
70 write!(f, "{}: {}", loc.positioning, self.kind)
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, thiserror::Error)]
83#[non_exhaustive]
84pub enum WarningKind {
85 #[error("expected level 1 (==) as first section, got level {got} ({markers})")]
89 SectionLevelOutOfSequence {
90 got: u8,
92 markers: String,
94 },
95
96 #[error("unterminated table block (opened by `{delimiter}`)")]
103 UnterminatedTable {
104 delimiter: String,
106 },
107
108 #[error("{0}")]
110 Other(Cow<'static, str>),
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::{Position, Positioning};
117
118 #[test]
119 fn display_without_location() {
120 let w = Warning::new(WarningKind::Other("something happened".into()), None);
121 assert_eq!(format!("{w}"), "something happened");
122 }
123
124 #[test]
125 fn display_with_location_no_file() {
126 let loc = SourceLocation {
127 file: None,
128 positioning: Positioning::Position(Position { line: 5, column: 1 }),
129 };
130 let w = Warning::new(
131 WarningKind::SectionLevelOutOfSequence {
132 got: 3,
133 markers: "====".into(),
134 },
135 Some(loc),
136 );
137 assert_eq!(
138 format!("{w}"),
139 "line: 5, column: 1: expected level 1 (==) as first section, got level 3 (====)",
140 );
141 }
142
143 #[test]
144 fn display_with_location_and_file() {
145 let loc = SourceLocation {
146 file: Some(std::path::PathBuf::from("/docs/guide.adoc")),
147 positioning: Positioning::Position(Position { line: 5, column: 1 }),
148 };
149 let w = Warning::new(
150 WarningKind::SectionLevelOutOfSequence {
151 got: 3,
152 markers: "====".into(),
153 },
154 Some(loc),
155 );
156 assert_eq!(
157 format!("{w}"),
158 "guide.adoc: line: 5, column: 1: expected level 1 (==) as first section, got level 3 (====)",
159 );
160 }
161
162 #[test]
163 fn equality_holds_on_kind_and_location() {
164 let a = Warning::new(WarningKind::Other("x".into()), None);
165 let b = Warning::new(WarningKind::Other("x".into()), None);
166 let c = Warning::new(WarningKind::Other("y".into()), None);
167 assert_eq!(a, b);
168 assert_ne!(a, c);
169 }
170
171 #[test]
172 fn unterminated_table_display_renders_original_token() {
173 let w = Warning::new(
174 WarningKind::UnterminatedTable {
175 delimiter: "|===".into(),
176 },
177 None,
178 );
179 assert_eq!(
180 format!("{w}"),
181 "unterminated table block (opened by `|===`)",
182 );
183 }
184
185 #[test]
186 fn unterminated_table_display_preserves_longer_tokens() {
187 let w = Warning::new(
188 WarningKind::UnterminatedTable {
189 delimiter: "!=====".into(),
190 },
191 None,
192 );
193 assert_eq!(
194 format!("{w}"),
195 "unterminated table block (opened by `!=====`)",
196 );
197 }
198}