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;
16use ytil_sys::cli::Args;
17
18const RUST_EXTENSIONS: &[&str] = &[".rs", "Cargo.toml"];
20
21const LINTS_CHECK: &[(&str, LintBuilder)] = &[
34 ("clippy", |ctx| {
35 build_conditional_lint(ctx, 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", |ctx| {
47 build_conditional_lint(ctx, 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", |ctx| {
59 build_conditional_lint(ctx, 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", |ctx| {
72 build_conditional_lint(ctx, 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", |ctx| {
84 build_conditional_lint(ctx, 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", |ctx| {
96 build_conditional_lint(ctx, 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", |ctx| {
125 build_conditional_lint(ctx, 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", |ctx| {
137 build_conditional_lint(ctx, 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", |ctx| {
149 build_conditional_lint(ctx, 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", |ctx| {
160 build_conditional_lint(ctx, 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", |ctx| {
172 build_conditional_lint(ctx, 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", |ctx| {
184 build_conditional_lint(ctx, 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(&LintScope) -> Lint;
202
203#[derive(Clone)]
204enum LintScope {
205 Changed(Vec<String>),
206 Full,
207}
208
209#[ytil_sys::main]
211fn main() -> rootcause::Result<()> {
212 let args = ytil_sys::cli::get();
213 if args.has_help() {
214 println!("{}", include_str!("../help.txt"));
215 return Ok(());
216 }
217 let fix_mode = args.iter().any(|s| matches!(s.as_str(), "--fix" | "fix"));
218 let workspace_root = ytil_sys::dir::get_workspace_root()?;
219
220 let ctx = if args.iter().any(|s| s == "--all") {
221 LintScope::Full
222 } else {
223 let repo = ytil_git::repo::discover(&workspace_root)?;
224 LintScope::Changed(
225 repo.statuses(None)?
226 .iter()
227 .filter_map(|entry| entry.path().ok().map(str::to_string))
228 .collect::<Vec<_>>(),
229 )
230 };
231
232 let (start_msg, lints) = if fix_mode {
233 ("lints fix", LINTS_FIX)
234 } else {
235 ("lints check", LINTS_CHECK)
236 };
237
238 println!(
239 "\nRunning {} {} in {}\n",
240 start_msg.cyan().bold(),
241 format!("{:#?}", lints.iter().map(|(lint, _)| lint).collect::<Vec<_>>())
242 .white()
243 .bold(),
244 workspace_root.display().to_string().white().bold(),
245 );
246
247 let lints_handles: Vec<_> = lints
249 .iter()
250 .map(|(lint_name, lint_builder)| {
251 (
252 lint_name,
253 std::thread::spawn({
254 let workspace_root = workspace_root.clone();
255 let ctx = ctx.clone();
256 move || run_and_report(lint_name, &workspace_root, lint_builder(&ctx))
257 }),
258 )
259 })
260 .collect();
261
262 let mut errors_count: i32 = 0;
263 for (_lint_name, handle) in lints_handles {
264 match handle.join().as_deref() {
265 Ok(Ok(_)) => (),
266 Ok(Err(_)) => errors_count = errors_count.saturating_add(1),
267 Err(join_err) => {
268 errors_count = errors_count.saturating_add(1);
269 eprintln!(
270 "{} error={}",
271 "Error joining thread".red().bold(),
272 format!("{join_err:#?}").red()
273 );
274 }
275 }
276 }
277
278 println!(); if errors_count > 0 {
281 std::process::exit(1);
282 }
283
284 Ok(())
285}
286
287struct LintFnResult(Result<LintFnSuccess, LintFnError>);
289
290impl Deref for LintFnResult {
291 type Target = Result<LintFnSuccess, LintFnError>;
292
293 fn deref(&self) -> &Self::Target {
294 &self.0
295 }
296}
297
298impl From<Result<LintFnSuccess, LintFnError>> for LintFnResult {
299 fn from(value: Result<LintFnSuccess, LintFnError>) -> Self {
300 Self(value)
301 }
302}
303
304#[derive(Debug, thiserror::Error)]
309enum LintFnError {
310 #[error(transparent)]
311 CmdError(Box<CmdError>),
312}
313
314impl From<CmdError> for LintFnError {
315 fn from(err: CmdError) -> Self {
316 Self::CmdError(Box::new(err))
317 }
318}
319
320impl From<Box<CmdError>> for LintFnError {
321 fn from(err: Box<CmdError>) -> Self {
322 Self::CmdError(err)
323 }
324}
325
326enum LintFnSuccess {
332 CmdOutput(Output),
333 PlainMsg(String),
334}
335
336fn build_conditional_lint(ctx: &LintScope, extensions: &[&str], lint: Lint) -> Lint {
340 match ctx {
341 LintScope::Full => lint,
342 LintScope::Changed(changed_paths)
343 if extensions.is_empty()
344 || changed_paths
345 .iter()
346 .any(|path| extensions.iter().any(|ext| path.ends_with(ext))) =>
347 {
348 lint
349 }
350 LintScope::Changed(_) => LINT_NO_OP,
351 }
352}
353
354fn run_and_report(lint_name: &str, path: &Path, run: Lint) -> LintFnResult {
356 let start = Instant::now();
357 let lint_res = run(path);
358 report(lint_name, &lint_res, start.elapsed());
359 lint_res
360}
361
362fn report(lint_name: &str, lint_res: &Result<LintFnSuccess, LintFnError>, elapsed: Duration) {
364 match lint_res {
365 Ok(LintFnSuccess::CmdOutput(output)) => {
366 println!(
367 "{} {} status={:?} \n{}",
368 lint_name.green().bold(),
369 format_timing(elapsed),
370 output.status.code(),
371 str::from_utf8(&output.stdout).unwrap_or_default()
372 );
373 }
374 Ok(LintFnSuccess::PlainMsg(msg)) => {
375 println!("{} {} \n{msg}", lint_name.green().bold(), format_timing(elapsed));
376 }
377 Err(err) => {
378 eprintln!("{} {} \n{err}", lint_name.red().bold(), format_timing(elapsed));
379 }
380 }
381}
382
383fn format_timing(duration: Duration) -> String {
385 format!("time={duration:?}")
386}
387
388#[cfg(test)]
389mod tests {
390 use rstest::rstest;
391
392 use super::*;
393
394 #[rstest]
395 #[case::multiple_files_no_extension_filter(
396 &["README.md".to_string(), "src/main.rs".to_string()],
397 &[] as &[&str],
398 "dummy success"
399 )]
400 #[case::multiple_files_with_rs_extension_filter(
401 &["README.md".to_string(), "src/main.rs".to_string()],
402 &[".rs"],
403 "dummy success"
404 )]
405 #[case::single_non_rs_file_with_rs_extension_filter(
406 &["README.md".to_string()],
407 &[".rs"],
408 "skipped"
409 )]
410 #[case::cargo_toml_change_triggers_rust_extensions(
411 &["yog/yog/tec/Cargo.toml".to_string()],
412 RUST_EXTENSIONS,
413 "dummy success"
414 )]
415 #[case::non_rust_file_with_rust_extensions(
416 &["README.md".to_string()],
417 RUST_EXTENSIONS,
418 "skipped"
419 )]
420 fn build_conditional_lint_returns_expected_result(
421 #[case] changed_paths: &[String],
422 #[case] extensions: &[&str],
423 #[case] expected: &str,
424 ) {
425 let ctx = LintScope::Changed(changed_paths.to_vec());
426 let result_lint = build_conditional_lint(&ctx, extensions, dummy_lint);
427 let lint_result = result_lint(Path::new("/tmp"));
428
429 assert2::assert!(let Ok(LintFnSuccess::PlainMsg(msg)) = lint_result.0);
430 assert!(msg.contains(expected));
432 }
433
434 #[test]
435 fn test_build_conditional_lint_when_scope_full_returns_lint() {
436 let ctx = LintScope::Full;
437 let result_lint = build_conditional_lint(&ctx, RUST_EXTENSIONS, dummy_lint);
438 let lint_result = result_lint(Path::new("/tmp"));
439
440 assert2::assert!(let Ok(LintFnSuccess::PlainMsg(msg)) = lint_result.0);
441 assert!(msg.contains("dummy success"));
442 }
443
444 fn dummy_lint(_path: &Path) -> LintFnResult {
445 LintFnResult(Ok(LintFnSuccess::PlainMsg("dummy success".to_string())))
446 }
447}