Skip to main content

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