src/tools/tidy/src/extra_checks/mod.rs RUST 1,146 lines View on github.com → Search inside
1//! Optional checks for file types other than Rust source2//!3//! Handles python tool version management via a virtual environment in4//! `build/venv`.5//!6//! # Functional outline7//!8//! 1. Run tidy with an extra option: `--extra-checks=py,shell`,9//!    `--extra-checks=py:lint`, or similar. Optionally provide specific10//!    configuration after a double dash (`--extra-checks=py -- foo.py`)11//! 2. Build configuration based on args/environment:12//!    - Formatters by default are in check only mode13//!    - If in CI (TIDY_PRINT_DIFF=1 is set), check and print the diff14//!    - If `--bless` is provided, formatters may run15//!    - Pass any additional config after the `--`. If no files are specified,16//!      use a default.17//! 3. Print the output of the given command. If it fails and `TIDY_PRINT_DIFF`18//!    is set, rerun the tool to print a suggestion diff (for e.g. CI)1920use std::ffi::{OsStr, OsString};21use std::path::{Path, PathBuf};22use std::process::Command;23use std::str::FromStr;24use std::{env, fmt, fs, io};2526use crate::diagnostics::TidyCtx;2728mod rustdoc_js;2930#[cfg(test)]31mod tests;3233const MIN_PY_REV: (u32, u32) = (3, 9);34const MIN_PY_REV_STR: &str = "≥3.9";3536/// Path to find the python executable within a virtual environment37#[cfg(target_os = "windows")]38const REL_PY_PATH: &[&str] = &["Scripts", "python.exe"];39#[cfg(not(target_os = "windows"))]40const REL_PY_PATH: &[&str] = &["bin", "python3"];4142const RUFF_CONFIG_PATH: &[&str] = &["src", "tools", "tidy", "config", "ruff.toml"];43/// Location within build directory44const RUFF_CACHE_PATH: &[&str] = &["cache", "ruff_cache"];45const PIP_REQ_PATH: &[&str] = &["src", "tools", "tidy", "config", "requirements.txt"];4647const SPELLCHECK_DIRS: &[&str] = &["compiler", "library", "src/bootstrap", "src/librustdoc"];48const SPELLCHECK_VER: &str = "1.38.1";4950pub fn check(51    root_path: &Path,52    outdir: &Path,53    librustdoc_path: &Path,54    tools_path: &Path,55    npm: &Path,56    cargo: &Path,57    extra_checks: Option<Vec<String>>,58    pos_args: Vec<String>,59    tidy_ctx: TidyCtx,60) {61    // Split comma-separated args up62    let mut lint_args = match extra_checks {63        Some(s) => s64            .iter()65            .map(|s| {66                if s == "spellcheck:fix" {67                    eprintln!("warning: `spellcheck:fix` is no longer valid, use `--extra-checks=spellcheck --bless`");68                }69                (ExtraCheckArg::from_str(s), s)70            })71            .filter_map(|(res, src)| match res {72                Ok(arg) => {73                    Some(arg)74                }75                Err(err) => {76                    // only warn because before bad extra checks would be silently ignored.77                    eprintln!("warning: bad extra check argument {src:?}: {err:?}");78                    None79                }80            })81            .collect(),82        None => vec![],83    };84    lint_args.retain(|ck| ck.is_non_if_installed_or_matches(root_path, outdir));85    if lint_args.iter().any(|ck| ck.auto) {86        crate::files_modified_batch_filter(87            &tidy_ctx.base_commit,88            tidy_ctx.is_running_on_ci(),89            &mut lint_args,90            |ck, path| ck.is_non_auto_or_matches(path),91        );92    }9394    macro_rules! extra_check {95        ($lang:ident, $kind:ident) => {96            lint_args.iter().any(|arg| arg.matches(ExtraCheckLang::$lang, ExtraCheckKind::$kind))97        };98    }99100    let python_lint = extra_check!(Py, Lint);101    let python_fmt = extra_check!(Py, Fmt);102    let shell_lint = extra_check!(Shell, Lint);103    let cpp_fmt = extra_check!(Cpp, Fmt);104    let spellcheck = extra_check!(Spellcheck, None);105    let js_lint = extra_check!(Js, Lint);106    let js_typecheck = extra_check!(Js, Typecheck);107108    let mut py_path = None;109110    let (cfg_args, file_args): (Vec<_>, Vec<_>) = pos_args111        .iter()112        .map(OsStr::new)113        .partition(|arg| arg.to_str().is_some_and(|s| s.starts_with('-')));114115    if python_lint || python_fmt || cpp_fmt {116        // Since python lint, format and cpp format share python env, we need to ensure python env is installed before running those checks.117        let p = py_prepare(root_path, outdir, &tidy_ctx);118        if p.is_none() {119            return;120        }121        py_path = p;122    }123124    if python_lint {125        check_python_lint(126            root_path,127            outdir,128            &cfg_args,129            &file_args,130            py_path.as_ref().unwrap(),131            &tidy_ctx,132        );133    }134135    if python_fmt {136        check_python_fmt(137            root_path,138            outdir,139            &cfg_args,140            &file_args,141            py_path.as_ref().unwrap(),142            &tidy_ctx,143        );144    }145146    if cpp_fmt {147        check_cpp_fmt(root_path, &cfg_args, &file_args, py_path.as_ref().unwrap(), &tidy_ctx);148    }149150    if shell_lint {151        check_shell_lint(root_path, &cfg_args, &file_args, &tidy_ctx);152    }153154    if spellcheck {155        check_spellcheck(root_path, outdir, cargo, &tidy_ctx);156    }157158    if js_lint || js_typecheck {159        // Since js lint and format share node env, we need to ensure node env is installed before running those checks.160        if js_prepare(root_path, outdir, npm, &tidy_ctx).is_none() {161            return;162        }163    }164165    if js_lint {166        check_js_lint(outdir, librustdoc_path, tools_path, &tidy_ctx);167    }168169    if js_typecheck {170        check_js_typecheck(outdir, librustdoc_path, &tidy_ctx);171    }172}173174fn py_prepare(root_path: &Path, outdir: &Path, tidy_ctx: &TidyCtx) -> Option<PathBuf> {175    let mut check = tidy_ctx.start_check("extra_checks:py_prepare");176177    let venv_path = outdir.join("venv");178    let mut reqs_path = root_path.to_owned();179    reqs_path.extend(PIP_REQ_PATH);180181    match get_or_create_venv(&venv_path, &reqs_path) {182        Ok(p) => Some(p),183        Err(e) => {184            check.error(e);185            None186        }187    }188}189190fn js_prepare(root_path: &Path, outdir: &Path, npm: &Path, tidy_ctx: &TidyCtx) -> Option<()> {191    let mut check = tidy_ctx.start_check("extra_checks:js_prepare");192193    if let Err(e) = rustdoc_js::npm_install(root_path, outdir, npm) {194        check.error(e.to_string());195        return None;196    }197198    Some(())199}200201fn show_bless_help(mode: &str, action: &str, bless: bool) {202    if !bless {203        eprintln!("rerun tidy with `--extra-checks={mode} --bless` to {action}");204    }205}206207fn show_diff() -> bool {208    std::env::var("TIDY_PRINT_DIFF").is_ok_and(|v| v.eq_ignore_ascii_case("true") || v == "1")209}210211fn check_spellcheck(root_path: &Path, outdir: &Path, cargo: &Path, tidy_ctx: &TidyCtx) {212    let mut check = tidy_ctx.start_check("extra_checks:spellcheck");213214    let bless = tidy_ctx.is_bless_enabled();215216    let config_path = root_path.join("typos.toml");217    let mut args = vec!["-c", config_path.as_os_str().to_str().unwrap()];218    args.extend_from_slice(SPELLCHECK_DIRS);219220    if bless {221        eprintln!("spellchecking files and fixing typos");222        args.push("--write-changes");223    } else {224        eprintln!("spellchecking files");225    }226227    if let Err(e) =228        spellcheck_runner(root_path, &outdir, &cargo, &args, tidy_ctx.is_running_on_ci())229    {230        show_bless_help("spellcheck", "fix typos", bless);231        check.error(e);232    }233}234235fn check_js_lint(outdir: &Path, librustdoc_path: &Path, tools_path: &Path, tidy_ctx: &TidyCtx) {236    let mut check = tidy_ctx.start_check("extra_checks:js_lint");237238    let bless = tidy_ctx.is_bless_enabled();239240    if bless {241        eprintln!("linting javascript files and applying suggestions");242    } else {243        eprintln!("linting javascript files");244    }245246    if let Err(e) = rustdoc_js::lint(outdir, librustdoc_path, tools_path, bless) {247        show_bless_help("js:lint", "apply esplint suggestion", bless);248        check.error(e);249        return;250    }251252    if let Err(e) = rustdoc_js::es_check(outdir, librustdoc_path) {253        check.error(e);254    }255}256257fn check_js_typecheck(outdir: &Path, librustdoc_path: &Path, tidy_ctx: &TidyCtx) {258    let mut check = tidy_ctx.start_check("extra_checks:js_typecheck");259260    eprintln!("typechecking javascript files");261    if let Err(e) = rustdoc_js::typecheck(outdir, librustdoc_path) {262        check.error(e);263    }264}265266fn check_shell_lint(267    root_path: &Path,268    cfg_args: &Vec<&OsStr>,269    file_args: &Vec<&OsStr>,270    tidy_ctx: &TidyCtx,271) {272    let mut check = tidy_ctx.start_check("extra_checks:shell_lint");273274    eprintln!("linting shell files");275276    let mut file_args_shc = file_args.clone();277    let files;278    if file_args.is_empty() {279        match find_with_extension(root_path, None, &[OsStr::new("sh")]) {280            Ok(f) => files = f,281            Err(e) => {282                check.error(e);283                return;284            }285        }286287        file_args_shc.extend(files.iter().map(|p| p.as_os_str()));288    }289290    if let Err(e) = shellcheck_runner(&merge_args(&cfg_args, &file_args_shc)) {291        check.error(e);292    }293}294295fn check_python_lint(296    root_path: &Path,297    outdir: &Path,298    cfg_args: &Vec<&OsStr>,299    file_args: &Vec<&OsStr>,300    py_path: &Path,301    tidy_ctx: &TidyCtx,302) {303    let mut check = tidy_ctx.start_check("extra_checks:python_lint");304305    let bless = tidy_ctx.is_bless_enabled();306307    let args: &[&OsStr] = if bless {308        eprintln!("linting python files and applying suggestions");309        &["check".as_ref(), "--fix".as_ref()]310    } else {311        eprintln!("linting python files");312        &["check".as_ref()]313    };314315    let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, args);316317    if res.is_err() && !bless && show_diff() {318        eprintln!("\npython linting failed! Printing diff suggestions:");319320        let diff_res = run_ruff(321            root_path,322            outdir,323            py_path,324            &cfg_args,325            &file_args,326            &["check".as_ref(), "--diff".as_ref()],327        );328        // `ruff check --diff` will return status 0 if there are no suggestions.329        if diff_res.is_err() {330            show_bless_help("py:lint", "apply ruff suggestions", bless);331        }332    }333    if let Err(e) = res {334        check.error(e);335    }336}337338fn check_python_fmt(339    root_path: &Path,340    outdir: &Path,341    cfg_args: &Vec<&OsStr>,342    file_args: &Vec<&OsStr>,343    py_path: &Path,344    tidy_ctx: &TidyCtx,345) {346    let mut check = tidy_ctx.start_check("extra_checks:python_fmt");347348    let bless = tidy_ctx.is_bless_enabled();349350    let mut args: Vec<&OsStr> = vec!["format".as_ref()];351    if bless {352        eprintln!("formatting python files");353    } else {354        eprintln!("checking python file formatting");355        args.push("--check".as_ref());356    }357358    let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, &args);359360    if res.is_err() && !bless {361        if show_diff() {362            eprintln!("\npython formatting does not match! Printing diff:");363364            let _ = run_ruff(365                root_path,366                outdir,367                py_path,368                &cfg_args,369                &file_args,370                &["format".as_ref(), "--diff".as_ref()],371            );372        }373        show_bless_help("py:fmt", "reformat Python code", bless);374    }375376    if let Err(e) = res {377        check.error(e);378    }379}380381fn check_cpp_fmt(382    root_path: &Path,383    cfg_args: &Vec<&OsStr>,384    file_args: &Vec<&OsStr>,385    py_path: &Path,386    tidy_ctx: &TidyCtx,387) {388    let mut check = tidy_ctx.start_check("extra_checks:cpp_fmt");389390    let bless = tidy_ctx.is_bless_enabled();391392    let mut cfg_args_clang_format = cfg_args.clone();393    let mut file_args_clang_format = file_args.clone();394    let config_path = root_path.join(".clang-format");395    let mut config_file_arg = OsString::from("file:");396    config_file_arg.push(&config_path);397    cfg_args_clang_format.extend(&["--style".as_ref(), config_file_arg.as_ref()]);398    if bless {399        eprintln!("formatting C++ files");400        cfg_args_clang_format.push("-i".as_ref());401    } else {402        eprintln!("checking C++ file formatting");403        cfg_args_clang_format.extend(&["--dry-run".as_ref(), "--Werror".as_ref()]);404    }405    let files;406    if file_args_clang_format.is_empty() {407        let llvm_wrapper = root_path.join("compiler/rustc_llvm/llvm-wrapper");408        match find_with_extension(409            root_path,410            Some(llvm_wrapper.as_path()),411            &[OsStr::new("h"), OsStr::new("cpp")],412        ) {413            Ok(f) => files = f,414            Err(e) => {415                check.error(e);416                return;417            }418        }419        file_args_clang_format.extend(files.iter().map(|p| p.as_os_str()));420    }421    let args = merge_args(&cfg_args_clang_format, &file_args_clang_format);422    let res = py_runner(py_path, false, None, "clang-format", &args);423424    if res.is_err() && !bless && show_diff() {425        eprintln!("\nclang-format linting failed! Printing diff suggestions:");426427        let mut cfg_args_diff = cfg_args.clone();428        cfg_args_diff.extend(&["--style".as_ref(), config_file_arg.as_ref()]);429        for file in file_args {430            let mut formatted = String::new();431            let mut diff_args = cfg_args_diff.clone();432            diff_args.push(file);433            let _ = py_runner(py_path, false, Some(&mut formatted), "clang-format", &diff_args);434            if formatted.is_empty() {435                eprintln!(436                    "failed to obtain the formatted content for '{}'",437                    file.to_string_lossy()438                );439                continue;440            }441            let actual = std::fs::read_to_string(file).unwrap_or_else(|e| {442                panic!("failed to read the C++ file at '{}' due to '{e}'", file.to_string_lossy())443            });444            if formatted != actual {445                let diff = similar::TextDiff::from_lines(&actual, &formatted);446                eprintln!(447                    "{}",448                    diff.unified_diff().context_radius(4).header(449                        &format!("{} (actual)", file.to_string_lossy()),450                        &format!("{} (formatted)", file.to_string_lossy())451                    )452                );453            }454        }455        show_bless_help("cpp:fmt", "reformat C++ code", bless);456    }457458    if let Err(e) = res {459        check.error(e);460    }461}462463fn run_ruff(464    root_path: &Path,465    outdir: &Path,466    py_path: &Path,467    cfg_args: &[&OsStr],468    file_args: &[&OsStr],469    ruff_args: &[&OsStr],470) -> Result<(), Error> {471    let mut cfg_args_ruff = cfg_args.to_vec();472    let mut file_args_ruff = file_args.to_vec();473474    let mut cfg_path = root_path.to_owned();475    cfg_path.extend(RUFF_CONFIG_PATH);476    let mut cache_dir = outdir.to_owned();477    cache_dir.extend(RUFF_CACHE_PATH);478479    cfg_args_ruff.extend([480        "--config".as_ref(),481        cfg_path.as_os_str(),482        "--cache-dir".as_ref(),483        cache_dir.as_os_str(),484    ]);485486    if file_args_ruff.is_empty() {487        file_args_ruff.push(root_path.as_os_str());488    }489490    let mut args: Vec<&OsStr> = ruff_args.to_vec();491    args.extend(merge_args(&cfg_args_ruff, &file_args_ruff));492    py_runner(py_path, true, None, "ruff", &args)493}494495/// Helper to create `cfg1 cfg2 -- file1 file2` output496fn merge_args<'a>(cfg_args: &[&'a OsStr], file_args: &[&'a OsStr]) -> Vec<&'a OsStr> {497    let mut args = cfg_args.to_owned();498    args.push("--".as_ref());499    args.extend(file_args);500    args501}502503/// Run a python command with given arguments. `py_path` should be a virtualenv.504///505/// Captures `stdout` to a string if provided, otherwise prints the output.506fn py_runner(507    py_path: &Path,508    as_module: bool,509    stdout: Option<&mut String>,510    bin: &'static str,511    args: &[&OsStr],512) -> Result<(), Error> {513    let mut cmd = Command::new(py_path);514    if as_module {515        cmd.arg("-m").arg(bin).args(args);516    } else {517        let bin_path = py_path.with_file_name(bin);518        cmd.arg(bin_path).args(args);519    }520    let status = if let Some(stdout) = stdout {521        let output = cmd.output()?;522        if let Ok(s) = std::str::from_utf8(&output.stdout) {523            stdout.push_str(s);524        }525        output.status526    } else {527        cmd.status()?528    };529    if status.success() { Ok(()) } else { Err(Error::FailedCheck(bin)) }530}531532/// Create a virtuaenv at a given path if it doesn't already exist, or validate533/// the install if it does. Returns the path to that venv's python executable.534fn get_or_create_venv(venv_path: &Path, src_reqs_path: &Path) -> Result<PathBuf, Error> {535    let mut py_path = venv_path.to_owned();536    py_path.extend(REL_PY_PATH);537538    if !has_py_tools(venv_path, src_reqs_path)? {539        let dst_reqs_path = venv_path.join("requirements.txt");540        eprintln!("removing old virtual environment");541        if venv_path.is_dir() {542            fs::remove_dir_all(venv_path).unwrap_or_else(|_| {543                panic!("failed to remove directory at {}", venv_path.display())544            });545        }546        create_venv_at_path(venv_path)?;547        install_requirements(&py_path, src_reqs_path, &dst_reqs_path)?;548    }549550    verify_py_version(&py_path)?;551    Ok(py_path)552}553554fn has_py_tools(venv_path: &Path, src_reqs_path: &Path) -> Result<bool, Error> {555    let dst_reqs_path = venv_path.join("requirements.txt");556    if let Ok(req) = fs::read_to_string(&dst_reqs_path) {557        if req == fs::read_to_string(src_reqs_path)? {558            return Ok(true);559        }560        eprintln!("requirements.txt file mismatch");561    }562563    Ok(false)564}565566/// Attempt to create a virtualenv at this path. Cycles through all expected567/// valid python versions to find one that is installed.568fn create_venv_at_path(path: &Path) -> Result<(), Error> {569    /// Preferred python versions in order. Newest to oldest then current570    /// development versions571    const TRY_PY: &[&str] = &[572        "python3.13",573        "python3.12",574        "python3.11",575        "python3.10",576        "python3.9",577        "python3",578        "python",579        "python3.14",580    ];581582    let mut sys_py = None;583    let mut found = Vec::new();584585    for py in TRY_PY {586        match verify_py_version(Path::new(py)) {587            Ok(_) => {588                sys_py = Some(*py);589                break;590            }591            // Skip not found errors592            Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => (),593            // Skip insufficient version errors594            Err(Error::Version { installed, .. }) => found.push(installed),595            // just log and skip unrecognized errors596            Err(e) => eprintln!("note: error running '{py}': {e}"),597        }598    }599600    let Some(sys_py) = sys_py else {601        let ret = if found.is_empty() {602            Error::MissingReq("python3", "python file checks", None)603        } else {604            found.sort();605            found.dedup();606            Error::Version {607                program: "python3",608                required: MIN_PY_REV_STR,609                installed: found.join(", "),610            }611        };612        return Err(ret);613    };614615    // First try venv, which should be packaged in the Python3 standard library.616    // If it is not available, try to create the virtual environment using the617    // virtualenv package.618    if try_create_venv(sys_py, path, "venv").is_ok() {619        return Ok(());620    }621    try_create_venv(sys_py, path, "virtualenv")622}623624fn try_create_venv(python: &str, path: &Path, module: &str) -> Result<(), Error> {625    eprintln!(626        "creating virtual environment at '{}' using '{python}' and '{module}'",627        path.display()628    );629    let out = Command::new(python).args(["-m", module]).arg(path).output().unwrap();630631    if out.status.success() {632        return Ok(());633    }634635    let stderr = String::from_utf8_lossy(&out.stderr);636    let err = if stderr.contains(&format!("No module named {module}")) {637        Error::Generic(format!(638            r#"{module} not found: you may need to install it:639`{python} -m pip install {module}`640If you see an error about "externally managed environment" when running the above command,641either install `{module}` using your system package manager642(e.g. `sudo apt-get install {python}-{module}`) or create a virtual environment manually, install643`{module}` in it and then activate it before running tidy.644"#645        ))646    } else {647        Error::Generic(format!(648            "failed to create venv at '{}' using {python} -m {module}: {stderr}",649            path.display()650        ))651    };652    Err(err)653}654655/// Parse python's version output (`Python x.y.z`) and ensure we have a656/// suitable version.657fn verify_py_version(py_path: &Path) -> Result<(), Error> {658    let out = Command::new(py_path).arg("--version").output()?;659    let outstr = String::from_utf8_lossy(&out.stdout);660    let vers = outstr.trim().split_ascii_whitespace().nth(1).unwrap().trim();661    let mut vers_comps = vers.split('.');662    let major: u32 = vers_comps.next().unwrap().parse().unwrap();663    let minor: u32 = vers_comps.next().unwrap().parse().unwrap();664665    if (major, minor) < MIN_PY_REV {666        Err(Error::Version {667            program: "python",668            required: MIN_PY_REV_STR,669            installed: vers.to_owned(),670        })671    } else {672        Ok(())673    }674}675676fn install_requirements(677    py_path: &Path,678    src_reqs_path: &Path,679    dst_reqs_path: &Path,680) -> Result<(), Error> {681    let stat = Command::new(py_path)682        .args(["-m", "pip", "install", "--upgrade", "pip"])683        .status()684        .expect("failed to launch pip");685    if !stat.success() {686        return Err(Error::Generic(format!("pip install failed with status {stat}")));687    }688689    let stat = Command::new(py_path)690        .args(["-m", "pip", "install", "--quiet", "--require-hashes", "-r"])691        .arg(src_reqs_path)692        .status()?;693    if !stat.success() {694        return Err(Error::Generic(format!(695            "failed to install requirements at {}",696            src_reqs_path.display()697        )));698    }699    fs::copy(src_reqs_path, dst_reqs_path)?;700    assert_eq!(701        fs::read_to_string(src_reqs_path).unwrap(),702        fs::read_to_string(dst_reqs_path).unwrap()703    );704    Ok(())705}706707/// Returns `Ok` if shellcheck is installed, `Err` otherwise.708fn has_shellcheck() -> Result<(), Error> {709    match Command::new("shellcheck").arg("--version").status() {710        Ok(_) => Ok(()),711        Err(e) if e.kind() == io::ErrorKind::NotFound => Err(Error::MissingReq(712            "shellcheck",713            "shell file checks",714            Some(715                "see <https://github.com/koalaman/shellcheck#installing> \716                for installation instructions"717                    .to_owned(),718            ),719        )),720        Err(e) => Err(e.into()),721    }722}723724/// Check that shellcheck is installed then run it at the given path725fn shellcheck_runner(args: &[&OsStr]) -> Result<(), Error> {726    has_shellcheck()?;727728    let status = Command::new("shellcheck").args(args).status()?;729    if status.success() { Ok(()) } else { Err(Error::FailedCheck("shellcheck")) }730}731732/// Ensure that spellchecker is installed then run it at the given path733fn spellcheck_runner(734    src_root: &Path,735    outdir: &Path,736    cargo: &Path,737    args: &[&str],738    is_ci: bool,739) -> Result<(), Error> {740    let bin_path = ensure_version_or_cargo_install(741        outdir,742        cargo,743        "typos-cli",744        "typos",745        SPELLCHECK_VER,746        is_ci,747    )?;748    match Command::new(bin_path).current_dir(src_root).args(args).status() {749        Ok(status) => {750            if status.success() {751                Ok(())752            } else {753                Err(Error::FailedCheck("typos"))754            }755        }756        Err(err) => Err(Error::Generic(format!("failed to run typos tool: {err:?}"))),757    }758}759760/// Check git for tracked files matching an extension761fn find_with_extension(762    root_path: &Path,763    find_dir: Option<&Path>,764    extensions: &[&OsStr],765) -> Result<Vec<PathBuf>, Error> {766    // Untracked files show up for short status and are indicated with a leading `?`767    // -C changes git to be as if run from that directory768    let stat_output =769        Command::new("git").arg("-C").arg(root_path).args(["status", "--short"]).output()?.stdout;770771    if String::from_utf8_lossy(&stat_output).lines().filter(|ln| ln.starts_with('?')).count() > 0 {772        eprintln!("found untracked files, ignoring");773    }774775    let mut output = Vec::new();776    let binding = {777        let mut command = Command::new("git");778        command.arg("-C").arg(root_path).args(["ls-files"]);779        if let Some(find_dir) = find_dir {780            command.arg(find_dir);781        }782        command.output()?783    };784    let tracked = String::from_utf8_lossy(&binding.stdout);785786    for line in tracked.lines() {787        let line = line.trim();788        let path = Path::new(line);789790        let Some(ref extension) = path.extension() else {791            continue;792        };793        if extensions.contains(extension) {794            output.push(root_path.join(path));795        }796    }797798    Ok(output)799}800801/// Check if the given executable is installed and the version is expected.802fn ensure_version(build_dir: &Path, bin_name: &str, version: &str) -> Result<PathBuf, Error> {803    let bin_path = build_dir.join("misc-tools").join("bin").join(bin_name);804805    match Command::new(&bin_path).arg("--version").output() {806        Ok(output) => {807            let Some(v) = str::from_utf8(&output.stdout).unwrap().trim().split_whitespace().last()808            else {809                return Err(Error::Generic("version check failed".to_string()));810            };811812            if v != version {813                return Err(Error::Version { program: "", required: "", installed: v.to_string() });814            }815            Ok(bin_path)816        }817        Err(e) => Err(Error::Io(e)),818    }819}820821/// If the given executable is installed with the given version, use that,822/// otherwise install via cargo.823fn ensure_version_or_cargo_install(824    build_dir: &Path,825    cargo: &Path,826    pkg_name: &str,827    bin_name: &str,828    version: &str,829    is_ci: bool,830) -> Result<PathBuf, Error> {831    if let Ok(bin_path) = ensure_version(build_dir, bin_name, version) {832        return Ok(bin_path);833    }834835    eprintln!("building external tool {bin_name} from package {pkg_name}@{version}");836837    let tool_root_dir = build_dir.join("misc-tools");838    let tool_bin_dir = tool_root_dir.join("bin");839    let bin_path = tool_bin_dir.join(bin_name).with_extension(env::consts::EXE_EXTENSION);840841    // use --force to ensure that if the required version is bumped, we update it.842    // use --target-dir to ensure we have a build cache so repeated invocations aren't slow.843    // modify PATH so that cargo doesn't print a warning telling the user to modify the path.844    let mut cmd = Command::new(cargo);845    cmd.args(["install", "--locked", "--force", "--quiet"])846        .arg("--root")847        .arg(&tool_root_dir)848        .arg("--target-dir")849        .arg(tool_root_dir.join("target"))850        .arg(format!("{pkg_name}@{version}"))851        .env(852            "PATH",853            env::join_paths(854                env::split_paths(&env::var("PATH").unwrap())855                    .chain(std::iter::once(tool_bin_dir.clone())),856            )857            .expect("build dir contains invalid char"),858        );859860    // On CI, we set opt-level flag for quicker installation.861    // Since lower opt-level decreases the tool's performance,862    // we don't set this option on local.863    if is_ci {864        cmd.env("RUSTFLAGS", "-Copt-level=0");865    }866867    let cargo_exit_code = cmd.spawn()?.wait()?;868    if !cargo_exit_code.success() {869        return Err(Error::Generic("cargo install failed".to_string()));870    }871    assert!(872        matches!(bin_path.try_exists(), Ok(true)),873        "cargo install did not produce the expected binary"874    );875    eprintln!("finished building tool {bin_name}");876    Ok(bin_path)877}878879#[derive(Debug)]880enum Error {881    Io(io::Error),882    /// a is required to run b. c is extra info883    MissingReq(&'static str, &'static str, Option<String>),884    /// Tool x failed the check885    FailedCheck(&'static str),886    /// Any message, just print it887    Generic(String),888    /// Installed but wrong version889    Version {890        program: &'static str,891        required: &'static str,892        installed: String,893    },894}895896impl fmt::Display for Error {897    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {898        match self {899            Self::MissingReq(a, b, ex) => {900                write!(901                    f,902                    "{a} is required to run {b} but it could not be located. Is it installed?"903                )?;904                if let Some(s) = ex {905                    write!(f, "\n{s}")?;906                };907                Ok(())908            }909            Self::Version { program, required, installed } => write!(910                f,911                "insufficient version of '{program}' to run external tools: \912                {required} required but found {installed}",913            ),914            Self::Generic(s) => f.write_str(s),915            Self::Io(e) => write!(f, "IO error: {e}"),916            Self::FailedCheck(s) => write!(f, "checks with external tool '{s}' failed"),917        }918    }919}920921impl From<io::Error> for Error {922    fn from(value: io::Error) -> Self {923        Self::Io(value)924    }925}926927#[derive(Debug, PartialEq)]928enum ExtraCheckParseError {929    #[allow(dead_code, reason = "shown through Debug")]930    UnknownKind(String),931    #[allow(dead_code)]932    UnknownLang(String),933    UnsupportedKindForLang,934    /// Too many `:`935    TooManyParts,936    /// Tried to parse the empty string937    Empty,938    /// `auto` specified without lang part.939    AutoRequiresLang,940    /// `if-installed` specified without lang part.941    IfInstalledRequiresLang,942}943944#[derive(PartialEq, Debug)]945struct ExtraCheckArg {946    /// Only run the check if files to check have been modified.947    auto: bool,948    /// Only run the check if the requisite software is already installed.949    if_installed: bool,950    lang: ExtraCheckLang,951    /// None = run all extra checks for the given lang952    kind: Option<ExtraCheckKind>,953}954955impl ExtraCheckArg {956    fn matches(&self, lang: ExtraCheckLang, kind: ExtraCheckKind) -> bool {957        self.lang == lang && self.kind.map(|k| k == kind).unwrap_or(true)958    }959960    fn is_non_if_installed_or_matches(&self, root_path: &Path, build_dir: &Path) -> bool {961        if !self.if_installed {962            return true;963        }964965        match self.lang {966            ExtraCheckLang::Spellcheck => {967                match ensure_version(build_dir, "typos", SPELLCHECK_VER) {968                    Ok(_) => true,969                    Err(Error::Version { installed, .. }) => {970                        eprintln!(971                            "warning: the tool `typos` is detected, but version {installed} doesn't match with the expected version {SPELLCHECK_VER}"972                        );973                        false974                    }975                    _ => false,976                }977            }978            ExtraCheckLang::Shell => has_shellcheck().is_ok(),979            ExtraCheckLang::Js => {980                match self.kind {981                    Some(ExtraCheckKind::Lint) => {982                        // If Lint is enabled, check both eslint and es-check.983                        rustdoc_js::has_tool(build_dir, "eslint")984                            && rustdoc_js::has_tool(build_dir, "es-check")985                    }986                    Some(ExtraCheckKind::Typecheck) => {987                        // If Typecheck is enabled, check tsc.988                        rustdoc_js::has_tool(build_dir, "tsc")989                    }990                    None => {991                        // No kind means it will check both Lint and Typecheck.992                        rustdoc_js::has_tool(build_dir, "eslint")993                            && rustdoc_js::has_tool(build_dir, "es-check")994                            && rustdoc_js::has_tool(build_dir, "tsc")995                    }996                    Some(_) => unreachable!("js shouldn't have other type of ExtraCheckKind"),997                }998            }999            ExtraCheckLang::Py | ExtraCheckLang::Cpp => {1000                let venv_path = build_dir.join("venv");1001                let mut reqs_path = root_path.to_owned();1002                reqs_path.extend(PIP_REQ_PATH);1003                let Ok(v) = has_py_tools(&venv_path, &reqs_path) else {1004                    return false;1005                };10061007                v1008            }1009        }1010    }10111012    /// Returns `false` if this is an auto arg and the passed filename does not trigger the auto rule1013    fn is_non_auto_or_matches(&self, filepath: &str) -> bool {1014        if !self.auto {1015            return true;1016        }1017        let exts: &[&str] = match self.lang {1018            ExtraCheckLang::Py => &[".py"],1019            ExtraCheckLang::Cpp => &[".cpp"],1020            ExtraCheckLang::Shell => &[".sh"],1021            ExtraCheckLang::Js => &[".js", ".ts"],1022            ExtraCheckLang::Spellcheck => {1023                if SPELLCHECK_DIRS.iter().any(|dir| Path::new(filepath).starts_with(dir)) {1024                    return true;1025                }1026                &[]1027            }1028        };1029        exts.iter().any(|ext| filepath.ends_with(ext))1030    }10311032    fn has_supported_kind(&self) -> bool {1033        let Some(kind) = self.kind else {1034            // "run all extra checks" mode is supported for all languages.1035            return true;1036        };1037        use ExtraCheckKind::*;1038        let supported_kinds: &[_] = match self.lang {1039            ExtraCheckLang::Py => &[Fmt, Lint],1040            ExtraCheckLang::Cpp => &[Fmt],1041            ExtraCheckLang::Shell => &[Lint],1042            ExtraCheckLang::Spellcheck => &[],1043            ExtraCheckLang::Js => &[Lint, Typecheck],1044        };1045        supported_kinds.contains(&kind)1046    }1047}10481049impl FromStr for ExtraCheckArg {1050    type Err = ExtraCheckParseError;10511052    fn from_str(s: &str) -> Result<Self, Self::Err> {1053        let mut auto = false;1054        let mut if_installed = false;1055        let mut parts = s.split(':');1056        let mut first = match parts.next() {1057            Some("") | None => return Err(ExtraCheckParseError::Empty),1058            Some(part) => part,1059        };10601061        // The loop allows users to specify `auto` and `if-installed` in any order.1062        // Both auto:if-installed:<check> and if-installed:auto:<check> are valid.1063        loop {1064            match (first, auto, if_installed) {1065                ("auto", false, _) => {1066                    let Some(part) = parts.next() else {1067                        return Err(ExtraCheckParseError::AutoRequiresLang);1068                    };1069                    auto = true;1070                    first = part;1071                }1072                ("if-installed", _, false) => {1073                    let Some(part) = parts.next() else {1074                        return Err(ExtraCheckParseError::IfInstalledRequiresLang);1075                    };1076                    if_installed = true;1077                    first = part;1078                }1079                _ => break,1080            }1081        }1082        let second = parts.next();1083        if parts.next().is_some() {1084            return Err(ExtraCheckParseError::TooManyParts);1085        }1086        let arg = Self {1087            auto,1088            if_installed,1089            lang: first.parse()?,1090            kind: second.map(|s| s.parse()).transpose()?,1091        };1092        if !arg.has_supported_kind() {1093            return Err(ExtraCheckParseError::UnsupportedKindForLang);1094        }10951096        Ok(arg)1097    }1098}10991100#[derive(PartialEq, Copy, Clone, Debug)]1101enum ExtraCheckLang {1102    Py,1103    Shell,1104    Cpp,1105    Spellcheck,1106    Js,1107}11081109impl FromStr for ExtraCheckLang {1110    type Err = ExtraCheckParseError;11111112    fn from_str(s: &str) -> Result<Self, Self::Err> {1113        Ok(match s {1114            "py" => Self::Py,1115            "shell" => Self::Shell,1116            "cpp" => Self::Cpp,1117            "spellcheck" => Self::Spellcheck,1118            "js" => Self::Js,1119            _ => return Err(ExtraCheckParseError::UnknownLang(s.to_string())),1120        })1121    }1122}11231124#[derive(PartialEq, Copy, Clone, Debug)]1125enum ExtraCheckKind {1126    Lint,1127    Fmt,1128    Typecheck,1129    /// Never parsed, but used as a placeholder for1130    /// langs that never have a specific kind.1131    None,1132}11331134impl FromStr for ExtraCheckKind {1135    type Err = ExtraCheckParseError;11361137    fn from_str(s: &str) -> Result<Self, Self::Err> {1138        Ok(match s {1139            "lint" => Self::Lint,1140            "fmt" => Self::Fmt,1141            "typecheck" => Self::Typecheck,1142            _ => return Err(ExtraCheckParseError::UnknownKind(s.to_string())),1143        })1144    }1145}

Code quality findings 58

Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
py_path.as_ref().unwrap(),
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
py_path.as_ref().unwrap(),
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
check_cpp_fmt(root_path, &cfg_args, &file_args, py_path.as_ref().unwrap(), &tidy_ctx);
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let mut args = vec!["-c", config_path.as_os_str().to_str().unwrap()];
Warning: Ignoring a Result or Option using 'let _ =' can hide errors or unexpected None values. Ensure the value is handled appropriately (match, if let, ?, expect) unless intentionally discarded with justification.
warning correctness discarded-result
let _ = py_runner(py_path, false, Some(&mut formatted), "clang-format", &diff_args);
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let out = Command::new(python).args(["-m", module]).arg(path).output().unwrap();
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let vers = outstr.trim().split_ascii_whitespace().nth(1).unwrap().trim();
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let major: u32 = vers_comps.next().unwrap().parse().unwrap();
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let minor: u32 = vers_comps.next().unwrap().parse().unwrap();
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
.expect("failed to launch pip");
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
fs::read_to_string(src_reqs_path).unwrap(),
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
fs::read_to_string(dst_reqs_path).unwrap()
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let Some(v) = str::from_utf8(&output.stdout).unwrap().trim().split_whitespace().last()
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
env::split_paths(&env::var("PATH").unwrap())
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
.expect("build dir contains invalid char"),
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("warning: `spellcheck:fix` is no longer valid, use `--extra-checks=spellcheck --bless`");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("warning: bad extra check argument {src:?}: {err:?}");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("rerun tidy with `--extra-checks={mode} --bless` to {action}");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("spellchecking files and fixing typos");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("spellchecking files");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("linting javascript files and applying suggestions");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("linting javascript files");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("typechecking javascript files");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("linting shell files");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("linting python files and applying suggestions");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("linting python files");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("\npython linting failed! Printing diff suggestions:");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("formatting python files");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("checking python file formatting");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("\npython formatting does not match! Printing diff:");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("formatting C++ files");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("checking C++ file formatting");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("\nclang-format linting failed! Printing diff suggestions:");
Performance Info: Frequent cloning, especially of Strings, Vecs, or other heap-allocated types inside loops, can be expensive. Consider using references/borrowing where possible.
info performance clone-in-loop
let mut cfg_args_diff = cfg_args.clone();
Performance Info: Frequent cloning, especially of Strings, Vecs, or other heap-allocated types inside loops, can be expensive. Consider using references/borrowing where possible.
info performance clone-in-loop
let mut diff_args = cfg_args_diff.clone();
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
diff_args.push(file);
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!(
Info: This standard library function returns a Result. Ensure the Result is handled properly (e.g., using '?', match, if let) rather than potentially panicking with .unwrap() or .expect().
info correctness unhandled-result
let actual = std::fs::read_to_string(file).unwrap_or_else(|e| {
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!(
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("removing old virtual environment");
Info: This standard library function returns a Result. Ensure the Result is handled properly (e.g., using '?', match, if let) rather than potentially panicking with .unwrap() or .expect().
info correctness unhandled-result
if let Ok(req) = fs::read_to_string(&dst_reqs_path) {
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("requirements.txt file mismatch");
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
Err(Error::Version { installed, .. }) => found.push(installed),
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
Err(e) => eprintln!("note: error running '{py}': {e}"),
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!(
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("found untracked files, ignoring");
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
output.push(root_path.join(path));
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("building external tool {bin_name} from package {pkg_name}@{version}");
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!("finished building tool {bin_name}");
Info: Usage of `#[allow(...)]` suppresses compiler lints. Ensure the allowance is justified, well-scoped, and ideally temporary. Overuse can hide potential issues.
info maintainability allow-lint
#[allow(dead_code, reason = "shown through Debug")]
Info: Usage of `#[allow(...)]` suppresses compiler lints. Ensure the allowance is justified, well-scoped, and ideally temporary. Overuse can hide potential issues.
info maintainability allow-lint
#[allow(dead_code)]
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
match self.lang {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
match ensure_version(build_dir, "typos", SPELLCHECK_VER) {
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
eprintln!(
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
"warning: the tool `typos` is detected, but version {installed} doesn't match with the expected version {SPELLCHECK_VER}"
Info: Wildcard imports (`use some::path::*;`) can obscure the origin of names and lead to conflicts. Prefer importing specific items explicitly.
info maintainability wildcard-import
use ExtraCheckKind::*;
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
Ok(match s {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
Ok(match s {

Security findings 6

Security Info: Reading environment variables. Treat them as untrusted input; validate or sanitize values before use, especially if controlling paths, commands, or network addresses.
security env-var-untrusted
std::env::var("TIDY_PRINT_DIFF").is_ok_and(|v| v.eq_ignore_ascii_case("true") || v == "1")
Security Info: Spawning external process. Ensure arguments are trusted or properly sanitized, especially if derived from external input, to prevent command injection. Avoid `.shell()` if possible.
security command-injection
let mut cmd = Command::new(py_path);
Security Info: Spawning external process. Ensure arguments are trusted or properly sanitized, especially if derived from external input, to prevent command injection. Avoid `.shell()` if possible.
security command-injection
match Command::new(bin_path).current_dir(src_root).args(args).status() {
Security Info: Spawning external process. Ensure arguments are trusted or properly sanitized, especially if derived from external input, to prevent command injection. Avoid `.shell()` if possible.
security command-injection
Command::new("git").arg("-C").arg(root_path).args(["status", "--short"]).output()?.stdout;
Security Info: Spawning external process. Ensure arguments are trusted or properly sanitized, especially if derived from external input, to prevent command injection. Avoid `.shell()` if possible.
security command-injection
let mut command = Command::new("git");
Security Info: Spawning external process. Ensure arguments are trusted or properly sanitized, especially if derived from external input, to prevent command injection. Avoid `.shell()` if possible.
security command-injection
let mut cmd = Command::new(cargo);

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.