Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5615,6 +5615,7 @@ dependencies = [
"semver",
"serde",
"similar",
"tempfile",
"termcolor",
"toml 0.7.8",
"walkdir",
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_type_ir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
#![allow(rustc::usage_of_ty_tykind)]
#![allow(rustc::usage_of_type_ir_inherent)]
#![allow(rustc::usage_of_type_ir_traits)]
#![cfg_attr(feature = "nightly", allow(internal_features))]
#![cfg_attr(
feature = "nightly",
feature(associated_type_defaults, never_type, rustc_attrs, negative_impls)
)]
#![cfg_attr(feature = "nightly", allow(internal_features))]
// tidy-alphabetical-end

extern crate self as rustc_type_ir;
Expand Down
1 change: 1 addition & 0 deletions src/tools/tidy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rustc-hash = "2.0.0"
fluent-syntax = "0.12"
similar = "2.5.0"
toml = "0.7.8"
tempfile = "3.15.0"

[features]
build-metrics = ["dep:serde"]
Expand Down
305 changes: 223 additions & 82 deletions src/tools/tidy/src/alphabetical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,33 @@
//! // tidy-alphabetical-end
//! ```
//!
//! The following lines are ignored:
//! - Empty lines
//! - Lines that are indented with more or less spaces than the first line
//! - Lines starting with `//`, `#` (except those starting with `#!`), `)`, `]`, `}` if the comment
//! has the same indentation as the first line
//! - Lines starting with a closing delimiter (`)`, `[`, `}`) are ignored.
//! Empty lines and lines starting (ignoring spaces) with `//` or `#` (except those starting with
//! `#!`) are considered comments are are sorted together with the next line (but do not affect
//! sorting).
//!
//! If a line ends with an opening delimiter, we effectively join the following line to it before
//! checking it. E.g. `foo(\nbar)` is treated like `foo(bar)`.
//! If the following lines have higher indentation we effectively join them with the current line
//! before comparing it. If the next line with the same indentation starts (ignoring spaces) with
//! a closing delimiter (`)`, `[`, `}`) it is joined as well.
//!
//! E.g.
//!
//! ```rust,ignore ilustrative example for sorting mentioning non-existent functions
//! foo(a,
//! b);
//! bar(
//! a,
//! b
//! );
//! // are treated for sorting purposes as
//! foo(a, b);
//! bar(a, b);
//! ```

use std::cmp::Ordering;
use std::fmt::Display;
use std::fs;
use std::io::{Seek, Write};
use std::iter::Peekable;
use std::ops::{Range, RangeBounds};
use std::path::Path;

use crate::diagnostics::{CheckId, RunningCheck, TidyCtx};
Expand All @@ -38,108 +52,221 @@ fn is_close_bracket(c: char) -> bool {
matches!(c, ')' | ']' | '}')
}

fn is_empty_or_comment(line: &&str) -> bool {
let trimmed_line = line.trim_start_matches(' ').trim_end_matches('\n');

trimmed_line.is_empty()
|| trimmed_line.starts_with("//")
|| (trimmed_line.starts_with('#') && !trimmed_line.starts_with("#!"))
}

const START_MARKER: &str = "tidy-alphabetical-start";
const END_MARKER: &str = "tidy-alphabetical-end";

fn check_section<'a>(
file: impl Display,
lines: impl Iterator<Item = (usize, &'a str)>,
check: &mut RunningCheck,
) {
let mut prev_line = String::new();
let mut first_indent = None;
let mut in_split_line = None;

for (idx, line) in lines {
if line.is_empty() {
continue;
}
/// Given contents of a section that is enclosed between [`START_MARKER`] and [`END_MARKER`], sorts
/// them according to the rules described at the top of the module.
fn sort_section(section: &str) -> String {
/// A sortable item
struct Item {
/// Full contents including comments and whitespace
full: String,
/// Trimmed contents for sorting
trimmed: String,
}

if line.contains(START_MARKER) {
check.error(format!(
"{file}:{} found `{START_MARKER}` expecting `{END_MARKER}`",
idx + 1
));
return;
}
let mut items = Vec::new();
let mut lines = section.split_inclusive('\n').peekable();

let end_comments = loop {
let mut full = String::new();
let mut trimmed = String::new();

if line.contains(END_MARKER) {
return;
while let Some(comment) = lines.next_if(is_empty_or_comment) {
full.push_str(comment);
}

let indent = first_indent.unwrap_or_else(|| {
let indent = indentation(line);
first_indent = Some(indent);
indent
});

let line = if let Some(prev_split_line) = in_split_line {
// Join the split lines.
in_split_line = None;
format!("{prev_split_line}{}", line.trim_start())
} else {
line.to_string()
let Some(line) = lines.next() else {
// remember comments at the end of a block
break full;
};

if indentation(&line) != indent {
continue;
}
let mut push = |line| {
full.push_str(line);
trimmed.push_str(line.trim_start_matches(' ').trim_end_matches('\n'))
};

let trimmed_line = line.trim_start_matches(' ');
push(line);

if trimmed_line.starts_with("//")
|| (trimmed_line.starts_with('#') && !trimmed_line.starts_with("#!"))
|| trimmed_line.starts_with(is_close_bracket)
let indent = indentation(line);
let mut multiline = false;

// If the item is split between multiple lines...
while let Some(more_indented) =
lines.next_if(|&line: &&_| indent < indentation(line) || line == "\n")
{
continue;
multiline = true;
push(more_indented);
}

if line.trim_end().ends_with('(') {
in_split_line = Some(line);
continue;
if multiline
&& let Some(indented) =
// Only append next indented line if it looks like a closing bracket.
// Otherwise we incorrectly merge code like this (can be seen in
// compiler/rustc_session/src/options.rs):
//
// force_unwind_tables: Option<bool> = (None, parse_opt_bool, [TRACKED],
// "force use of unwind tables"),
// incremental: Option<String> = (None, parse_opt_string, [UNTRACKED],
// "enable incremental compilation"),
lines.next_if(|l| {
indentation(l) == indent
&& l.trim_start_matches(' ').starts_with(is_close_bracket)
})
{
push(indented);
}

let prev_line_trimmed_lowercase = prev_line.trim_start_matches(' ');
items.push(Item { full, trimmed });
};

if version_sort(trimmed_line, prev_line_trimmed_lowercase).is_lt() {
check.error(format!("{file}:{}: line not in alphabetical order", idx + 1));
}
items.sort_by(|a, b| version_sort(&a.trimmed, &b.trimmed));
items.into_iter().map(|l| l.full).chain([end_comments]).collect()
}

prev_line = line;
}
fn check_lines<'a>(path: &Path, content: &'a str, tidy_ctx: &TidyCtx, check: &mut RunningCheck) {
let mut offset = 0;

loop {
let rest = &content[offset..];
let start = rest.find(START_MARKER);
let end = rest.find(END_MARKER);

match (start, end) {
// error handling

// end before start
(Some(start), Some(end)) if end < start => {
check.error(format!(
"{path}:{line_number} found `{END_MARKER}` expecting `{START_MARKER}`",
path = path.display(),
line_number = content[..offset + end].lines().count(),
));
break;
}

check.error(format!("{file}: reached end of file expecting `{END_MARKER}`"));
}
// end without a start
(None, Some(end)) => {
check.error(format!(
"{path}:{line_number} found `{END_MARKER}` expecting `{START_MARKER}`",
path = path.display(),
line_number = content[..offset + end].lines().count(),
));
break;
}

fn check_lines<'a>(
file: &impl Display,
mut lines: impl Iterator<Item = (usize, &'a str)>,
check: &mut RunningCheck,
) {
while let Some((idx, line)) = lines.next() {
if line.contains(END_MARKER) {
check.error(format!(
"{file}:{} found `{END_MARKER}` expecting `{START_MARKER}`",
idx + 1
));
}
// start without an end
(Some(start), None) => {
check.error(format!(
"{path}:{line_number} `{START_MARKER}` without a matching `{END_MARKER}`",
path = path.display(),
line_number = content[..offset + start].lines().count(),
));
break;
}

// a second start in between start/end pair
(Some(start), Some(end))
if rest[start + START_MARKER.len()..end].contains(START_MARKER) =>
{
check.error(format!(
"{path}:{line_number} found `{START_MARKER}` expecting `{END_MARKER}`",
path = path.display(),
line_number = content[..offset
+ sub_find(rest, start + START_MARKER.len()..end, START_MARKER)
.unwrap()
.start]
.lines()
.count()
));
break;
}

if line.contains(START_MARKER) {
check_section(file, &mut lines, check);
// happy happy path :3
(Some(start), Some(end)) => {
assert!(start <= end);

// "...␤// tidy-alphabetical-start␤...␤// tidy-alphabetical-end␤..."
// start_nl_end --^ ^-- end_nl_start ^-- end_nl_end

// Position after the newline after start marker
let start_nl_end = sub_find(rest, start + START_MARKER.len().., "\n").unwrap().end;

// Position before the new line before the end marker
let end_nl_start = rest[..end].rfind('\n').unwrap();

// Position after the newline after end marker
let end_nl_end = sub_find(rest, end + END_MARKER.len().., "\n")
.map(|r| r.end)
.unwrap_or(content.len() - offset);

let section = &rest[start_nl_end..=end_nl_start];
let sorted = sort_section(section);

// oh nyooo :(
if sorted != section {
if !tidy_ctx.is_bless_enabled() {
let base_line_number = content[..offset + start_nl_end].lines().count();
let line_offset = sorted
.lines()
.zip(section.lines())
.enumerate()
.find(|(_, (a, b))| a != b)
.unwrap()
.0;
let line_number = base_line_number + line_offset;

check.error(format!(
"{path}:{line_number}: line not in alphabetical order (tip: use --bless to sort this list)",
path = path.display(),
));
} else {
// Use atomic rename as to not corrupt the file upon crashes/ctrl+c
let mut tempfile =
tempfile::Builder::new().tempfile_in(path.parent().unwrap()).unwrap();

fs::copy(path, tempfile.path()).unwrap();

tempfile
.as_file_mut()
.seek(std::io::SeekFrom::Start((offset + start_nl_end) as u64))
.unwrap();
tempfile.as_file_mut().write_all(sorted.as_bytes()).unwrap();

tempfile.persist(path).unwrap();
}
}

// Start the next search after the end section
offset += end_nl_end;
}

// No more alphabetical lists, yay :3
(None, None) => break,
}
}
}

pub fn check(path: &Path, tidy_ctx: TidyCtx) {
let mut check = tidy_ctx.start_check(CheckId::new("alphabetical").path(path));

let skip =
|path: &_, _is_dir| filter_dirs(path) || path.ends_with("tidy/src/alphabetical/tests.rs");
let skip = |path: &_, _is_dir| {
filter_dirs(path)
|| path.ends_with("tidy/src/alphabetical.rs")
|| path.ends_with("tidy/src/alphabetical/tests.rs")
};

walk(path, skip, &mut |entry, contents| {
let file = &entry.path().display();
let lines = contents.lines().enumerate();
check_lines(file, lines, &mut check)
walk(path, skip, &mut |entry, content| {
check_lines(entry.path(), content, &tidy_ctx, &mut check)
});
}

Expand Down Expand Up @@ -195,3 +322,17 @@ fn version_sort(a: &str, b: &str) -> Ordering {

it1.next().cmp(&it2.next())
}

/// Finds `pat` in `s[range]` and returns a range such that `s[ret] == pat`.
fn sub_find(s: &str, range: impl RangeBounds<usize>, pat: &str) -> Option<Range<usize>> {
s[(range.start_bound().cloned(), range.end_bound().cloned())]
.find(pat)
.map(|pos| {
pos + match range.start_bound().cloned() {
std::ops::Bound::Included(x) => x,
std::ops::Bound::Excluded(x) => x + 1,
std::ops::Bound::Unbounded => 0,
}
})
.map(|pos| pos..pos + pat.len())
}
Loading
Loading