1use std::ops::Deref;
7use std::path::Path;
8use std::process::Command;
9use std::process::Output;
10use std::time::Duration;
11use std::time::Instant;
12
13use owo_colors::OwoColorize;
14use ytil_cmd::CmdError;
15use ytil_cmd::CmdExt as _;
16use ytil_sys::cli::Args;
17
18const RUST_EXTENSIONS: &[&str] = &[".rs", "Cargo.toml"];
20
21const LINTS_CHECK: &[(&str, LintBuilder)] = &[
34 ("clippy", |changed_paths| {
35 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
36 LintFnResult::from(
37 Command::new("cargo")
38 .args(["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"])
39 .current_dir(path)
40 .exec()
41 .map(LintFnSuccess::CmdOutput)
42 .map_err(LintFnError::from),
43 )
44 })
45 }),
46 ("cargo fmt", |changed_paths| {
47 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
48 LintFnResult::from(
49 Command::new("cargo")
50 .args(["fmt", "--check"])
51 .current_dir(path)
52 .exec()
53 .map(LintFnSuccess::CmdOutput)
54 .map_err(LintFnError::from),
55 )
56 })
57 }),
58 ("cargo-machete", |changed_paths| {
59 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
60 LintFnResult::from(
61 Command::new("cargo-machete")
64 .args(["--with-metadata", &path.display().to_string()])
65 .exec()
66 .map(LintFnSuccess::CmdOutput)
67 .map_err(LintFnError::from),
68 )
69 })
70 }),
71 ("cargo-sort", |changed_paths| {
72 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
73 LintFnResult::from(
74 Command::new("cargo-sort")
75 .args(["--workspace", "--check", "--check-format"])
76 .current_dir(path)
77 .exec()
78 .map(LintFnSuccess::CmdOutput)
79 .map_err(LintFnError::from),
80 )
81 })
82 }),
83 ("cargo-sort-derives", |changed_paths| {
84 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
85 LintFnResult::from(
86 Command::new("cargo-sort-derives")
87 .args(["sort-derives", "--check"])
88 .current_dir(path)
89 .exec()
90 .map(LintFnSuccess::CmdOutput)
91 .map_err(LintFnError::from),
92 )
93 })
94 }),
95 ("rust-doc-build", |changed_paths| {
96 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
97 LintFnResult::from(
98 nomicon::generate_rust_doc(path)
99 .map(LintFnSuccess::CmdOutput)
100 .map_err(LintFnError::from),
101 )
102 })
103 }),
104];
105
106const LINTS_FIX: &[(&str, LintBuilder)] = &[
124 ("clippy", |changed_paths| {
125 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
126 LintFnResult::from(
127 Command::new("cargo")
128 .args(["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"])
129 .current_dir(path)
130 .exec()
131 .map(LintFnSuccess::CmdOutput)
132 .map_err(LintFnError::from),
133 )
134 })
135 }),
136 ("cargo fmt", |changed_paths| {
137 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
138 LintFnResult::from(
139 Command::new("cargo")
140 .args(["fmt"])
141 .current_dir(path)
142 .exec()
143 .map(LintFnSuccess::CmdOutput)
144 .map_err(LintFnError::from),
145 )
146 })
147 }),
148 ("cargo-machete", |changed_paths| {
149 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
150 LintFnResult::from(
151 Command::new("cargo-machete")
152 .args(["--fix", "--with-metadata", &path.display().to_string()])
153 .exec()
154 .map(LintFnSuccess::CmdOutput)
155 .map_err(LintFnError::from),
156 )
157 })
158 }),
159 ("cargo-sort", |changed_paths| {
160 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
161 LintFnResult::from(
162 Command::new("cargo-sort")
163 .args(["--workspace"])
164 .current_dir(path)
165 .exec()
166 .map(LintFnSuccess::CmdOutput)
167 .map_err(LintFnError::from),
168 )
169 })
170 }),
171 ("cargo-sort-derives", |changed_paths| {
172 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
173 LintFnResult::from(
174 Command::new("cargo-sort-derives")
175 .args(["sort-derives"])
176 .current_dir(path)
177 .exec()
178 .map(LintFnSuccess::CmdOutput)
179 .map_err(LintFnError::from),
180 )
181 })
182 }),
183 ("rust-doc-build", |changed_paths| {
184 build_conditional_lint(changed_paths, RUST_EXTENSIONS, |path| {
185 LintFnResult::from(
186 nomicon::generate_rust_doc(path)
187 .map(LintFnSuccess::CmdOutput)
188 .map_err(LintFnError::from),
189 )
190 })
191 }),
192];
193
194const LINT_NO_OP: Lint = |_| LintFnResult(Ok(LintFnSuccess::PlainMsg(format!("{}\n", "skipped".bold()))));
196
197type Lint = fn(&Path) -> LintFnResult;
199
200type LintBuilder = fn(&[String]) -> Lint;
202
203struct LintFnResult(Result<LintFnSuccess, LintFnError>);
205
206impl Deref for LintFnResult {
207 type Target = Result<LintFnSuccess, LintFnError>;
208
209 fn deref(&self) -> &Self::Target {
210 &self.0
211 }
212}
213
214impl From<Result<LintFnSuccess, LintFnError>> for LintFnResult {
215 fn from(value: Result<LintFnSuccess, LintFnError>) -> Self {
216 Self(value)
217 }
218}
219
220#[derive(Debug, thiserror::Error)]
225enum LintFnError {
226 #[error(transparent)]
227 CmdError(Box<CmdError>),
228}
229
230impl From<CmdError> for LintFnError {
231 fn from(err: CmdError) -> Self {
232 Self::CmdError(Box::new(err))
233 }
234}
235
236impl From<Box<CmdError>> for LintFnError {
237 fn from(err: Box<CmdError>) -> Self {
238 Self::CmdError(err)
239 }
240}
241
242enum LintFnSuccess {
248 CmdOutput(Output),
249 PlainMsg(String),
250}
251
252fn build_conditional_lint(changed_paths: &[String], extensions: &[&str], lint: Lint) -> Lint {
256 if extensions.is_empty()
257 || changed_paths
258 .iter()
259 .any(|path| extensions.iter().any(|ext| path.ends_with(ext)))
260 {
261 lint
262 } else {
263 LINT_NO_OP
264 }
265}
266
267fn run_and_report(lint_name: &str, path: &Path, run: Lint) -> LintFnResult {
269 let start = Instant::now();
270 let lint_res = run(path);
271 report(lint_name, &lint_res, start.elapsed());
272 lint_res
273}
274
275fn report(lint_name: &str, lint_res: &Result<LintFnSuccess, LintFnError>, elapsed: Duration) {
277 match lint_res {
278 Ok(LintFnSuccess::CmdOutput(output)) => {
279 println!(
280 "{} {} status={:?} \n{}",
281 lint_name.green().bold(),
282 format_timing(elapsed),
283 output.status.code(),
284 str::from_utf8(&output.stdout).unwrap_or_default()
285 );
286 }
287 Ok(LintFnSuccess::PlainMsg(msg)) => {
288 println!("{} {} \n{msg}", lint_name.green().bold(), format_timing(elapsed));
289 }
290 Err(err) => {
291 eprintln!("{} {} \n{err}", lint_name.red().bold(), format_timing(elapsed));
292 }
293 }
294}
295
296fn format_timing(duration: Duration) -> String {
298 format!("time={duration:?}")
299}
300
301#[ytil_sys::main]
303fn main() -> rootcause::Result<()> {
304 let args = ytil_sys::cli::get();
305 if args.has_help() {
306 println!("{}", include_str!("../help.txt"));
307 return Ok(());
308 }
309 let fix_mode = args.first().is_some_and(|s| s == "fix");
310
311 let workspace_root = ytil_sys::dir::get_workspace_root()?;
312
313 let repo = ytil_git::repo::discover(&workspace_root)?;
314 let changed_paths = repo
315 .statuses(None)?
316 .iter()
317 .filter_map(|entry| entry.path().map(str::to_string))
318 .collect::<Vec<_>>();
319
320 let (start_msg, lints) = if fix_mode {
321 ("lints fix", LINTS_FIX)
322 } else {
323 ("lints check", LINTS_CHECK)
324 };
325
326 println!(
327 "\nRunning {} {} in {}\n",
328 start_msg.cyan().bold(),
329 format!("{:#?}", lints.iter().map(|(lint, _)| lint).collect::<Vec<_>>())
330 .white()
331 .bold(),
332 workspace_root.display().to_string().white().bold(),
333 );
334
335 let lints_handles: Vec<_> = lints
337 .iter()
338 .map(|(lint_name, lint_builder)| {
339 (
340 lint_name,
341 std::thread::spawn({
342 let workspace_root = workspace_root.clone();
343 let changed_paths = changed_paths.clone();
344 move || run_and_report(lint_name, &workspace_root, lint_builder(&changed_paths))
345 }),
346 )
347 })
348 .collect();
349
350 let mut errors_count: i32 = 0;
351 for (_lint_name, handle) in lints_handles {
352 match handle.join().as_deref() {
353 Ok(Ok(_)) => (),
354 Ok(Err(_)) => errors_count = errors_count.saturating_add(1),
355 Err(join_err) => {
356 errors_count = errors_count.saturating_add(1);
357 eprintln!(
358 "{} error={}",
359 "Error joining thread".red().bold(),
360 format!("{join_err:#?}").red()
361 );
362 }
363 }
364 }
365
366 println!(); if errors_count > 0 {
369 std::process::exit(1);
370 }
371
372 Ok(())
373}
374
375#[cfg(test)]
376mod tests {
377 use rstest::rstest;
378
379 use super::*;
380
381 #[rstest]
382 #[case::multiple_files_no_extension_filter(
383 &["README.md".to_string(), "src/main.rs".to_string()],
384 &[] as &[&str],
385 "dummy success"
386 )]
387 #[case::multiple_files_with_rs_extension_filter(
388 &["README.md".to_string(), "src/main.rs".to_string()],
389 &[".rs"],
390 "dummy success"
391 )]
392 #[case::single_non_rs_file_with_rs_extension_filter(
393 &["README.md".to_string()],
394 &[".rs"],
395 "skipped"
396 )]
397 #[case::cargo_toml_change_triggers_rust_extensions(
398 &["yog/yog/tec/Cargo.toml".to_string()],
399 RUST_EXTENSIONS,
400 "dummy success"
401 )]
402 #[case::non_rust_file_with_rust_extensions(
403 &["README.md".to_string()],
404 RUST_EXTENSIONS,
405 "skipped"
406 )]
407 fn build_conditional_lint_returns_expected_result(
408 #[case] changed_paths: &[String],
409 #[case] extensions: &[&str],
410 #[case] expected: &str,
411 ) {
412 let result_lint = build_conditional_lint(changed_paths, extensions, dummy_lint);
413 let lint_result = result_lint(Path::new("/tmp"));
414
415 assert2::assert!(let Ok(LintFnSuccess::PlainMsg(msg)) = lint_result.0);
416 assert!(msg.contains(expected));
418 }
419
420 fn dummy_lint(_path: &Path) -> LintFnResult {
421 LintFnResult(Ok(LintFnSuccess::PlainMsg("dummy success".to_string())))
422 }
423}