pub mod baseline;
pub mod checkstyle;
pub mod format;
pub mod html;
pub mod java_map;
pub mod json;
pub mod plain;
pub mod plain_summary;
pub mod sarif;
use std::path::{Path, PathBuf};
use crate::ktlint::console::Printer;
pub const KTLINT_VERSION: &str = "2.0.0-ALPHA-4";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Status {
BaselineIgnored,
LintCanNotBeAutocorrected,
LintCanBeAutocorrected,
FormatIsAutocorrected,
KotlinParseException,
KtlintRuleEngineException,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct KtlintCliError {
pub line: usize,
pub col: usize,
pub rule_id: String,
pub detail: String,
pub status: Status,
}
impl KtlintCliError {
pub fn new(line: usize, col: usize, rule_id: &str, detail: &str, status: Status) -> KtlintCliError {
KtlintCliError { line, col, rule_id: rule_id.to_owned(), detail: detail.to_owned(), status }
}
}
pub trait ReporterV2: Send {
fn before_all(&mut self) {}
fn before(&mut self, _file: &str) {}
fn on_lint_error(&mut self, file: &str, ktlint_cli_error: &KtlintCliError);
fn after(&mut self, _file: &str) {}
fn after_all(&mut self) {}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReporterOptions(pub Vec<(String, String)>);
impl ReporterOptions {
pub fn get(&self, key: &str) -> Option<&str> {
self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
}
pub fn put(&mut self, key: &str, value: &str) {
match self.0.iter_mut().find(|(k, _)| k == key) {
Some(entry) => entry.1 = value.to_owned(),
None => self.0.push((key.to_owned(), value.to_owned())),
}
}
pub fn to_java_string(&self) -> String {
let entries: Vec<String> = self.0.iter().map(|(k, v)| format!("{k}={v}")).collect();
format!("{{{}}}", entries.join(", "))
}
}
#[derive(Clone, Debug)]
pub struct ReporterEnvironment {
pub user_home: Option<PathBuf>,
pub working_dir: PathBuf,
}
pub const REPORTER_PROVIDER_IDS: [&str; 8] =
["baseline", "checkstyle", "format", "html", "json", "plain", "plain-summary", "sarif"];
pub fn get_reporter(
id: &str,
out: Printer,
opt: &ReporterOptions,
env: &ReporterEnvironment,
) -> Option<Result<Box<dyn ReporterV2>, String>> {
let reporter: Result<Box<dyn ReporterV2>, String> = match id {
"baseline" => Ok(Box::new(baseline::BaselineReporter::new(out, env.working_dir.clone()))),
"checkstyle" => Ok(Box::new(checkstyle::CheckStyleReporter::new(out))),
"format" => format::FormatReporterProvider::get(out, opt).map(|r| Box::new(r) as Box<dyn ReporterV2>),
"html" => Ok(Box::new(html::HtmlReporter::new(out))),
"json" => Ok(Box::new(json::JsonReporter::new(out))),
"plain" => plain::PlainReporterProvider::get(out, opt).map(|r| Box::new(r) as Box<dyn ReporterV2>),
"plain-summary" => Ok(Box::new(plain_summary::PlainSummaryReporter::new(out))),
"sarif" => Ok(Box::new(sarif::SarifReporter::new(out, env.user_home.clone()))),
_ => return None,
};
Some(reporter)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Color {
Black = 30,
Red = 31,
Green = 32,
Yellow = 33,
Blue = 34,
Magenta = 35,
Cyan = 36,
LightGray = 37,
DarkGray = 90,
LightRed = 91,
LightGreen = 92,
LightYellow = 93,
LightBlue = 94,
LightMagenta = 95,
LightCyan = 96,
White = 97,
}
impl Color {
const ENTRIES: [(&'static str, Color); 16] = [
("BLACK", Color::Black),
("RED", Color::Red),
("GREEN", Color::Green),
("YELLOW", Color::Yellow),
("BLUE", Color::Blue),
("MAGENTA", Color::Magenta),
("CYAN", Color::Cyan),
("LIGHT_GRAY", Color::LightGray),
("DARK_GRAY", Color::DarkGray),
("LIGHT_RED", Color::LightRed),
("LIGHT_GREEN", Color::LightGreen),
("LIGHT_YELLOW", Color::LightYellow),
("LIGHT_BLUE", Color::LightBlue),
("LIGHT_MAGENTA", Color::LightMagenta),
("LIGHT_CYAN", Color::LightCyan),
("WHITE", Color::White),
];
pub fn from_option(name: Option<&str>) -> Result<Color, String> {
Color::ENTRIES
.iter()
.find(|(n, _)| Some(*n) == name)
.map(|(_, c)| *c)
.ok_or_else(|| "java.lang.IllegalArgumentException: Invalid color parameter.".to_owned())
}
pub fn paint(self, text: &str) -> String {
format!("\u{1b}[{}m{text}\u{1b}[0m", self as u8)
}
}
pub fn empty_or_true(value: Option<&str>) -> bool {
matches!(value, Some("" | "true"))
}
pub const FILE_SEPARATOR: &str = if cfg!(windows) { "\\" } else { "/" };
pub fn color_file_name(file_name: &str, colored: impl Fn(&str) -> String) -> String {
let name = match file_name.rfind(FILE_SEPARATOR) {
Some(i) => &file_name[i + FILE_SEPARATOR.len()..],
None => file_name,
};
colored(&file_name[..file_name.len() - name.len()]) + name
}
pub fn print_summary(out: &mut Printer, header: &str, counts: &[(String, u64)]) {
let mut sorted = counts.to_vec();
sorted.sort_by(|(ra, ca), (rb, cb)| cb.cmp(ca).then_with(|| java_compare(ra, rb)));
out.println(header);
for (rule, count) in sorted {
out.println(&format!(" {rule}: {count}"));
}
}
pub fn java_compare(a: &str, b: &str) -> std::cmp::Ordering {
a.encode_utf16().cmp(b.encode_utf16())
}
pub fn increment(counts: &mut Vec<(String, u64)>, key: &str) {
match counts.iter_mut().rev().find(|(k, _)| k == key) {
Some(entry) => entry.1 += 1,
None => counts.push((key.to_owned(), 1)),
}
}
pub fn caused_by(error: &KtlintCliError) -> String {
match error.status {
Status::KotlinParseException => "Not a valid Kotlin file".to_owned(),
Status::KtlintRuleEngineException => "An internal error occurred in the Ktlint Rule Engine".to_owned(),
_ if error.rule_id.is_empty() => "Unknown".to_owned(),
_ => error.rule_id.clone(),
}
}
pub fn escape_xml_attr_value(value: &str) -> String {
value
.replace('&', "&")
.replace('"', """)
.replace('\'', "'")
.replace('<', "<")
.replace('>', ">")
}
pub fn accumulate(acc: &mut Vec<(String, Vec<KtlintCliError>)>, file: &str, error: &KtlintCliError) {
match acc.iter_mut().rev().find(|(f, _)| f == file) {
Some((_, errors)) => errors.push(error.clone()),
None => acc.push((file.to_owned(), vec![error.clone()])),
}
}
pub fn relative_to_or_self(path: &Path, base: &Path) -> PathBuf {
use std::path::Component;
fn normalize(p: &Path) -> Vec<Component<'_>> {
let mut parts: Vec<Component> = Vec::new();
for c in p.components() {
match c {
Component::CurDir => {}
Component::ParentDir if matches!(parts.last(), Some(Component::Normal(_))) => {
parts.pop();
}
c => parts.push(c),
}
}
parts
}
fn root<'a>(parts: &[Component<'a>]) -> Vec<Component<'a>> {
parts.iter().take_while(|c| matches!(c, Component::Prefix(_) | Component::RootDir)).cloned().collect()
}
let (p, b) = (normalize(path), normalize(base));
if path.is_absolute() != base.is_absolute() || root(&p) != root(&b) {
return path.to_path_buf();
}
let common = p.iter().zip(&b).take_while(|(x, y)| x == y).count();
let mut relative = PathBuf::new();
for _ in common..b.len() {
relative.push("..");
}
for c in &p[common..] {
relative.push(c.as_os_str());
}
relative
}
pub fn sorted_by_file(acc: &[(String, Vec<KtlintCliError>)]) -> Vec<&(String, Vec<KtlintCliError>)> {
let mut sorted: Vec<_> = acc.iter().collect();
sorted.sort_by(|a, b| java_compare(&a.0, &b.0));
sorted
}