1use std::collections::HashMap;
2use std::path::Path;
3
4use rigsql_rules::{LintViolation, Rule};
5use serde::Serialize;
6
7pub struct JsonFormatter;
9
10#[derive(Serialize)]
11pub struct JsonReport {
12 pub version: &'static str,
13 pub tool: ToolInfo,
14 pub summary: Summary,
15 pub files: Vec<FileReport>,
16}
17
18#[derive(Serialize)]
19pub struct ToolInfo {
20 pub name: &'static str,
21 pub version: &'static str,
22}
23
24#[derive(Serialize)]
25pub struct Summary {
26 pub files_scanned: usize,
27 pub files_with_violations: usize,
28 pub total_violations: usize,
29 pub by_rule: HashMap<String, usize>,
30}
31
32#[derive(Serialize)]
33pub struct FileReport {
34 pub path: String,
35 pub violations: Vec<ViolationReport>,
36}
37
38#[derive(Serialize)]
39pub struct ViolationReport {
40 pub rule: RuleInfo,
41 pub message: String,
42 pub severity: String,
43 pub location: Location,
44 pub context: Context,
45}
46
47#[derive(Serialize)]
48pub struct RuleInfo {
49 pub code: String,
50 pub name: String,
51 pub description: String,
52 pub explanation: String,
53}
54
55#[derive(Serialize)]
56pub struct Location {
57 pub line: usize,
58 pub column: usize,
59}
60
61#[derive(Serialize)]
62pub struct Context {
63 pub source_line: String,
64}
65
66impl JsonFormatter {
67 pub fn format(file_results: &[(&Path, &str, &[LintViolation])]) -> String {
68 Self::format_with_rules(file_results, &rigsql_rules::default_rules())
69 }
70
71 pub fn format_with_rules(
72 file_results: &[(&Path, &str, &[LintViolation])],
73 rules: &[Box<dyn Rule>],
74 ) -> String {
75 let rule_map: HashMap<&str, &dyn Rule> =
76 rules.iter().map(|r| (r.code(), r.as_ref())).collect();
77
78 let mut by_rule: HashMap<String, usize> = HashMap::new();
79 let mut total_violations = 0;
80 let mut files_with_violations = 0;
81
82 let files: Vec<FileReport> = file_results
83 .iter()
84 .map(|(path, source, violations)| {
85 if !violations.is_empty() {
86 files_with_violations += 1;
87 }
88 total_violations += violations.len();
89
90 let violation_reports: Vec<ViolationReport> = violations
91 .iter()
92 .map(|v| {
93 *by_rule.entry(v.rule_code.to_string()).or_insert(0) += 1;
94 let (line, col) = v.line_col(source);
95
96 let source_line = source
97 .lines()
98 .nth(line.saturating_sub(1))
99 .unwrap_or("")
100 .to_string();
101
102 let rule_info = rule_map.get(v.rule_code);
103
104 ViolationReport {
105 rule: RuleInfo {
106 code: v.rule_code.to_string(),
107 name: rule_info.map_or("", |r| r.name()).to_string(),
108 description: rule_info.map_or("", |r| r.description()).to_string(),
109 explanation: rule_info.map_or("", |r| r.explanation()).to_string(),
110 },
111 message: v.message.clone(),
112 severity: "warning".to_string(),
113 location: Location { line, column: col },
114 context: Context { source_line },
115 }
116 })
117 .collect();
118
119 FileReport {
120 path: path.display().to_string(),
121 violations: violation_reports,
122 }
123 })
124 .collect();
125
126 let report = JsonReport {
127 version: "1.0",
128 tool: ToolInfo {
129 name: "rigsql",
130 version: env!("CARGO_PKG_VERSION"),
131 },
132 summary: Summary {
133 files_scanned: file_results.len(),
134 files_with_violations,
135 total_violations,
136 by_rule,
137 },
138 files,
139 };
140
141 serde_json::to_string_pretty(&report).unwrap()
142 }
143}