1use std::path::PathBuf;
2
3use serde::{Serialize, Serializer, ser::SerializeStruct};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
6pub enum DiagnosticLevel {
7 Warning,
8 Error,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
12pub enum DiagnosticClass {
13 ToolError,
14 ToolWarning,
15 PolicyError { code: String },
16 PolicyWarning { code: String },
17 AdvisoryWarning { code: String },
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum DiagnosticFixKind {
23 ReplacePath,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
27pub struct DiagnosticFix {
28 pub kind: DiagnosticFixKind,
29 pub replacement: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
33pub struct Diagnostic {
34 pub class: DiagnosticClass,
35 pub file: Option<PathBuf>,
36 pub line: Option<usize>,
37 pub fix: Option<DiagnosticFix>,
38 pub message: String,
39}
40
41impl Diagnostic {
42 pub fn error(file: Option<PathBuf>, line: Option<usize>, message: impl Into<String>) -> Self {
43 Self {
44 class: DiagnosticClass::ToolError,
45 file,
46 line,
47 fix: None,
48 message: message.into(),
49 }
50 }
51
52 pub fn warning(file: Option<PathBuf>, line: Option<usize>, message: impl Into<String>) -> Self {
53 Self {
54 class: DiagnosticClass::ToolWarning,
55 file,
56 line,
57 fix: None,
58 message: message.into(),
59 }
60 }
61
62 pub fn policy(
63 file: Option<PathBuf>,
64 line: Option<usize>,
65 code: impl Into<String>,
66 message: impl Into<String>,
67 ) -> Self {
68 Self {
69 class: DiagnosticClass::PolicyWarning { code: code.into() },
70 file,
71 line,
72 fix: None,
73 message: message.into(),
74 }
75 }
76
77 pub fn policy_error(
78 file: Option<PathBuf>,
79 line: Option<usize>,
80 code: impl Into<String>,
81 message: impl Into<String>,
82 ) -> Self {
83 Self {
84 class: DiagnosticClass::PolicyError { code: code.into() },
85 file,
86 line,
87 fix: None,
88 message: message.into(),
89 }
90 }
91
92 pub fn advisory(
93 file: Option<PathBuf>,
94 line: Option<usize>,
95 code: impl Into<String>,
96 message: impl Into<String>,
97 ) -> Self {
98 Self {
99 class: DiagnosticClass::AdvisoryWarning { code: code.into() },
100 file,
101 line,
102 fix: None,
103 message: message.into(),
104 }
105 }
106
107 pub fn with_fix(mut self, fix: DiagnosticFix) -> Self {
108 self.fix = Some(fix);
109 self
110 }
111
112 pub fn level(&self) -> DiagnosticLevel {
113 match self.class {
114 DiagnosticClass::ToolError | DiagnosticClass::PolicyError { .. } => {
115 DiagnosticLevel::Error
116 }
117 DiagnosticClass::ToolWarning
118 | DiagnosticClass::PolicyWarning { .. }
119 | DiagnosticClass::AdvisoryWarning { .. } => DiagnosticLevel::Warning,
120 }
121 }
122
123 pub fn code(&self) -> Option<&str> {
124 match &self.class {
125 DiagnosticClass::PolicyError { code }
126 | DiagnosticClass::PolicyWarning { code }
127 | DiagnosticClass::AdvisoryWarning { code } => Some(code),
128 DiagnosticClass::ToolError | DiagnosticClass::ToolWarning => None,
129 }
130 }
131
132 pub fn is_error(&self) -> bool {
133 matches!(
134 self.class,
135 DiagnosticClass::ToolError | DiagnosticClass::PolicyError { .. }
136 )
137 }
138
139 pub fn is_policy_warning(&self) -> bool {
140 matches!(self.class, DiagnosticClass::PolicyWarning { .. })
141 }
142
143 pub fn is_advisory_warning(&self) -> bool {
144 matches!(
145 self.class,
146 DiagnosticClass::ToolWarning | DiagnosticClass::AdvisoryWarning { .. }
147 )
148 }
149
150 pub fn is_policy_violation(&self) -> bool {
151 matches!(
152 self.class,
153 DiagnosticClass::PolicyError { .. } | DiagnosticClass::PolicyWarning { .. }
154 )
155 }
156}
157
158impl Serialize for Diagnostic {
159 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160 where
161 S: Serializer,
162 {
163 let mut state = serializer.serialize_struct("Diagnostic", 7)?;
164 state.serialize_field("level", &self.level())?;
165 state.serialize_field("file", &self.file)?;
166 state.serialize_field("line", &self.line)?;
167 state.serialize_field("code", &self.code())?;
168 state.serialize_field("policy", &self.is_policy_violation())?;
169 state.serialize_field("fix", &self.fix)?;
170 state.serialize_field("message", &self.message)?;
171 state.end()
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum DiagnosticSelection {
177 All,
178 Policy,
179 Advisory,
180}
181
182impl DiagnosticSelection {
183 pub fn includes(self, diagnostic: &Diagnostic) -> bool {
184 match self {
185 Self::All => true,
186 Self::Policy => diagnostic.is_error() || diagnostic.is_policy_violation(),
187 Self::Advisory => diagnostic.is_error() || diagnostic.is_advisory_warning(),
188 }
189 }
190
191 pub fn report_label(self) -> Option<&'static str> {
192 match self {
193 Self::All => None,
194 Self::Policy => Some("policy diagnostics and errors only"),
195 Self::Advisory => Some("advisory diagnostics and errors only"),
196 }
197 }
198}
199
200impl std::str::FromStr for DiagnosticSelection {
201 type Err = String;
202
203 fn from_str(raw: &str) -> Result<Self, Self::Err> {
204 match raw {
205 "all" => Ok(Self::All),
206 "policy" => Ok(Self::Policy),
207 "advisory" => Ok(Self::Advisory),
208 _ => Err(format!(
209 "invalid show mode `{raw}`; expected all|policy|advisory"
210 )),
211 }
212 }
213}