]>
Commit | Line | Data |
---|---|---|
9fa01778 | 1 | //! Tidy check to ensure that unstable features are all in order. |
a7813a04 XL |
2 | //! |
3 | //! This check will ensure properties like: | |
4 | //! | |
9fa01778 XL |
5 | //! * All stability attributes look reasonably well formed. |
6 | //! * The set of library features is disjoint from the set of language features. | |
7 | //! * Library features have at most one stability level. | |
8 | //! * Library features have at most one `since` value. | |
9 | //! * All unstable lang features have tests to ensure they are actually unstable. | |
48663c56 | 10 | //! * Language features in a group are sorted by `since` value. |
a7813a04 XL |
11 | |
12 | use std::collections::HashMap; | |
c30ab7b3 | 13 | use std::fmt; |
dc9dc135 | 14 | use std::fs; |
a7813a04 XL |
15 | use std::path::Path; |
16 | ||
dc9dc135 | 17 | use regex::Regex; |
48663c56 XL |
18 | |
19 | mod version; | |
20 | use version::Version; | |
21 | ||
22 | const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start"; | |
23 | const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end"; | |
24 | ||
041b39d2 | 25 | #[derive(Debug, PartialEq, Clone)] |
cc61c64b | 26 | pub enum Status { |
c30ab7b3 | 27 | Stable, |
32a655c1 | 28 | Removed, |
c30ab7b3 SL |
29 | Unstable, |
30 | } | |
31 | ||
32 | impl fmt::Display for Status { | |
532ac7d7 | 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
c30ab7b3 SL |
34 | let as_str = match *self { |
35 | Status::Stable => "stable", | |
36 | Status::Unstable => "unstable", | |
32a655c1 | 37 | Status::Removed => "removed", |
c30ab7b3 SL |
38 | }; |
39 | fmt::Display::fmt(as_str, f) | |
40 | } | |
41 | } | |
42 | ||
041b39d2 | 43 | #[derive(Debug, Clone)] |
cc61c64b XL |
44 | pub struct Feature { |
45 | pub level: Status, | |
48663c56 | 46 | pub since: Option<Version>, |
cc61c64b | 47 | pub has_gate_test: bool, |
041b39d2 | 48 | pub tracking_issue: Option<u32>, |
a7813a04 XL |
49 | } |
50 | ||
041b39d2 XL |
51 | pub type Features = HashMap<String, Feature>; |
52 | ||
dc9dc135 XL |
53 | pub struct CollectedFeatures { |
54 | pub lib: Features, | |
55 | pub lang: Features, | |
56 | } | |
57 | ||
58 | // Currently only used for unstable book generation | |
59 | pub fn collect_lib_features(base_src_path: &Path) -> Features { | |
60 | let mut lib_features = Features::new(); | |
61 | ||
62 | // This library feature is defined in the `compiler_builtins` crate, which | |
63 | // has been moved out-of-tree. Now it can no longer be auto-discovered by | |
64 | // `tidy`, because we need to filter out its (submodule) directory. Manually | |
65 | // add it to the set of known library features so we can still generate docs. | |
66 | lib_features.insert("compiler_builtins_lib".to_owned(), Feature { | |
67 | level: Status::Unstable, | |
68 | since: None, | |
69 | has_gate_test: false, | |
70 | tracking_issue: None, | |
71 | }); | |
72 | ||
73 | map_lib_features(base_src_path, | |
74 | &mut |res, _, _| { | |
75 | if let Ok((name, feature)) = res { | |
76 | lib_features.insert(name.to_owned(), feature); | |
77 | } | |
78 | }); | |
79 | lib_features | |
80 | } | |
81 | ||
82 | pub fn check(path: &Path, bad: &mut bool, verbose: bool) -> CollectedFeatures { | |
94b46f34 | 83 | let mut features = collect_lang_features(path, bad); |
c30ab7b3 | 84 | assert!(!features.is_empty()); |
a7813a04 | 85 | |
041b39d2 | 86 | let lib_features = get_and_check_lib_features(path, bad, &features); |
cc61c64b | 87 | assert!(!lib_features.is_empty()); |
a7813a04 | 88 | |
0731742a XL |
89 | super::walk_many(&[&path.join("test/ui"), |
90 | &path.join("test/ui-fulldeps"), | |
91 | &path.join("test/compile-fail")], | |
32a655c1 | 92 | &mut |path| super::filter_dirs(path), |
dc9dc135 XL |
93 | &mut |entry, contents| { |
94 | let file = entry.path(); | |
32a655c1 SL |
95 | let filename = file.file_name().unwrap().to_string_lossy(); |
96 | if !filename.ends_with(".rs") || filename == "features.rs" || | |
97 | filename == "diagnostic_list.rs" { | |
98 | return; | |
99 | } | |
100 | ||
b7449926 | 101 | let filen_underscore = filename.replace('-',"_").replace(".rs",""); |
7cac9316 | 102 | let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features); |
32a655c1 | 103 | |
32a655c1 SL |
104 | for (i, line) in contents.lines().enumerate() { |
105 | let mut err = |msg: &str| { | |
cc61c64b | 106 | tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg); |
32a655c1 SL |
107 | }; |
108 | ||
109 | let gate_test_str = "gate-test-"; | |
110 | ||
32a655c1 SL |
111 | let feature_name = match line.find(gate_test_str) { |
112 | Some(i) => { | |
b7449926 | 113 | line[i+gate_test_str.len()..].splitn(2, ' ').next().unwrap() |
32a655c1 SL |
114 | }, |
115 | None => continue, | |
116 | }; | |
7cac9316 XL |
117 | match features.get_mut(feature_name) { |
118 | Some(f) => { | |
119 | if filename_is_gate_test { | |
120 | err(&format!("The file is already marked as gate test \ | |
121 | through its name, no need for a \ | |
122 | 'gate-test-{}' comment", | |
123 | feature_name)); | |
124 | } | |
125 | f.has_gate_test = true; | |
126 | } | |
127 | None => { | |
128 | err(&format!("gate-test test found referencing a nonexistent feature '{}'", | |
129 | feature_name)); | |
130 | } | |
32a655c1 SL |
131 | } |
132 | } | |
133 | }); | |
134 | ||
32a655c1 SL |
135 | // Only check the number of lang features. |
136 | // Obligatory testing for library features is dumb. | |
137 | let gate_untested = features.iter() | |
138 | .filter(|&(_, f)| f.level == Status::Unstable) | |
139 | .filter(|&(_, f)| !f.has_gate_test) | |
32a655c1 SL |
140 | .collect::<Vec<_>>(); |
141 | ||
142 | for &(name, _) in gate_untested.iter() { | |
143 | println!("Expected a gate test for the feature '{}'.", name); | |
ff7c6d11 XL |
144 | println!("Hint: create a failing test file named 'feature-gate-{}.rs'\ |
145 | \n in the 'ui' test suite, with its failures due to\ | |
146 | \n missing usage of #![feature({})].", name, name); | |
32a655c1 SL |
147 | println!("Hint: If you already have such a test and don't want to rename it,\ |
148 | \n you can also add a // gate-test-{} line to the test file.", | |
149 | name); | |
150 | } | |
151 | ||
b7449926 | 152 | if !gate_untested.is_empty() { |
cc61c64b | 153 | tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len()); |
32a655c1 SL |
154 | } |
155 | ||
a7813a04 | 156 | if *bad { |
dc9dc135 | 157 | return CollectedFeatures { lib: lib_features, lang: features }; |
7cac9316 | 158 | } |
a7813a04 | 159 | |
dc9dc135 XL |
160 | if verbose { |
161 | let mut lines = Vec::new(); | |
162 | lines.extend(format_features(&features, "lang")); | |
163 | lines.extend(format_features(&lib_features, "lib")); | |
a7813a04 | 164 | |
dc9dc135 XL |
165 | lines.sort(); |
166 | for line in lines { | |
167 | println!("* {}", line); | |
168 | } | |
169 | } else { | |
170 | println!("* {} features", features.len()); | |
a7813a04 | 171 | } |
dc9dc135 XL |
172 | |
173 | CollectedFeatures { lib: lib_features, lang: features } | |
a7813a04 XL |
174 | } |
175 | ||
48663c56 XL |
176 | fn format_features<'a>(features: &'a Features, family: &'a str) -> impl Iterator<Item = String> + 'a { |
177 | features.iter().map(move |(name, feature)| { | |
178 | format!("{:<32} {:<8} {:<12} {:<8}", | |
179 | name, | |
180 | family, | |
181 | feature.level, | |
182 | feature.since.map_or("None".to_owned(), | |
183 | |since| since.to_string())) | |
184 | }) | |
185 | } | |
186 | ||
a7813a04 | 187 | fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> { |
dc9dc135 XL |
188 | lazy_static::lazy_static! { |
189 | static ref ISSUE: Regex = Regex::new(r#"issue\s*=\s*"([^"]*)""#).unwrap(); | |
190 | static ref FEATURE: Regex = Regex::new(r#"feature\s*=\s*"([^"]*)""#).unwrap(); | |
191 | static ref SINCE: Regex = Regex::new(r#"since\s*=\s*"([^"]*)""#).unwrap(); | |
192 | } | |
193 | ||
194 | let r = match attr { | |
195 | "issue" => &*ISSUE, | |
196 | "feature" => &*FEATURE, | |
197 | "since" => &*SINCE, | |
198 | _ => unimplemented!("{} not handled", attr), | |
199 | }; | |
200 | ||
48663c56 XL |
201 | r.captures(line) |
202 | .and_then(|c| c.get(1)) | |
203 | .map(|m| m.as_str()) | |
204 | } | |
205 | ||
206 | #[test] | |
207 | fn test_find_attr_val() { | |
208 | let s = r#"#[unstable(feature = "checked_duration_since", issue = "58402")]"#; | |
209 | assert_eq!(find_attr_val(s, "feature"), Some("checked_duration_since")); | |
210 | assert_eq!(find_attr_val(s, "issue"), Some("58402")); | |
211 | assert_eq!(find_attr_val(s, "since"), None); | |
a7813a04 XL |
212 | } |
213 | ||
041b39d2 | 214 | fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool { |
dc9dc135 XL |
215 | let prefix = "feature_gate_"; |
216 | if filen_underscore.starts_with(prefix) { | |
32a655c1 | 217 | for (n, f) in features.iter_mut() { |
dc9dc135 XL |
218 | // Equivalent to filen_underscore == format!("feature_gate_{}", n) |
219 | if &filen_underscore[prefix.len()..] == n { | |
32a655c1 SL |
220 | f.has_gate_test = true; |
221 | return true; | |
222 | } | |
223 | } | |
224 | } | |
225 | return false; | |
226 | } | |
227 | ||
94b46f34 | 228 | pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features { |
0731742a | 229 | let contents = t!(fs::read_to_string(base_src_path.join("libsyntax/feature_gate.rs"))); |
a7813a04 | 230 | |
9fa01778 | 231 | // We allow rustc-internal features to omit a tracking issue. |
48663c56 XL |
232 | // To make tidy accept omitting a tracking issue, group the list of features |
233 | // without one inside `// no-tracking-issue` and `// no-tracking-issue-end`. | |
234 | let mut next_feature_omits_tracking_issue = false; | |
235 | ||
236 | let mut in_feature_group = false; | |
237 | let mut prev_since = None; | |
94b46f34 XL |
238 | |
239 | contents.lines().zip(1..) | |
240 | .filter_map(|(line, line_number)| { | |
241 | let line = line.trim(); | |
48663c56 XL |
242 | |
243 | // Within -start and -end, the tracking issue can be omitted. | |
244 | match line { | |
245 | "// no-tracking-issue-start" => { | |
246 | next_feature_omits_tracking_issue = true; | |
247 | return None; | |
248 | } | |
249 | "// no-tracking-issue-end" => { | |
250 | next_feature_omits_tracking_issue = false; | |
251 | return None; | |
252 | } | |
253 | _ => {} | |
254 | } | |
255 | ||
256 | if line.starts_with(FEATURE_GROUP_START_PREFIX) { | |
257 | if in_feature_group { | |
258 | tidy_error!( | |
259 | bad, | |
260 | // ignore-tidy-linelength | |
261 | "libsyntax/feature_gate.rs:{}: new feature group is started without ending the previous one", | |
262 | line_number, | |
263 | ); | |
264 | } | |
265 | ||
266 | in_feature_group = true; | |
267 | prev_since = None; | |
94b46f34 | 268 | return None; |
48663c56 XL |
269 | } else if line.starts_with(FEATURE_GROUP_END_PREFIX) { |
270 | in_feature_group = false; | |
271 | prev_since = None; | |
94b46f34 XL |
272 | return None; |
273 | } | |
274 | ||
275 | let mut parts = line.split(','); | |
9fa01778 | 276 | let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) { |
c30ab7b3 | 277 | Some("active") => Status::Unstable, |
32a655c1 | 278 | Some("removed") => Status::Removed, |
c30ab7b3 SL |
279 | Some("accepted") => Status::Stable, |
280 | _ => return None, | |
281 | }; | |
282 | let name = parts.next().unwrap().trim(); | |
48663c56 XL |
283 | |
284 | let since_str = parts.next().unwrap().trim().trim_matches('"'); | |
285 | let since = match since_str.parse() { | |
286 | Ok(since) => Some(since), | |
287 | Err(err) => { | |
288 | tidy_error!( | |
289 | bad, | |
290 | "libsyntax/feature_gate.rs:{}: failed to parse since: {} ({:?})", | |
291 | line_number, | |
292 | since_str, | |
293 | err, | |
294 | ); | |
295 | None | |
296 | } | |
297 | }; | |
298 | if in_feature_group { | |
299 | if prev_since > since { | |
300 | tidy_error!( | |
301 | bad, | |
302 | "libsyntax/feature_gate.rs:{}: feature {} is not sorted by since", | |
303 | line_number, | |
304 | name, | |
305 | ); | |
306 | } | |
307 | prev_since = since; | |
308 | } | |
309 | ||
041b39d2 XL |
310 | let issue_str = parts.next().unwrap().trim(); |
311 | let tracking_issue = if issue_str.starts_with("None") { | |
48663c56 | 312 | if level == Status::Unstable && !next_feature_omits_tracking_issue { |
94b46f34 XL |
313 | *bad = true; |
314 | tidy_error!( | |
315 | bad, | |
316 | "libsyntax/feature_gate.rs:{}: no tracking issue for feature {}", | |
317 | line_number, | |
318 | name, | |
319 | ); | |
320 | } | |
041b39d2 XL |
321 | None |
322 | } else { | |
8faf50e0 | 323 | let s = issue_str.split('(').nth(1).unwrap().split(')').nth(0).unwrap(); |
041b39d2 XL |
324 | Some(s.parse().unwrap()) |
325 | }; | |
32a655c1 SL |
326 | Some((name.to_owned(), |
327 | Feature { | |
041b39d2 | 328 | level, |
48663c56 | 329 | since, |
32a655c1 | 330 | has_gate_test: false, |
041b39d2 | 331 | tracking_issue, |
32a655c1 | 332 | })) |
c30ab7b3 SL |
333 | }) |
334 | .collect() | |
a7813a04 | 335 | } |
cc61c64b | 336 | |
041b39d2 XL |
337 | fn get_and_check_lib_features(base_src_path: &Path, |
338 | bad: &mut bool, | |
339 | lang_features: &Features) -> Features { | |
340 | let mut lib_features = Features::new(); | |
341 | map_lib_features(base_src_path, | |
342 | &mut |res, file, line| { | |
343 | match res { | |
344 | Ok((name, f)) => { | |
3b2f2976 XL |
345 | let mut check_features = |f: &Feature, list: &Features, display: &str| { |
346 | if let Some(ref s) = list.get(name) { | |
b7449926 | 347 | if f.tracking_issue != s.tracking_issue { |
3b2f2976 | 348 | tidy_error!(bad, |
b7449926 | 349 | "{}:{}: mismatches the `issue` in {}", |
3b2f2976 XL |
350 | file.display(), |
351 | line, | |
b7449926 | 352 | display); |
3b2f2976 | 353 | } |
041b39d2 | 354 | } |
3b2f2976 XL |
355 | }; |
356 | check_features(&f, &lang_features, "corresponding lang feature"); | |
357 | check_features(&f, &lib_features, "previous"); | |
041b39d2 XL |
358 | lib_features.insert(name.to_owned(), f); |
359 | }, | |
360 | Err(msg) => { | |
361 | tidy_error!(bad, "{}:{}: {}", file.display(), line, msg); | |
362 | }, | |
363 | } | |
364 | ||
365 | }); | |
366 | lib_features | |
367 | } | |
368 | ||
369 | fn map_lib_features(base_src_path: &Path, | |
8faf50e0 | 370 | mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) { |
cc61c64b XL |
371 | super::walk(base_src_path, |
372 | &mut |path| super::filter_dirs(path) || path.ends_with("src/test"), | |
dc9dc135 XL |
373 | &mut |entry, contents| { |
374 | let file = entry.path(); | |
cc61c64b XL |
375 | let filename = file.file_name().unwrap().to_string_lossy(); |
376 | if !filename.ends_with(".rs") || filename == "features.rs" || | |
377 | filename == "diagnostic_list.rs" { | |
378 | return; | |
379 | } | |
380 | ||
dc9dc135 XL |
381 | // This is an early exit -- all the attributes we're concerned with must contain this: |
382 | // * rustc_const_unstable( | |
383 | // * unstable( | |
384 | // * stable( | |
385 | if !contents.contains("stable(") { | |
386 | return; | |
387 | } | |
cc61c64b | 388 | |
dc9dc135 | 389 | let mut becoming_feature: Option<(&str, Feature)> = None; |
cc61c64b | 390 | for (i, line) in contents.lines().enumerate() { |
041b39d2 XL |
391 | macro_rules! err { |
392 | ($msg:expr) => {{ | |
393 | mf(Err($msg), file, i + 1); | |
394 | continue; | |
395 | }}; | |
cc61c64b | 396 | }; |
041b39d2 XL |
397 | if let Some((ref name, ref mut f)) = becoming_feature { |
398 | if f.tracking_issue.is_none() { | |
399 | f.tracking_issue = find_attr_val(line, "issue") | |
400 | .map(|s| s.parse().unwrap()); | |
401 | } | |
b7449926 | 402 | if line.ends_with(']') { |
041b39d2 | 403 | mf(Ok((name, f.clone())), file, i + 1); |
b7449926 | 404 | } else if !line.ends_with(',') && !line.ends_with('\\') { |
041b39d2 | 405 | // We need to bail here because we might have missed the |
b7449926 | 406 | // end of a stability attribute above because the ']' |
041b39d2 XL |
407 | // might not have been at the end of the line. |
408 | // We could then get into the very unfortunate situation that | |
409 | // we continue parsing the file assuming the current stability | |
410 | // attribute has not ended, and ignoring possible feature | |
411 | // attributes in the process. | |
412 | err!("malformed stability attribute"); | |
413 | } else { | |
414 | continue; | |
415 | } | |
416 | } | |
417 | becoming_feature = None; | |
abe05a73 | 418 | if line.contains("rustc_const_unstable(") { |
9fa01778 | 419 | // `const fn` features are handled specially. |
abe05a73 XL |
420 | let feature_name = match find_attr_val(line, "feature") { |
421 | Some(name) => name, | |
48663c56 | 422 | None => err!("malformed stability attribute: missing `feature` key"), |
abe05a73 XL |
423 | }; |
424 | let feature = Feature { | |
425 | level: Status::Unstable, | |
48663c56 | 426 | since: None, |
abe05a73 | 427 | has_gate_test: false, |
0731742a | 428 | // FIXME(#57563): #57563 is now used as a common tracking issue, |
9fa01778 XL |
429 | // although we would like to have specific tracking issues for each |
430 | // `rustc_const_unstable` in the future. | |
0731742a | 431 | tracking_issue: Some(57563), |
abe05a73 XL |
432 | }; |
433 | mf(Ok((feature_name, feature)), file, i + 1); | |
434 | continue; | |
435 | } | |
cc61c64b XL |
436 | let level = if line.contains("[unstable(") { |
437 | Status::Unstable | |
438 | } else if line.contains("[stable(") { | |
439 | Status::Stable | |
440 | } else { | |
441 | continue; | |
442 | }; | |
443 | let feature_name = match find_attr_val(line, "feature") { | |
444 | Some(name) => name, | |
48663c56 | 445 | None => err!("malformed stability attribute: missing `feature` key"), |
cc61c64b | 446 | }; |
48663c56 XL |
447 | let since = match find_attr_val(line, "since").map(|x| x.parse()) { |
448 | Some(Ok(since)) => Some(since), | |
449 | Some(Err(_err)) => { | |
450 | err!("malformed stability attribute: can't parse `since` key"); | |
451 | }, | |
cc61c64b | 452 | None if level == Status::Stable => { |
48663c56 | 453 | err!("malformed stability attribute: missing the `since` key"); |
cc61c64b | 454 | } |
48663c56 | 455 | None => None, |
cc61c64b | 456 | }; |
041b39d2 | 457 | let tracking_issue = find_attr_val(line, "issue").map(|s| s.parse().unwrap()); |
cc61c64b | 458 | |
041b39d2 XL |
459 | let feature = Feature { |
460 | level, | |
48663c56 | 461 | since, |
041b39d2 XL |
462 | has_gate_test: false, |
463 | tracking_issue, | |
464 | }; | |
b7449926 | 465 | if line.contains(']') { |
041b39d2 XL |
466 | mf(Ok((feature_name, feature)), file, i + 1); |
467 | } else { | |
dc9dc135 | 468 | becoming_feature = Some((feature_name, feature)); |
cc61c64b | 469 | } |
cc61c64b XL |
470 | } |
471 | }); | |
7cac9316 | 472 | } |