1use std::ffi::OsStr;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23use std::str::FromStr;
24use std::{fmt, fs, io};
25
26use crate::CiInfo;
27
28mod rustdoc_js;
29
30const MIN_PY_REV: (u32, u32) = (3, 9);
31const MIN_PY_REV_STR: &str = "≥3.9";
32
33#[cfg(target_os = "windows")]
35const REL_PY_PATH: &[&str] = &["Scripts", "python3.exe"];
36#[cfg(not(target_os = "windows"))]
37const REL_PY_PATH: &[&str] = &["bin", "python3"];
38
39const RUFF_CONFIG_PATH: &[&str] = &["src", "tools", "tidy", "config", "ruff.toml"];
40const RUFF_CACHE_PATH: &[&str] = &["cache", "ruff_cache"];
42const PIP_REQ_PATH: &[&str] = &["src", "tools", "tidy", "config", "requirements.txt"];
43
44const SPELLCHECK_DIRS: &[&str] = &["compiler", "library", "src/bootstrap", "src/librustdoc"];
45
46pub fn check(
47 root_path: &Path,
48 outdir: &Path,
49 ci_info: &CiInfo,
50 librustdoc_path: &Path,
51 tools_path: &Path,
52 npm: &Path,
53 cargo: &Path,
54 bless: bool,
55 extra_checks: Option<&str>,
56 pos_args: &[String],
57 bad: &mut bool,
58) {
59 if let Err(e) = check_impl(
60 root_path,
61 outdir,
62 ci_info,
63 librustdoc_path,
64 tools_path,
65 npm,
66 cargo,
67 bless,
68 extra_checks,
69 pos_args,
70 ) {
71 tidy_error!(bad, "{e}");
72 }
73}
74
75fn check_impl(
76 root_path: &Path,
77 outdir: &Path,
78 ci_info: &CiInfo,
79 librustdoc_path: &Path,
80 tools_path: &Path,
81 npm: &Path,
82 cargo: &Path,
83 bless: bool,
84 extra_checks: Option<&str>,
85 pos_args: &[String],
86) -> Result<(), Error> {
87 let show_diff =
88 std::env::var("TIDY_PRINT_DIFF").is_ok_and(|v| v.eq_ignore_ascii_case("true") || v == "1");
89
90 let mut lint_args = match extra_checks {
92 Some(s) => s
93 .strip_prefix("--extra-checks=")
94 .unwrap()
95 .split(',')
96 .map(|s| {
97 if s == "spellcheck:fix" {
98 eprintln!("warning: `spellcheck:fix` is no longer valid, use `--extra-checks=spellcheck --bless`");
99 }
100 (ExtraCheckArg::from_str(s), s)
101 })
102 .filter_map(|(res, src)| match res {
103 Ok(arg) => {
104 Some(arg)
105 }
106 Err(err) => {
107 eprintln!("warning: bad extra check argument {src:?}: {err:?}");
109 None
110 }
111 })
112 .collect(),
113 None => vec![],
114 };
115 if lint_args.iter().any(|ck| ck.auto) {
116 crate::files_modified_batch_filter(ci_info, &mut lint_args, |ck, path| {
117 ck.is_non_auto_or_matches(path)
118 });
119 }
120
121 macro_rules! extra_check {
122 ($lang:ident, $kind:ident) => {
123 lint_args.iter().any(|arg| arg.matches(ExtraCheckLang::$lang, ExtraCheckKind::$kind))
124 };
125 }
126
127 let rerun_with_bless = |mode: &str, action: &str| {
128 if !bless {
129 eprintln!("rerun tidy with `--extra-checks={mode} --bless` to {action}");
130 }
131 };
132
133 let python_lint = extra_check!(Py, Lint);
134 let python_fmt = extra_check!(Py, Fmt);
135 let shell_lint = extra_check!(Shell, Lint);
136 let cpp_fmt = extra_check!(Cpp, Fmt);
137 let spellcheck = extra_check!(Spellcheck, None);
138 let js_lint = extra_check!(Js, Lint);
139 let js_typecheck = extra_check!(Js, Typecheck);
140
141 let mut py_path = None;
142
143 let (cfg_args, file_args): (Vec<_>, Vec<_>) = pos_args
144 .iter()
145 .map(OsStr::new)
146 .partition(|arg| arg.to_str().is_some_and(|s| s.starts_with('-')));
147
148 if python_lint || python_fmt || cpp_fmt {
149 let venv_path = outdir.join("venv");
150 let mut reqs_path = root_path.to_owned();
151 reqs_path.extend(PIP_REQ_PATH);
152 py_path = Some(get_or_create_venv(&venv_path, &reqs_path)?);
153 }
154
155 if python_lint {
156 let py_path = py_path.as_ref().unwrap();
157 let args: &[&OsStr] = if bless {
158 eprintln!("linting python files and applying suggestions");
159 &["check".as_ref(), "--fix".as_ref()]
160 } else {
161 eprintln!("linting python files");
162 &["check".as_ref()]
163 };
164
165 let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, args);
166
167 if res.is_err() && show_diff && !bless {
168 eprintln!("\npython linting failed! Printing diff suggestions:");
169
170 let diff_res = run_ruff(
171 root_path,
172 outdir,
173 py_path,
174 &cfg_args,
175 &file_args,
176 &["check".as_ref(), "--diff".as_ref()],
177 );
178 if diff_res.is_err() {
180 rerun_with_bless("py:lint", "apply ruff suggestions");
181 }
182 }
183 res?;
185 }
186
187 if python_fmt {
188 let mut args: Vec<&OsStr> = vec!["format".as_ref()];
189 if bless {
190 eprintln!("formatting python files");
191 } else {
192 eprintln!("checking python file formatting");
193 args.push("--check".as_ref());
194 }
195
196 let py_path = py_path.as_ref().unwrap();
197 let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, &args);
198
199 if res.is_err() && !bless {
200 if show_diff {
201 eprintln!("\npython formatting does not match! Printing diff:");
202
203 let _ = run_ruff(
204 root_path,
205 outdir,
206 py_path,
207 &cfg_args,
208 &file_args,
209 &["format".as_ref(), "--diff".as_ref()],
210 );
211 }
212 rerun_with_bless("py:fmt", "reformat Python code");
213 }
214
215 res?;
217 }
218
219 if cpp_fmt {
220 let mut cfg_args_clang_format = cfg_args.clone();
221 let mut file_args_clang_format = file_args.clone();
222 let config_path = root_path.join(".clang-format");
223 let config_file_arg = format!("file:{}", config_path.display());
224 cfg_args_clang_format.extend(&["--style".as_ref(), config_file_arg.as_ref()]);
225 if bless {
226 eprintln!("formatting C++ files");
227 cfg_args_clang_format.push("-i".as_ref());
228 } else {
229 eprintln!("checking C++ file formatting");
230 cfg_args_clang_format.extend(&["--dry-run".as_ref(), "--Werror".as_ref()]);
231 }
232 let files;
233 if file_args_clang_format.is_empty() {
234 let llvm_wrapper = root_path.join("compiler/rustc_llvm/llvm-wrapper");
235 files = find_with_extension(
236 root_path,
237 Some(llvm_wrapper.as_path()),
238 &[OsStr::new("h"), OsStr::new("cpp")],
239 )?;
240 file_args_clang_format.extend(files.iter().map(|p| p.as_os_str()));
241 }
242 let args = merge_args(&cfg_args_clang_format, &file_args_clang_format);
243 let res = py_runner(py_path.as_ref().unwrap(), false, None, "clang-format", &args);
244
245 if res.is_err() && show_diff && !bless {
246 eprintln!("\nclang-format linting failed! Printing diff suggestions:");
247
248 let mut cfg_args_clang_format_diff = cfg_args.clone();
249 cfg_args_clang_format_diff.extend(&["--style".as_ref(), config_file_arg.as_ref()]);
250 for file in file_args_clang_format {
251 let mut formatted = String::new();
252 let mut diff_args = cfg_args_clang_format_diff.clone();
253 diff_args.push(file);
254 let _ = py_runner(
255 py_path.as_ref().unwrap(),
256 false,
257 Some(&mut formatted),
258 "clang-format",
259 &diff_args,
260 );
261 if formatted.is_empty() {
262 eprintln!(
263 "failed to obtain the formatted content for '{}'",
264 file.to_string_lossy()
265 );
266 continue;
267 }
268 let actual = std::fs::read_to_string(file).unwrap_or_else(|e| {
269 panic!(
270 "failed to read the C++ file at '{}' due to '{e}'",
271 file.to_string_lossy()
272 )
273 });
274 if formatted != actual {
275 let diff = similar::TextDiff::from_lines(&actual, &formatted);
276 eprintln!(
277 "{}",
278 diff.unified_diff().context_radius(4).header(
279 &format!("{} (actual)", file.to_string_lossy()),
280 &format!("{} (formatted)", file.to_string_lossy())
281 )
282 );
283 }
284 }
285 rerun_with_bless("cpp:fmt", "reformat C++ code");
286 }
287 res?;
289 }
290
291 if shell_lint {
292 eprintln!("linting shell files");
293
294 let mut file_args_shc = file_args.clone();
295 let files;
296 if file_args_shc.is_empty() {
297 files = find_with_extension(root_path, None, &[OsStr::new("sh")])?;
298 file_args_shc.extend(files.iter().map(|p| p.as_os_str()));
299 }
300
301 shellcheck_runner(&merge_args(&cfg_args, &file_args_shc))?;
302 }
303
304 if spellcheck {
305 let config_path = root_path.join("typos.toml");
306 let mut args = vec!["-c", config_path.as_os_str().to_str().unwrap()];
307
308 args.extend_from_slice(SPELLCHECK_DIRS);
309
310 if bless {
311 eprintln!("spellchecking files and fixing typos");
312 args.push("--write-changes");
313 } else {
314 eprintln!("spellchecking files");
315 }
316 let res = spellcheck_runner(root_path, &outdir, &cargo, &args);
317 if res.is_err() {
318 rerun_with_bless("spellcheck", "fix typos");
319 }
320 res?;
321 }
322
323 if js_lint || js_typecheck {
324 rustdoc_js::npm_install(root_path, outdir, npm)?;
325 }
326
327 if js_lint {
328 if bless {
329 eprintln!("linting javascript files");
330 } else {
331 eprintln!("linting javascript files and applying suggestions");
332 }
333 let res = rustdoc_js::lint(outdir, librustdoc_path, tools_path, bless);
334 if res.is_err() {
335 rerun_with_bless("js:lint", "apply eslint suggestions");
336 }
337 res?;
338 rustdoc_js::es_check(outdir, librustdoc_path)?;
339 }
340
341 if js_typecheck {
342 eprintln!("typechecking javascript files");
343 rustdoc_js::typecheck(outdir, librustdoc_path)?;
344 }
345
346 Ok(())
347}
348
349fn run_ruff(
350 root_path: &Path,
351 outdir: &Path,
352 py_path: &Path,
353 cfg_args: &[&OsStr],
354 file_args: &[&OsStr],
355 ruff_args: &[&OsStr],
356) -> Result<(), Error> {
357 let mut cfg_args_ruff = cfg_args.to_vec();
358 let mut file_args_ruff = file_args.to_vec();
359
360 let mut cfg_path = root_path.to_owned();
361 cfg_path.extend(RUFF_CONFIG_PATH);
362 let mut cache_dir = outdir.to_owned();
363 cache_dir.extend(RUFF_CACHE_PATH);
364
365 cfg_args_ruff.extend([
366 "--config".as_ref(),
367 cfg_path.as_os_str(),
368 "--cache-dir".as_ref(),
369 cache_dir.as_os_str(),
370 ]);
371
372 if file_args_ruff.is_empty() {
373 file_args_ruff.push(root_path.as_os_str());
374 }
375
376 let mut args: Vec<&OsStr> = ruff_args.to_vec();
377 args.extend(merge_args(&cfg_args_ruff, &file_args_ruff));
378 py_runner(py_path, true, None, "ruff", &args)
379}
380
381fn merge_args<'a>(cfg_args: &[&'a OsStr], file_args: &[&'a OsStr]) -> Vec<&'a OsStr> {
383 let mut args = cfg_args.to_owned();
384 args.push("--".as_ref());
385 args.extend(file_args);
386 args
387}
388
389fn py_runner(
393 py_path: &Path,
394 as_module: bool,
395 stdout: Option<&mut String>,
396 bin: &'static str,
397 args: &[&OsStr],
398) -> Result<(), Error> {
399 let mut cmd = Command::new(py_path);
400 if as_module {
401 cmd.arg("-m").arg(bin).args(args);
402 } else {
403 let bin_path = py_path.with_file_name(bin);
404 cmd.arg(bin_path).args(args);
405 }
406 let status = if let Some(stdout) = stdout {
407 let output = cmd.output()?;
408 if let Ok(s) = std::str::from_utf8(&output.stdout) {
409 stdout.push_str(s);
410 }
411 output.status
412 } else {
413 cmd.status()?
414 };
415 if status.success() { Ok(()) } else { Err(Error::FailedCheck(bin)) }
416}
417
418fn get_or_create_venv(venv_path: &Path, src_reqs_path: &Path) -> Result<PathBuf, Error> {
421 let mut should_create = true;
422 let dst_reqs_path = venv_path.join("requirements.txt");
423 let mut py_path = venv_path.to_owned();
424 py_path.extend(REL_PY_PATH);
425
426 if let Ok(req) = fs::read_to_string(&dst_reqs_path) {
427 if req == fs::read_to_string(src_reqs_path)? {
428 should_create = false;
430 } else {
431 eprintln!("requirements.txt file mismatch, recreating environment");
432 }
433 }
434
435 if should_create {
436 eprintln!("removing old virtual environment");
437 if venv_path.is_dir() {
438 fs::remove_dir_all(venv_path).unwrap_or_else(|_| {
439 panic!("failed to remove directory at {}", venv_path.display())
440 });
441 }
442 create_venv_at_path(venv_path)?;
443 install_requirements(&py_path, src_reqs_path, &dst_reqs_path)?;
444 }
445
446 verify_py_version(&py_path)?;
447 Ok(py_path)
448}
449
450fn create_venv_at_path(path: &Path) -> Result<(), Error> {
453 const TRY_PY: &[&str] = &[
456 "python3.13",
457 "python3.12",
458 "python3.11",
459 "python3.10",
460 "python3.9",
461 "python3",
462 "python",
463 "python3.14",
464 ];
465
466 let mut sys_py = None;
467 let mut found = Vec::new();
468
469 for py in TRY_PY {
470 match verify_py_version(Path::new(py)) {
471 Ok(_) => {
472 sys_py = Some(*py);
473 break;
474 }
475 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => (),
477 Err(Error::Version { installed, .. }) => found.push(installed),
479 Err(e) => eprintln!("note: error running '{py}': {e}"),
481 }
482 }
483
484 let Some(sys_py) = sys_py else {
485 let ret = if found.is_empty() {
486 Error::MissingReq("python3", "python file checks", None)
487 } else {
488 found.sort();
489 found.dedup();
490 Error::Version {
491 program: "python3",
492 required: MIN_PY_REV_STR,
493 installed: found.join(", "),
494 }
495 };
496 return Err(ret);
497 };
498
499 if try_create_venv(sys_py, path, "venv").is_ok() {
503 return Ok(());
504 }
505 try_create_venv(sys_py, path, "virtualenv")
506}
507
508fn try_create_venv(python: &str, path: &Path, module: &str) -> Result<(), Error> {
509 eprintln!(
510 "creating virtual environment at '{}' using '{python}' and '{module}'",
511 path.display()
512 );
513 let out = Command::new(python).args(["-m", module]).arg(path).output().unwrap();
514
515 if out.status.success() {
516 return Ok(());
517 }
518
519 let stderr = String::from_utf8_lossy(&out.stderr);
520 let err = if stderr.contains(&format!("No module named {module}")) {
521 Error::Generic(format!(
522 r#"{module} not found: you may need to install it:
523`{python} -m pip install {module}`
524If you see an error about "externally managed environment" when running the above command,
525either install `{module}` using your system package manager
526(e.g. `sudo apt-get install {python}-{module}`) or create a virtual environment manually, install
527`{module}` in it and then activate it before running tidy.
528"#
529 ))
530 } else {
531 Error::Generic(format!(
532 "failed to create venv at '{}' using {python} -m {module}: {stderr}",
533 path.display()
534 ))
535 };
536 Err(err)
537}
538
539fn verify_py_version(py_path: &Path) -> Result<(), Error> {
542 let out = Command::new(py_path).arg("--version").output()?;
543 let outstr = String::from_utf8_lossy(&out.stdout);
544 let vers = outstr.trim().split_ascii_whitespace().nth(1).unwrap().trim();
545 let mut vers_comps = vers.split('.');
546 let major: u32 = vers_comps.next().unwrap().parse().unwrap();
547 let minor: u32 = vers_comps.next().unwrap().parse().unwrap();
548
549 if (major, minor) < MIN_PY_REV {
550 Err(Error::Version {
551 program: "python",
552 required: MIN_PY_REV_STR,
553 installed: vers.to_owned(),
554 })
555 } else {
556 Ok(())
557 }
558}
559
560fn install_requirements(
561 py_path: &Path,
562 src_reqs_path: &Path,
563 dst_reqs_path: &Path,
564) -> Result<(), Error> {
565 let stat = Command::new(py_path)
566 .args(["-m", "pip", "install", "--upgrade", "pip"])
567 .status()
568 .expect("failed to launch pip");
569 if !stat.success() {
570 return Err(Error::Generic(format!("pip install failed with status {stat}")));
571 }
572
573 let stat = Command::new(py_path)
574 .args(["-m", "pip", "install", "--quiet", "--require-hashes", "-r"])
575 .arg(src_reqs_path)
576 .status()?;
577 if !stat.success() {
578 return Err(Error::Generic(format!(
579 "failed to install requirements at {}",
580 src_reqs_path.display()
581 )));
582 }
583 fs::copy(src_reqs_path, dst_reqs_path)?;
584 assert_eq!(
585 fs::read_to_string(src_reqs_path).unwrap(),
586 fs::read_to_string(dst_reqs_path).unwrap()
587 );
588 Ok(())
589}
590
591fn shellcheck_runner(args: &[&OsStr]) -> Result<(), Error> {
593 match Command::new("shellcheck").arg("--version").status() {
594 Ok(_) => (),
595 Err(e) if e.kind() == io::ErrorKind::NotFound => {
596 return Err(Error::MissingReq(
597 "shellcheck",
598 "shell file checks",
599 Some(
600 "see <https://github.com/koalaman/shellcheck#installing> \
601 for installation instructions"
602 .to_owned(),
603 ),
604 ));
605 }
606 Err(e) => return Err(e.into()),
607 }
608
609 let status = Command::new("shellcheck").args(args).status()?;
610 if status.success() { Ok(()) } else { Err(Error::FailedCheck("shellcheck")) }
611}
612
613fn spellcheck_runner(
615 src_root: &Path,
616 outdir: &Path,
617 cargo: &Path,
618 args: &[&str],
619) -> Result<(), Error> {
620 let bin_path =
621 crate::ensure_version_or_cargo_install(outdir, cargo, "typos-cli", "typos", "1.34.0")?;
622 match Command::new(bin_path).current_dir(src_root).args(args).status() {
623 Ok(status) => {
624 if status.success() {
625 Ok(())
626 } else {
627 Err(Error::FailedCheck("typos"))
628 }
629 }
630 Err(err) => Err(Error::Generic(format!("failed to run typos tool: {err:?}"))),
631 }
632}
633
634fn find_with_extension(
636 root_path: &Path,
637 find_dir: Option<&Path>,
638 extensions: &[&OsStr],
639) -> Result<Vec<PathBuf>, Error> {
640 let stat_output =
643 Command::new("git").arg("-C").arg(root_path).args(["status", "--short"]).output()?.stdout;
644
645 if String::from_utf8_lossy(&stat_output).lines().filter(|ln| ln.starts_with('?')).count() > 0 {
646 eprintln!("found untracked files, ignoring");
647 }
648
649 let mut output = Vec::new();
650 let binding = {
651 let mut command = Command::new("git");
652 command.arg("-C").arg(root_path).args(["ls-files"]);
653 if let Some(find_dir) = find_dir {
654 command.arg(find_dir);
655 }
656 command.output()?
657 };
658 let tracked = String::from_utf8_lossy(&binding.stdout);
659
660 for line in tracked.lines() {
661 let line = line.trim();
662 let path = Path::new(line);
663
664 let Some(ref extension) = path.extension() else {
665 continue;
666 };
667 if extensions.contains(extension) {
668 output.push(root_path.join(path));
669 }
670 }
671
672 Ok(output)
673}
674
675#[derive(Debug)]
676enum Error {
677 Io(io::Error),
678 MissingReq(&'static str, &'static str, Option<String>),
680 FailedCheck(&'static str),
682 Generic(String),
684 Version {
686 program: &'static str,
687 required: &'static str,
688 installed: String,
689 },
690}
691
692impl fmt::Display for Error {
693 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694 match self {
695 Self::MissingReq(a, b, ex) => {
696 write!(
697 f,
698 "{a} is required to run {b} but it could not be located. Is it installed?"
699 )?;
700 if let Some(s) = ex {
701 write!(f, "\n{s}")?;
702 };
703 Ok(())
704 }
705 Self::Version { program, required, installed } => write!(
706 f,
707 "insufficient version of '{program}' to run external tools: \
708 {required} required but found {installed}",
709 ),
710 Self::Generic(s) => f.write_str(s),
711 Self::Io(e) => write!(f, "IO error: {e}"),
712 Self::FailedCheck(s) => write!(f, "checks with external tool '{s}' failed"),
713 }
714 }
715}
716
717impl From<io::Error> for Error {
718 fn from(value: io::Error) -> Self {
719 Self::Io(value)
720 }
721}
722
723#[derive(Debug)]
724enum ExtraCheckParseError {
725 #[allow(dead_code, reason = "shown through Debug")]
726 UnknownKind(String),
727 #[allow(dead_code)]
728 UnknownLang(String),
729 UnsupportedKindForLang,
730 TooManyParts,
732 Empty,
734 AutoRequiresLang,
736}
737
738struct ExtraCheckArg {
739 auto: bool,
740 lang: ExtraCheckLang,
741 kind: Option<ExtraCheckKind>,
743}
744
745impl ExtraCheckArg {
746 fn matches(&self, lang: ExtraCheckLang, kind: ExtraCheckKind) -> bool {
747 self.lang == lang && self.kind.map(|k| k == kind).unwrap_or(true)
748 }
749
750 fn is_non_auto_or_matches(&self, filepath: &str) -> bool {
752 if !self.auto {
753 return true;
754 }
755 let exts: &[&str] = match self.lang {
756 ExtraCheckLang::Py => &[".py"],
757 ExtraCheckLang::Cpp => &[".cpp"],
758 ExtraCheckLang::Shell => &[".sh"],
759 ExtraCheckLang::Js => &[".js", ".ts"],
760 ExtraCheckLang::Spellcheck => {
761 if SPELLCHECK_DIRS.iter().any(|dir| Path::new(filepath).starts_with(dir)) {
762 return true;
763 }
764 &[]
765 }
766 };
767 exts.iter().any(|ext| filepath.ends_with(ext))
768 }
769
770 fn has_supported_kind(&self) -> bool {
771 let Some(kind) = self.kind else {
772 return true;
774 };
775 use ExtraCheckKind::*;
776 let supported_kinds: &[_] = match self.lang {
777 ExtraCheckLang::Py => &[Fmt, Lint],
778 ExtraCheckLang::Cpp => &[Fmt],
779 ExtraCheckLang::Shell => &[Lint],
780 ExtraCheckLang::Spellcheck => &[],
781 ExtraCheckLang::Js => &[Lint, Typecheck],
782 };
783 supported_kinds.contains(&kind)
784 }
785}
786
787impl FromStr for ExtraCheckArg {
788 type Err = ExtraCheckParseError;
789
790 fn from_str(s: &str) -> Result<Self, Self::Err> {
791 let mut auto = false;
792 let mut parts = s.split(':');
793 let Some(mut first) = parts.next() else {
794 return Err(ExtraCheckParseError::Empty);
795 };
796 if first == "auto" {
797 let Some(part) = parts.next() else {
798 return Err(ExtraCheckParseError::AutoRequiresLang);
799 };
800 auto = true;
801 first = part;
802 }
803 let second = parts.next();
804 if parts.next().is_some() {
805 return Err(ExtraCheckParseError::TooManyParts);
806 }
807 let arg = Self { auto, lang: first.parse()?, kind: second.map(|s| s.parse()).transpose()? };
808 if !arg.has_supported_kind() {
809 return Err(ExtraCheckParseError::UnsupportedKindForLang);
810 }
811
812 Ok(arg)
813 }
814}
815
816#[derive(PartialEq, Copy, Clone)]
817enum ExtraCheckLang {
818 Py,
819 Shell,
820 Cpp,
821 Spellcheck,
822 Js,
823}
824
825impl FromStr for ExtraCheckLang {
826 type Err = ExtraCheckParseError;
827
828 fn from_str(s: &str) -> Result<Self, Self::Err> {
829 Ok(match s {
830 "py" => Self::Py,
831 "shell" => Self::Shell,
832 "cpp" => Self::Cpp,
833 "spellcheck" => Self::Spellcheck,
834 "js" => Self::Js,
835 _ => return Err(ExtraCheckParseError::UnknownLang(s.to_string())),
836 })
837 }
838}
839
840#[derive(PartialEq, Copy, Clone)]
841enum ExtraCheckKind {
842 Lint,
843 Fmt,
844 Typecheck,
845 None,
848}
849
850impl FromStr for ExtraCheckKind {
851 type Err = ExtraCheckParseError;
852
853 fn from_str(s: &str) -> Result<Self, Self::Err> {
854 Ok(match s {
855 "lint" => Self::Lint,
856 "fmt" => Self::Fmt,
857 "typecheck" => Self::Typecheck,
858 _ => return Err(ExtraCheckParseError::UnknownKind(s.to_string())),
859 })
860 }
861}