tidy/
lib.rs

1//! Library used by tidy and other tools.
2//!
3//! This library contains the tidy lints and exposes it
4//! to be used by tools.
5
6use std::ffi::OsStr;
7use std::process::Command;
8
9use build_helper::ci::CiEnv;
10use build_helper::git::{GitConfig, get_closest_upstream_commit};
11use build_helper::stage0_parser::{Stage0Config, parse_stage0_file};
12use termcolor::WriteColor;
13
14macro_rules! static_regex {
15    ($re:literal) => {{
16        static RE: ::std::sync::OnceLock<::regex::Regex> = ::std::sync::OnceLock::new();
17        RE.get_or_init(|| ::regex::Regex::new($re).unwrap())
18    }};
19}
20
21/// A helper macro to `unwrap` a result except also print out details like:
22///
23/// * The expression that failed
24/// * The error itself
25/// * (optionally) a path connected to the error (e.g. failure to open a file)
26#[macro_export]
27macro_rules! t {
28    ($e:expr, $p:expr) => {
29        match $e {
30            Ok(e) => e,
31            Err(e) => panic!("{} failed on {} with {}", stringify!($e), ($p).display(), e),
32        }
33    };
34
35    ($e:expr) => {
36        match $e {
37            Ok(e) => e,
38            Err(e) => panic!("{} failed with {}", stringify!($e), e),
39        }
40    };
41}
42
43macro_rules! tidy_error {
44    ($bad:expr, $($fmt:tt)*) => ({
45        $crate::tidy_error(&format_args!($($fmt)*).to_string()).expect("failed to output error");
46        *$bad = true;
47    });
48}
49
50macro_rules! tidy_error_ext {
51    ($tidy_error:path, $bad:expr, $($fmt:tt)*) => ({
52        $tidy_error(&format_args!($($fmt)*).to_string()).expect("failed to output error");
53        *$bad = true;
54    });
55}
56
57fn tidy_error(args: &str) -> std::io::Result<()> {
58    use std::io::Write;
59
60    use termcolor::{Color, ColorChoice, ColorSpec, StandardStream};
61
62    let mut stderr = StandardStream::stdout(ColorChoice::Auto);
63    stderr.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
64
65    write!(&mut stderr, "tidy error")?;
66    stderr.set_color(&ColorSpec::new())?;
67
68    writeln!(&mut stderr, ": {args}")?;
69    Ok(())
70}
71
72pub struct CiInfo {
73    pub git_merge_commit_email: String,
74    pub nightly_branch: String,
75    pub base_commit: Option<String>,
76    pub ci_env: CiEnv,
77}
78
79impl CiInfo {
80    pub fn new(bad: &mut bool) -> Self {
81        let stage0 = parse_stage0_file();
82        let Stage0Config { nightly_branch, git_merge_commit_email, .. } = stage0.config;
83
84        let mut info = Self {
85            nightly_branch,
86            git_merge_commit_email,
87            ci_env: CiEnv::current(),
88            base_commit: None,
89        };
90        let base_commit = match get_closest_upstream_commit(None, &info.git_config(), info.ci_env) {
91            Ok(Some(commit)) => Some(commit),
92            Ok(None) => {
93                info.error_if_in_ci("no base commit found", bad);
94                None
95            }
96            Err(error) => {
97                info.error_if_in_ci(&format!("failed to retrieve base commit: {error}"), bad);
98                None
99            }
100        };
101        info.base_commit = base_commit;
102        info
103    }
104
105    pub fn git_config(&self) -> GitConfig<'_> {
106        GitConfig {
107            nightly_branch: &self.nightly_branch,
108            git_merge_commit_email: &self.git_merge_commit_email,
109        }
110    }
111
112    pub fn error_if_in_ci(&self, msg: &str, bad: &mut bool) {
113        if self.ci_env.is_running_in_ci() {
114            *bad = true;
115            eprintln!("tidy check error: {msg}");
116        } else {
117            eprintln!("tidy check warning: {msg}. Some checks will be skipped.");
118        }
119    }
120}
121
122pub fn git_diff<S: AsRef<OsStr>>(base_commit: &str, extra_arg: S) -> Option<String> {
123    let output = Command::new("git").arg("diff").arg(base_commit).arg(extra_arg).output().ok()?;
124    Some(String::from_utf8_lossy(&output.stdout).into())
125}
126
127/// Returns true if any modified file matches the predicate, if we are in CI, or if unable to list modified files.
128pub fn files_modified(ci_info: &CiInfo, pred: impl Fn(&str) -> bool) -> bool {
129    if CiEnv::is_ci() {
130        // assume everything is modified on CI because we really don't want false positives there.
131        return true;
132    }
133    let Some(base_commit) = &ci_info.base_commit else {
134        eprintln!("No base commit, assuming all files are modified");
135        return true;
136    };
137    match crate::git_diff(&base_commit, "--name-status") {
138        Some(output) => {
139            let modified_files = output.lines().filter_map(|ln| {
140                let (status, name) = ln
141                    .trim_end()
142                    .split_once('\t')
143                    .expect("bad format from `git diff --name-status`");
144                if status == "M" { Some(name) } else { None }
145            });
146            for modified_file in modified_files {
147                if pred(modified_file) {
148                    return true;
149                }
150            }
151            false
152        }
153        None => {
154            eprintln!("warning: failed to run `git diff` to check for changes");
155            eprintln!("warning: assuming all files are modified");
156            true
157        }
158    }
159}
160
161pub mod alphabetical;
162pub mod bins;
163pub mod debug_artifacts;
164pub mod deps;
165pub mod edition;
166pub mod error_codes;
167pub mod ext_tool_checks;
168pub mod extdeps;
169pub mod features;
170pub mod filenames;
171pub mod fluent_alphabetical;
172pub mod fluent_period;
173mod fluent_used;
174pub mod gcc_submodule;
175pub(crate) mod iter_header;
176pub mod known_bug;
177pub mod mir_opt_tests;
178pub mod pal;
179pub mod rustdoc_css_themes;
180pub mod rustdoc_gui_tests;
181pub mod rustdoc_js;
182pub mod rustdoc_json;
183pub mod rustdoc_templates;
184pub mod style;
185pub mod target_policy;
186pub mod target_specific_tests;
187pub mod tests_placement;
188pub mod tests_revision_unpaired_stdout_stderr;
189pub mod triagebot;
190pub mod ui_tests;
191pub mod unit_tests;
192pub mod unknown_revision;
193pub mod unstable_book;
194pub mod walk;
195pub mod x_version;