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