syncable_cli/analyzer/dclint/formatter/
stylish.rs

1//! Stylish (colored terminal) output formatter for dclint.
2
3use crate::analyzer::dclint::lint::LintResult;
4use crate::analyzer::dclint::types::Severity;
5
6/// Format lint results in stylish format (colored terminal output).
7pub fn format(results: &[LintResult]) -> String {
8    let mut output = String::new();
9    let mut total_errors = 0;
10    let mut total_warnings = 0;
11    let mut total_fixable = 0;
12
13    for result in results {
14        if result.failures.is_empty() && result.parse_errors.is_empty() {
15            continue;
16        }
17
18        // File header
19        output.push_str(&format!("\n{}\n", result.file_path));
20
21        // Parse errors
22        for err in &result.parse_errors {
23            output.push_str(&format!("  error  {}\n", err));
24            total_errors += 1;
25        }
26
27        // Failures
28        for failure in &result.failures {
29            let severity_str = match failure.severity {
30                Severity::Error => "error",
31                Severity::Warning => "warning",
32                Severity::Info => "info",
33                Severity::Style => "style",
34            };
35
36            let fixable_str = if failure.fixable { " (fixable)" } else { "" };
37
38            output.push_str(&format!(
39                "  {}:{}  {}  {}  {}{}\n",
40                failure.line,
41                failure.column,
42                severity_str,
43                failure.message,
44                failure.code,
45                fixable_str
46            ));
47
48            match failure.severity {
49                Severity::Error => total_errors += 1,
50                Severity::Warning => total_warnings += 1,
51                _ => {}
52            }
53
54            if failure.fixable {
55                total_fixable += 1;
56            }
57        }
58    }
59
60    // Summary
61    if total_errors > 0 || total_warnings > 0 {
62        output.push('\n');
63
64        let mut parts = Vec::new();
65        if total_errors > 0 {
66            parts.push(format!(
67                "{} {}",
68                total_errors,
69                if total_errors == 1 { "error" } else { "errors" }
70            ));
71        }
72        if total_warnings > 0 {
73            parts.push(format!(
74                "{} {}",
75                total_warnings,
76                if total_warnings == 1 {
77                    "warning"
78                } else {
79                    "warnings"
80                }
81            ));
82        }
83
84        output.push_str(&format!(
85            "  {} problem{}\n",
86            parts.join(" and "),
87            if total_errors + total_warnings == 1 {
88                ""
89            } else {
90                "s"
91            }
92        ));
93
94        if total_fixable > 0 {
95            output.push_str(&format!(
96                "  {} {} potentially fixable with --fix\n",
97                total_fixable,
98                if total_fixable == 1 { "is" } else { "are" }
99            ));
100        }
101    }
102
103    output
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::analyzer::dclint::types::{CheckFailure, RuleCategory};
110
111    #[test]
112    fn test_stylish_format() {
113        let mut result = LintResult::new("docker-compose.yml");
114        result.failures.push(CheckFailure::new(
115            "DCL001",
116            "no-build-and-image",
117            Severity::Error,
118            RuleCategory::BestPractice,
119            "Service has both build and image",
120            5,
121            1,
122        ));
123        result.error_count = 1;
124
125        let output = format(&[result]);
126        assert!(output.contains("docker-compose.yml"));
127        assert!(output.contains("5:1"));
128        assert!(output.contains("error"));
129        assert!(output.contains("DCL001"));
130        assert!(output.contains("1 error"));
131    }
132
133    #[test]
134    fn test_stylish_format_multiple() {
135        let mut result = LintResult::new("docker-compose.yml");
136        result.failures.push(CheckFailure::new(
137            "DCL001",
138            "test",
139            Severity::Error,
140            RuleCategory::BestPractice,
141            "Error 1",
142            5,
143            1,
144        ));
145        result.failures.push(
146            CheckFailure::new(
147                "DCL006",
148                "test",
149                Severity::Warning,
150                RuleCategory::Style,
151                "Warning 1",
152                1,
153                1,
154            )
155            .with_fixable(true),
156        );
157        result.error_count = 1;
158        result.warning_count = 1;
159
160        let output = format(&[result]);
161        assert!(output.contains("1 error"));
162        assert!(output.contains("1 warning"));
163        assert!(output.contains("fixable"));
164    }
165
166    #[test]
167    fn test_stylish_format_empty() {
168        let result = LintResult::new("docker-compose.yml");
169        let output = format(&[result]);
170        assert!(output.is_empty());
171    }
172}