tidy/
pal.rs

1//! Tidy check to enforce rules about platform-specific code in std.
2//!
3//! This is intended to maintain existing standards of code
4//! organization in hopes that the standard library will continue to
5//! be refactored to isolate platform-specific bits, making porting
6//! easier; where "standard library" roughly means "all the
7//! dependencies of the std and test crates".
8//!
9//! This generally means placing restrictions on where `cfg(unix)`,
10//! `cfg(windows)`, `cfg(target_os)` and `cfg(target_env)` may appear,
11//! the basic objective being to isolate platform-specific code to the
12//! platform-specific `std::sys` modules, and to the allocation,
13//! unwinding, and libc crates.
14//!
15//! Following are the basic rules, though there are currently
16//! exceptions:
17//!
18//! - core may not have platform-specific code.
19//! - libpanic_abort may have platform-specific code.
20//! - libpanic_unwind may have platform-specific code.
21//! - libunwind may have platform-specific code.
22//! - other crates in the std facade may not.
23//! - std may have platform-specific code in the following places:
24//!   - `sys/`
25//!   - `os/`
26//!
27//! `std/sys_common` should _not_ contain platform-specific code.
28//! Finally, because std contains tests with platform-specific
29//! `ignore` attributes, once the parser encounters `mod tests`,
30//! platform-specific cfgs are allowed. Not sure yet how to deal with
31//! this in the long term.
32
33use std::path::Path;
34
35use crate::diagnostics::{CheckId, RunningCheck, TidyCtx};
36use crate::walk::{filter_dirs, walk};
37
38// Paths that may contain platform-specific code.
39const EXCEPTION_PATHS: &[&str] = &[
40    "library/compiler-builtins",
41    "library/std_detect",
42    "library/windows_targets",
43    "library/panic_abort",
44    "library/panic_unwind",
45    "library/unwind",
46    "library/rtstartup", // Not sure what to do about this. magic stuff for mingw
47    "library/test",      // Probably should defer to unstable `std::sys` APIs.
48    // The `VaList` implementation must have platform specific code.
49    // The Windows implementation of a `va_list` is always a character
50    // pointer regardless of the target architecture. As a result,
51    // we must use `#[cfg(windows)]` to conditionally compile the
52    // correct `VaList` structure for windows.
53    "library/core/src/ffi/va_list.rs",
54    // core::ffi contains platform-specific type and linkage configuration
55    "library/core/src/ffi/mod.rs",
56    "library/core/src/ffi/primitives.rs",
57    "library/core/src/os", // Platform-specific public interfaces
58    "library/std/src/sys", // Platform-specific code for std lives here.
59    "library/std/src/os",  // Platform-specific public interfaces
60    // Temporary `std` exceptions
61    // FIXME: platform-specific code should be moved to `sys`
62    "library/std/src/io/stdio.rs",
63    "library/std/src/lib.rs", // for miniz_oxide leaking docs, which itself workaround
64    "library/std/src/path.rs",
65    "library/std/src/io/error.rs", // Repr unpacked needed for UEFI
66];
67
68pub fn check(library_path: &Path, tidy_ctx: TidyCtx) {
69    let mut check = tidy_ctx.start_check(CheckId::new("pal").path(library_path));
70
71    let root_path = library_path.parent().unwrap();
72    // Let's double-check that this is the root path by making sure it has `x.py`.
73    assert!(root_path.join("x.py").is_file());
74
75    // Sanity check that the complex parsing here works.
76    let mut saw_target_arch = false;
77    let mut saw_cfg_bang = false;
78    walk(library_path, |path, _is_dir| filter_dirs(path), &mut |entry, contents| {
79        let file = entry.path();
80        // We don't want the absolute path to matter, so make it relative.
81        let file = file.strip_prefix(root_path).unwrap();
82        let filestr = file.to_string_lossy().replace("\\", "/");
83        if !filestr.ends_with(".rs") {
84            return;
85        }
86
87        let is_exception_path = EXCEPTION_PATHS.iter().any(|s| filestr.contains(&**s));
88        if is_exception_path {
89            return;
90        }
91
92        // exclude tests and benchmarks as some platforms do not support all tests
93        if filestr.contains("tests") || filestr.contains("benches") {
94            return;
95        }
96
97        check_cfgs(contents, file, &mut check, &mut saw_target_arch, &mut saw_cfg_bang);
98    });
99
100    assert!(saw_target_arch);
101    assert!(saw_cfg_bang);
102}
103
104fn check_cfgs(
105    contents: &str,
106    file: &Path,
107    check: &mut RunningCheck,
108    saw_target_arch: &mut bool,
109    saw_cfg_bang: &mut bool,
110) {
111    // Pull out all `cfg(...)` and `cfg!(...)` strings.
112    let cfgs = parse_cfgs(contents);
113
114    let mut line_numbers: Option<Vec<usize>> = None;
115    let mut err = |idx: usize, cfg: &str| {
116        if line_numbers.is_none() {
117            line_numbers = Some(contents.match_indices('\n').map(|(i, _)| i).collect());
118        }
119        let line_numbers = line_numbers.as_ref().expect("");
120        let line = match line_numbers.binary_search(&idx) {
121            Ok(_) => unreachable!(),
122            Err(i) => i + 1,
123        };
124        check.error(format!("{}:{line}: platform-specific cfg: {cfg}", file.display()));
125    };
126
127    for (idx, cfg) in cfgs {
128        // Sanity check that the parsing here works.
129        if !*saw_target_arch && cfg.contains("target_arch") {
130            *saw_target_arch = true
131        }
132        if !*saw_cfg_bang && cfg.contains("cfg!") {
133            *saw_cfg_bang = true
134        }
135
136        let contains_platform_specific_cfg = cfg.contains("target_os")
137            || cfg.contains("target_env")
138            || cfg.contains("target_abi")
139            || cfg.contains("target_vendor")
140            || cfg.contains("target_family")
141            || cfg.contains("unix")
142            || cfg.contains("windows");
143
144        if !contains_platform_specific_cfg {
145            continue;
146        }
147
148        let preceded_by_doc_comment = {
149            let pre_contents = &contents[..idx];
150            let pre_newline = pre_contents.rfind('\n');
151            let pre_doc_comment = pre_contents.rfind("///");
152            match (pre_newline, pre_doc_comment) {
153                (Some(n), Some(c)) => n < c,
154                (None, Some(_)) => true,
155                (_, None) => false,
156            }
157        };
158
159        if preceded_by_doc_comment {
160            continue;
161        }
162
163        // exclude tests as some platforms do not support all tests
164        if cfg.contains("test") {
165            continue;
166        }
167
168        err(idx, cfg);
169    }
170}
171
172fn parse_cfgs(contents: &str) -> Vec<(usize, &str)> {
173    let candidate_cfgs = contents.match_indices("cfg");
174    let candidate_cfg_idxs = candidate_cfgs.map(|(i, _)| i);
175    // This is puling out the indexes of all "cfg" strings
176    // that appear to be tokens followed by a parenthesis.
177    let cfgs = candidate_cfg_idxs.filter(|i| {
178        let pre_idx = i.saturating_sub(1);
179        let succeeds_non_ident = !contents
180            .as_bytes()
181            .get(pre_idx)
182            .cloned()
183            .map(char::from)
184            .map(char::is_alphanumeric)
185            .unwrap_or(false);
186        let contents_after = &contents[*i..];
187        let first_paren = contents_after.find('(');
188        let paren_idx = first_paren.map(|ip| i + ip);
189        let preceeds_whitespace_and_paren = paren_idx
190            .map(|ip| {
191                let maybe_space = &contents[*i + "cfg".len()..ip];
192                maybe_space.chars().all(|c| char::is_whitespace(c) || c == '!')
193            })
194            .unwrap_or(false);
195
196        succeeds_non_ident && preceeds_whitespace_and_paren
197    });
198
199    cfgs.flat_map(|i| {
200        let mut depth = 0;
201        let contents_from = &contents[i..];
202        for (j, byte) in contents_from.bytes().enumerate() {
203            match byte {
204                b'(' => {
205                    depth += 1;
206                }
207                b')' => {
208                    depth -= 1;
209                    if depth == 0 {
210                        return Some((i, &contents_from[..=j]));
211                    }
212                }
213                _ => {}
214            }
215        }
216
217        // if the parentheses are unbalanced just ignore this cfg -- it'll be caught when attempting
218        // to run the compiler, and there's no real reason to lint it separately here
219        None
220    })
221    .collect()
222}