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 }
70
71pub 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 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 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 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 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 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 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
475fn 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
486fn 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 Heading,
539
540 Code(usize),
546
547 Note,
550
551 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}