Skip to main content

rigsql_output/
json.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use rigsql_rules::{LintViolation, Rule};
5use serde::Serialize;
6
7/// AI-friendly JSON output formatter.
8pub 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}