ktrs_cli/ktlint/baseline/
mod.rs1pub mod xml;
4
5use std::collections::HashMap;
6use std::path::Path;
7
8use ktrs_lint::editorconfig::KtlintVersion;
9
10use crate::ktlint::console::Console;
11use crate::ktlint::logger::{BASELINE, Logger};
12use crate::ktlint::reporter::{KtlintCliError, Status};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum BaselineStatus {
16 Disabled,
18 Valid,
19 NotFound,
21 Invalid,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct Baseline {
27 pub path: Option<String>,
28 pub status: BaselineStatus,
29 pub lint_errors_per_file: HashMap<String, Vec<KtlintCliError>>,
31}
32
33impl Baseline {
34 pub fn disabled() -> Baseline {
35 Baseline { path: None, status: BaselineStatus::Disabled, lint_errors_per_file: HashMap::new() }
36 }
37
38 fn with_status(path: &str, status: BaselineStatus) -> Baseline {
39 Baseline { path: Some(path.to_owned()), status, lint_errors_per_file: HashMap::new() }
40 }
41}
42
43struct LoadError {
45 message: String,
46 fatal_error: Option<xml::XmlError>,
47}
48
49pub fn load_baseline(path: &str, working_dir: &Path, logger: &Logger, console: &Console, ktlint_version: KtlintVersion) -> Baseline {
53 let baseline_path = Some(working_dir.join(path)).filter(|p| p.exists());
54 match load(path, baseline_path.as_deref()) {
55 Ok(mut baseline) if ktlint_version.is_1_8() => {
56 let rule_reference_without_rule_set_id_prefix = prefix_rule_ids_without_rule_set(&mut baseline);
57 if rule_reference_without_rule_set_id_prefix > 0 {
58 logger.warn(BASELINE, || {
59 format!(
60 "Baseline file '{path}' contains {rule_reference_without_rule_set_id_prefix} reference(s) to rule ids \
61 without a rule set id. For those references the rule set id 'standard' is assumed. It is advised to \
62 regenerate this baseline file."
63 )
64 });
65 }
66 baseline
67 }
68 Ok(baseline) => baseline,
69 Err(e) => {
70 if let Some(fatal) = &e.fatal_error {
71 console.println_err(&format!("[Fatal Error] :{}:{}: {}", fatal.line, fatal.col, fatal.message));
72 }
73 if let Some(file) = &baseline_path {
74 let _ = std::fs::remove_file(file);
75 }
76 logger.error(BASELINE, || e.message);
77 Baseline::with_status(path, BaselineStatus::Invalid)
78 }
79 }
80}
81
82fn load(path: &str, baseline_path: Option<&Path>) -> Result<Baseline, LoadError> {
83 if path.trim().is_empty() {
84 return Err(LoadError {
85 message: "Path for loading baseline may not be blank or empty".to_owned(),
86 fatal_error: None,
87 });
88 }
89 let Some(baseline_file) = baseline_path else {
90 return Ok(Baseline::with_status(path, BaselineStatus::NotFound));
91 };
92 let unable_to_parse = |fatal_error| LoadError { message: format!("Unable to parse baseline file: {path}"), fatal_error };
93 let bytes = std::fs::read(baseline_file).map_err(|_| unable_to_parse(None))?;
94 let document = xml::parse_document(&String::from_utf8_lossy(&bytes)).map_err(|e| unable_to_parse(Some(e)))?;
95 let lint_errors_per_file = parse_baseline(&document)
96 .map_err(|message| LoadError { message, fatal_error: None })?;
97 Ok(Baseline { path: Some(path.to_owned()), status: BaselineStatus::Valid, lint_errors_per_file })
98}
99
100fn parse_baseline(document: &xml::Element) -> Result<HashMap<String, Vec<KtlintCliError>>, String> {
101 let mut lint_errors_per_file = HashMap::new();
102 let mut files = Vec::new();
103 document.elements_by_tag_name("file", &mut files);
104 for file in files {
105 let mut errors = Vec::new();
106 file.elements_by_tag_name("error", &mut errors);
107 let errors = errors.into_iter().map(parse_baseline_error_element).collect::<Result<Vec<_>, _>>()?;
108 lint_errors_per_file.insert(file.attribute("name").to_owned(), errors);
109 }
110 Ok(lint_errors_per_file)
111}
112
113fn parse_baseline_error_element(element: &xml::Element) -> Result<KtlintCliError, String> {
114 Ok(KtlintCliError {
115 line: to_int(element.attribute("line"))?,
116 col: to_int(element.attribute("column"))?,
117 rule_id: element.attribute("source").to_owned(),
118 detail: String::new(),
119 status: Status::BaselineIgnored,
120 corrected: false,
121 })
122}
123
124fn prefix_rule_ids_without_rule_set(baseline: &mut Baseline) -> usize {
126 let mut prefixed = 0;
127 for error in baseline.lint_errors_per_file.values_mut().flatten() {
128 if !error.rule_id.contains(':') {
129 error.rule_id = format!("standard:{}", error.rule_id);
130 prefixed += 1;
131 }
132 }
133 prefixed
134}
135
136fn to_int(value: &str) -> Result<usize, String> {
139 value
140 .strip_prefix('+')
141 .unwrap_or(value)
142 .parse::<u32>()
143 .ok()
144 .filter(|&v| v <= i32::MAX as u32 && !value.starts_with("++"))
145 .map(|v| v as usize)
146 .ok_or_else(|| format!("For input string: \"{value}\""))
147}
148
149pub fn contains_lint_error(list: &[KtlintCliError], ktlint_cli_error: &KtlintCliError) -> bool {
151 list.iter().any(|e| is_same_as(e, ktlint_cli_error))
152}
153
154pub fn does_not_contain(list: &[KtlintCliError], ktlint_cli_error: &KtlintCliError) -> bool {
156 !contains_lint_error(list, ktlint_cli_error)
157}
158
159fn is_same_as(a: &KtlintCliError, b: &KtlintCliError) -> bool {
160 a.col == b.col && a.line == b.line && a.rule_id == b.rule_id
161}