trybuild_internals_api/
normalize.rs

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