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 }