Skip to main content

ktrs_cli/ktlint/
command_line.rs

1//! Port of ktlint-cli `internal/KtlintCommandLine.kt` (and `Main.kt`): the `ktlint` command.
2
3use std::sync::Mutex;
4use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
5use std::time::Instant;
6
7use ktrs_lint::editorconfig::{RuleExecution, create_rule_execution_editor_config_property};
8use ktrs_lint::rule_provider::{RuleV2Provider, property_types};
9use ktrs_lint::rules::standard_rule_providers;
10use ktrs_lint::{Code, EditorConfigDefaults, EditorConfigOverride, KtLintRuleEngine};
11
12use crate::ktlint::args::{KtlintArgs, Parsed, Subcommand, USAGE_MAIN, parse_args};
13use crate::ktlint::baseline::{Baseline, load_baseline};
14use crate::ktlint::console::Console;
15use crate::ktlint::file_utils::{expand_tilde_to_full_path, file_sequence, java_list, location};
16use crate::ktlint::patterns::replace_with_patterns_from_stdin_or_default_patterns_when_empty;
17use crate::ktlint::jar_providers::{RULE_SET_PROVIDER_V3, RULE_SET_V2_PROVIDER, load_from_jar_file, to_files_uri_list};
18use crate::ktlint::jpath::JPath;
19use crate::ktlint::logger::{
20    EDITOR_CONFIG_DEFAULTS_LOADER, KTLINT_COMMAND_LINE, KTLINT_SERVICE_LOADER, LOAD_RULE_PROVIDERS, Logger,
21};
22use crate::ktlint::parallel::parallel;
23use crate::ktlint::process::Processor;
24use crate::ktlint::reporter::{KtlintCliError, ReporterEnvironment, ReporterV2, Status};
25use crate::ktlint::reporter_aggregator::{Context, ReporterSettings, aggregated_reporter};
26use crate::ktlint::subcommands;
27
28/// `ExitCode`: values external integrations depend on.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum ExitCode {
31    Ok = 0,
32    HasUnfixedLintErrorsAfterFormat = 1,
33    IoException = 2,
34    ParseExceptionStdin = 3,
35    ExceptionStdin = 4,
36    FileNotFound = 5,
37    InvalidRulesetJar = 6,
38    InvalidReporterConfiguration = 7,
39    ParseExceptionAfterFormat = 123,
40}
41
42/// How a run ends early: `exitKtLintProcess(code)`, a Clikt usage error, or an uncaught JVM exception.
43#[derive(Debug)]
44pub enum Exit {
45    Code(ExitCode),
46    Usage(String),
47    Crash(String),
48}
49
50/// The `ktlint` command, run in `working_dir` (the JVM's `user.dir`).
51pub struct KtlintCli {
52    pub console: Console,
53    pub working_dir: JPath,
54    pub user_home: String,
55}
56
57impl KtlintCli {
58    pub fn from_env() -> KtlintCli {
59        let working_dir = std::env::current_dir().map(|d| JPath::from_path(&d)).unwrap_or_else(|_| JPath::parse(".").unwrap());
60        let user_home = std::env::var(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).unwrap_or_default();
61        KtlintCli { console: Console::std(), working_dir, user_home }
62    }
63
64    pub fn run(&self, args: &[String]) -> i32 {
65        let (args, subcommand) = match parse_args(args, &self.working_dir.to_path_buf()) {
66            Parsed::Message(text) => {
67                self.console.out(&text);
68                return 0;
69            }
70            Parsed::UsageError { usage, message } => return self.usage_error(&usage, &message),
71            Parsed::Run(args, subcommand) => (args, subcommand),
72        };
73        let logger = Logger::new(self.console.clone(), args.min_log_level);
74        let result = match subcommand {
75            None => self.lint_or_format(&args, &logger),
76            Some(Subcommand::GenerateEditorConfig(code_style)) => {
77                self.rule_providers(&args, &logger).and_then(|providers| subcommands::generate_editor_config(self, providers, code_style, &logger))
78            }
79            Some(Subcommand::InstallGitPreCommitHook) => subcommands::install_git_hook(self, subcommands::PRE_COMMIT),
80            Some(Subcommand::InstallGitPrePushHook) => subcommands::install_git_hook(self, subcommands::PRE_PUSH),
81        };
82        self.exit_code(result, &logger)
83    }
84
85    /// Lints (or formats) with already parsed options: `ktrs lint`.
86    pub fn run_lint(&self, args: &KtlintArgs) -> i32 {
87        let logger = Logger::new(self.console.clone(), args.min_log_level);
88        let result = self.lint_or_format(args, &logger);
89        self.exit_code(result, &logger)
90    }
91
92    fn exit_code(&self, result: Result<(), Exit>, logger: &Logger) -> i32 {
93        match result {
94            Ok(()) => 0,
95            Err(Exit::Code(code)) => {
96                logger.debug(KTLINT_COMMAND_LINE, || format!("Exit ktlint with exit code: {}", code as i32));
97                code as i32
98            }
99            Err(Exit::Usage(message)) => self.usage_error(USAGE_MAIN, &message),
100            Err(Exit::Crash(exception)) => {
101                self.console.println_err(&format!("Exception in thread \"main\" {exception}"));
102                1
103            }
104        }
105    }
106
107    fn usage_error(&self, usage: &str, message: &str) -> i32 {
108        self.console.err(&format!("{usage}\n\nError: {message}\n"));
109        1
110    }
111
112    fn lint_or_format(&self, args: &KtlintArgs, logger: &Logger) -> Result<(), Exit> {
113        if args.stdin && args.patterns_from_stdin.is_some() {
114            return Err(Exit::Usage("option --stdin cannot be used with --patterns-from-stdin".to_owned()));
115        }
116        let patterns = replace_with_patterns_from_stdin_or_default_patterns_when_empty(args, &self.console, logger);
117        let rule_providers = self.rule_providers(args, logger)?;
118        let mut editor_config_override = EditorConfigOverride::empty();
119        if args.stdin && args.stdin_path.as_deref().is_none_or(|p| p.trim().is_empty()) {
120            logger.debug(KTLINT_COMMAND_LINE, || {
121                "Add editor config override to disable 'filename' rule which can not be used in combination with reading from <stdin>"
122                    .to_owned()
123            });
124            let filename = create_rule_execution_editor_config_property("standard:filename", RuleExecution::Disabled);
125            editor_config_override = EditorConfigOverride::from(vec![(filename.into(), Some("disabled".to_owned()))]);
126        }
127        let start = Instant::now();
128        let editor_config_defaults = self.editor_config_defaults(args, &rule_providers, logger)?;
129        let engine = KtLintRuleEngine::with_editor_config(rule_providers, editor_config_defaults, editor_config_override);
130        let baseline = if args.stdin || args.baseline_path.trim().is_empty() {
131            Baseline::disabled()
132        } else {
133            load_baseline(&args.baseline_path, &self.working_dir.to_path_buf(), logger, &self.console)
134        };
135        let env = ReporterEnvironment { user_home: Some(self.user_home.clone().into()), working_dir: self.working_dir.to_path_buf() };
136        let settings = ReporterSettings {
137            reporter_configurations: &args.reporter_configurations,
138            color: args.color,
139            color_name: &args.color_name,
140            stdin: args.stdin,
141            format: args.format,
142            relative: args.relative,
143        };
144        let cx = Context { console: &self.console, logger, working_dir: &self.working_dir, user_home: &self.user_home, env: &env };
145        let mut reporter = aggregated_reporter(&baseline, &settings, &cx)?;
146        let run = Run {
147            processor: Processor {
148                engine: &engine,
149                console: &self.console,
150                logger,
151                format: args.format,
152                ignore_autocorrect_failures: args.ignore_autocorrect_failures,
153                force_lint_after_format: args.force_lint_after_format,
154                contains_unfixed_lint_errors: AtomicBool::new(false),
155            },
156            args,
157            file_number: AtomicUsize::new(0),
158            error_number: AtomicUsize::new(0),
159            advise_to_use_format: AtomicBool::new(false),
160        };
161
162        reporter.before_all();
163        if args.stdin {
164            self.lint_stdin(&run, &mut reporter)?;
165        } else {
166            self.lint_files(&run, &patterns, &baseline, &mut reporter, logger)?;
167            if run.advise_to_use_format.load(Ordering::SeqCst) {
168                if args.format {
169                    logger.error(KTLINT_COMMAND_LINE, || {
170                        "Format was not able to autocorrect all errors that theoretically can be autocorrected.".to_owned()
171                    });
172                } else {
173                    logger.warn(KTLINT_COMMAND_LINE, || "Lint has found errors than can be autocorrected using 'ktlint --format'".to_owned());
174                }
175            }
176        }
177        reporter.after_all();
178        drop(reporter);
179
180        let (file_number, error_number) = (run.file_number.load(Ordering::SeqCst), run.error_number.load(Ordering::SeqCst));
181        logger.debug(KTLINT_COMMAND_LINE, || {
182            format!(
183                "Finished processing in {}ms / {file_number} file(s) scanned / {error_number} error(s) found",
184                start.elapsed().as_millis()
185            )
186        });
187        if file_number == 0 {
188            logger.warn(KTLINT_COMMAND_LINE, || format!("No files matched {}", java_list(&patterns)));
189        }
190        if run.processor.contains_unfixed_lint_errors.load(Ordering::SeqCst) {
191            Err(Exit::Code(ExitCode::HasUnfixedLintErrorsAfterFormat))
192        } else {
193            Err(Exit::Code(ExitCode::Ok))
194        }
195    }
196
197    /// `ruleProviders`: the standard rules; a `-R` JAR can't be loaded (see [`load_from_jar_file`]).
198    pub(crate) fn rule_providers(&self, args: &KtlintArgs, logger: &Logger) -> Result<Vec<RuleV2Provider>, Exit> {
199        let urls = to_files_uri_list(&args.ruleset_jar_paths, &self.working_dir, &self.user_home, logger)?;
200        logger.debug(KTLINT_SERVICE_LOADER, || "Discovered RuleSetV2Provider with id 'standard' in ktlint JAR".to_owned());
201        if let Some(url) = urls.first() {
202            for message in [
203                format!("Try loading ruleset provider of type 'RuleSetProviderV3' for file:{url}"),
204                format!("Found 0 rule providers of type 'RuleSetProviderV3' for file:{url}"),
205                format!("Try loading ruleset provider of type 'RuleSetV2Provider' for file:{url}"),
206            ] {
207                logger.debug(LOAD_RULE_PROVIDERS, || message);
208            }
209            return Err(load_from_jar_file(url, RULE_SET_V2_PROVIDER, &[RULE_SET_PROVIDER_V3], logger));
210        }
211        Ok(standard_rule_providers())
212    }
213
214    fn editor_config_defaults(&self, args: &KtlintArgs, rule_providers: &[RuleV2Provider], logger: &Logger) -> Result<EditorConfigDefaults, Exit> {
215        let Some(path) = args.editor_config_path.as_ref().map(|p| expand_tilde_to_full_path(p, &self.user_home)) else {
216            return Ok(EditorConfigDefaults::empty());
217        };
218        if path.trim().is_empty() {
219            return Ok(EditorConfigDefaults::empty());
220        }
221        let resolved = self.working_dir.resolve(&path).map(|p| p.to_path_buf()).unwrap_or_else(|| path.clone().into());
222        let file = if resolved.is_dir() { resolved.join(".editorconfig") } else { resolved.clone() };
223        if !file.exists() {
224            logger.warn(EDITOR_CONFIG_DEFAULTS_LOADER, || format!("File or directory '{path}' is not found. Can not load '.editorconfig' properties"));
225            return Ok(EditorConfigDefaults::empty());
226        }
227        EditorConfigDefaults::load(Some(&resolved), &property_types(rule_providers))
228            .map_err(|e| Exit::Crash(format!("org.ec4j.core.parser.ParseException: {e}")))
229    }
230
231    fn lint_files(&self, run: &Run, patterns: &[String], baseline: &Baseline, reporter: &mut dyn ReporterV2, logger: &Logger) -> Result<(), Exit> {
232        let root_dir = self.working_dir.normalize();
233        let files = file_sequence(patterns, &root_dir, &self.user_home, logger)
234            .map_err(|e| Exit::Crash(format!("java.util.regex.PatternSyntaxException: {e}")))?;
235        let failure: Mutex<Option<Exit>> = Mutex::new(None);
236        let reporter = Mutex::new(reporter);
237        parallel(
238            &files,
239            || run.error_number.load(Ordering::SeqCst) >= run.args.limit || failure.lock().unwrap().is_some(),
240            |file| {
241                // Baseline stores the lint violations as relative path to work dir
242                let baseline_lint_errors = baseline.lint_errors_per_file.get(&location(file, true, &self.working_dir));
243                let code = Code::from_file(&file.to_path_buf())
244                    .map_err(|e| Exit::Crash(format!("java.io.IOException: {e}")))?;
245                run.processor.process(&code, baseline_lint_errors.map_or(&[][..], Vec::as_slice))
246            },
247            |file, result| match result {
248                Ok(errors) => run.report(&location(file, run.args.relative, &self.working_dir), &errors, &mut **reporter.lock().unwrap()),
249                Err(exit) => {
250                    failure.lock().unwrap().get_or_insert(exit);
251                }
252            },
253        );
254        match failure.into_inner().unwrap() {
255            Some(exit) => Err(exit),
256            None => Ok(()),
257        }
258    }
259
260    fn lint_stdin(&self, run: &Run, reporter: &mut dyn ReporterV2) -> Result<(), Exit> {
261        let content = String::from_utf8_lossy(&self.console.read_stdin()).into_owned();
262        let code = match &run.args.stdin_path {
263            Some(path) => {
264                let path = expand_tilde_to_full_path(path, &self.user_home);
265                let virtual_path = self.working_dir.resolve(&path).map(|p| p.to_path_buf()).unwrap_or_else(|| path.into());
266                Code::from_snippet_with_path(&content, Some(&virtual_path))
267            }
268            None => Code::from_snippet_with_path(&content, None),
269        };
270        let errors = run.processor.process(&code, &[])?;
271        run.report("<stdin>", &errors, reporter);
272        Ok(())
273    }
274}
275
276/// The per-run state `lintOrFormat` shares with the file workers.
277struct Run<'a> {
278    processor: Processor<'a>,
279    args: &'a KtlintArgs,
280    file_number: AtomicUsize,
281    error_number: AtomicUsize,
282    advise_to_use_format: AtomicBool,
283}
284
285impl Run<'_> {
286    fn report(&self, relative_route: &str, ktlint_cli_errors: &[KtlintCliError], reporter: &mut dyn ReporterV2) {
287        self.file_number.fetch_add(1, Ordering::SeqCst);
288        let err_list_limit = ktlint_cli_errors.len().min(self.args.limit.saturating_sub(self.error_number.load(Ordering::SeqCst)));
289        self.error_number.fetch_add(err_list_limit, Ordering::SeqCst);
290        if ktlint_cli_errors.iter().any(|e| e.status == Status::LintCanBeAutocorrected) {
291            self.advise_to_use_format.store(true, Ordering::SeqCst);
292        }
293        reporter.before(relative_route);
294        ktlint_cli_errors
295            .iter()
296            .take(err_list_limit)
297            .filter(|e| !(self.args.ignore_autocorrect_failures && e.status == Status::LintCanNotBeAutocorrected))
298            .for_each(|e| reporter.on_lint_error(relative_route, e));
299        reporter.after(relative_route);
300    }
301}