syncable_cli/analyzer/dclint/formatter/
mod.rs1pub mod github;
12pub mod json;
13pub mod stylish;
14
15use crate::analyzer::dclint::lint::LintResult;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum OutputFormat {
20 Json,
22 #[default]
24 Stylish,
25 Compact,
27 GitHub,
29 CodeClimate,
31 JUnit,
33}
34
35impl OutputFormat {
36 pub fn parse(s: &str) -> Option<Self> {
38 match s.to_lowercase().as_str() {
39 "json" => Some(Self::Json),
40 "stylish" => Some(Self::Stylish),
41 "compact" => Some(Self::Compact),
42 "github" | "github-actions" => Some(Self::GitHub),
43 "codeclimate" | "code-climate" => Some(Self::CodeClimate),
44 "junit" => Some(Self::JUnit),
45 _ => None,
46 }
47 }
48}
49
50pub fn format_results(results: &[LintResult], format: OutputFormat) -> String {
52 match format {
53 OutputFormat::Json => json::format(results),
54 OutputFormat::Stylish => stylish::format(results),
55 OutputFormat::Compact => format_compact(results),
56 OutputFormat::GitHub => github::format(results),
57 OutputFormat::CodeClimate => format_codeclimate(results),
58 OutputFormat::JUnit => format_junit(results),
59 }
60}
61
62pub fn format_result(result: &LintResult, format: OutputFormat) -> String {
64 format_results(std::slice::from_ref(result), format)
65}
66
67pub fn format_result_to_string(result: &LintResult, format: OutputFormat) -> String {
69 format_result(result, format)
70}
71
72fn format_compact(results: &[LintResult]) -> String {
74 let mut output = String::new();
75
76 for result in results {
77 for failure in &result.failures {
78 output.push_str(&format!(
79 "{}:{}:{}: {} [{}] {}\n",
80 result.file_path,
81 failure.line,
82 failure.column,
83 failure.severity,
84 failure.code,
85 failure.message
86 ));
87 }
88 }
89
90 output
91}
92
93fn format_codeclimate(results: &[LintResult]) -> String {
95 let mut issues = Vec::new();
96
97 for result in results {
98 for failure in &result.failures {
99 issues.push(serde_json::json!({
100 "type": "issue",
101 "check_name": failure.code.as_str(),
102 "description": failure.message,
103 "content": {
104 "body": failure.message
105 },
106 "categories": [failure.category.as_str()],
107 "location": {
108 "path": result.file_path,
109 "lines": {
110 "begin": failure.line,
111 "end": failure.end_line.unwrap_or(failure.line)
112 }
113 },
114 "severity": match failure.severity {
115 crate::analyzer::dclint::types::Severity::Error => "critical",
116 crate::analyzer::dclint::types::Severity::Warning => "major",
117 crate::analyzer::dclint::types::Severity::Info => "minor",
118 crate::analyzer::dclint::types::Severity::Style => "info",
119 },
120 "fingerprint": format!("{}-{}-{}", failure.code, result.file_path, failure.line)
121 }));
122 }
123 }
124
125 serde_json::to_string_pretty(&issues).unwrap_or_else(|_| "[]".to_string())
126}
127
128fn format_junit(results: &[LintResult]) -> String {
130 let mut output = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
131 output.push('\n');
132
133 let total_tests: usize = results.iter().map(|r| r.failures.len().max(1)).sum();
134 let total_failures: usize = results.iter().map(|r| r.failures.len()).sum();
135
136 output.push_str(&format!(
137 r#"<testsuite name="dclint" tests="{}" failures="{}">"#,
138 total_tests, total_failures
139 ));
140 output.push('\n');
141
142 for result in results {
143 if result.failures.is_empty() {
144 output.push_str(&format!(
145 r#" <testcase name="{}" classname="dclint"/>"#,
146 escape_xml(&result.file_path)
147 ));
148 output.push('\n');
149 } else {
150 for failure in &result.failures {
151 output.push_str(&format!(
152 r#" <testcase name="{}:{}" classname="dclint.{}">"#,
153 escape_xml(&result.file_path),
154 failure.line,
155 failure.code
156 ));
157 output.push('\n');
158 output.push_str(&format!(
159 r#" <failure message="{}" type="{}"/>"#,
160 escape_xml(&failure.message),
161 failure.severity
162 ));
163 output.push('\n');
164 output.push_str(" </testcase>\n");
165 }
166 }
167 }
168
169 output.push_str("</testsuite>\n");
170 output
171}
172
173fn escape_xml(s: &str) -> String {
175 s.replace('&', "&")
176 .replace('<', "<")
177 .replace('>', ">")
178 .replace('"', """)
179 .replace('\'', "'")
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
186
187 fn make_result() -> LintResult {
188 let mut result = LintResult::new("docker-compose.yml");
189 result.failures.push(CheckFailure::new(
190 "DCL001",
191 "no-build-and-image",
192 Severity::Error,
193 RuleCategory::BestPractice,
194 "Test message",
195 5,
196 1,
197 ));
198 result
199 }
200
201 #[test]
202 fn test_compact_format() {
203 let result = make_result();
204 let output = format_compact(&[result]);
205 assert!(output.contains("docker-compose.yml"));
206 assert!(output.contains("DCL001"));
207 assert!(output.contains("5:1"));
208 }
209
210 #[test]
211 fn test_junit_format() {
212 let result = make_result();
213 let output = format_junit(&[result]);
214 assert!(output.contains("<?xml"));
215 assert!(output.contains("testsuite"));
216 assert!(output.contains("DCL001"));
217 }
218
219 #[test]
220 fn test_output_format_from_str() {
221 assert_eq!(OutputFormat::parse("json"), Some(OutputFormat::Json));
222 assert_eq!(OutputFormat::parse("JSON"), Some(OutputFormat::Json));
223 assert_eq!(OutputFormat::parse("stylish"), Some(OutputFormat::Stylish));
224 assert_eq!(OutputFormat::parse("github"), Some(OutputFormat::GitHub));
225 assert_eq!(OutputFormat::parse("invalid"), None);
226 }
227}