use crate::{CapturedNode, MatchResult, Severity, Span};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: Severity,
pub rule_id: String,
pub message: String,
pub file_path: PathBuf,
pub span: Span,
pub snippet: Option<String>,
pub suggestion: Option<String>,
pub notes: Vec<String>,
}
impl Diagnostic {
pub fn from_match_result(
result: &MatchResult,
file_path: impl Into<PathBuf>,
primary_capture: Option<&str>,
) -> Option<Self> {
if !result.matched {
return None;
}
let span = primary_capture
.and_then(|name| result.captures.get(name))
.or_else(|| result.captures.values().next())
.map(|c| c.span)
.unwrap_or(Span::point(1, 1));
Some(Self {
severity: result.severity.unwrap_or(Severity::Warning),
rule_id: result.rule_id.clone().unwrap_or_else(|| "unknown".into()),
message: result.message.clone().unwrap_or_default(),
file_path: file_path.into(),
span,
snippet: None,
suggestion: result.suggestion.clone(),
notes: Vec::new(),
})
}
pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
self.snippet = Some(snippet.into());
self
}
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
pub fn format_clippy(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"{}[{}]: {}\n",
self.severity, self.rule_id, self.message
));
output.push_str(&format!(
" --> {}:{}:{}\n",
self.file_path.display(),
self.span.start.line,
self.span.start.column
));
if let Some(ref snippet) = self.snippet {
output.push_str(" |\n");
for (i, line) in snippet.lines().enumerate() {
let line_num = self.span.start.line as usize + i;
output.push_str(&format!("{:>3} | {}\n", line_num, line));
}
output.push_str(" |\n");
}
if let Some(ref suggestion) = self.suggestion {
output.push_str(&format!(" = help: {}\n", suggestion));
}
for note in &self.notes {
output.push_str(&format!(" = note: {}\n", note));
}
output
}
pub fn format_json(&self) -> String {
serde_json::to_string(&DiagnosticJson::from(self)).unwrap_or_default()
}
}
#[derive(Debug, Clone, serde::Serialize)]
struct DiagnosticJson {
severity: String,
rule_id: String,
message: String,
file_path: String,
line: u32,
column: u32,
end_line: u32,
end_column: u32,
suggestion: Option<String>,
notes: Vec<String>,
}
impl From<&Diagnostic> for DiagnosticJson {
fn from(d: &Diagnostic) -> Self {
Self {
severity: d.severity.to_string(),
rule_id: d.rule_id.clone(),
message: d.message.clone(),
file_path: d.file_path.to_string_lossy().to_string(),
line: d.span.start.line,
column: d.span.start.column,
end_line: d.span.end.line,
end_column: d.span.end.column,
suggestion: d.suggestion.clone(),
notes: d.notes.clone(),
}
}
}
pub fn interpolate_message(template: &str, captures: &HashMap<String, CapturedNode>) -> String {
let mut result = template.to_string();
for (name, node) in captures {
let placeholder = if name.starts_with('$') {
name.clone()
} else {
format!("${}", name)
};
let text_access = format!("{{{}.text}}", placeholder);
result = result.replace(&text_access, &node.text);
let braced = format!("{{{}}}", placeholder);
result = result.replace(&braced, &node.text);
result = result.replace(&placeholder, &node.text);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Position;
#[test]
fn test_interpolate_message() {
let mut captures = HashMap::new();
captures.insert(
"$UNWRAP".to_string(),
CapturedNode::new(Span::point(1, 1), "result.unwrap()"),
);
captures.insert(
"$RECEIVER".to_string(),
CapturedNode::new(Span::point(1, 1), "result"),
);
let msg = interpolate_message("Found $UNWRAP on receiver $RECEIVER", &captures);
assert_eq!(msg, "Found result.unwrap() on receiver result");
let msg2 = interpolate_message("Replace {$UNWRAP} with {$RECEIVER}?", &captures);
assert_eq!(msg2, "Replace result.unwrap() with result?");
}
#[test]
fn test_diagnostic_format_clippy() {
let diag = Diagnostic {
severity: Severity::Warning,
rule_id: "RL001".to_string(),
message: "Avoid unwrap() in public function".to_string(),
file_path: PathBuf::from("src/lib.rs"),
span: Span::new(
Position {
line: 42,
column: 10,
},
Position {
line: 42,
column: 25,
},
),
snippet: Some(" let x = result.unwrap();".to_string()),
suggestion: Some("Use ? operator or expect()".to_string()),
notes: vec!["Consider error handling".to_string()],
};
let output = diag.format_clippy();
assert!(output.contains("warning[RL001]"));
assert!(output.contains("src/lib.rs:42:10"));
assert!(output.contains("Avoid unwrap()"));
assert!(output.contains("help: Use ? operator"));
assert!(output.contains("note: Consider error handling"));
}
#[test]
fn test_diagnostic_from_match_result() {
let result = MatchResult::matched().capture(
"$UNWRAP",
CapturedNode::new(
Span::new(
Position {
line: 10,
column: 5,
},
Position {
line: 10,
column: 20,
},
),
"x.unwrap()",
),
);
let mut result_with_rule = result;
result_with_rule.rule_id = Some("RL001".to_string());
result_with_rule.severity = Some(Severity::Warning);
result_with_rule.message = Some("Found unwrap".to_string());
let diag = Diagnostic::from_match_result(&result_with_rule, "src/main.rs", Some("$UNWRAP"));
assert!(diag.is_some());
let d = diag.unwrap();
assert_eq!(d.rule_id, "RL001");
assert_eq!(d.span.start.line, 10);
}
}