Skip to main content

spec_driven_docs/domain/
finding.rs

1//! A gate finding: one violation, addressed to the rule it breaks.
2//!
3//! Rendering is fixed here so every failure line a consumer sees has one
4//! shape — `FAIL <domain>:<rule> [<path>[:<line>]]: <detail>` — and always
5//! cites a rule a spec defines. What counts as a violation is each gate's
6//! business, not this type's.
7
8use std::fmt;
9
10use camino::Utf8PathBuf;
11
12use crate::domain::rule_id::RuleId;
13
14/// One violation found by a check.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Finding {
17    /// The requirement the violation breaks.
18    pub rule: RuleId,
19    /// The offending file, when the violation has one.
20    pub path: Option<Utf8PathBuf>,
21    /// The offending line within `path`, when the violation has one.
22    pub line: Option<usize>,
23    /// What is wrong, stated for the reader who will fix it.
24    pub detail: String,
25}
26
27impl Finding {
28    /// A finding anchored to a file.
29    #[must_use]
30    pub fn on_file(rule: RuleId, path: impl Into<Utf8PathBuf>, detail: impl Into<String>) -> Self {
31        Self {
32            rule,
33            path: Some(path.into()),
34            line: None,
35            detail: detail.into(),
36        }
37    }
38
39    /// A finding anchored to one line of a file.
40    #[must_use]
41    pub fn on_line(
42        rule: RuleId,
43        path: impl Into<Utf8PathBuf>,
44        line: usize,
45        detail: impl Into<String>,
46    ) -> Self {
47        Self {
48            rule,
49            path: Some(path.into()),
50            line: Some(line),
51            detail: detail.into(),
52        }
53    }
54
55    /// A finding about the repository as a whole.
56    #[must_use]
57    pub fn global(rule: RuleId, detail: impl Into<String>) -> Self {
58        Self {
59            rule,
60            path: None,
61            line: None,
62            detail: detail.into(),
63        }
64    }
65}
66
67impl fmt::Display for Finding {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "FAIL {}", self.rule)?;
70        if let Some(path) = &self.path {
71            write!(f, " {path}")?;
72            if let Some(line) = self.line {
73                write!(f, ":{line}")?;
74            }
75        }
76        if self.detail.is_empty() {
77            return Ok(());
78        }
79        write!(f, ": {}", self.detail)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn renders_with_path_and_line() {
89        let finding = Finding::on_line(
90            RuleId::BugzillaReportBodyFitsReportWidth,
91            "_docs/reference/known-issues/KI-vendor-500.md",
92            12,
93            "83 columns",
94        );
95        assert_eq!(
96            finding.to_string(),
97            "FAIL known-issues:a-bugzilla-report-body-fits-in-79-columns \
98             _docs/reference/known-issues/KI-vendor-500.md:12: 83 columns"
99        );
100    }
101
102    #[test]
103    fn renders_without_a_path() {
104        let finding = Finding::global(
105            RuleId::GateMessageCitesTheRule,
106            "x:y resolves to no requirement",
107        );
108        assert_eq!(
109            finding.to_string(),
110            "FAIL spec-to-code:a-gate-message-cites-the-rule: x:y resolves to no requirement"
111        );
112    }
113}