| 1 | #[cfg (test)] |
| 2 | #[path = "tests.rs" ] |
| 3 | mod tests; |
| 4 | |
| 5 | use self::Normalization::*; |
| 6 | use crate::directory::Directory; |
| 7 | use crate::run::PathDependency; |
| 8 | use std::cmp; |
| 9 | use std::path::Path; |
| 10 | |
| 11 | #[derive(Copy, Clone)] |
| 12 | pub struct Context<'a> { |
| 13 | pub krate: &'a str, |
| 14 | pub source_dir: &'a Directory, |
| 15 | pub workspace: &'a Directory, |
| 16 | pub input_file: &'a Path, |
| 17 | pub target_dir: &'a Directory, |
| 18 | pub path_dependencies: &'a [PathDependency], |
| 19 | } |
| 20 | |
| 21 | macro_rules! normalizations { |
| 22 | ($($name:ident,)*) => { |
| 23 | #[derive(PartialOrd, PartialEq, Copy, Clone)] |
| 24 | enum Normalization { |
| 25 | $($name,)* |
| 26 | } |
| 27 | |
| 28 | impl Normalization { |
| 29 | const ALL: &'static [Self] = &[$($name),*]; |
| 30 | } |
| 31 | |
| 32 | impl Default for Variations { |
| 33 | fn default() -> Self { |
| 34 | Variations { |
| 35 | variations: [$(($name, String::new()).1),*], |
| 36 | } |
| 37 | } |
| 38 | } |
| 39 | }; |
| 40 | } |
| 41 | |
| 42 | normalizations! { |
| 43 | Basic, |
| 44 | StripCouldNotCompile, |
| 45 | StripCouldNotCompile2, |
| 46 | StripForMoreInformation, |
| 47 | StripForMoreInformation2, |
| 48 | TrimEnd, |
| 49 | RustLib, |
| 50 | TypeDirBackslash, |
| 51 | WorkspaceLines, |
| 52 | PathDependencies, |
| 53 | CargoRegistry, |
| 54 | ArrowOtherCrate, |
| 55 | RelativeToDir, |
| 56 | LinesOutsideInputFile, |
| 57 | Unindent, |
| 58 | AndOthers, |
| 59 | StripLongTypeNameFiles, |
| 60 | UnindentAfterHelp, |
| 61 | // New normalization steps are to be inserted here at the end so that any |
| 62 | // snapshots saved before your normalization change remain passing. |
| 63 | } |
| 64 | |
| 65 | /// For a given compiler output, produces the set of saved outputs against which |
| 66 | /// the compiler's output would be considered correct. If the test's saved |
| 67 | /// stderr file is identical to any one of these variations, the test will pass. |
| 68 | /// |
| 69 | /// This is a set rather than just one normalized output in order to avoid |
| 70 | /// breaking existing tests when introducing new normalization steps. Someone |
| 71 | /// may have saved stderr snapshots with an older version of trybuild, and those |
| 72 | /// tests need to continue to pass with newer versions of trybuild. |
| 73 | /// |
| 74 | /// There is one "preferred" variation which is what we print when the stderr |
| 75 | /// file is absent or not a match. |
| 76 | pub fn diagnostics(output: &str, context: Context) -> Variations { |
| 77 | let output = output.replace(" \r\n" , " \n" ); |
| 78 | |
| 79 | let mut result = Variations::default(); |
| 80 | for (i, normalization) in Normalization::ALL.iter().enumerate() { |
| 81 | result.variations[i] = apply(&output, *normalization, context); |
| 82 | } |
| 83 | |
| 84 | result |
| 85 | } |
| 86 | |
| 87 | pub struct Variations { |
| 88 | variations: [String; Normalization::ALL.len()], |
| 89 | } |
| 90 | |
| 91 | impl Variations { |
| 92 | pub fn preferred(&self) -> &str { |
| 93 | self.variations.last().unwrap() |
| 94 | } |
| 95 | |
| 96 | pub fn any<F: FnMut(&str) -> bool>(&self, mut f: F) -> bool { |
| 97 | self.variations.iter().any(|stderr| f(stderr)) |
| 98 | } |
| 99 | |
| 100 | pub fn concat(&mut self, other: &Self) { |
| 101 | for (this, other) in self.variations.iter_mut().zip(&other.variations) { |
| 102 | if !this.is_empty() && !other.is_empty() { |
| 103 | this.push(' \n' ); |
| 104 | } |
| 105 | this.push_str(other); |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | pub fn trim<S: AsRef<[u8]>>(output: S) -> String { |
| 111 | let bytes = output.as_ref(); |
| 112 | let mut normalized = String::from_utf8_lossy(bytes).into_owned(); |
| 113 | |
| 114 | let len = normalized.trim_end().len(); |
| 115 | normalized.truncate(len); |
| 116 | |
| 117 | if !normalized.is_empty() { |
| 118 | normalized.push(' \n' ); |
| 119 | } |
| 120 | |
| 121 | normalized |
| 122 | } |
| 123 | |
| 124 | fn apply(original: &str, normalization: Normalization, context: Context) -> String { |
| 125 | let mut normalized = String::new(); |
| 126 | |
| 127 | let lines: Vec<&str> = original.lines().collect(); |
| 128 | let mut filter = Filter { |
| 129 | all_lines: &lines, |
| 130 | normalization, |
| 131 | context, |
| 132 | hide_numbers: 0, |
| 133 | }; |
| 134 | for i in 0..lines.len() { |
| 135 | if let Some(line) = filter.apply(i) { |
| 136 | normalized += &line; |
| 137 | if !normalized.ends_with(" \n\n" ) { |
| 138 | normalized.push(' \n' ); |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | normalized = unindent(normalized, normalization); |
| 144 | |
| 145 | trim(normalized) |
| 146 | } |
| 147 | |
| 148 | struct Filter<'a> { |
| 149 | all_lines: &'a [&'a str], |
| 150 | normalization: Normalization, |
| 151 | context: Context<'a>, |
| 152 | hide_numbers: usize, |
| 153 | } |
| 154 | |
| 155 | impl<'a> Filter<'a> { |
| 156 | fn apply(&mut self, index: usize) -> Option<String> { |
| 157 | let mut line = self.all_lines[index].to_owned(); |
| 158 | |
| 159 | if self.hide_numbers > 0 { |
| 160 | hide_leading_numbers(&mut line); |
| 161 | self.hide_numbers -= 1; |
| 162 | } |
| 163 | |
| 164 | let trim_start = line.trim_start(); |
| 165 | let indent = line.len() - trim_start.len(); |
| 166 | let prefix = if trim_start.starts_with("--> " ) { |
| 167 | Some("--> " ) |
| 168 | } else if trim_start.starts_with("::: " ) { |
| 169 | Some("::: " ) |
| 170 | } else { |
| 171 | None |
| 172 | }; |
| 173 | |
| 174 | if prefix == Some("--> " ) && self.normalization < ArrowOtherCrate { |
| 175 | if let Some(cut_end) = line.rfind(&['/' , ' \\' ][..]) { |
| 176 | let cut_start = indent + 4; |
| 177 | line.replace_range(cut_start..cut_end + 1, "$DIR/" ); |
| 178 | return Some(line); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | if prefix.is_some() { |
| 183 | line = line.replace(' \\' , "/" ); |
| 184 | let line_lower = line.to_ascii_lowercase(); |
| 185 | let target_dir_pat = self |
| 186 | .context |
| 187 | .target_dir |
| 188 | .to_string_lossy() |
| 189 | .to_ascii_lowercase() |
| 190 | .replace(' \\' , "/" ); |
| 191 | let source_dir_pat = self |
| 192 | .context |
| 193 | .source_dir |
| 194 | .to_string_lossy() |
| 195 | .to_ascii_lowercase() |
| 196 | .replace(' \\' , "/" ); |
| 197 | let mut other_crate = false; |
| 198 | if line_lower.find(&target_dir_pat) == Some(indent + 4) { |
| 199 | let mut offset = indent + 4 + target_dir_pat.len(); |
| 200 | let mut out_dir_crate_name = None; |
| 201 | while let Some(slash) = line[offset..].find('/' ) { |
| 202 | let component = &line[offset..offset + slash]; |
| 203 | if component == "out" { |
| 204 | if let Some(out_dir_crate_name) = out_dir_crate_name { |
| 205 | let replacement = format!("$OUT_DIR[{}]" , out_dir_crate_name); |
| 206 | line.replace_range(indent + 4..offset + 3, &replacement); |
| 207 | other_crate = true; |
| 208 | break; |
| 209 | } |
| 210 | } else if component.len() > 17 |
| 211 | && component.rfind('-' ) == Some(component.len() - 17) |
| 212 | && is_ascii_lowercase_hex(&component[component.len() - 16..]) |
| 213 | { |
| 214 | out_dir_crate_name = Some(&component[..component.len() - 17]); |
| 215 | } else { |
| 216 | out_dir_crate_name = None; |
| 217 | } |
| 218 | offset += slash + 1; |
| 219 | } |
| 220 | } else if let Some(i) = line_lower.find(&source_dir_pat) { |
| 221 | if self.normalization >= RelativeToDir && i == indent + 4 { |
| 222 | line.replace_range(i..i + source_dir_pat.len(), "" ); |
| 223 | if self.normalization < LinesOutsideInputFile { |
| 224 | return Some(line); |
| 225 | } |
| 226 | let input_file_pat = self |
| 227 | .context |
| 228 | .input_file |
| 229 | .to_string_lossy() |
| 230 | .to_ascii_lowercase() |
| 231 | .replace(' \\' , "/" ); |
| 232 | if line_lower[i + source_dir_pat.len()..].starts_with(&input_file_pat) { |
| 233 | // Keep line numbers only within the input file (the |
| 234 | // path passed to our `fn compile_fail`. All other |
| 235 | // source files get line numbers erased below. |
| 236 | return Some(line); |
| 237 | } |
| 238 | } else { |
| 239 | line.replace_range(i..i + source_dir_pat.len() - 1, "$DIR" ); |
| 240 | if self.normalization < LinesOutsideInputFile { |
| 241 | return Some(line); |
| 242 | } |
| 243 | } |
| 244 | other_crate = true; |
| 245 | } else { |
| 246 | let workspace_pat = self |
| 247 | .context |
| 248 | .workspace |
| 249 | .to_string_lossy() |
| 250 | .to_ascii_lowercase() |
| 251 | .replace(' \\' , "/" ); |
| 252 | if let Some(i) = line_lower.find(&workspace_pat) { |
| 253 | line.replace_range(i..i + workspace_pat.len() - 1, "$WORKSPACE" ); |
| 254 | other_crate = true; |
| 255 | } |
| 256 | } |
| 257 | if self.normalization >= PathDependencies && !other_crate { |
| 258 | for path_dep in self.context.path_dependencies { |
| 259 | let path_dep_pat = path_dep |
| 260 | .normalized_path |
| 261 | .to_string_lossy() |
| 262 | .to_ascii_lowercase() |
| 263 | .replace(' \\' , "/" ); |
| 264 | if let Some(i) = line_lower.find(&path_dep_pat) { |
| 265 | let var = format!("${}" , path_dep.name.to_uppercase().replace('-' , "_" )); |
| 266 | line.replace_range(i..i + path_dep_pat.len() - 1, &var); |
| 267 | other_crate = true; |
| 268 | break; |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | if self.normalization >= RustLib && !other_crate { |
| 273 | if let Some(pos) = line.find("/rustlib/src/rust/src/" ) { |
| 274 | // --> /home/.rustup/toolchains/nightly/lib/rustlib/src/rust/src/libstd/net/ip.rs:83:1 |
| 275 | // --> $RUST/src/libstd/net/ip.rs:83:1 |
| 276 | line.replace_range(indent + 4..pos + 17, "$RUST" ); |
| 277 | other_crate = true; |
| 278 | } else if let Some(pos) = line.find("/rustlib/src/rust/library/" ) { |
| 279 | // --> /home/.rustup/toolchains/nightly/lib/rustlib/src/rust/library/std/src/net/ip.rs:83:1 |
| 280 | // --> $RUST/std/src/net/ip.rs:83:1 |
| 281 | line.replace_range(indent + 4..pos + 25, "$RUST" ); |
| 282 | other_crate = true; |
| 283 | } else if line[indent + 4..].starts_with("/rustc/" ) |
| 284 | && line |
| 285 | .get(indent + 11..indent + 51) |
| 286 | .map_or(false, is_ascii_lowercase_hex) |
| 287 | && line[indent + 51..].starts_with("/library/" ) |
| 288 | { |
| 289 | // --> /rustc/c5c7d2b37780dac1092e75f12ab97dd56c30861e/library/std/src/net/ip.rs:83:1 |
| 290 | // --> $RUST/std/src/net/ip.rs:83:1 |
| 291 | line.replace_range(indent + 4..indent + 59, "$RUST" ); |
| 292 | other_crate = true; |
| 293 | } |
| 294 | } |
| 295 | if self.normalization >= CargoRegistry && !other_crate { |
| 296 | if let Some(pos) = line |
| 297 | .find("/registry/src/github.com-" ) |
| 298 | .or_else(|| line.find("/registry/src/index.crates.io-" )) |
| 299 | { |
| 300 | let hash_start = pos + line[pos..].find('-' ).unwrap() + 1; |
| 301 | let hash_end = hash_start + 16; |
| 302 | if line |
| 303 | .get(hash_start..hash_end) |
| 304 | .map_or(false, is_ascii_lowercase_hex) |
| 305 | && line[hash_end..].starts_with('/' ) |
| 306 | { |
| 307 | // --> /home/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.64/src/de.rs:2584:8 |
| 308 | // --> $CARGO/serde_json-1.0.64/src/de.rs:2584:8 |
| 309 | line.replace_range(indent + 4..hash_end, "$CARGO" ); |
| 310 | other_crate = true; |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | if other_crate && self.normalization >= WorkspaceLines { |
| 315 | // Blank out line numbers for this particular error since rustc |
| 316 | // tends to reach into code from outside of the test case. The |
| 317 | // test stderr shouldn't need to be updated every time we touch |
| 318 | // those files. |
| 319 | hide_trailing_numbers(&mut line); |
| 320 | self.hide_numbers = 1; |
| 321 | while let Some(next_line) = self.all_lines.get(index + self.hide_numbers) { |
| 322 | match next_line.trim_start().chars().next().unwrap_or_default() { |
| 323 | '0' ..='9' | '|' | '.' => self.hide_numbers += 1, |
| 324 | _ => break, |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | return Some(line); |
| 329 | } |
| 330 | |
| 331 | if line.starts_with("error: aborting due to " ) { |
| 332 | return None; |
| 333 | } |
| 334 | |
| 335 | if line == "To learn more, run the command again with --verbose." { |
| 336 | return None; |
| 337 | } |
| 338 | |
| 339 | if self.normalization >= StripCouldNotCompile { |
| 340 | if line.starts_with("error: Could not compile `" ) { |
| 341 | return None; |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | if self.normalization >= StripCouldNotCompile2 { |
| 346 | if line.starts_with("error: could not compile `" ) { |
| 347 | return None; |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | if self.normalization >= StripForMoreInformation { |
| 352 | if line.starts_with("For more information about this error, try `rustc --explain" ) { |
| 353 | return None; |
| 354 | } |
| 355 | } |
| 356 | |
| 357 | if self.normalization >= StripForMoreInformation2 { |
| 358 | if line.starts_with("Some errors have detailed explanations:" ) { |
| 359 | return None; |
| 360 | } |
| 361 | if line.starts_with("For more information about an error, try `rustc --explain" ) { |
| 362 | return None; |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | if self.normalization >= TrimEnd { |
| 367 | line.truncate(line.trim_end().len()); |
| 368 | } |
| 369 | |
| 370 | if self.normalization >= TypeDirBackslash { |
| 371 | if line |
| 372 | .trim_start() |
| 373 | .starts_with("= note: required because it appears within the type" ) |
| 374 | { |
| 375 | line = line.replace(' \\' , "/" ); |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | if self.normalization >= AndOthers { |
| 380 | let trim_start = line.trim_start(); |
| 381 | if trim_start.starts_with("and " ) && line.ends_with(" others" ) { |
| 382 | let indent = line.len() - trim_start.len(); |
| 383 | let num_start = indent + "and " .len(); |
| 384 | let num_end = line.len() - " others" .len(); |
| 385 | if num_start < num_end |
| 386 | && line[num_start..num_end].bytes().all(|b| b.is_ascii_digit()) |
| 387 | { |
| 388 | line.replace_range(num_start..num_end, "$N" ); |
| 389 | } |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | if self.normalization >= StripLongTypeNameFiles { |
| 394 | let trimmed_line = line.trim_start(); |
| 395 | let trimmed_line = trimmed_line |
| 396 | .strip_prefix("= note: " ) |
| 397 | .unwrap_or(trimmed_line); |
| 398 | if trimmed_line.starts_with("the full type name has been written to" ) { |
| 399 | return None; |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | line = line.replace(self.context.krate, "$CRATE" ); |
| 404 | line = replace_case_insensitive(&line, &self.context.source_dir.to_string_lossy(), "$DIR/" ); |
| 405 | line = replace_case_insensitive( |
| 406 | &line, |
| 407 | &self.context.workspace.to_string_lossy(), |
| 408 | "$WORKSPACE/" , |
| 409 | ); |
| 410 | |
| 411 | Some(line) |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | fn is_ascii_lowercase_hex(s: &str) -> bool { |
| 416 | s.bytes().all(|b| matches!(b, b'0' ..=b'9' | b'a' ..=b'f' )) |
| 417 | } |
| 418 | |
| 419 | // "10 | T: Send," -> " | T: Send," |
| 420 | fn hide_leading_numbers(line: &mut String) { |
| 421 | let n = line.bytes().take_while(u8::is_ascii_digit).count(); |
| 422 | for i in 0..n { |
| 423 | line.replace_range(i..i + 1, " " ); |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | // "main.rs:22:29" -> "main.rs" |
| 428 | fn hide_trailing_numbers(line: &mut String) { |
| 429 | for _ in 0..2 { |
| 430 | let digits = line.bytes().rev().take_while(u8::is_ascii_digit).count(); |
| 431 | if digits == 0 || !line[..line.len() - digits].ends_with(':' ) { |
| 432 | return; |
| 433 | } |
| 434 | line.truncate(line.len() - digits - 1); |
| 435 | } |
| 436 | } |
| 437 | |
| 438 | fn replace_case_insensitive(line: &str, pattern: &str, replacement: &str) -> String { |
| 439 | let line_lower = line.to_ascii_lowercase().replace(' \\' , "/" ); |
| 440 | let pattern_lower = pattern.to_ascii_lowercase().replace(' \\' , "/" ); |
| 441 | let mut replaced = String::with_capacity(line.len()); |
| 442 | |
| 443 | let line_lower = line_lower.as_str(); |
| 444 | let mut split = line_lower.split(&pattern_lower); |
| 445 | let mut pos = 0; |
| 446 | let mut insert_replacement = false; |
| 447 | while let Some(keep) = split.next() { |
| 448 | if insert_replacement { |
| 449 | replaced.push_str(replacement); |
| 450 | pos += pattern.len(); |
| 451 | } |
| 452 | let mut keep = &line[pos..pos + keep.len()]; |
| 453 | if insert_replacement { |
| 454 | let end_of_maybe_path = keep.find(&[' ' , ':' ][..]).unwrap_or(keep.len()); |
| 455 | replaced.push_str(&keep[..end_of_maybe_path].replace(' \\' , "/" )); |
| 456 | pos += end_of_maybe_path; |
| 457 | keep = &keep[end_of_maybe_path..]; |
| 458 | } |
| 459 | replaced.push_str(keep); |
| 460 | pos += keep.len(); |
| 461 | insert_replacement = true; |
| 462 | if replaced.ends_with(|ch: char| ch.is_ascii_alphanumeric()) { |
| 463 | if let Some(ch) = line[pos..].chars().next() { |
| 464 | replaced.push(ch); |
| 465 | pos += ch.len_utf8(); |
| 466 | split = line_lower[pos..].split(&pattern_lower); |
| 467 | insert_replacement = false; |
| 468 | } |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | replaced |
| 473 | } |
| 474 | |
| 475 | #[derive(PartialEq)] |
| 476 | enum IndentedLineKind { |
| 477 | // `error` |
| 478 | // `warning` |
| 479 | Heading, |
| 480 | |
| 481 | // Contains max number of spaces that can be cut based on this line. |
| 482 | // ` --> foo` = 2 |
| 483 | // ` | foo` = 3 |
| 484 | // ` ::: foo` = 2 |
| 485 | // `10 | foo` = 1 |
| 486 | Code(usize), |
| 487 | |
| 488 | // `note:` |
| 489 | // `...` |
| 490 | Note, |
| 491 | |
| 492 | // Contains number of leading spaces. |
| 493 | Other(usize), |
| 494 | } |
| 495 | |
| 496 | fn unindent(diag: String, normalization: Normalization) -> String { |
| 497 | if normalization < Unindent { |
| 498 | return diag; |
| 499 | } |
| 500 | |
| 501 | let mut normalized = String::new(); |
| 502 | let mut lines = diag.lines(); |
| 503 | |
| 504 | while let Some(line) = lines.next() { |
| 505 | normalized.push_str(line); |
| 506 | normalized.push(' \n' ); |
| 507 | |
| 508 | if indented_line_kind(line, normalization) != IndentedLineKind::Heading { |
| 509 | continue; |
| 510 | } |
| 511 | |
| 512 | let mut ahead = lines.clone(); |
| 513 | let next_line = match ahead.next() { |
| 514 | Some(line) => line, |
| 515 | None => continue, |
| 516 | }; |
| 517 | |
| 518 | if let IndentedLineKind::Code(indent) = indented_line_kind(next_line, normalization) { |
| 519 | if next_line[indent + 1..].starts_with("--> " ) { |
| 520 | let mut lines_in_block = 1; |
| 521 | let mut least_indent = indent; |
| 522 | while let Some(line) = ahead.next() { |
| 523 | match indented_line_kind(line, normalization) { |
| 524 | IndentedLineKind::Heading => break, |
| 525 | IndentedLineKind::Code(indent) => { |
| 526 | lines_in_block += 1; |
| 527 | least_indent = cmp::min(least_indent, indent); |
| 528 | } |
| 529 | IndentedLineKind::Note => lines_in_block += 1, |
| 530 | IndentedLineKind::Other(spaces) => { |
| 531 | if spaces > 10 { |
| 532 | lines_in_block += 1; |
| 533 | } else { |
| 534 | break; |
| 535 | } |
| 536 | } |
| 537 | } |
| 538 | } |
| 539 | for _ in 0..lines_in_block { |
| 540 | let line = lines.next().unwrap(); |
| 541 | if let IndentedLineKind::Code(_) | IndentedLineKind::Other(_) = |
| 542 | indented_line_kind(line, normalization) |
| 543 | { |
| 544 | let space = line.find(' ' ).unwrap(); |
| 545 | normalized.push_str(&line[..space]); |
| 546 | normalized.push_str(&line[space + least_indent..]); |
| 547 | } else { |
| 548 | normalized.push_str(line); |
| 549 | } |
| 550 | normalized.push(' \n' ); |
| 551 | } |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | normalized |
| 557 | } |
| 558 | |
| 559 | fn indented_line_kind(line: &str, normalization: Normalization) -> IndentedLineKind { |
| 560 | if let Some(heading_len) = if line.starts_with("error" ) { |
| 561 | Some("error" .len()) |
| 562 | } else if line.starts_with("warning" ) { |
| 563 | Some("warning" .len()) |
| 564 | } else { |
| 565 | None |
| 566 | } { |
| 567 | if line[heading_len..].starts_with(&[':' , '[' ][..]) { |
| 568 | return IndentedLineKind::Heading; |
| 569 | } |
| 570 | } |
| 571 | |
| 572 | if line.starts_with("note:" ) |
| 573 | || line == "..." |
| 574 | || normalization >= UnindentAfterHelp && line.starts_with("help:" ) |
| 575 | { |
| 576 | return IndentedLineKind::Note; |
| 577 | } |
| 578 | |
| 579 | let is_space = |b: &u8| *b == b' ' ; |
| 580 | if let Some(rest) = line.strip_prefix("... " ) { |
| 581 | let spaces = rest.bytes().take_while(is_space).count(); |
| 582 | return IndentedLineKind::Code(spaces); |
| 583 | } |
| 584 | |
| 585 | let digits = line.bytes().take_while(u8::is_ascii_digit).count(); |
| 586 | let spaces = line[digits..].bytes().take_while(|b| *b == b' ' ).count(); |
| 587 | let rest = &line[digits + spaces..]; |
| 588 | if spaces > 0 |
| 589 | && (rest == "|" |
| 590 | || rest.starts_with("| " ) |
| 591 | || digits == 0 |
| 592 | && (rest.starts_with("--> " ) || rest.starts_with("::: " ) || rest.starts_with("= " ))) |
| 593 | { |
| 594 | return IndentedLineKind::Code(spaces - 1); |
| 595 | } |
| 596 | |
| 597 | IndentedLineKind::Other(if digits == 0 { spaces } else { 0 }) |
| 598 | } |
| 599 | |