1use std::path::Path;
4
5use ktrs_editorconfig::EnumValue;
6use ktrs_lint::editorconfig::CodeStyleValue;
7
8use crate::ktlint::clikt::{Arity, Invocation, OptionSpec, expand_argument_files, parse_tokens};
9use crate::ktlint::logger::Level;
10use crate::ktlint::reporter::KTLINT_VERSION;
11
12pub const HELP_MAIN: &str = include_str!("help/help-main.txt");
13const HELP_GENERATE_EDITOR_CONFIG: &str = include_str!("help/help-gen.txt");
14const HELP_PRE_COMMIT: &str = include_str!("help/help-pre.txt");
15const HELP_PRE_PUSH: &str = include_str!("help/help-push.txt");
16
17pub const USAGE_MAIN: &str = "Usage: ktlint [<options>] [<arguments>]... <command> [<args>]...";
18pub const GENERATE_EDITOR_CONFIG: &str = "generateEditorConfig";
19pub const INSTALL_GIT_PRE_COMMIT_HOOK: &str = "installGitPreCommitHook";
20pub const INSTALL_GIT_PRE_PUSH_HOOK: &str = "installGitPrePushHook";
21
22const fn spec(names: &'static [&'static str], arity: Arity) -> OptionSpec {
23 OptionSpec { names, arity, hidden: false }
24}
25
26static MAIN_OPTIONS: [OptionSpec; 17] = [
27 spec(&["--version", "-v"], Arity::Flag),
28 spec(&["--color"], Arity::Flag),
29 spec(&["--color-name"], Arity::Value),
30 spec(&["--format", "-F"], Arity::Flag),
31 spec(&["--ignore-autocorrect-failures"], Arity::Flag),
32 spec(&["--limit"], Arity::Value),
33 spec(&["--relative"], Arity::Flag),
34 spec(&["--reporter"], Arity::Value),
35 spec(&["--ruleset", "-R"], Arity::Value),
36 spec(&["--stdin"], Arity::Flag),
37 spec(&["--stdin-path"], Arity::Value),
38 spec(&["--patterns-from-stdin"], Arity::OptionalAttached),
39 spec(&["--editorconfig"], Arity::Value),
40 OptionSpec { names: &["--force-lint-after-format"], arity: Arity::Flag, hidden: true },
41 spec(&["--baseline"], Arity::Value),
42 spec(&["--log-level", "-l"], Arity::Value),
43 spec(&["--help", "-h"], Arity::Flag),
44];
45
46static GENERATE_EDITOR_CONFIG_OPTIONS: [OptionSpec; 2] =
47 [spec(&["--code-style"], Arity::Value), spec(&["--help", "-h"], Arity::Flag)];
48static HELP_ONLY_OPTIONS: [OptionSpec; 1] = [spec(&["--help", "-h"], Arity::Flag)];
49
50#[derive(Clone, Debug)]
52pub struct KtlintArgs {
53 pub color: bool,
54 pub color_name: String,
55 pub format: bool,
56 pub ignore_autocorrect_failures: bool,
57 pub limit: usize,
58 pub relative: bool,
59 pub reporter_configurations: Vec<String>,
60 pub ruleset_jar_paths: Vec<String>,
61 pub stdin: bool,
62 pub stdin_path: Option<String>,
63 pub patterns_from_stdin: Option<String>,
64 pub editor_config_path: Option<String>,
65 pub force_lint_after_format: bool,
66 pub baseline_path: String,
67 pub arguments: Vec<String>,
68 pub min_log_level: Level,
69}
70
71impl Default for KtlintArgs {
72 fn default() -> KtlintArgs {
73 KtlintArgs {
74 color: false,
75 color_name: "DARK_GRAY".to_owned(),
76 format: false,
77 ignore_autocorrect_failures: false,
78 limit: i32::MAX as usize,
79 relative: false,
80 reporter_configurations: Vec::new(),
81 ruleset_jar_paths: Vec::new(),
82 stdin: false,
83 stdin_path: None,
84 patterns_from_stdin: None,
85 editor_config_path: None,
86 force_lint_after_format: false,
87 baseline_path: String::new(),
88 arguments: Vec::new(),
89 min_log_level: Level::Info,
90 }
91 }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum Subcommand {
96 GenerateEditorConfig(CodeStyleValue),
97 InstallGitPreCommitHook,
98 InstallGitPrePushHook,
99}
100
101pub enum Parsed {
102 Run(KtlintArgs, Option<Subcommand>),
103 Message(String),
105 UsageError { usage: String, message: String },
107}
108
109pub fn parse_args(tokens: &[String], working_dir: &Path) -> Parsed {
110 let tokens = match expand_argument_files(tokens, working_dir) {
111 Ok(tokens) => tokens,
112 Err(message) => return usage_error(USAGE_MAIN, message),
113 };
114 let subcommands = [GENERATE_EDITOR_CONFIG, INSTALL_GIT_PRE_COMMIT_HOOK, INSTALL_GIT_PRE_PUSH_HOOK];
115 let parsed = match parse_tokens(&tokens, &MAIN_OPTIONS, &subcommands) {
116 Ok(parsed) => parsed,
117 Err(_) if has_token(&tokens, &["--help", "-h"]) => return Parsed::Message(HELP_MAIN.to_owned()),
119 Err(_) if has_token(&tokens, &["--version", "-v"]) => return version(),
120 Err(message) => return usage_error(USAGE_MAIN, message),
121 };
122 let has = |name: &str| parsed.invocations.iter().any(|i| i.name == name);
123 if has("--help") {
124 return Parsed::Message(HELP_MAIN.to_owned());
125 }
126 if has("--version") {
127 return version();
128 }
129 let args = match to_ktlint_args(&parsed.invocations, parsed.arguments) {
130 Ok(args) => args,
131 Err(message) => return usage_error(USAGE_MAIN, message),
132 };
133 let subcommand = match parsed.subcommand {
134 None => None,
135 Some((name, tokens)) => match parse_subcommand(name, &tokens) {
136 Ok(subcommand) => Some(subcommand),
137 Err(parsed) => return parsed,
138 },
139 };
140 Parsed::Run(args, subcommand)
141}
142
143fn to_ktlint_args(invocations: &[Invocation], arguments: Vec<String>) -> Result<KtlintArgs, String> {
144 let mut args = KtlintArgs { arguments, ..KtlintArgs::default() };
145 let last = |name: &str| invocations.iter().rev().find(|i| i.name == name).and_then(|i| i.value.clone());
146 let all = |name: &'static str| invocations.iter().filter(move |i| i.name == name).filter_map(|i| i.value.clone());
147 let has = |name: &str| invocations.iter().any(|i| i.name == name);
148 args.color = has("--color");
149 if let Some(color_name) = last("--color-name") {
150 args.color_name = color_name;
151 }
152 args.format = has("--format");
153 args.ignore_autocorrect_failures = has("--ignore-autocorrect-failures");
154 if let Some(limit) = last("--limit") {
155 let limit: i32 =
156 limit.parse().map_err(|_| format!("invalid value for --limit: {limit} is not a valid integer"))?;
157 if limit <= 0 {
158 return Err("invalid value for --limit: Value must be bigger than 0".to_owned());
159 }
160 args.limit = limit as usize;
161 }
162 args.relative = has("--relative");
163 args.reporter_configurations = all("--reporter").collect();
164 if let Some(rulesets) = last("--ruleset") {
165 args.ruleset_jar_paths = rulesets.split(',').map(str::to_owned).collect();
166 }
167 args.stdin = has("--stdin");
168 args.stdin_path = last("--stdin-path");
169 args.patterns_from_stdin = last("--patterns-from-stdin");
170 args.editor_config_path = last("--editorconfig");
171 args.force_lint_after_format = has("--force-lint-after-format");
172 if let Some(baseline) = last("--baseline") {
173 args.baseline_path = baseline;
174 }
175 if let Some(level) = last("--log-level") {
176 args.min_log_level = Level::parse(&level).map_err(|e| format!("invalid value for --log-level: {e}"))?;
177 }
178 Ok(args)
179}
180
181fn parse_subcommand(name: &'static str, tokens: &[String]) -> Result<Subcommand, Parsed> {
182 let usage = format!("Usage: ktlint {name} [<options>]");
183 let (options, help): (&'static [OptionSpec], &str) = match name {
184 GENERATE_EDITOR_CONFIG => (&GENERATE_EDITOR_CONFIG_OPTIONS, HELP_GENERATE_EDITOR_CONFIG),
185 INSTALL_GIT_PRE_COMMIT_HOOK => (&HELP_ONLY_OPTIONS, HELP_PRE_COMMIT),
186 _ => (&HELP_ONLY_OPTIONS, HELP_PRE_PUSH),
187 };
188 let parsed = parse_tokens(tokens, options, &[]).map_err(|message| {
189 if has_token(tokens, &["--help", "-h"]) { Parsed::Message(help.to_owned()) } else { usage_error(&usage, message) }
190 })?;
191 if parsed.invocations.iter().any(|i| i.name == "--help") {
192 return Err(Parsed::Message(help.to_owned()));
193 }
194 if let Some(argument) = parsed.arguments.first() {
195 return Err(usage_error(&usage, format!("got unexpected extra argument ({argument})")));
196 }
197 match name {
198 GENERATE_EDITOR_CONFIG => {
199 let code_style = parsed.invocations.iter().rev().find_map(|i| i.value.clone());
200 let code_style = code_style.ok_or_else(|| usage_error(&usage, "missing option --code-style".to_owned()))?;
201 CodeStyleValue::ENTRIES
202 .iter()
203 .copied()
204 .find(|c| c.name().eq_ignore_ascii_case(&code_style))
205 .map(Subcommand::GenerateEditorConfig)
206 .ok_or_else(|| {
207 usage_error(
208 &usage,
209 format!(
210 "invalid value for --code-style: invalid choice: {code_style}. (choose from android_studio, \
211 intellij_idea, ktlint_official)"
212 ),
213 )
214 })
215 }
216 INSTALL_GIT_PRE_COMMIT_HOOK => Ok(Subcommand::InstallGitPreCommitHook),
217 _ => Ok(Subcommand::InstallGitPrePushHook),
218 }
219}
220
221fn version() -> Parsed {
222 Parsed::Message(format!("ktlint version {KTLINT_VERSION}\n"))
223}
224
225fn has_token(tokens: &[String], names: &[&str]) -> bool {
226 tokens.iter().take_while(|t| *t != "--").any(|t| names.contains(&t.as_str()))
227}
228
229fn usage_error(usage: &str, message: String) -> Parsed {
230 Parsed::UsageError { usage: usage.to_owned(), message }
231}