1use std::path::Path;
2
3use rigsql_rules::{LintViolation, Rule};
4use serde::Serialize;
5use std::collections::HashMap;
6
7pub struct SarifFormatter;
13
14#[derive(Serialize)]
15#[serde(rename_all = "camelCase")]
16struct SarifLog {
17 #[serde(rename = "$schema")]
18 schema: &'static str,
19 version: &'static str,
20 runs: Vec<SarifRun>,
21}
22
23#[derive(Serialize)]
24#[serde(rename_all = "camelCase")]
25struct SarifRun {
26 tool: SarifTool,
27 results: Vec<SarifResult>,
28}
29
30#[derive(Serialize)]
31#[serde(rename_all = "camelCase")]
32struct SarifTool {
33 driver: SarifDriver,
34}
35
36#[derive(Serialize)]
37#[serde(rename_all = "camelCase")]
38struct SarifDriver {
39 name: &'static str,
40 version: &'static str,
41 information_uri: &'static str,
42 rules: Vec<SarifRuleDescriptor>,
43}
44
45#[derive(Serialize)]
46#[serde(rename_all = "camelCase")]
47struct SarifRuleDescriptor {
48 id: String,
49 name: String,
50 short_description: SarifMessage,
51 full_description: SarifMessage,
52 default_configuration: SarifRuleConfig,
53}
54
55#[derive(Serialize)]
56#[serde(rename_all = "camelCase")]
57struct SarifRuleConfig {
58 level: &'static str,
59}
60
61#[derive(Serialize)]
62#[serde(rename_all = "camelCase")]
63struct SarifResult {
64 rule_id: String,
65 rule_index: usize,
66 level: &'static str,
67 message: SarifMessage,
68 locations: Vec<SarifLocation>,
69}
70
71#[derive(Serialize)]
72struct SarifMessage {
73 text: String,
74}
75
76#[derive(Serialize)]
77#[serde(rename_all = "camelCase")]
78struct SarifLocation {
79 physical_location: SarifPhysicalLocation,
80}
81
82#[derive(Serialize)]
83#[serde(rename_all = "camelCase")]
84struct SarifPhysicalLocation {
85 artifact_location: SarifArtifactLocation,
86 region: SarifRegion,
87}
88
89#[derive(Serialize)]
90struct SarifArtifactLocation {
91 uri: String,
92}
93
94#[derive(Serialize)]
95#[serde(rename_all = "camelCase")]
96struct SarifRegion {
97 start_line: usize,
98 start_column: usize,
99}
100
101impl SarifFormatter {
102 pub fn format_with_rules(
103 file_results: &[(&Path, &str, &[LintViolation])],
104 rules: &[Box<dyn Rule>],
105 ) -> String {
106 let mut rule_indices: HashMap<&str, usize> = HashMap::new();
108 let rule_descriptors: Vec<SarifRuleDescriptor> = rules
109 .iter()
110 .enumerate()
111 .map(|(i, r)| {
112 rule_indices.insert(r.code(), i);
113 SarifRuleDescriptor {
114 id: r.code().to_string(),
115 name: r.name().to_string(),
116 short_description: SarifMessage {
117 text: r.description().to_string(),
118 },
119 full_description: SarifMessage {
120 text: r.explanation().to_string(),
121 },
122 default_configuration: SarifRuleConfig { level: "warning" },
123 }
124 })
125 .collect();
126
127 let results: Vec<SarifResult> = file_results
129 .iter()
130 .flat_map(|(path, source, violations)| {
131 let indices = &rule_indices;
132 violations.iter().map(move |v| {
133 let (line, col) = v.line_col(source);
134 let rule_index = indices.get(v.rule_code).copied().unwrap_or(0);
135
136 SarifResult {
137 rule_id: v.rule_code.to_string(),
138 rule_index,
139 level: "warning",
140 message: SarifMessage {
141 text: v.message.clone(),
142 },
143 locations: vec![SarifLocation {
144 physical_location: SarifPhysicalLocation {
145 artifact_location: SarifArtifactLocation {
146 uri: path.display().to_string(),
147 },
148 region: SarifRegion {
149 start_line: line,
150 start_column: col,
151 },
152 },
153 }],
154 }
155 })
156 })
157 .collect();
158
159 let log = SarifLog {
160 schema:
161 "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
162 version: "2.1.0",
163 runs: vec![SarifRun {
164 tool: SarifTool {
165 driver: SarifDriver {
166 name: "rigsql",
167 version: env!("CARGO_PKG_VERSION"),
168 information_uri: "https://github.com/yukonsky/rigsql",
169 rules: rule_descriptors,
170 },
171 },
172 results,
173 }],
174 };
175
176 serde_json::to_string_pretty(&log).unwrap()
177 }
178}