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