tidy/
rustdoc_js.rs

1//! Tidy check to ensure that rustdoc templates didn't forget a `{# #}` to strip extra whitespace
2//! characters.
3
4use std::ffi::OsStr;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8use ignore::DirEntry;
9
10use crate::walk::walk_no_read;
11
12fn run_eslint(args: &[PathBuf], config_folder: PathBuf, bad: &mut bool) {
13    let mut child = match Command::new("npx")
14        .arg("eslint")
15        .arg("-c")
16        .arg(config_folder.join(".eslintrc.js"))
17        .args(args)
18        .spawn()
19    {
20        Ok(child) => child,
21        Err(error) => {
22            *bad = true;
23            eprintln!("failed to run eslint: {error:?}");
24            return;
25        }
26    };
27    match child.wait() {
28        Ok(exit_status) => {
29            if exit_status.success() {
30                return;
31            }
32            eprintln!("eslint command failed");
33        }
34        Err(error) => eprintln!("eslint command failed: {error:?}"),
35    }
36    *bad = true;
37}
38
39fn get_eslint_version_inner(global: bool) -> Option<String> {
40    let mut command = Command::new("npm");
41    command.arg("list").arg("--parseable").arg("--long").arg("--depth=0");
42    if global {
43        command.arg("--global");
44    }
45    let output = command.output().ok()?;
46    let lines = String::from_utf8_lossy(&output.stdout);
47    lines.lines().find_map(|l| l.split(':').nth(1)?.strip_prefix("eslint@")).map(|v| v.to_owned())
48}
49
50fn get_eslint_version() -> Option<String> {
51    get_eslint_version_inner(false).or_else(|| get_eslint_version_inner(true))
52}
53
54pub fn check(librustdoc_path: &Path, tools_path: &Path, src_path: &Path, bad: &mut bool) {
55    let eslint_version_path = src_path.join("ci/docker/host-x86_64/tidy/eslint.version");
56    let eslint_version = match std::fs::read_to_string(&eslint_version_path) {
57        Ok(version) => version.trim().to_string(),
58        Err(error) => {
59            *bad = true;
60            eprintln!("failed to read `{}`: {error:?}", eslint_version_path.display());
61            return;
62        }
63    };
64    // Having the correct `eslint` version installed via `npm` isn't strictly necessary, since we're invoking it via `npx`,
65    // but this check allows the vast majority that is not working on the rustdoc frontend to avoid the penalty of running
66    // `eslint` in tidy. See also: https://github.com/rust-lang/rust/pull/142851
67    match get_eslint_version() {
68        Some(version) => {
69            if version != eslint_version {
70                *bad = true;
71                eprintln!(
72                    "⚠️ Installed version of eslint (`{version}`) is different than the \
73                     one used in the CI (`{eslint_version}`)",
74                );
75                eprintln!(
76                    "You can install this version using `npm update eslint` or by using \
77                     `npm install eslint@{eslint_version}`",
78                );
79                return;
80            }
81        }
82        None => {
83            eprintln!("`eslint` doesn't seem to be installed. Skipping tidy check for JS files.");
84            eprintln!("You can install it using `npm install eslint@{eslint_version}`");
85            return;
86        }
87    }
88    let mut files_to_check = Vec::new();
89    walk_no_read(
90        &[&librustdoc_path.join("html/static/js")],
91        |path, is_dir| is_dir || !path.extension().is_some_and(|ext| ext == OsStr::new("js")),
92        &mut |path: &DirEntry| {
93            files_to_check.push(path.path().into());
94        },
95    );
96    println!("Running eslint on rustdoc JS files");
97    run_eslint(&files_to_check, librustdoc_path.join("html/static"), bad);
98
99    run_eslint(&[tools_path.join("rustdoc-js/tester.js")], tools_path.join("rustdoc-js"), bad);
100    run_eslint(&[tools_path.join("rustdoc-gui/tester.js")], tools_path.join("rustdoc-gui"), bad);
101}