1use std::fs;
6use std::io::{self, Read, Write};
7use std::path::{Path, PathBuf};
8use std::sync::Mutex;
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
10
11use ktrs_fmt::FormatError;
12use ktrs_syntax::caught_panic::catch_quietly;
13
14use super::editor_config_resolver;
15use super::parsed_args::{ParseResult, ParsedArgs, process_args};
16
17const EXIT_CODE_FAILURE: i32 = 1;
18const EXIT_CODE_SUCCESS: i32 = 0;
19const UTF8_BOM: &str = "\u{feff}";
20const LINE_SEPARATOR: &str = if cfg!(windows) { "\r\n" } else { "\n" };
22
23const USAGE: &str = "\
24Usage:
25 ktfmt [OPTIONS] File1.kt File2.kt ...
26 ktfmt @ARGFILE
27
28For more details see `ktfmt --help`
29";
30
31pub struct Main<R, O, E> {
32 input: Mutex<R>,
33 out: Mutex<O>,
34 err: Mutex<E>,
35}
36
37#[derive(Default)]
39struct Report {
40 out: String,
41 err: String,
42}
43
44impl Report {
45 fn out(&mut self, line: &str) {
46 self.out.push_str(line);
47 self.out.push_str(LINE_SEPARATOR);
48 }
49
50 fn err(&mut self, line: &str) {
51 self.err.push_str(line);
52 self.err.push_str(LINE_SEPARATOR);
53 }
54}
55
56impl<R: Read + Send, O: Write + Send, E: Write + Send> Main<R, O, E> {
57 pub fn new(input: R, out: O, err: E) -> Self {
58 Main { input: Mutex::new(input), out: Mutex::new(out), err: Mutex::new(err) }
59 }
60
61 pub fn into_streams(self) -> (O, E) {
63 (self.out.into_inner().unwrap(), self.err.into_inner().unwrap())
64 }
65
66 pub fn run(&self, input_args: &[String]) -> i32 {
67 let mut report = Report::default();
68 let code = match process_args(input_args) {
69 Ok(ParseResult::Ok(parsed_args)) => return self.run_parsed(&parsed_args),
70 Ok(ParseResult::ShowMessage(message)) => {
71 report.out(&message);
72 EXIT_CODE_SUCCESS
73 }
74 Ok(ParseResult::Error(error_message)) => {
75 report.err(&error_message);
76 EXIT_CODE_FAILURE
77 }
78 Err(e) => {
80 let file = Path::new(&input_args[0][1..]);
81 report.err(&format!("Exception in thread \"main\" java.io.FileNotFoundException: {}", java_io_message(Some(file), &e)));
82 EXIT_CODE_FAILURE
83 }
84 };
85 self.flush(&report);
86 code
87 }
88
89 pub fn run_parsed(&self, parsed_args: &ParsedArgs) -> i32 {
90 if parsed_args.file_names.is_empty() {
91 self.flush(&Report { err: format!("{USAGE}{LINE_SEPARATOR}"), ..Report::default() });
92 return EXIT_CODE_FAILURE;
93 }
94
95 if parsed_args.file_names.len() == 1 && parsed_args.file_names[0] == "-" {
96 let mut report = Report::default();
98 let result = self.format(None, parsed_args, &mut report);
99 self.flush(&report);
100 return match result {
101 Ok(already_formatted) if !already_formatted && parsed_args.set_exit_if_changed => EXIT_CODE_FAILURE,
102 Ok(_) => EXIT_CODE_SUCCESS,
103 Err(()) => EXIT_CODE_FAILURE,
104 };
105 }
106
107 let files = expand_args_to_file_names(&parsed_args.file_names);
108 if files.is_empty() {
109 self.flush(&Report { err: format!("Error: no .kt files found{LINE_SEPARATOR}"), ..Report::default() });
110 return EXIT_CODE_FAILURE;
111 }
112
113 let failed = AtomicBool::new(false);
115 let next = AtomicUsize::new(0);
116 let done: Mutex<(usize, Vec<Option<Report>>)> = Mutex::new((0, files.iter().map(|_| None).collect()));
117 let threads = std::thread::available_parallelism().map_or(1, |n| n.get()).min(files.len());
118 std::thread::scope(|s| {
119 for _ in 0..threads {
120 s.spawn(|| {
121 loop {
122 let i = next.fetch_add(1, Ordering::Relaxed);
123 let Some(file) = files.get(i) else { break };
124 let mut report = Report::default();
125 match self.format(Some(file), parsed_args, &mut report) {
126 Ok(false) if parsed_args.set_exit_if_changed => failed.store(true, Ordering::Relaxed),
127 Ok(_) => {}
128 Err(()) => failed.store(true, Ordering::Relaxed),
129 }
130 let mut done = done.lock().unwrap();
131 let (next_to_print, reports) = &mut *done;
132 reports[i] = Some(report);
133 while let Some(report) = reports.get_mut(*next_to_print).and_then(Option::take) {
134 self.flush(&report);
135 *next_to_print += 1;
136 }
137 }
138 });
139 }
140 });
141 if failed.into_inner() { EXIT_CODE_FAILURE } else { EXIT_CODE_SUCCESS }
142 }
143
144 fn format(&self, file: Option<&Path>, args: &ParsedArgs, report: &mut Report) -> Result<bool, ()> {
147 let file_name = match file {
148 Some(file) => java_file_name(file),
149 None => args.stdin_name.clone().unwrap_or_else(|| "<stdin>".to_owned()),
150 };
151 let mut io_error = |e: io::Error| {
152 report.err(&format!("Error formatting {file_name}: {}; skipping.", java_io_message(file, &e)));
153 };
154
155 let formatting_options = match file {
156 Some(file) if args.editor_config => {
157 editor_config_resolver::resolve_formatting_options(file, &args.formatting_options)
158 }
159 _ => args.formatting_options,
160 };
161 let bytes = match file {
162 Some(file) => fs::read(file).map_err(&mut io_error)?,
163 None => {
164 let mut bytes = Vec::new();
165 self.input.lock().unwrap().read_to_end(&mut bytes).map_err(&mut io_error)?;
166 bytes
167 }
168 };
169 let text = String::from_utf8_lossy(&bytes);
170 let code = text.strip_prefix(UTF8_BOM).unwrap_or(&text);
171 let formatted_code = match catch_quietly(|| ktrs_fmt::format(code, &formatting_options)) {
172 Ok(Ok(formatted_code)) => formatted_code,
173 Ok(Err(e)) => {
174 report_error(report, &file_name, &e);
175 return Err(());
176 }
177 Err(_) => return Err(()),
179 };
180 let already_formatted = code == formatted_code;
181
182 if args.dry_run {
183 if !already_formatted {
184 report.out(&file_name);
185 }
186 } else if let Some(file) = file {
187 if !already_formatted {
188 fs::write(file, &formatted_code).map_err(&mut io_error)?;
189 }
190 if !args.quiet {
191 report.err(&format!("Done formatting {file_name}"));
192 }
193 } else {
194 report.out.push_str(&formatted_code);
195 }
196 Ok(already_formatted)
197 }
198
199 fn flush(&self, report: &Report) {
200 for (stream, text) in [(&self.out as &dyn Flush, &report.out), (&self.err as &dyn Flush, &report.err)] {
201 if !text.is_empty() {
202 stream.write_flush(text.as_bytes());
203 }
204 }
205 }
206}
207
208trait Flush {
209 fn write_flush(&self, bytes: &[u8]);
210}
211
212impl<W: Write> Flush for Mutex<W> {
213 fn write_flush(&self, bytes: &[u8]) {
214 let mut stream = self.lock().unwrap();
215 let _ = stream.write_all(bytes).and_then(|()| stream.flush());
216 }
217}
218
219fn report_error(report: &mut Report, file_name: &str, error: &FormatError) {
221 match error {
222 FormatError::Parse(e) => report.err(&format!("{file_name}:{e}")),
223 FormatError::Formatting(e) => {
224 for diagnostic in e.diagnostics() {
225 report.err(&format!("{file_name}:{diagnostic}"));
226 }
227 report.err(&format!("com.google.googlejavaformat.FormattingError: {error}"));
229 }
230 FormatError::Formatter(_) | FormatError::Runtime(_) => {}
231 FormatError::MissedTokens(e) => {
234 report.err(&e.log());
235 report.err(&format!("Exception in thread \"main\" {error}"));
236 }
237 }
238}
239
240pub fn expand_args_to_file_names(args: &[String]) -> Vec<PathBuf> {
243 if args.len() == 1 && Path::new(&args[0]).is_file() {
244 return vec![PathBuf::from(&args[0])];
245 }
246 let mut result = Vec::new();
247 for arg in args {
248 walk_top_down(Path::new(arg), &mut result);
249 }
250 result
251}
252
253fn walk_top_down(path: &Path, result: &mut Vec<PathBuf>) {
254 let Ok(metadata) = fs::metadata(path) else { return };
255 if metadata.is_file() {
256 let name = path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default();
258 if matches!(name.rsplit_once('.'), Some((_, "kt" | "kts"))) {
259 result.push(path.to_path_buf());
260 }
261 } else if metadata.is_dir() {
262 let Ok(entries) = fs::read_dir(path) else { return };
263 let mut children: Vec<PathBuf> = entries.filter_map(|e| Some(e.ok()?.path())).collect();
264 children.sort();
265 for child in children {
266 walk_top_down(&child, result);
267 }
268 }
269}
270
271fn java_file_name(file: &Path) -> String {
273 let name = file.to_string_lossy();
274 if cfg!(windows) { name.replace('/', "\\") } else { name.into_owned() }
275}
276
277fn java_io_message(file: Option<&Path>, e: &io::Error) -> String {
279 let message = e.to_string();
280 let reason = message.split(" (os error").next().unwrap_or(&message).trim_end_matches('.');
281 match file {
282 Some(file) => format!("{} ({reason})", java_file_name(file)),
283 None => reason.to_owned(),
284 }
285}