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