Skip to main content

pedant_core/
violation.rs

1use std::fmt;
2use std::sync::Arc;
3
4pub use crate::checks::{ViolationType, VisibilityDetail, lookup_rationale};
5
6/// Structured explanation of why a check exists, shown by `--explain`.
7#[derive(Debug, Clone, Copy, serde::Serialize)]
8pub struct CheckRationale {
9    /// The code smell or risk this check detects.
10    pub problem: &'static str,
11    /// Concrete refactoring steps to eliminate the violation.
12    pub fix: &'static str,
13    /// Situations where suppression is justified.
14    pub exception: &'static str,
15    /// `true` when the pattern is disproportionately common in LLM output.
16    pub llm_specific: bool,
17}
18
19impl fmt::Display for CheckRationale {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        writeln!(f, "Problem:      {}", self.problem)?;
22        writeln!(f, "Fix:          {}", self.fix)?;
23        writeln!(f, "Exception:    {}", self.exception)?;
24        write!(f, "LLM-specific: {}", self.llm_specific)
25    }
26}
27
28impl ViolationType {
29    /// The glob pattern that triggered this violation, for pattern-based checks only.
30    pub fn pattern(&self) -> Option<&str> {
31        match self {
32            Self::ForbiddenAttribute { pattern }
33            | Self::ForbiddenType { pattern }
34            | Self::ForbiddenCall { pattern }
35            | Self::ForbiddenMacro { pattern } => Some(pattern),
36            _ => None,
37        }
38    }
39
40    /// Structured detail for an `item-visibility-policy` finding, if this is one.
41    pub fn visibility_detail(&self) -> Option<&VisibilityDetail> {
42        match self {
43            Self::ItemVisibilityPolicy { detail } => Some(detail),
44            _ => None,
45        }
46    }
47}
48
49impl fmt::Display for ViolationType {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "{}", self.code())
52    }
53}
54
55/// Whether a violation blocks the run or is advisory.
56///
57/// Style violations default to [`Severity::Deny`]. Only checks with an explicit
58/// warning tier (e.g. `large-source-file`) emit [`Severity::Warn`], which is
59/// reported but does not affect the process exit code.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize)]
61#[serde(rename_all = "lowercase")]
62pub enum Severity {
63    /// Advisory: shown in output but does not fail the run.
64    Warn,
65    /// Blocking: fails the run with a non-zero exit code.
66    #[default]
67    Deny,
68}
69
70impl Severity {
71    /// `true` when this severity fails the run.
72    pub fn is_deny(self) -> bool {
73        matches!(self, Self::Deny)
74    }
75
76    /// Lowercase label for text output.
77    pub fn as_str(self) -> &'static str {
78        match self {
79            Self::Warn => "warn",
80            Self::Deny => "deny",
81        }
82    }
83}
84
85impl fmt::Display for Severity {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str(self.as_str())
88    }
89}
90
91/// A located style violation with diagnostic message.
92#[derive(Debug, Clone)]
93pub struct Violation {
94    /// Which check produced this violation.
95    pub violation_type: ViolationType,
96    /// Absolute path of the offending file.
97    pub file_path: Arc<str>,
98    /// 1-based line number.
99    pub line: usize,
100    /// 1-based column number.
101    pub column: usize,
102    /// Diagnostic message describing the specific issue.
103    pub message: Box<str>,
104    /// Whether this violation blocks the run (`Deny`) or is advisory (`Warn`).
105    pub severity: Severity,
106}
107
108impl Violation {
109    /// Construct a violation at a specific file location.
110    pub fn new(
111        violation_type: ViolationType,
112        file_path: Arc<str>,
113        line: usize,
114        column: usize,
115        message: impl Into<Box<str>>,
116    ) -> Self {
117        Self {
118            violation_type,
119            file_path,
120            line,
121            column,
122            message: message.into(),
123            severity: Severity::Deny,
124        }
125    }
126
127    /// Set the severity, overriding the [`Severity::Deny`] default.
128    pub fn with_severity(mut self, severity: Severity) -> Self {
129        self.severity = severity;
130        self
131    }
132
133    /// Delegates to the violation type's check rationale.
134    pub fn rationale(&self) -> CheckRationale {
135        self.violation_type.rationale()
136    }
137}
138
139impl fmt::Display for Violation {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(
142            f,
143            "{}:{}:{}: {}: {}",
144            self.file_path, self.line, self.column, self.violation_type, self.message
145        )
146    }
147}