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 code.starts_with("DL") {
68 let rule_num: u32 = code[2..].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>(&self, result: &LintResult, filename: &str, writer: &mut W) -> std::io::Result<()> {
118 let issues: Vec<CodeClimateIssue> = result
119 .failures
120 .iter()
121 .map(|f| {
122 let code = f.code.to_string();
123 CodeClimateIssue {
124 issue_type: "issue",
125 check_name: code.clone(),
126 description: f.message.clone(),
127 content: CodeClimateContent {
128 body: get_help_body(&code),
129 },
130 categories: get_categories(&code),
131 location: CodeClimateLocation {
132 path: filename.to_string(),
133 lines: CodeClimateLines {
134 begin: f.line,
135 end: f.line,
136 },
137 },
138 severity: severity_to_codeclimate(f.severity),
139 fingerprint: generate_fingerprint(filename, &code, f.line),
140 }
141 })
142 .collect();
143
144 for issue in &issues {
146 let json = serde_json::to_string(issue)
147 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
148 writeln!(writer, "{}", json)?;
149 }
150
151 Ok(())
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::analyzer::hadolint::types::CheckFailure;
159
160 #[test]
161 fn test_codeclimate_output() {
162 let mut result = LintResult::new();
163 result.failures.push(CheckFailure::new(
164 "DL3008",
165 Severity::Warning,
166 "Pin versions in apt get install",
167 5,
168 ));
169
170 let formatter = CodeClimateFormatter::new();
171 let output = formatter.format_to_string(&result, "Dockerfile");
172
173 assert!(output.contains("\"type\":\"issue\""));
174 assert!(output.contains("\"check_name\":\"DL3008\""));
175 assert!(output.contains("\"severity\":\"major\""));
176 assert!(output.contains("\"path\":\"Dockerfile\""));
177 assert!(output.contains("\"fingerprint\""));
178 }
179
180 #[test]
181 fn test_fingerprint_consistency() {
182 let fp1 = generate_fingerprint("Dockerfile", "DL3008", 5);
183 let fp2 = generate_fingerprint("Dockerfile", "DL3008", 5);
184 let fp3 = generate_fingerprint("Dockerfile", "DL3008", 6);
185
186 assert_eq!(fp1, fp2);
187 assert_ne!(fp1, fp3);
188 }
189
190 #[test]
191 fn test_categories() {
192 assert!(get_categories("DL3000").contains(&"Security"));
193 assert!(get_categories("SC2086").contains(&"Bug Risk"));
194 assert!(get_categories("DL4000").contains(&"Compatibility"));
195 }
196}