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