1#[cfg(feature = "mcp")]
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[cfg_attr(feature = "mcp", derive(JsonSchema))]
10#[serde(rename_all = "lowercase")]
11pub enum Severity {
12 Error,
13 Warning,
14 Info,
15}
16
17impl std::fmt::Display for Severity {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 Self::Error => write!(f, "error"),
21 Self::Warning => write!(f, "warning"),
22 Self::Info => write!(f, "info"),
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[cfg_attr(feature = "mcp", derive(JsonSchema))]
30#[serde(rename_all = "kebab-case")]
31pub enum Category {
32 ErrorHandling,
33 Performance,
34 Security,
35 Correctness,
36 Architecture,
37 Dependencies,
38 Async,
39 Framework,
40 Cargo,
41 Style,
42}
43
44impl std::fmt::Display for Category {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::ErrorHandling => write!(f, "Error Handling"),
48 Self::Performance => write!(f, "Performance"),
49 Self::Security => write!(f, "Security"),
50 Self::Correctness => write!(f, "Correctness"),
51 Self::Architecture => write!(f, "Architecture"),
52 Self::Dependencies => write!(f, "Dependencies"),
53 Self::Async => write!(f, "Async"),
54 Self::Framework => write!(f, "Framework"),
55 Self::Cargo => write!(f, "Cargo"),
56 Self::Style => write!(f, "Style"),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63#[cfg_attr(feature = "mcp", derive(JsonSchema))]
64pub struct CodeFix {
65 pub old_text: String,
67 pub new_text: String,
69 pub line: u32,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75#[cfg_attr(feature = "mcp", derive(JsonSchema))]
76pub struct Diagnostic {
77 pub file_path: PathBuf,
79 pub rule: String,
81 pub category: Category,
83 pub severity: Severity,
85 pub message: String,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub help: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub line: Option<u32>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub column: Option<u32>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub fix: Option<CodeFix>,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
103#[cfg_attr(feature = "mcp", derive(JsonSchema))]
104pub enum ScoreLabel {
105 #[serde(rename = "Great")]
106 Great,
107 #[serde(rename = "Needs work")]
108 NeedsWork,
109 #[serde(rename = "Critical")]
110 Critical,
111}
112
113impl std::fmt::Display for ScoreLabel {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Self::Great => write!(f, "Great"),
117 Self::NeedsWork => write!(f, "Needs work"),
118 Self::Critical => write!(f, "Critical"),
119 }
120 }
121}
122
123#[derive(Debug, Serialize)]
125#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
126pub struct DimensionScores {
127 pub security: u32,
129 pub reliability: u32,
131 pub maintainability: u32,
133 pub performance: u32,
135 pub dependencies: u32,
137}
138
139#[derive(Debug, Serialize)]
141pub struct ScanResult {
142 pub diagnostics: Vec<Diagnostic>,
144 pub score: u32,
146 pub score_label: ScoreLabel,
148 pub dimension_scores: DimensionScores,
150 pub source_file_count: usize,
152 #[serde(serialize_with = "serialize_duration")]
154 pub elapsed: Duration,
155 pub skipped_passes: Vec<String>,
157 pub error_count: usize,
159 pub warning_count: usize,
161 pub info_count: usize,
163 #[serde(serialize_with = "serialize_pass_timings")]
165 pub pass_timings: Vec<(String, Duration)>,
166}
167
168fn serialize_duration<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
169where
170 S: serde::Serializer,
171{
172 serializer.serialize_f64(duration.as_secs_f64())
173}
174
175fn serialize_pass_timings<S>(
176 timings: &[(String, Duration)],
177 serializer: S,
178) -> Result<S::Ok, S::Error>
179where
180 S: serde::Serializer,
181{
182 use serde::ser::SerializeSeq;
183 let mut seq = serializer.serialize_seq(Some(timings.len()))?;
184 for (name, duration) in timings {
185 seq.serialize_element(&serde_json::json!({
186 "pass": name,
187 "elapsed_secs": duration.as_secs_f64()
188 }))?;
189 }
190 seq.end()
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_severity_display() {
199 assert_eq!(Severity::Error.to_string(), "error");
200 assert_eq!(Severity::Warning.to_string(), "warning");
201 assert_eq!(Severity::Info.to_string(), "info");
202 }
203
204 #[test]
205 fn test_category_display() {
206 assert_eq!(Category::ErrorHandling.to_string(), "Error Handling");
207 assert_eq!(Category::Performance.to_string(), "Performance");
208 assert_eq!(Category::Security.to_string(), "Security");
209 }
210
211 #[test]
212 fn test_diagnostic_serialize() {
213 let diag = Diagnostic {
214 file_path: PathBuf::from("src/main.rs"),
215 rule: "unwrap-in-production".to_string(),
216 category: Category::ErrorHandling,
217 severity: Severity::Warning,
218 message: "Use of .unwrap() in production code".to_string(),
219 help: Some("Use ? operator or handle the error explicitly".to_string()),
220 line: Some(42),
221 column: Some(10),
222 fix: None,
223 };
224 let json = serde_json::to_value(&diag).unwrap();
225 assert_eq!(json["rule"], "unwrap-in-production");
226 assert_eq!(json["severity"], "warning");
227 assert_eq!(json["category"], "error-handling");
228 assert_eq!(json["line"], 42);
229 }
230
231 #[test]
232 fn test_diagnostic_serialize_no_optionals() {
233 let diag = Diagnostic {
234 file_path: PathBuf::from("Cargo.toml"),
235 rule: "unused-dependency".to_string(),
236 category: Category::Dependencies,
237 severity: Severity::Warning,
238 message: "Unused dependency: serde".to_string(),
239 help: None,
240 line: None,
241 column: None,
242 fix: None,
243 };
244 let json = serde_json::to_value(&diag).unwrap();
245 assert!(json.get("help").is_none());
246 assert!(json.get("line").is_none());
247 assert!(json.get("column").is_none());
248 }
249
250 #[test]
251 fn test_scan_result_serialize() {
252 let result = ScanResult {
253 diagnostics: vec![],
254 score: 100,
255 score_label: ScoreLabel::Great,
256 dimension_scores: DimensionScores {
257 security: 100,
258 reliability: 100,
259 maintainability: 100,
260 performance: 100,
261 dependencies: 100,
262 },
263 source_file_count: 10,
264 elapsed: Duration::from_millis(1500),
265 skipped_passes: vec![],
266 error_count: 0,
267 warning_count: 0,
268 info_count: 0,
269 pass_timings: vec![
270 ("clippy".to_string(), Duration::from_millis(800)),
271 ("custom rules".to_string(), Duration::from_millis(200)),
272 ],
273 };
274 let json = serde_json::to_value(&result).unwrap();
275 assert_eq!(json["score"], 100);
276 assert_eq!(json["score_label"], "Great");
277 assert_eq!(json["source_file_count"], 10);
278 assert_eq!(json["elapsed"], 1.5);
279 assert_eq!(json["error_count"], 0);
280 let timings = json["pass_timings"].as_array().unwrap();
282 assert_eq!(timings.len(), 2);
283 assert_eq!(timings[0]["pass"], "clippy");
284 assert!((timings[0]["elapsed_secs"].as_f64().unwrap() - 0.8).abs() < 0.001);
285 assert_eq!(timings[1]["pass"], "custom rules");
286 }
287}