lints

`rustc` lints.
git clone https://git.philomathiclife.com/repos/lints
Log | Files | Refs | README

parse.rs (24183B)


      1 use super::{
      2     ExitCode, HashSet, convert,
      3     io::{self, Write as _},
      4 };
      5 use core::{
      6     cmp::Ordering,
      7     hash::{Hash, Hasher},
      8 };
      9 /// Unit tests.
     10 #[cfg(test)]
     11 mod tests;
     12 // We skip all lines until: "name  default  meaning" trimming leading spaces.
     13 // We verify the next line is "----  -------  -------" trimming leading spaces.
     14 //
     15 // Next we grab the lints.
     16 // The regex for lints is verified to be `^ *[\-0-9A-Za-z]+ +(allow|warn|deny) +.*$`.
     17 //
     18 // We verify all three kinds of lints are non-empty and no lint exists more than once. We verify the
     19 // `warn` lints contain a lint called "warnings" which does not count towards the emptiness of
     20 // `warn` lints.
     21 //
     22 // Next we skip all lines until: "name  sub-lints" trimming leading spaces.
     23 // We verify the next line is "----  ---------" trimming leading spaces.
     24 //
     25 // Next we grab the lint groups.
     26 // The regex for lint groups is verified to be `^ +[\-0-9A-Za-z]+ +[ ,\-0-9A-Za-z]+$`.
     27 //
     28 // We verify lint group names are unique among themselves and the individual lints sans `"warnings"`
     29 // which is allowed to exist.
     30 // We verify the lint groups are non-empty except "warnings" if it is defined.
     31 // We verify each lint in a lint group is unique.
     32 // We verify each lint is the name of one of the lints unless `--allow-undefined-lints` was passed.
     33 // We verify when there is multiple lints, each lint is separated with a single comma and space.
     34 /// Error from parsing.
     35 #[cfg_attr(test, derive(Debug, PartialEq))]
     36 pub(crate) enum E<'a> {
     37     /// Output doesn't start as expected.
     38     Start,
     39     /// The contained line is not the format of a lint.
     40     UnexpectedLintLine(&'a [u8]),
     41     /// The contained lint appeared more than once.
     42     DuplicateLint(&'a [u8]),
     43     /// There wasn't a `warn`-by-default lint called `"warnings"`.
     44     MissingWarningLint,
     45     /// There were no `allow`-by-default lints.
     46     NoAllowLints,
     47     /// There were no `warn`-by-default lints except for `"warnings"`.
     48     NoWarnLints,
     49     /// There were no `deny`-by-default lints.
     50     NoDenyLints,
     51     /// Output doesn't contain what it should between the lints and lint groups.
     52     Middle,
     53     /// The contained line is not the format of a lint group.
     54     UnexpectedLintGroupLine(&'a [u8]),
     55     /// The contained lint group name appeared more than once.
     56     DuplicateLintGroup(&'a [u8]),
     57     /// The contained lint group name is the same as the name of a lint.
     58     LintSameNameAsLintGroup(&'a [u8]),
     59     /// The contained lint group name contained a lint more than once.
     60     LintGroupContainsDuplicateLint(&'a [u8], &'a [u8]),
     61     /// The contained lint group name contained an unknown lint.
     62     LintGroupContainsUnknownLint(&'a [u8], &'a [u8]),
     63     /// The contained lint group name has no lints.
     64     EmptyLintGroup(&'a [u8]),
     65     /// There were no lint groups.
     66     NoLintGroups,
     67     /// Output doesn't end as expected.
     68     End,
     69 }
     70 /// Lines before lints.
     71 const START: &str = "name  default  meaning
     72 ----  -------  -------";
     73 /// Lines between lints and lint groups.
     74 const MIDDLE: &str = "
     75 name  sub-lints
     76 ----  ---------";
     77 impl E<'_> {
     78     /// Writes `self` into `stderr`.
     79     pub(crate) fn into_exit_code(self) -> ExitCode {
     80         let mut stderr = io::stderr().lock();
     81         match self {
     82             Self::Start => writeln!(stderr, "rustc -Whelp doesn't contain '{START}' ignoring leading spaces"),
     83             Self::UnexpectedLintLine(line) => writeln!(
     84                 stderr,
     85                 "rustc -Whelp contained the following line that is not the expected format of a lint: {}.",
     86                 String::from_utf8_lossy(line),
     87             ),
     88             Self::DuplicateLint(lint) => writeln!(
     89                 stderr,
     90                 "rustc -Whelp contained the lint '{}' more than once.",
     91                 super::as_str(lint)
     92             ),
     93             Self::MissingWarningLint => writeln!(
     94                 stderr,
     95                 "rustc -Whelp didn't contain a warn-by-default lint called 'warnings'."
     96             ),
     97             Self::NoAllowLints => writeln!(
     98                 stderr,
     99                 "rustc -Whelp didn't contain any allow-by-default lints."
    100             ),
    101             Self::NoWarnLints => writeln!(
    102                 stderr,
    103                 "rustc -Whelp didn't contain any warn-by-default lints except for 'warnings'."
    104             ),
    105             Self::NoDenyLints => writeln!(
    106                 stderr,
    107                 "rustc -Whelp didn't contain any deny-by-default lints."
    108             ),
    109             Self::Middle => writeln!(
    110                 stderr,
    111                 "rustc -Whelp doesn't contain '{MIDDLE}' ignoring leading spaces after the lints."
    112             ),
    113             Self::UnexpectedLintGroupLine(line) => writeln!(
    114                 stderr,
    115                 "rustc -Whelp contained the following line that is not the expected format of a lint group: {}.",
    116                 String::from_utf8_lossy(line),
    117             ),
    118             Self::DuplicateLintGroup(group) => {
    119                 writeln!(
    120                     stderr,
    121                     "rustc -Whelp contained multiple lint groups called '{}'.",
    122                     super::as_str(group)
    123                 )
    124             }
    125             Self::LintSameNameAsLintGroup(group) => {
    126                 writeln!(
    127                     stderr,
    128                     "rustc -Whelp contained a lint and lint group both named '{}'.",
    129                     super::as_str(group)
    130                 )
    131             }
    132             Self::LintGroupContainsDuplicateLint(group, lint) => writeln!(
    133                 stderr,
    134                 "rustc -Whelp contained the lint group '{}' which has the lint '{}' more than once.",
    135                 super::as_str(group),
    136                 super::as_str(lint),
    137             ),
    138             Self::LintGroupContainsUnknownLint(group, lint) => writeln!(
    139                 stderr,
    140                 "rustc -Whelp contained the lint group '{}' which has the unknown lint '{}'.",
    141                 super::as_str(group),
    142                 super::as_str(lint),
    143             ),
    144             Self::EmptyLintGroup(group) => {
    145                 writeln!(
    146                     stderr,
    147                     "rustc -Whelp contained the empty lint group '{}'.",
    148                     super::as_str(group),
    149                 )
    150             }
    151             Self::NoLintGroups => writeln!(
    152                 stderr,
    153                 "rustc -Whelp didn't contain any lint groups."
    154             ),
    155             Self::End => writeln!(stderr, "rustc -Whelp did not have at least one empty line after the lint groups."),
    156         }.map_or(ExitCode::FAILURE, |()| ExitCode::FAILURE)
    157     }
    158 }
    159 /// Moves `line` to start at the first non-space.
    160 #[expect(
    161     clippy::arithmetic_side_effects,
    162     clippy::indexing_slicing,
    163     reason = "comments justifies correctness"
    164 )]
    165 fn skip_leading_space(line: &mut &[u8]) {
    166     // The `usize` contained in the `Result` is at most `line.len()`, so indexing is fine.
    167     *line = &line[line
    168         .iter()
    169         .try_fold(0, |idx, b| {
    170             if *b == b' ' {
    171                 // `idx < line.len()`, so overflow is not possible.
    172                 Ok(idx + 1)
    173             } else {
    174                 Err(idx)
    175             }
    176         })
    177         .unwrap_or_else(convert::identity)..];
    178 }
    179 /// [`Iterator`] of lines.
    180 ///
    181 /// In the event a `b'\n'` doesn't exist, `None` will be returned. This means in the event the last
    182 /// "line" does not end with `b'\n'`, it won't be returned.
    183 struct Lines<'a>(&'a [u8]);
    184 impl<'a> Iterator for Lines<'a> {
    185     type Item = &'a [u8];
    186     #[expect(
    187         clippy::arithmetic_side_effects,
    188         clippy::indexing_slicing,
    189         reason = "comments justify correctness"
    190     )]
    191     fn next(&mut self) -> Option<Self::Item> {
    192         self.0
    193             .iter()
    194             .try_fold(0, |idx, b| {
    195                 if *b == b'\n' {
    196                     Err(idx)
    197                 } else {
    198                     // `idx < self.0.len()`, so overflow is not possible.
    199                     Ok(idx + 1)
    200                 }
    201             })
    202             .map_or_else(
    203                 |idx| {
    204                     // `idx <= self.0.len()`, so this won't `panic`.
    205                     let (val, rem) = self.0.split_at(idx);
    206                     // `rem` starts with a newline, so this won't `panic`.
    207                     self.0 = &rem[1..];
    208                     Some(val)
    209                 },
    210                 |_| None,
    211             )
    212     }
    213 }
    214 /// Extracts the lint or lint group name from `line`.
    215 ///
    216 /// Returns `Some` iff a valid lint or lint group name is found ignoring leading spaces and
    217 /// if there is non-spaces after. The first `slice` is the name and the second slice is
    218 /// the remaining portion of `line` with leading spaces removed.
    219 #[expect(
    220     clippy::arithmetic_side_effects,
    221     reason = "comment justifies correctness"
    222 )]
    223 fn get_lint_name(mut line: &[u8]) -> Option<(&[u8], &[u8])> {
    224     skip_leading_space(&mut line);
    225     line.iter()
    226         .try_fold(0, |idx, b| {
    227             if *b == b' ' {
    228                 Err(Some(idx))
    229             } else if *b == b'-' || b.is_ascii_alphanumeric() {
    230                 // `idx < line.len()`, so overflow is not possible.
    231                 Ok(idx + 1)
    232             } else {
    233                 Err(None)
    234             }
    235         })
    236         .map_or_else(
    237             |opt| {
    238                 opt.and_then(|idx| {
    239                     let (name, mut rem) = line.split_at(idx);
    240                     let len = rem.len();
    241                     skip_leading_space(&mut rem);
    242                     if len == rem.len() {
    243                         None
    244                     } else {
    245                         Some((name, rem))
    246                     }
    247                 })
    248             },
    249             |_| None,
    250         )
    251 }
    252 /// The lints.
    253 ///
    254 /// All `HashSet`s are non-empty with no overlap. `warn` doesn't contain `"warnings"`.
    255 struct Lints<'a> {
    256     /// `allow`-by-default lints.
    257     allow: HashSet<&'a [u8]>,
    258     /// `warn`-by-default lints.
    259     warn: HashSet<&'a [u8]>,
    260     /// `deny`-by-default lints.
    261     deny: HashSet<&'a [u8]>,
    262 }
    263 /// `"warnings"`.
    264 pub(crate) const WARNINGS: &str = "warnings";
    265 impl<'a> Lints<'a> {
    266     /// Gets the lints from `lines` erring when there are duplicates, there is no `warn`-by-default
    267     /// lint called `"warnings"`, or the lints are empty (ignoring the `"warnings"` lint).
    268     #[expect(
    269         clippy::arithmetic_side_effects,
    270         clippy::indexing_slicing,
    271         reason = "comments justify correctness"
    272     )]
    273     fn new(lines: &mut Lines<'a>) -> Result<Self, E<'a>> {
    274         /// Get the lint from `line`.
    275         ///
    276         /// Returns `Some` iff a valid lint name is found after removing leading spaces.
    277         /// The first `slice` is the name, and the second `slice` is the next "word".
    278         fn get_lint(line: &[u8]) -> Option<(&[u8], &[u8])> {
    279             get_lint_name(line).and_then(|(lint, rem)| {
    280                 rem.iter()
    281                     // `idx < rem.len()`, so overflow is not possible.
    282                     .try_fold(0, |idx, b| if *b == b' ' { Err(idx) } else { Ok(idx + 1) })
    283                     // `idx <= rem.len()`, so this won't `panic`.
    284                     .map_or_else(|idx| Some((lint, &rem[..idx])), |_| None)
    285             })
    286         }
    287         let mut allow = HashSet::with_capacity(128);
    288         let mut warn = HashSet::with_capacity(128);
    289         let mut deny = HashSet::with_capacity(128);
    290         lines
    291             .try_fold((), |(), line| {
    292                 if line.is_empty() {
    293                     Err(None)
    294                 } else {
    295                     get_lint(line)
    296                         .ok_or(Some(E::UnexpectedLintLine(line)))
    297                         .and_then(|(lint, status)| match status {
    298                             b"allow" => {
    299                                 if !allow.insert(lint) || warn.contains(lint) || deny.contains(lint)
    300                                 {
    301                                     Err(Some(E::DuplicateLint(lint)))
    302                                 } else {
    303                                     Ok(())
    304                                 }
    305                             }
    306                             b"warn" => {
    307                                 if !warn.insert(lint) || allow.contains(lint) || deny.contains(lint)
    308                                 {
    309                                     Err(Some(E::DuplicateLint(lint)))
    310                                 } else {
    311                                     Ok(())
    312                                 }
    313                             }
    314                             b"deny" => {
    315                                 if !deny.insert(lint) || allow.contains(lint) || warn.contains(lint)
    316                                 {
    317                                     Err(Some(E::DuplicateLint(lint)))
    318                                 } else {
    319                                     Ok(())
    320                                 }
    321                             }
    322                             _ => Err(Some(E::UnexpectedLintLine(line))),
    323                         })
    324                 }
    325             })
    326             .map_or_else(
    327                 |opt| {
    328                     opt.map_or_else(
    329                         || {
    330                             if warn.remove(WARNINGS.as_bytes()) {
    331                                 if allow.is_empty() {
    332                                     Err(E::NoAllowLints)
    333                                 } else if warn.is_empty() {
    334                                     Err(E::NoWarnLints)
    335                                 } else if deny.is_empty() {
    336                                     Err(E::NoDenyLints)
    337                                 } else {
    338                                     Ok(Self { allow, warn, deny })
    339                                 }
    340                             } else {
    341                                 Err(E::MissingWarningLint)
    342                             }
    343                         },
    344                         Err,
    345                     )
    346                 },
    347                 |()| Err(E::Middle),
    348             )
    349     }
    350 }
    351 /// [`Iterator`] of values separated by commas and spaces.
    352 ///
    353 /// In the event a value comes after a comma, a single leading space is assumed to exist and will be removed.
    354 struct Csv<'a>(&'a [u8]);
    355 impl<'a> Iterator for Csv<'a> {
    356     type Item = Result<&'a [u8], ()>;
    357     #[expect(
    358         clippy::arithmetic_side_effects,
    359         reason = "comments justify correctness"
    360     )]
    361     fn next(&mut self) -> Option<Self::Item> {
    362         /// `b", "`.
    363         const COMMA_SPACE: &[u8; 2] = b", ";
    364         (!self.0.is_empty()).then(|| {
    365             match self.0.iter().try_fold(0, |idx, b| {
    366                 if *b == b',' {
    367                     Err(idx)
    368                 } else {
    369                     // `idx < self.0.len()`, so overflow is not possible.
    370                     Ok(idx + 1)
    371                 }
    372             }) {
    373                 Ok(_) => {
    374                     let val = self.0;
    375                     self.0 = &[];
    376                     Ok(val)
    377                 }
    378                 Err(idx) => {
    379                     // `idx <= self.0.len()`, so this won't `panic`.
    380                     let (val, rem) = self.0.split_at(idx);
    381                     rem.split_at_checked(COMMA_SPACE.len())
    382                         .ok_or(())
    383                         .and_then(|(fst, fst_rem)| {
    384                             if fst == COMMA_SPACE {
    385                                 self.0 = fst_rem;
    386                                 Ok(val)
    387                             } else {
    388                                 Err(())
    389                             }
    390                         })
    391                 }
    392             }
    393         })
    394     }
    395 }
    396 /// Group of lints.
    397 pub(crate) struct LintGroup<'a> {
    398     /// Name of the group.
    399     pub name: &'a [u8],
    400     /// Lints that make up the group.
    401     pub lints: HashSet<&'a [u8]>,
    402 }
    403 impl Eq for LintGroup<'_> {}
    404 impl PartialEq for LintGroup<'_> {
    405     fn eq(&self, other: &Self) -> bool {
    406         self.name == other.name
    407     }
    408 }
    409 impl Hash for LintGroup<'_> {
    410     fn hash<H: Hasher>(&self, state: &mut H) {
    411         self.name.hash(state);
    412     }
    413 }
    414 impl PartialOrd for LintGroup<'_> {
    415     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
    416         Some(self.cmp(other))
    417     }
    418 }
    419 impl Ord for LintGroup<'_> {
    420     fn cmp(&self, other: &Self) -> Ordering {
    421         self.name.cmp(other.name)
    422     }
    423 }
    424 /// Data from `rustc -Whelp`.
    425 pub(crate) struct Data<'a> {
    426     /// `allow`-by-default lints.
    427     pub allow: Vec<&'a [u8]>,
    428     /// `warn`-by-default lints.
    429     pub warn: Vec<&'a [u8]>,
    430     /// `deny`-by-default lints.
    431     pub deny: Vec<&'a [u8]>,
    432     /// Lint groups.
    433     pub groups: Vec<LintGroup<'a>>,
    434 }
    435 impl<'a> Data<'a> {
    436     /// Moves until the lints.
    437     ///
    438     /// Returns `true` iff the header was found.
    439     fn move_to_lints(lines: &mut Lines<'_>) -> bool {
    440         lines
    441             .try_fold((), |(), mut line| {
    442                 skip_leading_space(&mut line);
    443                 if line == b"name  default  meaning" {
    444                     Err(())
    445                 } else {
    446                     Ok(())
    447                 }
    448             })
    449             .map_or_else(
    450                 |()| {
    451                     lines.next().is_some_and(|mut line| {
    452                         skip_leading_space(&mut line);
    453                         line == b"----  -------  -------"
    454                     })
    455                 },
    456                 |()| false,
    457             )
    458     }
    459     /// Moves until the lint groups.
    460     ///
    461     /// Returns `true` iff the header was found.
    462     fn move_to_lint_groups(lines: &mut Lines<'_>) -> bool {
    463         lines
    464             .try_fold((), |(), mut line| {
    465                 skip_leading_space(&mut line);
    466                 if line == b"name  sub-lints" {
    467                     Err(())
    468                 } else {
    469                     Ok(())
    470                 }
    471             })
    472             .map_or_else(
    473                 |()| {
    474                     lines.next().is_some_and(|mut line| {
    475                         skip_leading_space(&mut line);
    476                         line == b"----  ---------"
    477                     })
    478                 },
    479                 |()| false,
    480             )
    481     }
    482     /// Gets the lint groups from `lines` erring when there are duplicate lint group names or if a lint group
    483     /// name is the name of a lint (with the exception of `"warnings"`), or if a group contains duplicate lints.
    484     /// In the event `allow_undefined_lints` is `false`, every lint in a lint group must exist in `single_lints`.
    485     fn get_lint_groups(
    486         output: &mut Lines<'a>,
    487         single_lints: &Lints<'_>,
    488         allow_undefined_lints: bool,
    489     ) -> Result<HashSet<LintGroup<'a>>, E<'a>> {
    490         let mut groups = HashSet::with_capacity(16);
    491         output
    492             .try_fold((), |(), line| {
    493                 if line.is_empty() {
    494                     Err(None)
    495                 } else {
    496                     get_lint_name(line)
    497                         .ok_or(Some(E::UnexpectedLintGroupLine(line)))
    498                         .and_then(|(name, group_lints)| {
    499                             if name == WARNINGS.as_bytes() {
    500                                 Ok(())
    501                             } else if single_lints.allow.contains(name)
    502                                 || single_lints.warn.contains(name)
    503                                 || single_lints.deny.contains(name)
    504                             {
    505                                 Err(Some(E::LintSameNameAsLintGroup(name)))
    506                             } else {
    507                                 let mut lints = HashSet::with_capacity(32);
    508                                 Csv(group_lints)
    509                                     .try_fold((), |(), res| {
    510                                         res.map_err(|()| Some(E::UnexpectedLintGroupLine(line)))
    511                                             .and_then(|lint| {
    512                                                 if allow_undefined_lints
    513                                                     || single_lints.allow.contains(lint)
    514                                                     || single_lints.warn.contains(lint)
    515                                                     || single_lints.deny.contains(lint)
    516                                                 {
    517                                                     if lints.insert(lint) {
    518                                                         Ok(())
    519                                                     } else {
    520                                                         Err(Some(
    521                                                             E::LintGroupContainsDuplicateLint(
    522                                                                 name, lint,
    523                                                             ),
    524                                                         ))
    525                                                     }
    526                                                 } else {
    527                                                     Err(Some(E::LintGroupContainsUnknownLint(
    528                                                         name, lint,
    529                                                     )))
    530                                                 }
    531                                             })
    532                                     })
    533                                     .and_then(|()| {
    534                                         if lints.is_empty() {
    535                                             Err(Some(E::EmptyLintGroup(name)))
    536                                         } else if groups.insert(LintGroup { name, lints }) {
    537                                             Ok(())
    538                                         } else {
    539                                             Err(Some(E::DuplicateLintGroup(name)))
    540                                         }
    541                                     })
    542                             }
    543                         })
    544                 }
    545             })
    546             .map_or_else(
    547                 |opt| {
    548                     opt.map_or_else(
    549                         || {
    550                             if groups.is_empty() {
    551                                 Err(E::NoLintGroups)
    552                             } else {
    553                                 Ok(groups)
    554                             }
    555                         },
    556                         Err,
    557                     )
    558                 },
    559                 |()| Err(E::End),
    560             )
    561     }
    562     /// Parses output and returns lints and lint groups.
    563     pub(crate) fn new(output: &'a [u8], allow_undefined_lints: bool) -> Result<Self, E<'a>> {
    564         let mut lines = Lines(output);
    565         if Self::move_to_lints(&mut lines) {
    566             Lints::new(&mut lines).and_then(|lints| {
    567                 if Self::move_to_lint_groups(&mut lines) {
    568                     Self::get_lint_groups(&mut lines, &lints, allow_undefined_lints).map(
    569                         |group_set| {
    570                             let mut allow = Vec::with_capacity(lints.allow.len());
    571                             lints.allow.into_iter().fold((), |(), lint| {
    572                                 allow.push(lint);
    573                             });
    574                             allow.sort_unstable();
    575                             let mut warn = Vec::with_capacity(lints.warn.len());
    576                             lints.warn.into_iter().fold((), |(), lint| {
    577                                 warn.push(lint);
    578                             });
    579                             warn.sort_unstable();
    580                             let mut deny = Vec::with_capacity(lints.deny.len());
    581                             lints.deny.into_iter().fold((), |(), lint| {
    582                                 deny.push(lint);
    583                             });
    584                             deny.sort_unstable();
    585                             let mut groups = Vec::with_capacity(group_set.len());
    586                             group_set.into_iter().fold((), |(), group| {
    587                                 groups.push(group);
    588                             });
    589                             groups.sort_unstable();
    590                             Self {
    591                                 allow,
    592                                 warn,
    593                                 deny,
    594                                 groups,
    595                             }
    596                         },
    597                     )
    598                 } else {
    599                     Err(E::Middle)
    600                 }
    601             })
    602         } else {
    603             Err(E::Start)
    604         }
    605     }
    606 }