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