1pub mod baseline;
5pub mod checkstyle;
6pub mod format;
7pub mod html;
8pub mod java_map;
9pub mod json;
10pub mod plain;
11pub mod plain_summary;
12pub mod sarif;
13
14use std::path::{Path, PathBuf};
15
16use crate::ktlint::console::Printer;
17
18pub const KTLINT_VERSION: &str = "2.0.0-ALPHA-4";
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub enum Status {
24 BaselineIgnored,
25 LintCanNotBeAutocorrected,
26 LintCanBeAutocorrected,
27 FormatIsAutocorrected,
28 KotlinParseException,
29 KtlintRuleEngineException,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
34pub struct KtlintCliError {
35 pub line: usize,
36 pub col: usize,
37 pub rule_id: String,
38 pub detail: String,
39 pub status: Status,
40}
41
42impl KtlintCliError {
43 pub fn new(line: usize, col: usize, rule_id: &str, detail: &str, status: Status) -> KtlintCliError {
44 KtlintCliError { line, col, rule_id: rule_id.to_owned(), detail: detail.to_owned(), status }
45 }
46}
47
48pub trait ReporterV2: Send {
50 fn before_all(&mut self) {}
51 fn before(&mut self, _file: &str) {}
52 fn on_lint_error(&mut self, file: &str, ktlint_cli_error: &KtlintCliError);
53 fn after(&mut self, _file: &str) {}
54 fn after_all(&mut self) {}
55}
56
57#[derive(Clone, Debug, Default, PartialEq, Eq)]
59pub struct ReporterOptions(pub Vec<(String, String)>);
60
61impl ReporterOptions {
62 pub fn get(&self, key: &str) -> Option<&str> {
63 self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
64 }
65
66 pub fn put(&mut self, key: &str, value: &str) {
67 match self.0.iter_mut().find(|(k, _)| k == key) {
68 Some(entry) => entry.1 = value.to_owned(),
69 None => self.0.push((key.to_owned(), value.to_owned())),
70 }
71 }
72
73 pub fn to_java_string(&self) -> String {
75 let entries: Vec<String> = self.0.iter().map(|(k, v)| format!("{k}={v}")).collect();
76 format!("{{{}}}", entries.join(", "))
77 }
78}
79
80#[derive(Clone, Debug)]
82pub struct ReporterEnvironment {
83 pub user_home: Option<PathBuf>,
84 pub working_dir: PathBuf,
85}
86
87pub const REPORTER_PROVIDER_IDS: [&str; 8] =
89 ["baseline", "checkstyle", "format", "html", "json", "plain", "plain-summary", "sarif"];
90
91pub fn get_reporter(
94 id: &str,
95 out: Printer,
96 opt: &ReporterOptions,
97 env: &ReporterEnvironment,
98) -> Option<Result<Box<dyn ReporterV2>, String>> {
99 let reporter: Result<Box<dyn ReporterV2>, String> = match id {
100 "baseline" => Ok(Box::new(baseline::BaselineReporter::new(out, env.working_dir.clone()))),
101 "checkstyle" => Ok(Box::new(checkstyle::CheckStyleReporter::new(out))),
102 "format" => format::FormatReporterProvider::get(out, opt).map(|r| Box::new(r) as Box<dyn ReporterV2>),
103 "html" => Ok(Box::new(html::HtmlReporter::new(out))),
104 "json" => Ok(Box::new(json::JsonReporter::new(out))),
105 "plain" => plain::PlainReporterProvider::get(out, opt).map(|r| Box::new(r) as Box<dyn ReporterV2>),
106 "plain-summary" => Ok(Box::new(plain_summary::PlainSummaryReporter::new(out))),
107 "sarif" => Ok(Box::new(sarif::SarifReporter::new(out, env.user_home.clone()))),
108 _ => return None,
109 };
110 Some(reporter)
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum Color {
116 Black = 30,
117 Red = 31,
118 Green = 32,
119 Yellow = 33,
120 Blue = 34,
121 Magenta = 35,
122 Cyan = 36,
123 LightGray = 37,
124 DarkGray = 90,
125 LightRed = 91,
126 LightGreen = 92,
127 LightYellow = 93,
128 LightBlue = 94,
129 LightMagenta = 95,
130 LightCyan = 96,
131 White = 97,
132}
133
134impl Color {
135 const ENTRIES: [(&'static str, Color); 16] = [
136 ("BLACK", Color::Black),
137 ("RED", Color::Red),
138 ("GREEN", Color::Green),
139 ("YELLOW", Color::Yellow),
140 ("BLUE", Color::Blue),
141 ("MAGENTA", Color::Magenta),
142 ("CYAN", Color::Cyan),
143 ("LIGHT_GRAY", Color::LightGray),
144 ("DARK_GRAY", Color::DarkGray),
145 ("LIGHT_RED", Color::LightRed),
146 ("LIGHT_GREEN", Color::LightGreen),
147 ("LIGHT_YELLOW", Color::LightYellow),
148 ("LIGHT_BLUE", Color::LightBlue),
149 ("LIGHT_MAGENTA", Color::LightMagenta),
150 ("LIGHT_CYAN", Color::LightCyan),
151 ("WHITE", Color::White),
152 ];
153
154 pub fn from_option(name: Option<&str>) -> Result<Color, String> {
156 Color::ENTRIES
157 .iter()
158 .find(|(n, _)| Some(*n) == name)
159 .map(|(_, c)| *c)
160 .ok_or_else(|| "java.lang.IllegalArgumentException: Invalid color parameter.".to_owned())
161 }
162
163 pub fn paint(self, text: &str) -> String {
165 format!("\u{1b}[{}m{text}\u{1b}[0m", self as u8)
166 }
167}
168
169pub fn empty_or_true(value: Option<&str>) -> bool {
171 matches!(value, Some("" | "true"))
172}
173
174pub const FILE_SEPARATOR: &str = if cfg!(windows) { "\\" } else { "/" };
176
177pub fn color_file_name(file_name: &str, colored: impl Fn(&str) -> String) -> String {
179 let name = match file_name.rfind(FILE_SEPARATOR) {
180 Some(i) => &file_name[i + FILE_SEPARATOR.len()..],
181 None => file_name,
182 };
183 colored(&file_name[..file_name.len() - name.len()]) + name
184}
185
186pub fn print_summary(out: &mut Printer, header: &str, counts: &[(String, u64)]) {
188 let mut sorted = counts.to_vec();
189 sorted.sort_by(|(ra, ca), (rb, cb)| cb.cmp(ca).then_with(|| java_compare(ra, rb)));
190 out.println(header);
191 for (rule, count) in sorted {
192 out.println(&format!(" {rule}: {count}"));
193 }
194}
195
196pub fn java_compare(a: &str, b: &str) -> std::cmp::Ordering {
198 a.encode_utf16().cmp(b.encode_utf16())
199}
200
201pub fn increment(counts: &mut Vec<(String, u64)>, key: &str) {
203 match counts.iter_mut().rev().find(|(k, _)| k == key) {
204 Some(entry) => entry.1 += 1,
205 None => counts.push((key.to_owned(), 1)),
206 }
207}
208
209pub fn caused_by(error: &KtlintCliError) -> String {
211 match error.status {
212 Status::KotlinParseException => "Not a valid Kotlin file".to_owned(),
213 Status::KtlintRuleEngineException => "An internal error occurred in the Ktlint Rule Engine".to_owned(),
214 _ if error.rule_id.is_empty() => "Unknown".to_owned(),
215 _ => error.rule_id.clone(),
216 }
217}
218
219pub fn escape_xml_attr_value(value: &str) -> String {
221 value
222 .replace('&', "&")
223 .replace('"', """)
224 .replace('\'', "'")
225 .replace('<', "<")
226 .replace('>', ">")
227}
228
229pub fn accumulate(acc: &mut Vec<(String, Vec<KtlintCliError>)>, file: &str, error: &KtlintCliError) {
232 match acc.iter_mut().rev().find(|(f, _)| f == file) {
233 Some((_, errors)) => errors.push(error.clone()),
234 None => acc.push((file.to_owned(), vec![error.clone()])),
235 }
236}
237
238pub fn relative_to_or_self(path: &Path, base: &Path) -> PathBuf {
241 use std::path::Component;
242 fn normalize(p: &Path) -> Vec<Component<'_>> {
243 let mut parts: Vec<Component> = Vec::new();
244 for c in p.components() {
245 match c {
246 Component::CurDir => {}
247 Component::ParentDir if matches!(parts.last(), Some(Component::Normal(_))) => {
248 parts.pop();
249 }
250 c => parts.push(c),
251 }
252 }
253 parts
254 }
255 fn root<'a>(parts: &[Component<'a>]) -> Vec<Component<'a>> {
256 parts.iter().take_while(|c| matches!(c, Component::Prefix(_) | Component::RootDir)).cloned().collect()
257 }
258 let (p, b) = (normalize(path), normalize(base));
259 if path.is_absolute() != base.is_absolute() || root(&p) != root(&b) {
260 return path.to_path_buf();
261 }
262 let common = p.iter().zip(&b).take_while(|(x, y)| x == y).count();
263 let mut relative = PathBuf::new();
264 for _ in common..b.len() {
265 relative.push("..");
266 }
267 for c in &p[common..] {
268 relative.push(c.as_os_str());
269 }
270 relative
271}
272
273pub fn sorted_by_file(acc: &[(String, Vec<KtlintCliError>)]) -> Vec<&(String, Vec<KtlintCliError>)> {
275 let mut sorted: Vec<_> = acc.iter().collect();
276 sorted.sort_by(|a, b| java_compare(&a.0, &b.0));
277 sorted
278}