Skip to main content

rigsql_output/
sarif.rs

1use std::path::Path;
2
3use rigsql_rules::{LintViolation, Rule};
4use serde::Serialize;
5use std::collections::HashMap;
6
7/// SARIF v2.1.0 output formatter.
8///
9/// Produces Static Analysis Results Interchange Format (SARIF) JSON,
10/// compatible with GitHub Code Scanning, VS Code SARIF Viewer, and
11/// other SARIF-consuming tools.
12pub 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        // Build rule descriptors and index map
107        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        // Build results
128        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}