syncable_cli/analyzer/hadolint/formatter/
codeclimate.rs1use crate::analyzer::hadolint::formatter::Formatter;
8use crate::analyzer::hadolint::lint::LintResult;
9use crate::analyzer::hadolint::types::Severity;
10use serde::Serialize;
11use std::io::Write;
12
13#[derive(Debug, Clone, Default)]
15pub struct CodeClimateFormatter;
16
17impl CodeClimateFormatter {
18 pub fn new() -> Self {
20 Self
21 }
22}
23
24#[derive(Debug, Serialize)]
26struct CodeClimateIssue {
27 #[serde(rename = "type")]
28 issue_type: &'static str,
29 check_name: String,
30 description: String,
31 content: CodeClimateContent,
32 categories: Vec<&'static str>,
33 location: CodeClimateLocation,
34 severity: &'static str,
35 fingerprint: String,
36}
37
38#[derive(Debug, Serialize)]
39struct CodeClimateContent {
40 body: String,
41}
42
43#[derive(Debug, Serialize)]
44struct CodeClimateLocation {
45 path: String,
46 lines: CodeClimateLines,
47}
48
49#[derive(Debug, Serialize)]
50struct CodeClimateLines {
51 begin: u32,
52 end: u32,
53}
54
55fn severity_to_codeclimate(severity: Severity) -> &'static str {
56 match severity {
57 Severity::Error => "critical",
58 Severity::Warning => "major",
59 Severity::Info => "minor",
60 Severity::Style => "info",
61 Severity::Ignore => "info",
62 }
63}
64
65fn get_categories(code: &str) -> Vec<&'static str> {
66 if let Some(suffix) = code.strip_prefix("DL") {
68 let rule_num: u32 = suffix.parse().unwrap_or(0);
70 match rule_num {
71 3000..=3010 => vec!["Security", "Bug Risk"],
73 3011..=3030 => vec!["Style", "Clarity"],
75 3031..=3050 => vec!["Performance"],
77 4000..=4999 => vec!["Compatibility", "Bug Risk"],
79 _ => vec!["Style"],
80 }
81 } else if code.starts_with("SC") {
82 vec!["Bug Risk", "Security"]
84 } else {
85 vec!["Style"]
86 }
87}
88
89fn generate_fingerprint(filename: &str, code: &str, line: u32) -> String {
90 use std::collections::hash_map::DefaultHasher;
91 use std::hash::{Hash, Hasher};
92
93 let mut hasher = DefaultHasher::new();
94 filename.hash(&mut hasher);
95 code.hash(&mut hasher);
96 line.hash(&mut hasher);
97 format!("{:016x}", hasher.finish())
98}
99
100fn get_help_body(code: &str) -> String {
101 if code.starts_with("DL") {
102 format!(
103 "See the hadolint wiki for more information: https://github.com/hadolint/hadolint/wiki/{}",
104 code
105 )
106 } else if code.starts_with("SC") {
107 format!(
108 "See the ShellCheck wiki for more information: https://www.shellcheck.net/wiki/{}",
109 code
110 )
111 } else {
112 "See hadolint documentation for more information.".to_string()
113 }
114}
115
116impl Formatter for CodeClimateFormatter {
117 fn format<W: Write>(
118 &self,
119 result: &LintResult,
120 filename: &str,
121 writer: &mut W,
122 ) -> std::io::Result<()> {
123 let issues: Vec<CodeClimateIssue> = result
124 .failures
125 .iter()
126 .map(|f| {
127 let code = f.code.to_string();
128 CodeClimateIssue {
129 issue_type: "issue",
130 check_name: code.clone(),
131 description: f.message.clone(),
132 content: CodeClimateContent {
133 body: get_help_body(&code),
134 },
135 categories: get_categories(&code),
136 location: CodeClimateLocation {
137 path: filename.to_string(),
138 lines: CodeClimateLines {
139 begin: f.line,
140 end: f.line,
141 },
142 },
143 severity: severity_to_codeclimate(f.severity),
144 fingerprint: generate_fingerprint(filename, &code, f.line),
145 }
146 })
147 .collect();
148
149 for issue in &issues {
151 let json = serde_json::to_string(issue).map_err(std::io::Error::other)?;
152 writeln!(writer, "{}", json)?;
153 }
154
155 Ok(())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::analyzer::hadolint::types::CheckFailure;
163
164 #[test]
165 fn test_codeclimate_output() {
166 let mut result = LintResult::new();
167 result.failures.push(CheckFailure::new(
168 "DL3008",
169 Severity::Warning,
170 "Pin versions in apt get install",
171 5,
172 ));
173
174 let formatter = CodeClimateFormatter::new();
175 let output = formatter.format_to_string(&result, "Dockerfile");
176
177 assert!(output.contains("\"type\":\"issue\""));
178 assert!(output.contains("\"check_name\":\"DL3008\""));
179 assert!(output.contains("\"severity\":\"major\""));
180 assert!(output.contains("\"path\":\"Dockerfile\""));
181 assert!(output.contains("\"fingerprint\""));
182 }
183
184 #[test]
185 fn test_fingerprint_consistency() {
186 let fp1 = generate_fingerprint("Dockerfile", "DL3008", 5);
187 let fp2 = generate_fingerprint("Dockerfile", "DL3008", 5);
188 let fp3 = generate_fingerprint("Dockerfile", "DL3008", 6);
189
190 assert_eq!(fp1, fp2);
191 assert_ne!(fp1, fp3);
192 }
193
194 #[test]
195 fn test_categories() {
196 assert!(get_categories("DL3000").contains(&"Security"));
197 assert!(get_categories("SC2086").contains(&"Bug Risk"));
198 assert!(get_categories("DL4000").contains(&"Compatibility"));
199 }
200}