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
33 use crate::walk::{filter_dirs, walk};
34 use std::path::Path;
35
36 // Paths that may contain platform-specific code.
37 const EXCEPTION_PATHS: &[&str] = &[
38 "library/panic_abort",
39 "library/panic_unwind",
40 "library/unwind",
41 "library/rtstartup", // Not sure what to do about this. magic stuff for mingw
42 "library/term", // Not sure how to make this crate portable, but test crate needs it.
43 "library/test", // Probably should defer to unstable `std::sys` APIs.
44 // The `VaList` implementation must have platform specific code.
45 // The Windows implementation of a `va_list` is always a character
46 // pointer regardless of the target architecture. As a result,
47 // we must use `#[cfg(windows)]` to conditionally compile the
48 // correct `VaList` structure for windows.
49 "library/core/src/ffi/mod.rs",
50 "library/std/src/sys/", // Platform-specific code for std lives here.
51 "library/std/src/os", // Platform-specific public interfaces
52 // Temporary `std` exceptions
53 // FIXME: platform-specific code should be moved to `sys`
54 "library/std/src/io/copy.rs",
55 "library/std/src/io/stdio.rs",
56 "library/std/src/f32.rs",
57 "library/std/src/f64.rs",
58 "library/std/src/path.rs",
59 "library/std/src/sys_common", // Should only contain abstractions over platforms
60 "library/std/src/net/test.rs", // Utility helpers for tests
61 "library/std/src/personality.rs",
62 "library/std/src/personality/",
63 ];
64
check(path: &Path, bad: &mut bool)65 pub fn check(path: &Path, bad: &mut bool) {
66 // Sanity check that the complex parsing here works.
67 let mut saw_target_arch = false;
68 let mut saw_cfg_bang = false;
69 walk(path, |path, _is_dir| filter_dirs(path), &mut |entry, contents| {
70 let file = entry.path();
71 let filestr = file.to_string_lossy().replace("\\", "/");
72 if !filestr.ends_with(".rs") {
73 return;
74 }
75
76 let is_exception_path = EXCEPTION_PATHS.iter().any(|s| filestr.contains(&**s));
77 if is_exception_path {
78 return;
79 }
80
81 // exclude tests and benchmarks as some platforms do not support all tests
82 if filestr.contains("tests") || filestr.contains("benches") {
83 return;
84 }
85
86 check_cfgs(contents, &file, bad, &mut saw_target_arch, &mut saw_cfg_bang);
87 });
88
89 assert!(saw_target_arch);
90 assert!(saw_cfg_bang);
91 }
92
93 fn check_cfgs(
94 contents: &str,
95 file: &Path,
96 bad: &mut bool,
97 saw_target_arch: &mut bool,
98 saw_cfg_bang: &mut bool,
99 ) {
100 // Pull out all `cfg(...)` and `cfg!(...)` strings.
101 let cfgs = parse_cfgs(contents);
102
103 let mut line_numbers: Option<Vec<usize>> = None;
104 let mut err = |idx: usize, cfg: &str| {
105 if line_numbers.is_none() {
106 line_numbers = Some(contents.match_indices('\n').map(|(i, _)| i).collect());
107 }
108 let line_numbers = line_numbers.as_ref().expect("");
109 let line = match line_numbers.binary_search(&idx) {
110 Ok(_) => unreachable!(),
111 Err(i) => i + 1,
112 };
113 tidy_error!(bad, "{}:{}: platform-specific cfg: {}", file.display(), line, cfg);
114 };
115
116 for (idx, cfg) in cfgs {
117 // Sanity check that the parsing here works.
118 if !*saw_target_arch && cfg.contains("target_arch") {
119 *saw_target_arch = true
120 }
121 if !*saw_cfg_bang && cfg.contains("cfg!") {
122 *saw_cfg_bang = true
123 }
124
125 let contains_platform_specific_cfg = cfg.contains("target_os")
126 || cfg.contains("target_env")
127 || cfg.contains("target_abi")
128 || cfg.contains("target_vendor")
129 || cfg.contains("target_family")
130 || cfg.contains("unix")
131 || cfg.contains("windows");
132
133 if !contains_platform_specific_cfg {
134 continue;
135 }
136
137 let preceded_by_doc_comment = {
138 let pre_contents = &contents[..idx];
139 let pre_newline = pre_contents.rfind('\n');
140 let pre_doc_comment = pre_contents.rfind("///");
141 match (pre_newline, pre_doc_comment) {
142 (Some(n), Some(c)) => n < c,
143 (None, Some(_)) => true,
144 (_, None) => false,
145 }
146 };
147
148 if preceded_by_doc_comment {
149 continue;
150 }
151
152 // exclude tests as some platforms do not support all tests
153 if cfg.contains("test") {
154 continue;
155 }
156
157 err(idx, cfg);
158 }
159 }
160
parse_cfgs(contents: &str) -> Vec<(usize, &str)>161 fn parse_cfgs(contents: &str) -> Vec<(usize, &str)> {
162 let candidate_cfgs = contents.match_indices("cfg");
163 let candidate_cfg_idxs = candidate_cfgs.map(|(i, _)| i);
164 // This is puling out the indexes of all "cfg" strings
165 // that appear to be tokens followed by a parenthesis.
166 let cfgs = candidate_cfg_idxs.filter(|i| {
167 let pre_idx = i.saturating_sub(1);
168 let succeeds_non_ident = !contents
169 .as_bytes()
170 .get(pre_idx)
171 .cloned()
172 .map(char::from)
173 .map(char::is_alphanumeric)
174 .unwrap_or(false);
175 let contents_after = &contents[*i..];
176 let first_paren = contents_after.find('(');
177 let paren_idx = first_paren.map(|ip| i + ip);
178 let preceeds_whitespace_and_paren = paren_idx
179 .map(|ip| {
180 let maybe_space = &contents[*i + "cfg".len()..ip];
181 maybe_space.chars().all(|c| char::is_whitespace(c) || c == '!')
182 })
183 .unwrap_or(false);
184
185 succeeds_non_ident && preceeds_whitespace_and_paren
186 });
187
188 cfgs.flat_map(|i| {
189 let mut depth = 0;
190 let contents_from = &contents[i..];
191 for (j, byte) in contents_from.bytes().enumerate() {
192 match byte {
193 b'(' => {
194 depth += 1;
195 }
196 b')' => {
197 depth -= 1;
198 if depth == 0 {
199 return Some((i, &contents_from[..=j]));
200 }
201 }
202 _ => {}
203 }
204 }
205
206 // if the parentheses are unbalanced just ignore this cfg -- it'll be caught when attempting
207 // to run the compiler, and there's no real reason to lint it separately here
208 None
209 })
210 .collect()
211 }
212