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