pub mod github;
pub mod json;
pub mod stylish;
use crate::analyzer::dclint::lint::LintResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
Json,
#[default]
Stylish,
Compact,
GitHub,
CodeClimate,
JUnit,
}
impl OutputFormat {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"json" => Some(Self::Json),
"stylish" => Some(Self::Stylish),
"compact" => Some(Self::Compact),
"github" | "github-actions" => Some(Self::GitHub),
"codeclimate" | "code-climate" => Some(Self::CodeClimate),
"junit" => Some(Self::JUnit),
_ => None,
}
}
}
pub fn format_results(results: &[LintResult], format: OutputFormat) -> String {
match format {
OutputFormat::Json => json::format(results),
OutputFormat::Stylish => stylish::format(results),
OutputFormat::Compact => format_compact(results),
OutputFormat::GitHub => github::format(results),
OutputFormat::CodeClimate => format_codeclimate(results),
OutputFormat::JUnit => format_junit(results),
}
}
pub fn format_result(result: &LintResult, format: OutputFormat) -> String {
format_results(std::slice::from_ref(result), format)
}
pub fn format_result_to_string(result: &LintResult, format: OutputFormat) -> String {
format_result(result, format)
}
fn format_compact(results: &[LintResult]) -> String {
let mut output = String::new();
for result in results {
for failure in &result.failures {
output.push_str(&format!(
"{}:{}:{}: {} [{}] {}\n",
result.file_path,
failure.line,
failure.column,
failure.severity,
failure.code,
failure.message
));
}
}
output
}
fn format_codeclimate(results: &[LintResult]) -> String {
let mut issues = Vec::new();
for result in results {
for failure in &result.failures {
issues.push(serde_json::json!({
"type": "issue",
"check_name": failure.code.as_str(),
"description": failure.message,
"content": {
"body": failure.message
},
"categories": [failure.category.as_str()],
"location": {
"path": result.file_path,
"lines": {
"begin": failure.line,
"end": failure.end_line.unwrap_or(failure.line)
}
},
"severity": match failure.severity {
crate::analyzer::dclint::types::Severity::Error => "critical",
crate::analyzer::dclint::types::Severity::Warning => "major",
crate::analyzer::dclint::types::Severity::Info => "minor",
crate::analyzer::dclint::types::Severity::Style => "info",
},
"fingerprint": format!("{}-{}-{}", failure.code, result.file_path, failure.line)
}));
}
}
serde_json::to_string_pretty(&issues).unwrap_or_else(|_| "[]".to_string())
}
fn format_junit(results: &[LintResult]) -> String {
let mut output = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
output.push('\n');
let total_tests: usize = results.iter().map(|r| r.failures.len().max(1)).sum();
let total_failures: usize = results.iter().map(|r| r.failures.len()).sum();
output.push_str(&format!(
r#"<testsuite name="dclint" tests="{}" failures="{}">"#,
total_tests, total_failures
));
output.push('\n');
for result in results {
if result.failures.is_empty() {
output.push_str(&format!(
r#" <testcase name="{}" classname="dclint"/>"#,
escape_xml(&result.file_path)
));
output.push('\n');
} else {
for failure in &result.failures {
output.push_str(&format!(
r#" <testcase name="{}:{}" classname="dclint.{}">"#,
escape_xml(&result.file_path),
failure.line,
failure.code
));
output.push('\n');
output.push_str(&format!(
r#" <failure message="{}" type="{}"/>"#,
escape_xml(&failure.message),
failure.severity
));
output.push('\n');
output.push_str(" </testcase>\n");
}
}
}
output.push_str("</testsuite>\n");
output
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
fn make_result() -> LintResult {
let mut result = LintResult::new("docker-compose.yml");
result.failures.push(CheckFailure::new(
"DCL001",
"no-build-and-image",
Severity::Error,
RuleCategory::BestPractice,
"Test message",
5,
1,
));
result
}
#[test]
fn test_compact_format() {
let result = make_result();
let output = format_compact(&[result]);
assert!(output.contains("docker-compose.yml"));
assert!(output.contains("DCL001"));
assert!(output.contains("5:1"));
}
#[test]
fn test_junit_format() {
let result = make_result();
let output = format_junit(&[result]);
assert!(output.contains("<?xml"));
assert!(output.contains("testsuite"));
assert!(output.contains("DCL001"));
}
#[test]
fn test_output_format_from_str() {
assert_eq!(OutputFormat::parse("json"), Some(OutputFormat::Json));
assert_eq!(OutputFormat::parse("JSON"), Some(OutputFormat::Json));
assert_eq!(OutputFormat::parse("stylish"), Some(OutputFormat::Stylish));
assert_eq!(OutputFormat::parse("github"), Some(OutputFormat::GitHub));
assert_eq!(OutputFormat::parse("invalid"), None);
}
}