1use std::ffi::OsStr;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23use std::str::FromStr;
24use std::{env, fmt, fs, io};
25
26use build_helper::ci::CiEnv;
27
28use crate::CiInfo;
29use crate::diagnostics::TidyCtx;
30
31mod rustdoc_js;
32
33#[cfg(test)]
34mod tests;
35
36const MIN_PY_REV: (u32, u32) = (3, 9);
37const MIN_PY_REV_STR: &str = "≥3.9";
38
39#[cfg(target_os = "windows")]
41const REL_PY_PATH: &[&str] = &["Scripts", "python3.exe"];
42#[cfg(not(target_os = "windows"))]
43const REL_PY_PATH: &[&str] = &["bin", "python3"];
44
45const RUFF_CONFIG_PATH: &[&str] = &["src", "tools", "tidy", "config", "ruff.toml"];
46const RUFF_CACHE_PATH: &[&str] = &["cache", "ruff_cache"];
48const PIP_REQ_PATH: &[&str] = &["src", "tools", "tidy", "config", "requirements.txt"];
49
50const SPELLCHECK_DIRS: &[&str] = &["compiler", "library", "src/bootstrap", "src/librustdoc"];
51const SPELLCHECK_VER: &str = "1.38.1";
52
53pub fn check(
54 root_path: &Path,
55 outdir: &Path,
56 ci_info: &CiInfo,
57 librustdoc_path: &Path,
58 tools_path: &Path,
59 npm: &Path,
60 cargo: &Path,
61 extra_checks: Option<Vec<String>>,
62 pos_args: Vec<String>,
63 tidy_ctx: TidyCtx,
64) {
65 let mut check = tidy_ctx.start_check("extra_checks");
66
67 if let Err(e) = check_impl(
68 root_path,
69 outdir,
70 ci_info,
71 librustdoc_path,
72 tools_path,
73 npm,
74 cargo,
75 extra_checks,
76 pos_args,
77 &tidy_ctx,
78 ) {
79 check.error(e);
80 }
81}
82
83fn check_impl(
84 root_path: &Path,
85 outdir: &Path,
86 ci_info: &CiInfo,
87 librustdoc_path: &Path,
88 tools_path: &Path,
89 npm: &Path,
90 cargo: &Path,
91 extra_checks: Option<Vec<String>>,
92 pos_args: Vec<String>,
93 tidy_ctx: &TidyCtx,
94) -> Result<(), Error> {
95 let show_diff =
96 std::env::var("TIDY_PRINT_DIFF").is_ok_and(|v| v.eq_ignore_ascii_case("true") || v == "1");
97 let bless = tidy_ctx.is_bless_enabled();
98
99 let mut lint_args = match extra_checks {
101 Some(s) => s
102 .iter()
103 .map(|s| {
104 if s == "spellcheck:fix" {
105 eprintln!("warning: `spellcheck:fix` is no longer valid, use `--extra-checks=spellcheck --bless`");
106 }
107 (ExtraCheckArg::from_str(s), s)
108 })
109 .filter_map(|(res, src)| match res {
110 Ok(arg) => {
111 Some(arg)
112 }
113 Err(err) => {
114 eprintln!("warning: bad extra check argument {src:?}: {err:?}");
116 None
117 }
118 })
119 .collect(),
120 None => vec![],
121 };
122 lint_args.retain(|ck| ck.is_non_if_installed_or_matches(root_path, outdir));
123 if lint_args.iter().any(|ck| ck.auto) {
124 crate::files_modified_batch_filter(ci_info, &mut lint_args, |ck, path| {
125 ck.is_non_auto_or_matches(path)
126 });
127 }
128
129 macro_rules! extra_check {
130 ($lang:ident, $kind:ident) => {
131 lint_args.iter().any(|arg| arg.matches(ExtraCheckLang::$lang, ExtraCheckKind::$kind))
132 };
133 }
134
135 let rerun_with_bless = |mode: &str, action: &str| {
136 if !bless {
137 eprintln!("rerun tidy with `--extra-checks={mode} --bless` to {action}");
138 }
139 };
140
141 let python_lint = extra_check!(Py, Lint);
142 let python_fmt = extra_check!(Py, Fmt);
143 let shell_lint = extra_check!(Shell, Lint);
144 let cpp_fmt = extra_check!(Cpp, Fmt);
145 let spellcheck = extra_check!(Spellcheck, None);
146 let js_lint = extra_check!(Js, Lint);
147 let js_typecheck = extra_check!(Js, Typecheck);
148
149 let mut py_path = None;
150
151 let (cfg_args, file_args): (Vec<_>, Vec<_>) = pos_args
152 .iter()
153 .map(OsStr::new)
154 .partition(|arg| arg.to_str().is_some_and(|s| s.starts_with('-')));
155
156 if python_lint || python_fmt || cpp_fmt {
157 let venv_path = outdir.join("venv");
158 let mut reqs_path = root_path.to_owned();
159 reqs_path.extend(PIP_REQ_PATH);
160 py_path = Some(get_or_create_venv(&venv_path, &reqs_path)?);
161 }
162
163 if python_lint {
164 let py_path = py_path.as_ref().unwrap();
165 let args: &[&OsStr] = if bless {
166 eprintln!("linting python files and applying suggestions");
167 &["check".as_ref(), "--fix".as_ref()]
168 } else {
169 eprintln!("linting python files");
170 &["check".as_ref()]
171 };
172
173 let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, args);
174
175 if res.is_err() && show_diff && !bless {
176 eprintln!("\npython linting failed! Printing diff suggestions:");
177
178 let diff_res = run_ruff(
179 root_path,
180 outdir,
181 py_path,
182 &cfg_args,
183 &file_args,
184 &["check".as_ref(), "--diff".as_ref()],
185 );
186 if diff_res.is_err() {
188 rerun_with_bless("py:lint", "apply ruff suggestions");
189 }
190 }
191 res?;
193 }
194
195 if python_fmt {
196 let mut args: Vec<&OsStr> = vec!["format".as_ref()];
197 if bless {
198 eprintln!("formatting python files");
199 } else {
200 eprintln!("checking python file formatting");
201 args.push("--check".as_ref());
202 }
203
204 let py_path = py_path.as_ref().unwrap();
205 let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, &args);
206
207 if res.is_err() && !bless {
208 if show_diff {
209 eprintln!("\npython formatting does not match! Printing diff:");
210
211 let _ = run_ruff(
212 root_path,
213 outdir,
214 py_path,
215 &cfg_args,
216 &file_args,
217 &["format".as_ref(), "--diff".as_ref()],
218 );
219 }
220 rerun_with_bless("py:fmt", "reformat Python code");
221 }
222
223 res?;
225 }
226
227 if cpp_fmt {
228 let mut cfg_args_clang_format = cfg_args.clone();
229 let mut file_args_clang_format = file_args.clone();
230 let config_path = root_path.join(".clang-format");
231 let config_file_arg = format!("file:{}", config_path.display());
232 cfg_args_clang_format.extend(&["--style".as_ref(), config_file_arg.as_ref()]);
233 if bless {
234 eprintln!("formatting C++ files");
235 cfg_args_clang_format.push("-i".as_ref());
236 } else {
237 eprintln!("checking C++ file formatting");
238 cfg_args_clang_format.extend(&["--dry-run".as_ref(), "--Werror".as_ref()]);
239 }
240 let files;
241 if file_args_clang_format.is_empty() {
242 let llvm_wrapper = root_path.join("compiler/rustc_llvm/llvm-wrapper");
243 files = find_with_extension(
244 root_path,
245 Some(llvm_wrapper.as_path()),
246 &[OsStr::new("h"), OsStr::new("cpp")],
247 )?;
248 file_args_clang_format.extend(files.iter().map(|p| p.as_os_str()));
249 }
250 let args = merge_args(&cfg_args_clang_format, &file_args_clang_format);
251 let res = py_runner(py_path.as_ref().unwrap(), false, None, "clang-format", &args);
252
253 if res.is_err() && show_diff && !bless {
254 eprintln!("\nclang-format linting failed! Printing diff suggestions:");
255
256 let mut cfg_args_clang_format_diff = cfg_args.clone();
257 cfg_args_clang_format_diff.extend(&["--style".as_ref(), config_file_arg.as_ref()]);
258 for file in file_args_clang_format {
259 let mut formatted = String::new();
260 let mut diff_args = cfg_args_clang_format_diff.clone();
261 diff_args.push(file);
262 let _ = py_runner(
263 py_path.as_ref().unwrap(),
264 false,
265 Some(&mut formatted),
266 "clang-format",
267 &diff_args,
268 );
269 if formatted.is_empty() {
270 eprintln!(
271 "failed to obtain the formatted content for '{}'",
272 file.to_string_lossy()
273 );
274 continue;
275 }
276 let actual = std::fs::read_to_string(file).unwrap_or_else(|e| {
277 panic!(
278 "failed to read the C++ file at '{}' due to '{e}'",
279 file.to_string_lossy()
280 )
281 });
282 if formatted != actual {
283 let diff = similar::TextDiff::from_lines(&actual, &formatted);
284 eprintln!(
285 "{}",
286 diff.unified_diff().context_radius(4).header(
287 &format!("{} (actual)", file.to_string_lossy()),
288 &format!("{} (formatted)", file.to_string_lossy())
289 )
290 );
291 }
292 }
293 rerun_with_bless("cpp:fmt", "reformat C++ code");
294 }
295 res?;
297 }
298
299 if shell_lint {
300 eprintln!("linting shell files");
301
302 let mut file_args_shc = file_args.clone();
303 let files;
304 if file_args_shc.is_empty() {
305 files = find_with_extension(root_path, None, &[OsStr::new("sh")])?;
306 file_args_shc.extend(files.iter().map(|p| p.as_os_str()));
307 }
308
309 shellcheck_runner(&merge_args(&cfg_args, &file_args_shc))?;
310 }
311
312 if spellcheck {
313 let config_path = root_path.join("typos.toml");
314 let mut args = vec!["-c", config_path.as_os_str().to_str().unwrap()];
315
316 args.extend_from_slice(SPELLCHECK_DIRS);
317
318 if bless {
319 eprintln!("spellchecking files and fixing typos");
320 args.push("--write-changes");
321 } else {
322 eprintln!("spellchecking files");
323 }
324 let res = spellcheck_runner(root_path, &outdir, &cargo, &args);
325 if res.is_err() {
326 rerun_with_bless("spellcheck", "fix typos");
327 }
328 res?;
329 }
330
331 if js_lint || js_typecheck {
332 rustdoc_js::npm_install(root_path, outdir, npm)?;
333 }
334
335 if js_lint {
336 if bless {
337 eprintln!("linting javascript files and applying suggestions");
338 } else {
339 eprintln!("linting javascript files");
340 }
341 let res = rustdoc_js::lint(outdir, librustdoc_path, tools_path, bless);
342 if res.is_err() {
343 rerun_with_bless("js:lint", "apply eslint suggestions");
344 }
345 res?;
346 rustdoc_js::es_check(outdir, librustdoc_path)?;
347 }
348
349 if js_typecheck {
350 eprintln!("typechecking javascript files");
351 rustdoc_js::typecheck(outdir, librustdoc_path)?;
352 }
353
354 Ok(())
355}
356
357fn run_ruff(
358 root_path: &Path,
359 outdir: &Path,
360 py_path: &Path,
361 cfg_args: &[&OsStr],
362 file_args: &[&OsStr],
363 ruff_args: &[&OsStr],
364) -> Result<(), Error> {
365 let mut cfg_args_ruff = cfg_args.to_vec();
366 let mut file_args_ruff = file_args.to_vec();
367
368 let mut cfg_path = root_path.to_owned();
369 cfg_path.extend(RUFF_CONFIG_PATH);
370 let mut cache_dir = outdir.to_owned();
371 cache_dir.extend(RUFF_CACHE_PATH);
372
373 cfg_args_ruff.extend([
374 "--config".as_ref(),
375 cfg_path.as_os_str(),
376 "--cache-dir".as_ref(),
377 cache_dir.as_os_str(),
378 ]);
379
380 if file_args_ruff.is_empty() {
381 file_args_ruff.push(root_path.as_os_str());
382 }
383
384 let mut args: Vec<&OsStr> = ruff_args.to_vec();
385 args.extend(merge_args(&cfg_args_ruff, &file_args_ruff));
386 py_runner(py_path, true, None, "ruff", &args)
387}
388
389fn merge_args<'a>(cfg_args: &[&'a OsStr], file_args: &[&'a OsStr]) -> Vec<&'a OsStr> {
391 let mut args = cfg_args.to_owned();
392 args.push("--".as_ref());
393 args.extend(file_args);
394 args
395}
396
397fn py_runner(
401 py_path: &Path,
402 as_module: bool,
403 stdout: Option<&mut String>,
404 bin: &'static str,
405 args: &[&OsStr],
406) -> Result<(), Error> {
407 let mut cmd = Command::new(py_path);
408 if as_module {
409 cmd.arg("-m").arg(bin).args(args);
410 } else {
411 let bin_path = py_path.with_file_name(bin);
412 cmd.arg(bin_path).args(args);
413 }
414 let status = if let Some(stdout) = stdout {
415 let output = cmd.output()?;
416 if let Ok(s) = std::str::from_utf8(&output.stdout) {
417 stdout.push_str(s);
418 }
419 output.status
420 } else {
421 cmd.status()?
422 };
423 if status.success() { Ok(()) } else { Err(Error::FailedCheck(bin)) }
424}
425
426fn get_or_create_venv(venv_path: &Path, src_reqs_path: &Path) -> Result<PathBuf, Error> {
429 let mut py_path = venv_path.to_owned();
430 py_path.extend(REL_PY_PATH);
431
432 if !has_py_tools(venv_path, src_reqs_path)? {
433 let dst_reqs_path = venv_path.join("requirements.txt");
434 eprintln!("removing old virtual environment");
435 if venv_path.is_dir() {
436 fs::remove_dir_all(venv_path).unwrap_or_else(|_| {
437 panic!("failed to remove directory at {}", venv_path.display())
438 });
439 }
440 create_venv_at_path(venv_path)?;
441 install_requirements(&py_path, src_reqs_path, &dst_reqs_path)?;
442 }
443
444 verify_py_version(&py_path)?;
445 Ok(py_path)
446}
447
448fn has_py_tools(venv_path: &Path, src_reqs_path: &Path) -> Result<bool, Error> {
449 let dst_reqs_path = venv_path.join("requirements.txt");
450 if let Ok(req) = fs::read_to_string(&dst_reqs_path) {
451 if req == fs::read_to_string(src_reqs_path)? {
452 return Ok(true);
453 }
454 eprintln!("requirements.txt file mismatch");
455 }
456
457 Ok(false)
458}
459
460fn create_venv_at_path(path: &Path) -> Result<(), Error> {
463 const TRY_PY: &[&str] = &[
466 "python3.13",
467 "python3.12",
468 "python3.11",
469 "python3.10",
470 "python3.9",
471 "python3",
472 "python",
473 "python3.14",
474 ];
475
476 let mut sys_py = None;
477 let mut found = Vec::new();
478
479 for py in TRY_PY {
480 match verify_py_version(Path::new(py)) {
481 Ok(_) => {
482 sys_py = Some(*py);
483 break;
484 }
485 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => (),
487 Err(Error::Version { installed, .. }) => found.push(installed),
489 Err(e) => eprintln!("note: error running '{py}': {e}"),
491 }
492 }
493
494 let Some(sys_py) = sys_py else {
495 let ret = if found.is_empty() {
496 Error::MissingReq("python3", "python file checks", None)
497 } else {
498 found.sort();
499 found.dedup();
500 Error::Version {
501 program: "python3",
502 required: MIN_PY_REV_STR,
503 installed: found.join(", "),
504 }
505 };
506 return Err(ret);
507 };
508
509 if try_create_venv(sys_py, path, "venv").is_ok() {
513 return Ok(());
514 }
515 try_create_venv(sys_py, path, "virtualenv")
516}
517
518fn try_create_venv(python: &str, path: &Path, module: &str) -> Result<(), Error> {
519 eprintln!(
520 "creating virtual environment at '{}' using '{python}' and '{module}'",
521 path.display()
522 );
523 let out = Command::new(python).args(["-m", module]).arg(path).output().unwrap();
524
525 if out.status.success() {
526 return Ok(());
527 }
528
529 let stderr = String::from_utf8_lossy(&out.stderr);
530 let err = if stderr.contains(&format!("No module named {module}")) {
531 Error::Generic(format!(
532 r#"{module} not found: you may need to install it:
533`{python} -m pip install {module}`
534If you see an error about "externally managed environment" when running the above command,
535either install `{module}` using your system package manager
536(e.g. `sudo apt-get install {python}-{module}`) or create a virtual environment manually, install
537`{module}` in it and then activate it before running tidy.
538"#
539 ))
540 } else {
541 Error::Generic(format!(
542 "failed to create venv at '{}' using {python} -m {module}: {stderr}",
543 path.display()
544 ))
545 };
546 Err(err)
547}
548
549fn verify_py_version(py_path: &Path) -> Result<(), Error> {
552 let out = Command::new(py_path).arg("--version").output()?;
553 let outstr = String::from_utf8_lossy(&out.stdout);
554 let vers = outstr.trim().split_ascii_whitespace().nth(1).unwrap().trim();
555 let mut vers_comps = vers.split('.');
556 let major: u32 = vers_comps.next().unwrap().parse().unwrap();
557 let minor: u32 = vers_comps.next().unwrap().parse().unwrap();
558
559 if (major, minor) < MIN_PY_REV {
560 Err(Error::Version {
561 program: "python",
562 required: MIN_PY_REV_STR,
563 installed: vers.to_owned(),
564 })
565 } else {
566 Ok(())
567 }
568}
569
570fn install_requirements(
571 py_path: &Path,
572 src_reqs_path: &Path,
573 dst_reqs_path: &Path,
574) -> Result<(), Error> {
575 let stat = Command::new(py_path)
576 .args(["-m", "pip", "install", "--upgrade", "pip"])
577 .status()
578 .expect("failed to launch pip");
579 if !stat.success() {
580 return Err(Error::Generic(format!("pip install failed with status {stat}")));
581 }
582
583 let stat = Command::new(py_path)
584 .args(["-m", "pip", "install", "--quiet", "--require-hashes", "-r"])
585 .arg(src_reqs_path)
586 .status()?;
587 if !stat.success() {
588 return Err(Error::Generic(format!(
589 "failed to install requirements at {}",
590 src_reqs_path.display()
591 )));
592 }
593 fs::copy(src_reqs_path, dst_reqs_path)?;
594 assert_eq!(
595 fs::read_to_string(src_reqs_path).unwrap(),
596 fs::read_to_string(dst_reqs_path).unwrap()
597 );
598 Ok(())
599}
600
601fn has_shellcheck() -> Result<(), Error> {
603 match Command::new("shellcheck").arg("--version").status() {
604 Ok(_) => Ok(()),
605 Err(e) if e.kind() == io::ErrorKind::NotFound => Err(Error::MissingReq(
606 "shellcheck",
607 "shell file checks",
608 Some(
609 "see <https://github.com/koalaman/shellcheck#installing> \
610 for installation instructions"
611 .to_owned(),
612 ),
613 )),
614 Err(e) => Err(e.into()),
615 }
616}
617
618fn shellcheck_runner(args: &[&OsStr]) -> Result<(), Error> {
620 has_shellcheck()?;
621
622 let status = Command::new("shellcheck").args(args).status()?;
623 if status.success() { Ok(()) } else { Err(Error::FailedCheck("shellcheck")) }
624}
625
626fn spellcheck_runner(
628 src_root: &Path,
629 outdir: &Path,
630 cargo: &Path,
631 args: &[&str],
632) -> Result<(), Error> {
633 let bin_path =
634 ensure_version_or_cargo_install(outdir, cargo, "typos-cli", "typos", SPELLCHECK_VER)?;
635 match Command::new(bin_path).current_dir(src_root).args(args).status() {
636 Ok(status) => {
637 if status.success() {
638 Ok(())
639 } else {
640 Err(Error::FailedCheck("typos"))
641 }
642 }
643 Err(err) => Err(Error::Generic(format!("failed to run typos tool: {err:?}"))),
644 }
645}
646
647fn find_with_extension(
649 root_path: &Path,
650 find_dir: Option<&Path>,
651 extensions: &[&OsStr],
652) -> Result<Vec<PathBuf>, Error> {
653 let stat_output =
656 Command::new("git").arg("-C").arg(root_path).args(["status", "--short"]).output()?.stdout;
657
658 if String::from_utf8_lossy(&stat_output).lines().filter(|ln| ln.starts_with('?')).count() > 0 {
659 eprintln!("found untracked files, ignoring");
660 }
661
662 let mut output = Vec::new();
663 let binding = {
664 let mut command = Command::new("git");
665 command.arg("-C").arg(root_path).args(["ls-files"]);
666 if let Some(find_dir) = find_dir {
667 command.arg(find_dir);
668 }
669 command.output()?
670 };
671 let tracked = String::from_utf8_lossy(&binding.stdout);
672
673 for line in tracked.lines() {
674 let line = line.trim();
675 let path = Path::new(line);
676
677 let Some(ref extension) = path.extension() else {
678 continue;
679 };
680 if extensions.contains(extension) {
681 output.push(root_path.join(path));
682 }
683 }
684
685 Ok(output)
686}
687
688fn ensure_version(build_dir: &Path, bin_name: &str, version: &str) -> Result<PathBuf, Error> {
690 let bin_path = build_dir.join("misc-tools").join("bin").join(bin_name);
691
692 match Command::new(&bin_path).arg("--version").output() {
693 Ok(output) => {
694 let Some(v) = str::from_utf8(&output.stdout).unwrap().trim().split_whitespace().last()
695 else {
696 return Err(Error::Generic("version check failed".to_string()));
697 };
698
699 if v != version {
700 return Err(Error::Version { program: "", required: "", installed: v.to_string() });
701 }
702 Ok(bin_path)
703 }
704 Err(e) => Err(Error::Io(e)),
705 }
706}
707
708fn ensure_version_or_cargo_install(
711 build_dir: &Path,
712 cargo: &Path,
713 pkg_name: &str,
714 bin_name: &str,
715 version: &str,
716) -> Result<PathBuf, Error> {
717 if let Ok(bin_path) = ensure_version(build_dir, bin_name, version) {
718 return Ok(bin_path);
719 }
720
721 eprintln!("building external tool {bin_name} from package {pkg_name}@{version}");
722
723 let tool_root_dir = build_dir.join("misc-tools");
724 let tool_bin_dir = tool_root_dir.join("bin");
725 let bin_path = tool_bin_dir.join(bin_name).with_extension(env::consts::EXE_EXTENSION);
726
727 let mut cmd = Command::new(cargo);
731 cmd.args(["install", "--locked", "--force", "--quiet"])
732 .arg("--root")
733 .arg(&tool_root_dir)
734 .arg("--target-dir")
735 .arg(tool_root_dir.join("target"))
736 .arg(format!("{pkg_name}@{version}"))
737 .env(
738 "PATH",
739 env::join_paths(
740 env::split_paths(&env::var("PATH").unwrap())
741 .chain(std::iter::once(tool_bin_dir.clone())),
742 )
743 .expect("build dir contains invalid char"),
744 );
745
746 if CiEnv::is_ci() {
750 cmd.env("RUSTFLAGS", "-Copt-level=0");
751 }
752
753 let cargo_exit_code = cmd.spawn()?.wait()?;
754 if !cargo_exit_code.success() {
755 return Err(Error::Generic("cargo install failed".to_string()));
756 }
757 assert!(
758 matches!(bin_path.try_exists(), Ok(true)),
759 "cargo install did not produce the expected binary"
760 );
761 eprintln!("finished building tool {bin_name}");
762 Ok(bin_path)
763}
764
765#[derive(Debug)]
766enum Error {
767 Io(io::Error),
768 MissingReq(&'static str, &'static str, Option<String>),
770 FailedCheck(&'static str),
772 Generic(String),
774 Version {
776 program: &'static str,
777 required: &'static str,
778 installed: String,
779 },
780}
781
782impl fmt::Display for Error {
783 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784 match self {
785 Self::MissingReq(a, b, ex) => {
786 write!(
787 f,
788 "{a} is required to run {b} but it could not be located. Is it installed?"
789 )?;
790 if let Some(s) = ex {
791 write!(f, "\n{s}")?;
792 };
793 Ok(())
794 }
795 Self::Version { program, required, installed } => write!(
796 f,
797 "insufficient version of '{program}' to run external tools: \
798 {required} required but found {installed}",
799 ),
800 Self::Generic(s) => f.write_str(s),
801 Self::Io(e) => write!(f, "IO error: {e}"),
802 Self::FailedCheck(s) => write!(f, "checks with external tool '{s}' failed"),
803 }
804 }
805}
806
807impl From<io::Error> for Error {
808 fn from(value: io::Error) -> Self {
809 Self::Io(value)
810 }
811}
812
813#[derive(Debug, PartialEq)]
814enum ExtraCheckParseError {
815 #[allow(dead_code, reason = "shown through Debug")]
816 UnknownKind(String),
817 #[allow(dead_code)]
818 UnknownLang(String),
819 UnsupportedKindForLang,
820 TooManyParts,
822 Empty,
824 AutoRequiresLang,
826 IfInstalledRequiresLang,
828}
829
830#[derive(PartialEq, Debug)]
831struct ExtraCheckArg {
832 auto: bool,
834 if_installed: bool,
836 lang: ExtraCheckLang,
837 kind: Option<ExtraCheckKind>,
839}
840
841impl ExtraCheckArg {
842 fn matches(&self, lang: ExtraCheckLang, kind: ExtraCheckKind) -> bool {
843 self.lang == lang && self.kind.map(|k| k == kind).unwrap_or(true)
844 }
845
846 fn is_non_if_installed_or_matches(&self, root_path: &Path, build_dir: &Path) -> bool {
847 if !self.if_installed {
848 return true;
849 }
850
851 match self.lang {
852 ExtraCheckLang::Spellcheck => {
853 match ensure_version(build_dir, "typos", SPELLCHECK_VER) {
854 Ok(_) => true,
855 Err(Error::Version { installed, .. }) => {
856 eprintln!(
857 "warning: the tool `typos` is detected, but version {installed} doesn't match with the expected version {SPELLCHECK_VER}"
858 );
859 false
860 }
861 _ => false,
862 }
863 }
864 ExtraCheckLang::Shell => has_shellcheck().is_ok(),
865 ExtraCheckLang::Js => {
866 match self.kind {
867 Some(ExtraCheckKind::Lint) => {
868 rustdoc_js::has_tool(build_dir, "eslint")
870 && rustdoc_js::has_tool(build_dir, "es-check")
871 }
872 Some(ExtraCheckKind::Typecheck) => {
873 rustdoc_js::has_tool(build_dir, "tsc")
875 }
876 None => {
877 rustdoc_js::has_tool(build_dir, "eslint")
879 && rustdoc_js::has_tool(build_dir, "es-check")
880 && rustdoc_js::has_tool(build_dir, "tsc")
881 }
882 Some(_) => unreachable!("js shouldn't have other type of ExtraCheckKind"),
883 }
884 }
885 ExtraCheckLang::Py | ExtraCheckLang::Cpp => {
886 let venv_path = build_dir.join("venv");
887 let mut reqs_path = root_path.to_owned();
888 reqs_path.extend(PIP_REQ_PATH);
889 let Ok(v) = has_py_tools(&venv_path, &reqs_path) else {
890 return false;
891 };
892
893 v
894 }
895 }
896 }
897
898 fn is_non_auto_or_matches(&self, filepath: &str) -> bool {
900 if !self.auto {
901 return true;
902 }
903 let exts: &[&str] = match self.lang {
904 ExtraCheckLang::Py => &[".py"],
905 ExtraCheckLang::Cpp => &[".cpp"],
906 ExtraCheckLang::Shell => &[".sh"],
907 ExtraCheckLang::Js => &[".js", ".ts"],
908 ExtraCheckLang::Spellcheck => {
909 if SPELLCHECK_DIRS.iter().any(|dir| Path::new(filepath).starts_with(dir)) {
910 return true;
911 }
912 &[]
913 }
914 };
915 exts.iter().any(|ext| filepath.ends_with(ext))
916 }
917
918 fn has_supported_kind(&self) -> bool {
919 let Some(kind) = self.kind else {
920 return true;
922 };
923 use ExtraCheckKind::*;
924 let supported_kinds: &[_] = match self.lang {
925 ExtraCheckLang::Py => &[Fmt, Lint],
926 ExtraCheckLang::Cpp => &[Fmt],
927 ExtraCheckLang::Shell => &[Lint],
928 ExtraCheckLang::Spellcheck => &[],
929 ExtraCheckLang::Js => &[Lint, Typecheck],
930 };
931 supported_kinds.contains(&kind)
932 }
933}
934
935impl FromStr for ExtraCheckArg {
936 type Err = ExtraCheckParseError;
937
938 fn from_str(s: &str) -> Result<Self, Self::Err> {
939 let mut auto = false;
940 let mut if_installed = false;
941 let mut parts = s.split(':');
942 let mut first = match parts.next() {
943 Some("") | None => return Err(ExtraCheckParseError::Empty),
944 Some(part) => part,
945 };
946
947 loop {
950 match (first, auto, if_installed) {
951 ("auto", false, _) => {
952 let Some(part) = parts.next() else {
953 return Err(ExtraCheckParseError::AutoRequiresLang);
954 };
955 auto = true;
956 first = part;
957 }
958 ("if-installed", _, false) => {
959 let Some(part) = parts.next() else {
960 return Err(ExtraCheckParseError::IfInstalledRequiresLang);
961 };
962 if_installed = true;
963 first = part;
964 }
965 _ => break,
966 }
967 }
968 let second = parts.next();
969 if parts.next().is_some() {
970 return Err(ExtraCheckParseError::TooManyParts);
971 }
972 let arg = Self {
973 auto,
974 if_installed,
975 lang: first.parse()?,
976 kind: second.map(|s| s.parse()).transpose()?,
977 };
978 if !arg.has_supported_kind() {
979 return Err(ExtraCheckParseError::UnsupportedKindForLang);
980 }
981
982 Ok(arg)
983 }
984}
985
986#[derive(PartialEq, Copy, Clone, Debug)]
987enum ExtraCheckLang {
988 Py,
989 Shell,
990 Cpp,
991 Spellcheck,
992 Js,
993}
994
995impl FromStr for ExtraCheckLang {
996 type Err = ExtraCheckParseError;
997
998 fn from_str(s: &str) -> Result<Self, Self::Err> {
999 Ok(match s {
1000 "py" => Self::Py,
1001 "shell" => Self::Shell,
1002 "cpp" => Self::Cpp,
1003 "spellcheck" => Self::Spellcheck,
1004 "js" => Self::Js,
1005 _ => return Err(ExtraCheckParseError::UnknownLang(s.to_string())),
1006 })
1007 }
1008}
1009
1010#[derive(PartialEq, Copy, Clone, Debug)]
1011enum ExtraCheckKind {
1012 Lint,
1013 Fmt,
1014 Typecheck,
1015 None,
1018}
1019
1020impl FromStr for ExtraCheckKind {
1021 type Err = ExtraCheckParseError;
1022
1023 fn from_str(s: &str) -> Result<Self, Self::Err> {
1024 Ok(match s {
1025 "lint" => Self::Lint,
1026 "fmt" => Self::Fmt,
1027 "typecheck" => Self::Typecheck,
1028 _ => return Err(ExtraCheckParseError::UnknownKind(s.to_string())),
1029 })
1030 }
1031}