Skip to main content

ktrs_cli/ktlint/
gradle.rs

1//! Hidden, ktrs-only options for the ktlint-gradle drop-in (`io.github.hexay.ktrs.ktlint`, research/29). That plugin
2//! runs ktlint's engine in-process with its own reporting; these options make a CLI run behave the same way:
3//!
4//! | option | effect |
5//! |---|---|
6//! | `--ktrs-gradle-events=<file>` | ktlint-gradle mode, and every reported error to `<file>` with its status ([`GradleEventsReporter`]). `--format` then formats like the engine's `format(code, callback: (LintError, Boolean))`: everything autocorrectable is fixed (baseline errors too), and the reporters get every error found, fixed ones included, with lint statuses and the plain detail |
7//! | `--ktrs-relative-to=<dir>` | with `--relative`, report paths relative to `<dir>` (ktlint-gradle: the root project) instead of the working directory; the baseline stays keyed by the working directory |
8//!
9//! In ktlint-gradle mode reporters also get paths with the platform's separators ([`native_separators`]).
10//! | `--ktrs-editorconfig-override=<name>=<value>` | repeatable: an `EditorConfigOverride` entry, which wins over every `.editorconfig` (`ktlint { additionalEditorconfig }`), resolved like `EditorConfigPropertyRegistry.find` |
11//!
12//! A run handed to the ktlint jar ([`crate::ktlint::ktlint_jar`]) drops them, and writes the events file with the
13//! `json` reporter instead ([`hand_off_token`]).
14
15use std::fs::File;
16use std::io::{BufWriter, Write};
17use std::sync::atomic::Ordering;
18
19use ktrs_lint::{AutocorrectDecision, Code};
20
21use ktrs_lint::editorconfig::{
22    CODE_STYLE_PROPERTY, END_OF_LINE_PROPERTY, INDENT_SIZE_PROPERTY, INDENT_STYLE_PROPERTY, INSERT_FINAL_NEWLINE_PROPERTY,
23    MAX_LINE_LENGTH_PROPERTY, PropertyRef, RuleExecution, create_rule_execution_editor_config_property,
24    create_rule_set_execution_editor_config_property,
25};
26use ktrs_lint::rule_provider::RuleV2Provider;
27
28use crate::ktlint::baseline::does_not_contain;
29use crate::ktlint::command_line::Exit;
30use crate::ktlint::process::Processor;
31use crate::ktlint::reporter::{KtlintCliError, ReporterV2, Status};
32
33pub const EVENTS_OPTION: &str = "--ktrs-gradle-events";
34pub const RELATIVE_TO_OPTION: &str = "--ktrs-relative-to";
35pub const EDITOR_CONFIG_OVERRIDE_OPTION: &str = "--ktrs-editorconfig-override";
36
37/// First line of an events file.
38const EVENTS_HEADER: &str = "# ktrs-gradle-events 1";
39
40/// A `file\t<file>` line per linted file (ktlint-gradle's `LintErrorResult`s), then one per error:
41/// `error\t<file>\t<line>\t<col>\t<rule id>\t<status>\t<corrected>\t<detail>`, the status as `KtlintCliError.Status`
42/// names it; `\`, tab, line feed and carriage return in the detail escaped as `\\`, `\t`, `\n`, `\r`. Files come
43/// in the order the run reports them (its arguments' order).
44pub struct GradleEventsReporter {
45    out: BufWriter<File>,
46}
47
48impl GradleEventsReporter {
49    pub fn new(file: File) -> GradleEventsReporter {
50        GradleEventsReporter { out: BufWriter::new(file) }
51    }
52}
53
54impl ReporterV2 for GradleEventsReporter {
55    fn before_all(&mut self) {
56        let _ = writeln!(self.out, "{EVENTS_HEADER}");
57    }
58
59    fn before(&mut self, file: &str) {
60        let _ = writeln!(self.out, "file\t{file}");
61    }
62
63    fn on_lint_error(&mut self, file: &str, e: &KtlintCliError) {
64        let status = match e.status {
65            Status::BaselineIgnored => "BASELINE_IGNORED",
66            Status::LintCanNotBeAutocorrected => "LINT_CAN_NOT_BE_AUTOCORRECTED",
67            Status::LintCanBeAutocorrected => "LINT_CAN_BE_AUTOCORRECTED",
68            Status::FormatIsAutocorrected => "FORMAT_IS_AUTOCORRECTED",
69            Status::KotlinParseException => "KOTLIN_PARSE_EXCEPTION",
70            Status::KtlintRuleEngineException => "KTLINT_RULE_ENGINE_EXCEPTION",
71        };
72        let detail = e.detail.replace('\\', "\\\\").replace('\t', "\\t").replace('\n', "\\n").replace('\r', "\\r");
73        let _ = writeln!(self.out, "error\t{file}\t{}\t{}\t{}\t{status}\t{}\t{detail}", e.line, e.col, e.rule_id, e.corrected);
74    }
75
76    fn after_all(&mut self) {
77        let _ = self.out.flush();
78    }
79}
80
81impl Processor<'_> {
82    /// ktlint-gradle's `KtLintInvocation100.invokeFormat` + its reports: the engine's `format(code, callback:
83    /// (LintError, Boolean))` fixes all it can; every distinct error is reported with its lint status and plain
84    /// detail, fixed ones flagged [`KtlintCliError::corrected`]; errors in the baseline are not reported.
85    pub(crate) fn gradle_format(&self, code: &Code, baseline_lint_errors: &[KtlintCliError]) -> Result<Vec<KtlintCliError>, Exit> {
86        let mut ktlint_cli_errors: Vec<KtlintCliError> = Vec::new();
87        let result = self.engine.format_reporting(code, &mut |_| AutocorrectDecision::AllowAutocorrect, &mut |e, corrected| {
88            let status = if e.can_be_auto_corrected { Status::LintCanBeAutocorrected } else { Status::LintCanNotBeAutocorrected };
89            let mut ktlint_cli_error = KtlintCliError::new(e.line, e.col, e.rule_id.value(), &e.detail, status);
90            ktlint_cli_error.corrected = corrected;
91            if does_not_contain(baseline_lint_errors, &ktlint_cli_error) {
92                if !corrected {
93                    self.contains_unfixed_lint_errors.store(true, Ordering::SeqCst);
94                }
95                ktlint_cli_errors.push(ktlint_cli_error);
96            }
97        });
98        match result {
99            Ok(formatted_file_content) => {
100                if code.content != formatted_file_content
101                    && let Some(path) = &code.file_path
102                    && let Err(e) = std::fs::write(path, &formatted_file_content)
103                {
104                    return Err(Exit::Crash(format!("java.io.FileNotFoundException: {} ({e})", path.display())));
105                }
106            }
107            Err(e) => {
108                ktlint_cli_errors.push(self.to_ktlint_cli_error(&e, code)?);
109                self.contains_unfixed_lint_errors.store(true, Ordering::SeqCst);
110            }
111        }
112        Ok(ktlint_cli_errors)
113    }
114}
115
116/// `EditorConfigOverride.from(entries.mapKeys { EditorConfigPropertyRegistry(providers).find(it.key) })`; `Err` is the
117/// `EditorConfigPropertyNotFoundException` message.
118pub fn editor_config_override(entries: &[String], providers: &[RuleV2Provider]) -> Result<Vec<(PropertyRef, Option<String>)>, String> {
119    let core: [PropertyRef; 6] = [
120        (&*CODE_STYLE_PROPERTY).into(),
121        (&*END_OF_LINE_PROPERTY).into(),
122        (&*INDENT_STYLE_PROPERTY).into(),
123        (&*INDENT_SIZE_PROPERTY).into(),
124        (&*INSERT_FINAL_NEWLINE_PROPERTY).into(),
125        (&*MAX_LINE_LENGTH_PROPERTY).into(),
126    ];
127    let known: Vec<&PropertyRef> = providers.iter().flat_map(|p| p.uses_editor_config_properties()).chain(&core).collect();
128    entries
129        .iter()
130        .map(|entry| {
131            let (name, value) = entry.split_once('=').ok_or_else(|| format!("{EDITOR_CONFIG_OVERRIDE_OPTION} needs name=value, got '{entry}'"))?;
132            let property = match known.iter().find(|p| p.name() == name) {
133                Some(property) => (*property).clone(),
134                None => rule_execution_property(name).ok_or_else(|| not_found(name, &known))?,
135            };
136            Ok((property, Some(value.to_owned())))
137        })
138        .collect()
139}
140
141/// `toRuleExecutionPropertyOrNull`: `ktlint_<rule set>_<rule>` / `ktlint_<rule set>` with `_` read as `:`.
142fn rule_execution_property(name: &str) -> Option<PropertyRef> {
143    let id = name.strip_prefix("ktlint_")?.replace('_', ":");
144    let valid = |part: &str| !part.is_empty() && part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
145    match id.split_once(':') {
146        Some((rule_set, rule)) if valid(rule_set) && valid(rule) => {
147            Some(create_rule_execution_editor_config_property(&id, RuleExecution::Enabled).into())
148        }
149        None if valid(&id) => Some(create_rule_set_execution_editor_config_property(&id, RuleExecution::Enabled).into()),
150        _ => None,
151    }
152}
153
154fn not_found(name: &str, known: &[&PropertyRef]) -> String {
155    let mut names: Vec<&str> = known.iter().map(|p| p.name()).collect();
156    names.sort_unstable();
157    names.dedup();
158    let list: String = names.iter().map(|n| format!("\n\t- {n}")).collect();
159    format!(
160        "EditorConfigPropertyNotFoundException: Property with name '{name}' is not found in any of given rules. Available \
161         properties:{list}\nNext to properties above, the properties to enable or disable ktlint rules are allowed as well."
162    )
163}
164
165/// ktlint-gradle hands reporters `File.absolutePath` / `File.toRelativeString`: the platform's separators (which
166/// also decide the order of the `html` report, kept in a `ConcurrentHashMap` by path).
167pub fn native_separators(route: &str) -> String {
168    if cfg!(windows) { route.replace('/', "\\") } else { route.to_owned() }
169}
170
171/// The hand-off's version of a token of these options (`None`: dropped): the events file becomes a `json` report.
172pub fn hand_off_token(option: &str, value: &str) -> Option<String> {
173    (option == EVENTS_OPTION).then(|| format!("--reporter=json,output={value}"))
174}