ailint_core/reporter/
json.rs1use std::collections::BTreeMap;
4use std::io::Write;
5
6use anyhow::Result;
7use chrono::{DateTime, SecondsFormat, Utc};
8use serde::Serialize;
9
10use crate::reporter::Reporter;
11use crate::rules::registry::rule_meta;
12use crate::rules::{Severity, Violation};
13use crate::VERSION;
14
15const SCHEMA_VERSION: &str = "2";
16
17#[derive(Debug)]
19pub struct JsonReporter {
20 now: fn() -> DateTime<Utc>,
21}
22
23impl Default for JsonReporter {
24 fn default() -> Self {
25 Self { now: Utc::now }
26 }
27}
28
29impl JsonReporter {
30 pub fn with_now(now: fn() -> DateTime<Utc>) -> Self {
32 Self { now }
33 }
34}
35
36impl Reporter for JsonReporter {
37 fn report(&self, violations: &[Violation], out: &mut dyn Write) -> Result<()> {
38 let report = build_report(violations, (self.now)());
39 serde_json::to_writer_pretty(&mut *out, &report)?;
40 writeln!(out)?;
41 Ok(())
42 }
43}
44
45#[derive(Serialize)]
46struct Report<'a> {
47 schema_version: &'static str,
48 tool: Tool,
49 generated_at: String,
50 summary: Summary,
51 files: Vec<FileEntry>,
52 violations: Vec<ViolationEntry<'a>>,
53}
54
55#[derive(Serialize)]
56struct Tool {
57 name: &'static str,
58 version: &'static str,
59}
60
61#[derive(Serialize)]
62struct Summary {
63 total: usize,
64 errors: usize,
65 warnings: usize,
66 info: usize,
67 file_count: usize,
68}
69
70#[derive(Serialize)]
71struct FileEntry {
72 path: String,
73 violation_count: usize,
74}
75
76#[derive(Serialize)]
77struct ViolationEntry<'a> {
78 rule: RuleEntry,
79 severity: Severity,
80 message: &'a str,
81 detail: Option<&'a str>,
82 file: String,
83 line: Option<usize>,
84 column: Option<usize>,
85 fix_hint: Option<&'a str>,
86 snippet: Option<&'a str>,
87 source_url: Option<&'a str>,
88}
89
90#[derive(Serialize)]
91struct RuleEntry {
92 code: String,
93 slug: &'static str,
94 description: &'static str,
95 fix_hint: &'static str,
96}
97
98fn build_report(violations: &[Violation], now: DateTime<Utc>) -> Report<'_> {
99 let mut errors = 0;
100 let mut warnings = 0;
101 let mut info = 0;
102 let mut per_file: BTreeMap<String, usize> = BTreeMap::new();
103
104 let mut entries = Vec::with_capacity(violations.len());
105 for v in violations {
106 match v.severity {
107 Severity::Error => errors += 1,
108 Severity::Warning => warnings += 1,
109 Severity::Info => info += 1,
110 }
111 let file = v.file.to_string_lossy().into_owned();
112 *per_file.entry(file.clone()).or_insert(0) += 1;
113 let meta = rule_meta(v.rule_id);
114 entries.push(ViolationEntry {
115 rule: RuleEntry {
116 code: v.rule_id.code_str(),
117 slug: v.rule_id.slug,
118 description: meta.map(|m| m.description).unwrap_or(""),
119 fix_hint: meta.map(|m| m.fix_hint).unwrap_or(""),
120 },
121 severity: v.severity,
122 message: &v.message,
123 detail: v.detail.as_deref(),
124 file,
125 line: v.line,
126 column: v.column,
127 fix_hint: v.fix_hint.as_deref(),
128 snippet: v.snippet.as_deref(),
129 source_url: v.source_url.as_deref(),
130 });
131 }
132
133 let files: Vec<FileEntry> = per_file
134 .into_iter()
135 .map(|(path, violation_count)| FileEntry {
136 path,
137 violation_count,
138 })
139 .collect();
140
141 Report {
142 schema_version: SCHEMA_VERSION,
143 tool: Tool {
144 name: "ailint",
145 version: VERSION,
146 },
147 generated_at: now.to_rfc3339_opts(SecondsFormat::Secs, true),
148 summary: Summary {
149 total: violations.len(),
150 errors,
151 warnings,
152 info,
153 file_count: files.len(),
154 },
155 files,
156 violations: entries,
157 }
158}