#[cfg(feature = "mcp")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Error => write!(f, "error"),
Self::Warning => write!(f, "warning"),
Self::Info => write!(f, "info"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum Category {
ErrorHandling,
Performance,
Security,
Correctness,
Architecture,
Dependencies,
Async,
Framework,
Cargo,
Style,
}
impl std::fmt::Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ErrorHandling => write!(f, "Error Handling"),
Self::Performance => write!(f, "Performance"),
Self::Security => write!(f, "Security"),
Self::Correctness => write!(f, "Correctness"),
Self::Architecture => write!(f, "Architecture"),
Self::Dependencies => write!(f, "Dependencies"),
Self::Async => write!(f, "Async"),
Self::Framework => write!(f, "Framework"),
Self::Cargo => write!(f, "Cargo"),
Self::Style => write!(f, "Style"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "mcp", derive(JsonSchema))]
pub struct CodeFix {
pub old_text: String,
pub new_text: String,
pub line: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "mcp", derive(JsonSchema))]
pub struct Diagnostic {
pub file_path: PathBuf,
pub rule: String,
pub category: Category,
pub severity: Severity,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fix: Option<CodeFix>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "mcp", derive(JsonSchema))]
pub enum ScoreLabel {
#[serde(rename = "Great")]
Great,
#[serde(rename = "Needs work")]
NeedsWork,
#[serde(rename = "Critical")]
Critical,
}
impl std::fmt::Display for ScoreLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Great => write!(f, "Great"),
Self::NeedsWork => write!(f, "Needs work"),
Self::Critical => write!(f, "Critical"),
}
}
}
#[derive(Debug, Serialize)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
pub struct DimensionScores {
pub security: u32,
pub reliability: u32,
pub maintainability: u32,
pub performance: u32,
pub dependencies: u32,
}
#[derive(Debug, Serialize)]
pub struct ScanResult {
pub diagnostics: Vec<Diagnostic>,
pub score: u32,
pub score_label: ScoreLabel,
pub dimension_scores: DimensionScores,
pub source_file_count: usize,
#[serde(serialize_with = "serialize_duration")]
pub elapsed: Duration,
pub skipped_passes: Vec<String>,
pub error_count: usize,
pub warning_count: usize,
pub info_count: usize,
#[serde(serialize_with = "serialize_pass_timings")]
pub pass_timings: Vec<(String, Duration)>,
}
fn serialize_duration<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_f64(duration.as_secs_f64())
}
fn serialize_pass_timings<S>(
timings: &[(String, Duration)],
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(timings.len()))?;
for (name, duration) in timings {
seq.serialize_element(&serde_json::json!({
"pass": name,
"elapsed_secs": duration.as_secs_f64()
}))?;
}
seq.end()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_display() {
assert_eq!(Severity::Error.to_string(), "error");
assert_eq!(Severity::Warning.to_string(), "warning");
assert_eq!(Severity::Info.to_string(), "info");
}
#[test]
fn test_category_display() {
assert_eq!(Category::ErrorHandling.to_string(), "Error Handling");
assert_eq!(Category::Performance.to_string(), "Performance");
assert_eq!(Category::Security.to_string(), "Security");
}
#[test]
fn test_diagnostic_serialize() {
let diag = Diagnostic {
file_path: PathBuf::from("src/main.rs"),
rule: "unwrap-in-production".to_string(),
category: Category::ErrorHandling,
severity: Severity::Warning,
message: "Use of .unwrap() in production code".to_string(),
help: Some("Use ? operator or handle the error explicitly".to_string()),
line: Some(42),
column: Some(10),
fix: None,
};
let json = serde_json::to_value(&diag).unwrap();
assert_eq!(json["rule"], "unwrap-in-production");
assert_eq!(json["severity"], "warning");
assert_eq!(json["category"], "error-handling");
assert_eq!(json["line"], 42);
}
#[test]
fn test_diagnostic_serialize_no_optionals() {
let diag = Diagnostic {
file_path: PathBuf::from("Cargo.toml"),
rule: "unused-dependency".to_string(),
category: Category::Dependencies,
severity: Severity::Warning,
message: "Unused dependency: serde".to_string(),
help: None,
line: None,
column: None,
fix: None,
};
let json = serde_json::to_value(&diag).unwrap();
assert!(json.get("help").is_none());
assert!(json.get("line").is_none());
assert!(json.get("column").is_none());
}
#[test]
fn test_scan_result_serialize() {
let result = ScanResult {
diagnostics: vec![],
score: 100,
score_label: ScoreLabel::Great,
dimension_scores: DimensionScores {
security: 100,
reliability: 100,
maintainability: 100,
performance: 100,
dependencies: 100,
},
source_file_count: 10,
elapsed: Duration::from_millis(1500),
skipped_passes: vec![],
error_count: 0,
warning_count: 0,
info_count: 0,
pass_timings: vec![
("clippy".to_string(), Duration::from_millis(800)),
("custom rules".to_string(), Duration::from_millis(200)),
],
};
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["score"], 100);
assert_eq!(json["score_label"], "Great");
assert_eq!(json["source_file_count"], 10);
assert_eq!(json["elapsed"], 1.5);
assert_eq!(json["error_count"], 0);
let timings = json["pass_timings"].as_array().unwrap();
assert_eq!(timings.len(), 2);
assert_eq!(timings[0]["pass"], "clippy");
assert!((timings[0]["elapsed_secs"].as_f64().unwrap() - 0.8).abs() < 0.001);
assert_eq!(timings[1]["pass"], "custom rules");
}
}