Skip to main content

libmandoc_rs/
diagnostics.rs

1//! Converts libmandoc's textual findings into stable structured diagnostics.
2
3/// Severity assigned by libmandoc's validation diagnostics.
4#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum DiagnosticLevel {
7    Unsupported,
8    Error,
9    Warning,
10    Style,
11}
12
13/// Optional source location extracted from a libmandoc diagnostic prefix.
14#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct SourceLocation {
17    pub line: u32,
18    pub column: u32,
19}
20
21/// One non-fatal finding emitted while parsing a manual source.
22#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct Diagnostic {
25    pub level: DiagnosticLevel,
26    pub message: String,
27    pub location: Option<SourceLocation>,
28}
29
30pub(crate) fn parse_diagnostics(output: &str) -> Vec<Diagnostic> {
31    output.lines().filter_map(parse_diagnostic).collect()
32}
33
34fn parse_diagnostic(line: &str) -> Option<Diagnostic> {
35    let line = line.trim();
36    if line.is_empty() {
37        return None;
38    }
39    let (level, marker) = [
40        (DiagnosticLevel::Unsupported, ": UNSUPP: "),
41        (DiagnosticLevel::Error, ": ERROR: "),
42        (DiagnosticLevel::Error, ": BADARG: "),
43        (DiagnosticLevel::Error, ": SYSERR: "),
44        (DiagnosticLevel::Warning, ": WARNING: "),
45        (DiagnosticLevel::Style, ": STYLE: "),
46    ]
47    .into_iter()
48    .find(|(_, marker)| line.contains(marker))
49    .unwrap_or((DiagnosticLevel::Warning, ": "));
50    let (prefix, message) = line.split_once(marker).unwrap_or(("", line));
51    Some(Diagnostic {
52        level,
53        message: message.to_owned(),
54        location: source_location(prefix),
55    })
56}
57
58fn source_location(prefix: &str) -> Option<SourceLocation> {
59    let mut fields = prefix.rsplitn(3, ':');
60    let column = fields.next()?.trim().parse().ok()?;
61    let line = fields.next()?.trim().parse().ok()?;
62    Some(SourceLocation { line, column })
63}
64
65#[cfg(test)]
66mod tests {
67    use super::{DiagnosticLevel, SourceLocation, parse_diagnostics};
68
69    #[test]
70    fn preserves_each_finding_and_classifies_known_levels() {
71        let diagnostics = parse_diagnostics(
72            "mant: page.1:8:2: UNSUPP: unsupported roff request: ab\n\
73             mant: page.1:9:1: WARNING: skipping paragraph macro\n",
74        );
75
76        assert_eq!(diagnostics.len(), 2);
77        assert_eq!(diagnostics[0].level, DiagnosticLevel::Unsupported);
78        assert_eq!(diagnostics[0].message, "unsupported roff request: ab");
79        assert_eq!(
80            diagnostics[0].location,
81            Some(SourceLocation { line: 8, column: 2 })
82        );
83        assert_eq!(diagnostics[1].level, DiagnosticLevel::Warning);
84    }
85}