use super::{
ExitCode, HashSet, convert,
io::{self, Write as _},
};
use core::{
cmp::Ordering,
hash::{Hash, Hasher},
};
#[cfg_attr(test, derive(Debug, PartialEq))]
pub(crate) enum E<'a> {
Start,
UnexpectedLintLine(&'a [u8]),
DuplicateLint(&'a [u8]),
MissingWarningLint,
NoAllowLints,
NoWarnLints,
NoDenyLints,
Middle,
UnexpectedLintGroupLine(&'a [u8]),
DuplicateLintGroup(&'a [u8]),
LintSameNameAsLintGroup(&'a [u8]),
LintGroupContainsDuplicateLint(&'a [u8], &'a [u8]),
LintGroupContainsUnknownLint(&'a [u8], &'a [u8]),
EmptyLintGroup(&'a [u8]),
NoLintGroups,
End,
}
const START: &str = "name default meaning
---- ------- -------";
const MIDDLE: &str = "
name sub-lints
---- ---------";
impl E<'_> {
pub(crate) fn into_exit_code(self) -> ExitCode {
let mut stderr = io::stderr().lock();
match self {
Self::Start => writeln!(stderr, "rustc -Whelp doesn't contain '{START}' ignoring leading spaces"),
Self::UnexpectedLintLine(line) => writeln!(
stderr,
"rustc -Whelp contained the following line that is not the expected format of a lint: {}.",
String::from_utf8_lossy(line),
),
Self::DuplicateLint(lint) => writeln!(
stderr,
"rustc -Whelp contained the lint '{}' more than once.",
super::as_str(lint)
),
Self::MissingWarningLint => writeln!(
stderr,
"rustc -Whelp didn't contain a warn-by-default lint called 'warnings'."
),
Self::NoAllowLints => writeln!(
stderr,
"rustc -Whelp didn't contain any allow-by-default lints."
),
Self::NoWarnLints => writeln!(
stderr,
"rustc -Whelp didn't contain any warn-by-default lints except for 'warnings'."
),
Self::NoDenyLints => writeln!(
stderr,
"rustc -Whelp didn't contain any deny-by-default lints."
),
Self::Middle => writeln!(
stderr,
"rustc -Whelp doesn't contain '{MIDDLE}' ignoring leading spaces after the lints."
),
Self::UnexpectedLintGroupLine(line) => writeln!(
stderr,
"rustc -Whelp contained the following line that is not the expected format of a lint group: {}.",
String::from_utf8_lossy(line),
),
Self::DuplicateLintGroup(group) => {
writeln!(
stderr,
"rustc -Whelp contained multiple lint groups called '{}'.",
super::as_str(group)
)
}
Self::LintSameNameAsLintGroup(group) => {
writeln!(
stderr,
"rustc -Whelp contained a lint and lint group both named '{}'.",
super::as_str(group)
)
}
Self::LintGroupContainsDuplicateLint(group, lint) => writeln!(
stderr,
"rustc -Whelp contained the lint group '{}' which has the lint '{}' more than once.",
super::as_str(group),
super::as_str(lint),
),
Self::LintGroupContainsUnknownLint(group, lint) => writeln!(
stderr,
"rustc -Whelp contained the lint group '{}' which has the unknown lint '{}'.",
super::as_str(group),
super::as_str(lint),
),
Self::EmptyLintGroup(group) => {
writeln!(
stderr,
"rustc -Whelp contained the empty lint group '{}'.",
super::as_str(group),
)
}
Self::NoLintGroups => writeln!(
stderr,
"rustc -Whelp didn't contain any lint groups."
),
Self::End => writeln!(stderr, "rustc -Whelp did not have at least one empty line after the lint groups."),
}.map_or(ExitCode::FAILURE, |()| ExitCode::FAILURE)
}
}
#[expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
reason = "comments justifies correctness"
)]
fn skip_leading_space(line: &mut &[u8]) {
*line = &line[line
.iter()
.try_fold(0, |idx, b| {
if *b == b' ' {
Ok(idx + 1)
} else {
Err(idx)
}
})
.map_or_else(convert::identity, convert::identity)..];
}
struct Lines<'a>(&'a [u8]);
impl<'a> Iterator for Lines<'a> {
type Item = &'a [u8];
#[expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
fn next(&mut self) -> Option<Self::Item> {
self.0
.iter()
.try_fold(0, |idx, b| {
if *b == b'\n' {
Err(idx)
} else {
Ok(idx + 1)
}
})
.map_or_else(
|idx| {
let (val, rem) = self.0.split_at(idx);
self.0 = &rem[1..];
Some(val)
},
|_| None,
)
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "comment justifies correctness"
)]
fn get_lint_name(mut line: &[u8]) -> Option<(&[u8], &[u8])> {
skip_leading_space(&mut line);
line.iter()
.try_fold(0, |idx, b| {
if *b == b' ' {
Err(Some(idx))
} else if *b == b'-' || b.is_ascii_alphanumeric() {
Ok(idx + 1)
} else {
Err(None)
}
})
.map_or_else(
|opt| {
opt.and_then(|idx| {
let (name, mut rem) = line.split_at(idx);
let len = rem.len();
skip_leading_space(&mut rem);
if len == rem.len() {
None
} else {
Some((name, rem))
}
})
},
|_| None,
)
}
struct Lints<'a> {
allow: HashSet<&'a [u8]>,
warn: HashSet<&'a [u8]>,
deny: HashSet<&'a [u8]>,
}
pub(crate) const WARNINGS: &str = "warnings";
impl<'a> Lints<'a> {
#[expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
fn new(lines: &mut Lines<'a>) -> Result<Self, E<'a>> {
fn get_lint(line: &[u8]) -> Option<(&[u8], &[u8])> {
get_lint_name(line).and_then(|(lint, rem)| {
rem.iter()
.try_fold(0, |idx, b| if *b == b' ' { Err(idx) } else { Ok(idx + 1) })
.map_or_else(|idx| Some((lint, &rem[..idx])), |_| None)
})
}
let mut allow = HashSet::with_capacity(128);
let mut warn = HashSet::with_capacity(128);
let mut deny = HashSet::with_capacity(128);
lines
.try_fold((), |(), line| {
if line.is_empty() {
Err(None)
} else {
get_lint(line)
.ok_or(Some(E::UnexpectedLintLine(line)))
.and_then(|(lint, status)| match status {
b"allow" => {
if !allow.insert(lint) || warn.contains(lint) || deny.contains(lint)
{
Err(Some(E::DuplicateLint(lint)))
} else {
Ok(())
}
}
b"warn" => {
if !warn.insert(lint) || allow.contains(lint) || deny.contains(lint)
{
Err(Some(E::DuplicateLint(lint)))
} else {
Ok(())
}
}
b"deny" => {
if !deny.insert(lint) || allow.contains(lint) || warn.contains(lint)
{
Err(Some(E::DuplicateLint(lint)))
} else {
Ok(())
}
}
_ => Err(Some(E::UnexpectedLintLine(line))),
})
}
})
.map_or_else(
|opt| {
opt.map_or_else(
|| {
if warn.remove(WARNINGS.as_bytes()) {
if allow.is_empty() {
Err(E::NoAllowLints)
} else if warn.is_empty() {
Err(E::NoWarnLints)
} else if deny.is_empty() {
Err(E::NoDenyLints)
} else {
Ok(Self { allow, warn, deny })
}
} else {
Err(E::MissingWarningLint)
}
},
Err,
)
},
|()| Err(E::Middle),
)
}
}
struct Csv<'a>(&'a [u8]);
impl<'a> Iterator for Csv<'a> {
type Item = Result<&'a [u8], ()>;
#[expect(
clippy::arithmetic_side_effects,
reason = "comments justify correctness"
)]
fn next(&mut self) -> Option<Self::Item> {
const COMMA_SPACE: &[u8; 2] = b", ";
(!self.0.is_empty()).then(|| {
match self.0.iter().try_fold(0, |idx, b| {
if *b == b',' {
Err(idx)
} else {
Ok(idx + 1)
}
}) {
Ok(_) => {
let val = self.0;
self.0 = &[];
Ok(val)
}
Err(idx) => {
let (val, rem) = self.0.split_at(idx);
rem.split_at_checked(COMMA_SPACE.len())
.ok_or(())
.and_then(|(fst, fst_rem)| {
if fst == COMMA_SPACE {
self.0 = fst_rem;
Ok(val)
} else {
Err(())
}
})
}
}
})
}
}
pub(crate) struct LintGroup<'a> {
pub name: &'a [u8],
pub lints: HashSet<&'a [u8]>,
}
impl Eq for LintGroup<'_> {}
impl PartialEq for LintGroup<'_> {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Hash for LintGroup<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl PartialOrd for LintGroup<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LintGroup<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(other.name)
}
}
pub(crate) struct Data<'a> {
pub allow: Vec<&'a [u8]>,
pub warn: Vec<&'a [u8]>,
pub deny: Vec<&'a [u8]>,
pub groups: Vec<LintGroup<'a>>,
}
impl<'a> Data<'a> {
fn move_to_lints(lines: &mut Lines<'_>) -> bool {
lines
.try_fold((), |(), mut line| {
skip_leading_space(&mut line);
if line == b"name default meaning" {
Err(())
} else {
Ok(())
}
})
.map_or_else(
|()| {
lines.next().is_some_and(|mut line| {
skip_leading_space(&mut line);
line == b"---- ------- -------"
})
},
|()| false,
)
}
fn move_to_lint_groups(lines: &mut Lines<'_>) -> bool {
lines
.try_fold((), |(), mut line| {
skip_leading_space(&mut line);
if line == b"name sub-lints" {
Err(())
} else {
Ok(())
}
})
.map_or_else(
|()| {
lines.next().is_some_and(|mut line| {
skip_leading_space(&mut line);
line == b"---- ---------"
})
},
|()| false,
)
}
fn get_lint_groups(
output: &mut Lines<'a>,
single_lints: &Lints<'_>,
allow_undefined_lints: bool,
) -> Result<HashSet<LintGroup<'a>>, E<'a>> {
let mut groups = HashSet::with_capacity(16);
output
.try_fold((), |(), line| {
if line.is_empty() {
Err(None)
} else {
get_lint_name(line)
.ok_or(Some(E::UnexpectedLintGroupLine(line)))
.and_then(|(name, group_lints)| {
if name == WARNINGS.as_bytes() {
Ok(())
} else if single_lints.allow.contains(name)
|| single_lints.warn.contains(name)
|| single_lints.deny.contains(name)
{
Err(Some(E::LintSameNameAsLintGroup(name)))
} else {
let mut lints = HashSet::with_capacity(32);
Csv(group_lints)
.try_fold((), |(), res| {
res.map_err(|()| Some(E::UnexpectedLintGroupLine(line)))
.and_then(|lint| {
if allow_undefined_lints
|| single_lints.allow.contains(lint)
|| single_lints.warn.contains(lint)
|| single_lints.deny.contains(lint)
{
if lints.insert(lint) {
Ok(())
} else {
Err(Some(
E::LintGroupContainsDuplicateLint(
name, lint,
),
))
}
} else {
Err(Some(E::LintGroupContainsUnknownLint(
name, lint,
)))
}
})
})
.and_then(|()| {
if lints.is_empty() {
Err(Some(E::EmptyLintGroup(name)))
} else if groups.insert(LintGroup { name, lints }) {
Ok(())
} else {
Err(Some(E::DuplicateLintGroup(name)))
}
})
}
})
}
})
.map_or_else(
|opt| {
opt.map_or_else(
|| {
if groups.is_empty() {
Err(E::NoLintGroups)
} else {
Ok(groups)
}
},
Err,
)
},
|()| Err(E::End),
)
}
pub(crate) fn new(output: &'a [u8], allow_undefined_lints: bool) -> Result<Self, E<'a>> {
let mut lines = Lines(output);
if Self::move_to_lints(&mut lines) {
Lints::new(&mut lines).and_then(|lints| {
if Self::move_to_lint_groups(&mut lines) {
Self::get_lint_groups(&mut lines, &lints, allow_undefined_lints).map(
|group_set| {
let mut allow = Vec::with_capacity(lints.allow.len());
lints.allow.into_iter().fold((), |(), lint| {
allow.push(lint);
});
allow.sort_unstable();
let mut warn = Vec::with_capacity(lints.warn.len());
lints.warn.into_iter().fold((), |(), lint| {
warn.push(lint);
});
warn.sort_unstable();
let mut deny = Vec::with_capacity(lints.deny.len());
lints.deny.into_iter().fold((), |(), lint| {
deny.push(lint);
});
deny.sort_unstable();
let mut groups = Vec::with_capacity(group_set.len());
group_set.into_iter().fold((), |(), group| {
groups.push(group);
});
groups.sort_unstable();
Self {
allow,
warn,
deny,
groups,
}
},
)
} else {
Err(E::Middle)
}
})
} else {
Err(E::Start)
}
}
}
#[cfg(all(test, not(target_pointer_width = "16")))]
mod tests {
use super::{Data, E, io::Read as _};
use std::fs::{self, File};
#[expect(
clippy::assertions_on_constants,
reason = "want to pretty-print problematic file"
)]
#[expect(clippy::verbose_file_reads, reason = "want to lock file")]
#[expect(clippy::tests_outside_test_module, reason = "false positive")]
#[test]
fn outputs() {
let mut output = Vec::with_capacity(u16::MAX.into());
assert!(
fs::read_dir("./outputs/").is_ok_and(|mut dir| {
dir.try_fold((), |(), ent_res| {
if ent_res.is_ok_and(|ent| {
File::options()
.read(true)
.open(ent.path())
.is_ok_and(|mut file| {
file.lock_shared().is_ok_and(|()| {
output.clear();
file.read_to_end(&mut output).is_ok_and(|_| {
drop(file);
let file_name = ent.file_name();
let file_name_bytes = file_name.as_encoded_bytes();
Data::new(&output, false).map_or_else(
|e| match file_name_bytes {
b"1.34.0.txt" | b"1.34.1.txt" | b"1.34.2.txt" => {
assert_eq!(
e,
E::LintGroupContainsUnknownLint(
b"future-incompatible",
b"duplicate-matcher-binding-name"
),
"1.34.0.txt, 1.34.1.txt, and 1.34.2.txt can't be parsed for a reason other than the expected reason"
);
Data::new(&output, true).is_ok()
}
b"1.48.0.txt" => {
assert_eq!(
e,
E::LintGroupContainsUnknownLint(
b"rustdoc",
b"private-intra-doc-links"
),
"1.48.0.txt can't be parsed for a reason other than the expected reason"
);
Data::new(&output, true).is_ok()
}
_ => {
assert!(
false,
"{} cannot be parsed due to {e:?}.",
String::from_utf8_lossy(file_name_bytes),
);
false
}
},
|_| {
if matches!(file_name_bytes, b"1.34.0.txt" | b"1.34.1.txt" | b"1.34.2.txt" | b"1.48.0.txt") {
assert!(false, "{} shouldn't be parsable", String::from_utf8_lossy(file_name_bytes));
false
} else {
true
}
},
)
})
})
})
}) {
Ok(())
} else {
Err(())
}
})
.is_ok()
})
);
}
}