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