Skip to content

Commit e5a30a6

Browse files
authored
Rollup merge of rust-lang#102971 - est31:tidy_duplicate_lang_features, r=jyn514
tidy: error if a lang feature is already present If a lang feature gets declared twice, like for example as a result of a mistake during stabilization, emit an error in tidy. Library features already have this logic. Inspired by a mistake done during `half_open_range_patterns` stabilization: https://github.com/rust-lang/rust/pull/102275/files#r991292215 The PR requires rust-lang#102883 to be merged before CI turns green because the check is doing its job. For reviewers, I suggest [turning off whitespace changes](https://github.com/rust-lang/rust/pull/102971/files?w=1) in the diff by adding `?w=1` to the url, as a large part of the diff is just about removing one level of indentation.
2 parents f903ddd + c278700 commit e5a30a6

File tree

1 file changed

+137
-126
lines changed

1 file changed

+137
-126
lines changed

src/tools/tidy/src/features.rs

+137-126
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//! * Language features in a group are sorted by feature name.
1111
1212
use crate::walk::{filter_dirs, walk, walk_many};
13-
use std::collections::HashMap;
13+
use std::collections::hash_map::{Entry, HashMap};
1414
use std::fmt;
1515
use std::fs;
1616
use std::num::NonZeroU32;
@@ -280,13 +280,14 @@ fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
280280
}
281281

282282
pub fn collect_lang_features(base_compiler_path: &Path, bad: &mut bool) -> Features {
283-
let mut all = collect_lang_features_in(base_compiler_path, "active.rs", bad);
284-
all.extend(collect_lang_features_in(base_compiler_path, "accepted.rs", bad));
285-
all.extend(collect_lang_features_in(base_compiler_path, "removed.rs", bad));
286-
all
283+
let mut features = Features::new();
284+
collect_lang_features_in(&mut features, base_compiler_path, "active.rs", bad);
285+
collect_lang_features_in(&mut features, base_compiler_path, "accepted.rs", bad);
286+
collect_lang_features_in(&mut features, base_compiler_path, "removed.rs", bad);
287+
features
287288
}
288289

289-
fn collect_lang_features_in(base: &Path, file: &str, bad: &mut bool) -> Features {
290+
fn collect_lang_features_in(features: &mut Features, base: &Path, file: &str, bad: &mut bool) {
290291
let path = base.join("rustc_feature").join("src").join(file);
291292
let contents = t!(fs::read_to_string(&path));
292293

@@ -298,135 +299,145 @@ fn collect_lang_features_in(base: &Path, file: &str, bad: &mut bool) -> Features
298299
let mut in_feature_group = false;
299300
let mut prev_names = vec![];
300301

301-
contents
302-
.lines()
303-
.zip(1..)
304-
.filter_map(|(line, line_number)| {
305-
let line = line.trim();
306-
307-
// Within -start and -end, the tracking issue can be omitted.
308-
match line {
309-
"// no-tracking-issue-start" => {
310-
next_feature_omits_tracking_issue = true;
311-
return None;
312-
}
313-
"// no-tracking-issue-end" => {
314-
next_feature_omits_tracking_issue = false;
315-
return None;
316-
}
317-
_ => {}
302+
let lines = contents.lines().zip(1..);
303+
for (line, line_number) in lines {
304+
let line = line.trim();
305+
306+
// Within -start and -end, the tracking issue can be omitted.
307+
match line {
308+
"// no-tracking-issue-start" => {
309+
next_feature_omits_tracking_issue = true;
310+
continue;
318311
}
312+
"// no-tracking-issue-end" => {
313+
next_feature_omits_tracking_issue = false;
314+
continue;
315+
}
316+
_ => {}
317+
}
319318

320-
if line.starts_with(FEATURE_GROUP_START_PREFIX) {
321-
if in_feature_group {
322-
tidy_error!(
323-
bad,
324-
"{}:{}: \
319+
if line.starts_with(FEATURE_GROUP_START_PREFIX) {
320+
if in_feature_group {
321+
tidy_error!(
322+
bad,
323+
"{}:{}: \
325324
new feature group is started without ending the previous one",
326-
path.display(),
327-
line_number,
328-
);
329-
}
330-
331-
in_feature_group = true;
332-
prev_names = vec![];
333-
return None;
334-
} else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
335-
in_feature_group = false;
336-
prev_names = vec![];
337-
return None;
325+
path.display(),
326+
line_number,
327+
);
338328
}
339329

340-
let mut parts = line.split(',');
341-
let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
342-
Some("active") => Status::Unstable,
343-
Some("incomplete") => Status::Unstable,
344-
Some("removed") => Status::Removed,
345-
Some("accepted") => Status::Stable,
346-
_ => return None,
347-
};
348-
let name = parts.next().unwrap().trim();
349-
350-
let since_str = parts.next().unwrap().trim().trim_matches('"');
351-
let since = match since_str.parse() {
352-
Ok(since) => Some(since),
353-
Err(err) => {
354-
tidy_error!(
355-
bad,
356-
"{}:{}: failed to parse since: {} ({:?})",
357-
path.display(),
358-
line_number,
359-
since_str,
360-
err,
361-
);
362-
None
363-
}
364-
};
365-
if in_feature_group {
366-
if prev_names.last() > Some(&name) {
367-
// This assumes the user adds the feature name at the end of the list, as we're
368-
// not looking ahead.
369-
let correct_index = match prev_names.binary_search(&name) {
370-
Ok(_) => {
371-
// This only occurs when the feature name has already been declared.
372-
tidy_error!(
373-
bad,
374-
"{}:{}: duplicate feature {}",
375-
path.display(),
376-
line_number,
377-
name,
378-
);
379-
// skip any additional checks for this line
380-
return None;
381-
}
382-
Err(index) => index,
383-
};
330+
in_feature_group = true;
331+
prev_names = vec![];
332+
continue;
333+
} else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
334+
in_feature_group = false;
335+
prev_names = vec![];
336+
continue;
337+
}
384338

385-
let correct_placement = if correct_index == 0 {
386-
"at the beginning of the feature group".to_owned()
387-
} else if correct_index == prev_names.len() {
388-
// I don't believe this is reachable given the above assumption, but it
389-
// doesn't hurt to be safe.
390-
"at the end of the feature group".to_owned()
391-
} else {
392-
format!(
393-
"between {} and {}",
394-
prev_names[correct_index - 1],
395-
prev_names[correct_index],
396-
)
397-
};
339+
let mut parts = line.split(',');
340+
let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
341+
Some("active") => Status::Unstable,
342+
Some("incomplete") => Status::Unstable,
343+
Some("removed") => Status::Removed,
344+
Some("accepted") => Status::Stable,
345+
_ => continue,
346+
};
347+
let name = parts.next().unwrap().trim();
348+
349+
let since_str = parts.next().unwrap().trim().trim_matches('"');
350+
let since = match since_str.parse() {
351+
Ok(since) => Some(since),
352+
Err(err) => {
353+
tidy_error!(
354+
bad,
355+
"{}:{}: failed to parse since: {} ({:?})",
356+
path.display(),
357+
line_number,
358+
since_str,
359+
err,
360+
);
361+
None
362+
}
363+
};
364+
if in_feature_group {
365+
if prev_names.last() > Some(&name) {
366+
// This assumes the user adds the feature name at the end of the list, as we're
367+
// not looking ahead.
368+
let correct_index = match prev_names.binary_search(&name) {
369+
Ok(_) => {
370+
// This only occurs when the feature name has already been declared.
371+
tidy_error!(
372+
bad,
373+
"{}:{}: duplicate feature {}",
374+
path.display(),
375+
line_number,
376+
name,
377+
);
378+
// skip any additional checks for this line
379+
continue;
380+
}
381+
Err(index) => index,
382+
};
398383

399-
tidy_error!(
400-
bad,
401-
"{}:{}: feature {} is not sorted by feature name (should be {})",
402-
path.display(),
403-
line_number,
404-
name,
405-
correct_placement,
406-
);
407-
}
408-
prev_names.push(name);
384+
let correct_placement = if correct_index == 0 {
385+
"at the beginning of the feature group".to_owned()
386+
} else if correct_index == prev_names.len() {
387+
// I don't believe this is reachable given the above assumption, but it
388+
// doesn't hurt to be safe.
389+
"at the end of the feature group".to_owned()
390+
} else {
391+
format!(
392+
"between {} and {}",
393+
prev_names[correct_index - 1],
394+
prev_names[correct_index],
395+
)
396+
};
397+
398+
tidy_error!(
399+
bad,
400+
"{}:{}: feature {} is not sorted by feature name (should be {})",
401+
path.display(),
402+
line_number,
403+
name,
404+
correct_placement,
405+
);
409406
}
407+
prev_names.push(name);
408+
}
410409

411-
let issue_str = parts.next().unwrap().trim();
412-
let tracking_issue = if issue_str.starts_with("None") {
413-
if level == Status::Unstable && !next_feature_omits_tracking_issue {
414-
tidy_error!(
415-
bad,
416-
"{}:{}: no tracking issue for feature {}",
417-
path.display(),
418-
line_number,
419-
name,
420-
);
421-
}
422-
None
423-
} else {
424-
let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
425-
Some(s.parse().unwrap())
426-
};
427-
Some((name.to_owned(), Feature { level, since, has_gate_test: false, tracking_issue }))
428-
})
429-
.collect()
410+
let issue_str = parts.next().unwrap().trim();
411+
let tracking_issue = if issue_str.starts_with("None") {
412+
if level == Status::Unstable && !next_feature_omits_tracking_issue {
413+
tidy_error!(
414+
bad,
415+
"{}:{}: no tracking issue for feature {}",
416+
path.display(),
417+
line_number,
418+
name,
419+
);
420+
}
421+
None
422+
} else {
423+
let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
424+
Some(s.parse().unwrap())
425+
};
426+
match features.entry(name.to_owned()) {
427+
Entry::Occupied(e) => {
428+
tidy_error!(
429+
bad,
430+
"{}:{} feature {name} already specified with status '{}'",
431+
path.display(),
432+
line_number,
433+
e.get().level,
434+
);
435+
}
436+
Entry::Vacant(e) => {
437+
e.insert(Feature { level, since, has_gate_test: false, tracking_issue });
438+
}
439+
}
440+
}
430441
}
431442

432443
fn get_and_check_lib_features(

0 commit comments

Comments
 (0)