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::path::Path;
10
11#[derive(Copy, Clone)]
12pub 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
21macro_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
42normalizations! {
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 AndOthersVerbose,
62 }
65
66pub fn diagnostics(output: &str, context: Context) -> Variations {
78 let output = output.replace("\r\n", "\n");
79
80 let mut result = Variations::default();
81 for (i, normalization) in Normalization::ALL.iter().enumerate() {
82 result.variations[i] = apply(&output, *normalization, context);
83 }
84
85 result
86}
87
88pub struct Variations {
89 variations: [String; Normalization::ALL.len()],
90}
91
92impl Variations {
93 pub fn preferred(&self) -> &str {
94 self.variations.last().unwrap()
95 }
96
97 pub fn any<F: FnMut(&str) -> bool>(&self, mut f: F) -> bool {
98 self.variations.iter().any(|stderr| f(stderr))
99 }
100
101 pub fn concat(&mut self, other: &Self) {
102 for (this, other) in self.variations.iter_mut().zip(&other.variations) {
103 if !this.is_empty() && !other.is_empty() {
104 this.push('\n');
105 }
106 this.push_str(other);
107 }
108 }
109}
110
111pub fn trim<S: AsRef<[u8]>>(output: S) -> String {
112 let bytes = output.as_ref();
113 let mut normalized = String::from_utf8_lossy(bytes).into_owned();
114
115 let len = normalized.trim_end().len();
116 normalized.truncate(len);
117
118 if !normalized.is_empty() {
119 normalized.push('\n');
120 }
121
122 normalized
123}
124
125fn apply(original: &str, normalization: Normalization, context: Context) -> String {
126 let mut normalized = String::new();
127
128 let lines: Vec<&str> = original.lines().collect();
129 let mut filter = Filter {
130 all_lines: &lines,
131 normalization,
132 context,
133 hide_numbers: 0,
134 other_types: None,
135 };
136 for i in 0..lines.len() {
137 if let Some(line) = filter.apply(i) {
138 normalized += &line;
139 if !normalized.ends_with("\n\n") {
140 normalized.push('\n');
141 }
142 }
143 }
144
145 normalized = unindent(normalized, normalization);
146
147 trim(normalized)
148}
149
150struct Filter<'a> {
151 all_lines: &'a [&'a str],
152 normalization: Normalization,
153 context: Context<'a>,
154 hide_numbers: usize,
155 other_types: Option<usize>,
156}
157
158impl<'a> Filter<'a> {
159 fn apply(&mut self, index: usize) -> Option<String> {
160 let mut line = self.all_lines[index].to_owned();
161
162 if self.hide_numbers > 0 {
163 hide_leading_numbers(&mut line);
164 self.hide_numbers -= 1;
165 }
166
167 let trim_start = line.trim_start();
168 let indent = line.len() - trim_start.len();
169 let prefix = if trim_start.starts_with("--> ") {
170 Some("--> ")
171 } else if trim_start.starts_with("::: ") {
172 Some("::: ")
173 } else {
174 None
175 };
176
177 if prefix == Some("--> ") && self.normalization < ArrowOtherCrate {
178 if let Some(cut_end) = line.rfind(&['/', '\\'][..]) {
179 let cut_start = indent + 4;
180 line.replace_range(cut_start..cut_end + 1, "$DIR/");
181 return Some(line);
182 }
183 }
184
185 if prefix.is_some() {
186 line = line.replace('\\', "/");
187 let line_lower = line.to_ascii_lowercase();
188 let target_dir_pat = self
189 .context
190 .target_dir
191 .to_string_lossy()
192 .to_ascii_lowercase()
193 .replace('\\', "/");
194 let source_dir_pat = self
195 .context
196 .source_dir
197 .to_string_lossy()
198 .to_ascii_lowercase()
199 .replace('\\', "/");
200 let mut other_crate = false;
201 if line_lower.find(&target_dir_pat) == Some(indent + 4) {
202 let mut offset = indent + 4 + target_dir_pat.len();
203 let mut out_dir_crate_name = None;
204 while let Some(slash) = line[offset..].find('/') {
205 let component = &line[offset..offset + slash];
206 if component == "out" {
207 if let Some(out_dir_crate_name) = out_dir_crate_name {
208 let replacement = format!("$OUT_DIR[{}]", out_dir_crate_name);
209 line.replace_range(indent + 4..offset + 3, &replacement);
210 other_crate = true;
211 break;
212 }
213 } else if component.len() > 17
214 && component.rfind('-') == Some(component.len() - 17)
215 && is_ascii_lowercase_hex(&component[component.len() - 16..])
216 {
217 out_dir_crate_name = Some(&component[..component.len() - 17]);
218 } else {
219 out_dir_crate_name = None;
220 }
221 offset += slash + 1;
222 }
223 } else if let Some(i) = line_lower.find(&source_dir_pat) {
224 if self.normalization >= RelativeToDir && i == indent + 4 {
225 line.replace_range(i..i + source_dir_pat.len(), "");
226 if self.normalization < LinesOutsideInputFile {
227 return Some(line);
228 }
229 let input_file_pat = self
230 .context
231 .input_file
232 .to_string_lossy()
233 .to_ascii_lowercase()
234 .replace('\\', "/");
235 if line_lower[i + source_dir_pat.len()..].starts_with(&input_file_pat) {
236 return Some(line);
240 }
241 } else {
242 line.replace_range(i..i + source_dir_pat.len() - 1, "$DIR");
243 if self.normalization < LinesOutsideInputFile {
244 return Some(line);
245 }
246 }
247 other_crate = true;
248 } else {
249 let workspace_pat = self
250 .context
251 .workspace
252 .to_string_lossy()
253 .to_ascii_lowercase()
254 .replace('\\', "/");
255 if let Some(i) = line_lower.find(&workspace_pat) {
256 line.replace_range(i..i + workspace_pat.len() - 1, "$WORKSPACE");
257 other_crate = true;
258 }
259 }
260 if self.normalization >= PathDependencies && !other_crate {
261 for path_dep in self.context.path_dependencies {
262 let path_dep_pat = path_dep
263 .normalized_path
264 .to_string_lossy()
265 .to_ascii_lowercase()
266 .replace('\\', "/");
267 if let Some(i) = line_lower.find(&path_dep_pat) {
268 let var = format!("${}", path_dep.name.to_uppercase().replace('-', "_"));
269 line.replace_range(i..i + path_dep_pat.len() - 1, &var);
270 other_crate = true;
271 break;
272 }
273 }
274 }
275 if self.normalization >= RustLib && !other_crate {
276 if let Some(pos) = line.find("/rustlib/src/rust/src/") {
277 line.replace_range(indent + 4..pos + 17, "$RUST");
280 other_crate = true;
281 } else if let Some(pos) = line.find("/rustlib/src/rust/library/") {
282 line.replace_range(indent + 4..pos + 25, "$RUST");
285 other_crate = true;
286 } else if line[indent + 4..].starts_with("/rustc/")
287 && line
288 .get(indent + 11..indent + 51)
289 .map_or(false, is_ascii_lowercase_hex)
290 && line[indent + 51..].starts_with("/library/")
291 {
292 line.replace_range(indent + 4..indent + 59, "$RUST");
295 other_crate = true;
296 }
297 }
298 if self.normalization >= CargoRegistry && !other_crate {
299 if let Some(pos) = line
300 .find("/registry/src/github.com-")
301 .or_else(|| line.find("/registry/src/index.crates.io-"))
302 {
303 let hash_start = pos + line[pos..].find('-').unwrap() + 1;
304 let hash_end = hash_start + 16;
305 if line
306 .get(hash_start..hash_end)
307 .map_or(false, is_ascii_lowercase_hex)
308 && line[hash_end..].starts_with('/')
309 {
310 line.replace_range(indent + 4..hash_end, "$CARGO");
313 other_crate = true;
314 }
315 }
316 }
317 if other_crate && self.normalization >= WorkspaceLines {
318 hide_trailing_numbers(&mut line);
323 self.hide_numbers = 1;
324 while let Some(next_line) = self.all_lines.get(index + self.hide_numbers) {
325 match next_line.trim_start().chars().next().unwrap_or_default() {
326 '0'..='9' | '|' | '.' => self.hide_numbers += 1,
327 _ => break,
328 }
329 }
330 }
331 return Some(line);
332 }
333
334 if line.starts_with("error: aborting due to ") {
335 return None;
336 }
337
338 if line == "To learn more, run the command again with --verbose." {
339 return None;
340 }
341
342 if trim_start.starts_with("= note: this compiler was built on 2")
343 && trim_start.ends_with("; consider upgrading it if it is out of date")
344 {
345 return None;
346 }
347
348 if self.normalization >= StripCouldNotCompile {
349 if line.starts_with("error: Could not compile `") {
350 return None;
351 }
352 }
353
354 if self.normalization >= StripCouldNotCompile2 {
355 if line.starts_with("error: could not compile `") {
356 return None;
357 }
358 }
359
360 if self.normalization >= StripForMoreInformation {
361 if line.starts_with("For more information about this error, try `rustc --explain") {
362 return None;
363 }
364 }
365
366 if self.normalization >= StripForMoreInformation2 {
367 if line.starts_with("Some errors have detailed explanations:") {
368 return None;
369 }
370 if line.starts_with("For more information about an error, try `rustc --explain") {
371 return None;
372 }
373 }
374
375 if self.normalization >= TrimEnd {
376 line.truncate(line.trim_end().len());
377 }
378
379 if self.normalization >= TypeDirBackslash {
380 if line
381 .trim_start()
382 .starts_with("= note: required because it appears within the type")
383 {
384 line = line.replace('\\', "/");
385 }
386 }
387
388 if self.normalization >= AndOthers {
389 let trim_start = line.trim_start();
390 if trim_start.starts_with("and ") && line.ends_with(" others") {
391 let indent = line.len() - trim_start.len();
392 let num_start = indent + "and ".len();
393 let num_end = line.len() - " others".len();
394 if num_start < num_end
395 && line[num_start..num_end].bytes().all(|b| b.is_ascii_digit())
396 {
397 line.replace_range(num_start..num_end, "$N");
398 }
399 }
400 }
401
402 if self.normalization >= StripLongTypeNameFiles {
403 let trimmed_line = line.trim_start();
404 let trimmed_line = trimmed_line
405 .strip_prefix("= note: ")
406 .unwrap_or(trimmed_line);
407 if trimmed_line.starts_with("the full type name has been written to")
408 || trimmed_line.starts_with("the full name for the type has been written to")
409 {
410 return None;
411 }
412 }
413
414 if self.normalization >= AndOthersVerbose {
415 let trim_start = line.trim_start();
416 if trim_start.starts_with("= help: the following types implement trait ")
417 || trim_start.starts_with("= help: the following other types implement trait ")
418 {
419 self.other_types = Some(0);
420 } else if let Some(count_other_types) = &mut self.other_types {
421 if indent >= 12 && trim_start != "and $N others" {
422 *count_other_types += 1;
423 if *count_other_types == 9 {
424 if let Some(next) = self.all_lines.get(index + 1) {
425 let next_trim_start = next.trim_start();
426 let next_indent = next.len() - next_trim_start.len();
427 if indent == next_indent {
428 line.replace_range(indent - 2.., "and $N others");
429 }
430 }
431 } else if *count_other_types > 9 {
432 return None;
433 }
434 } else {
435 self.other_types = None;
436 }
437 }
438 }
439
440 line = line.replace(self.context.krate, "$CRATE");
441 line = replace_case_insensitive(&line, &self.context.source_dir.to_string_lossy(), "$DIR/");
442 line = replace_case_insensitive(
443 &line,
444 &self.context.workspace.to_string_lossy(),
445 "$WORKSPACE/",
446 );
447
448 Some(line)
449 }
450}
451
452fn is_ascii_lowercase_hex(s: &str) -> bool {
453 s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
454}
455
456fn hide_leading_numbers(line: &mut String) {
458 let n = line.bytes().take_while(u8::is_ascii_digit).count();
459 for i in 0..n {
460 line.replace_range(i..i + 1, " ");
461 }
462}
463
464fn hide_trailing_numbers(line: &mut String) {
466 for _ in 0..2 {
467 let digits = line.bytes().rev().take_while(u8::is_ascii_digit).count();
468 if digits == 0 || !line[..line.len() - digits].ends_with(':') {
469 return;
470 }
471 line.truncate(line.len() - digits - 1);
472 }
473}
474
475fn replace_case_insensitive(line: &str, pattern: &str, replacement: &str) -> String {
476 let line_lower = line.to_ascii_lowercase().replace('\\', "/");
477 let pattern_lower = pattern.to_ascii_lowercase().replace('\\', "/");
478 let mut replaced = String::with_capacity(line.len());
479
480 let line_lower = line_lower.as_str();
481 let mut split = line_lower.split(&pattern_lower);
482 let mut pos = 0;
483 let mut insert_replacement = false;
484 while let Some(keep) = split.next() {
485 if insert_replacement {
486 replaced.push_str(replacement);
487 pos += pattern.len();
488 }
489 let mut keep = &line[pos..pos + keep.len()];
490 if insert_replacement {
491 let end_of_maybe_path = keep.find(&[' ', ':'][..]).unwrap_or(keep.len());
492 replaced.push_str(&keep[..end_of_maybe_path].replace('\\', "/"));
493 pos += end_of_maybe_path;
494 keep = &keep[end_of_maybe_path..];
495 }
496 replaced.push_str(keep);
497 pos += keep.len();
498 insert_replacement = true;
499 if replaced.ends_with(|ch: char| ch.is_ascii_alphanumeric()) {
500 if let Some(ch) = line[pos..].chars().next() {
501 replaced.push(ch);
502 pos += ch.len_utf8();
503 split = line_lower[pos..].split(&pattern_lower);
504 insert_replacement = false;
505 }
506 }
507 }
508
509 replaced
510}
511
512#[derive(PartialEq)]
513enum IndentedLineKind {
514 Heading,
517
518 Code(usize),
524
525 Note,
528
529 Other(usize),
531}
532
533fn unindent(diag: String, normalization: Normalization) -> String {
534 if normalization < Unindent {
535 return diag;
536 }
537
538 let mut normalized = String::new();
539 let mut lines = diag.lines();
540
541 while let Some(line) = lines.next() {
542 normalized.push_str(line);
543 normalized.push('\n');
544
545 if indented_line_kind(line, normalization) != IndentedLineKind::Heading {
546 continue;
547 }
548
549 let mut ahead = lines.clone();
550 let Some(next_line) = ahead.next() else {
551 continue;
552 };
553
554 if let IndentedLineKind::Code(indent) = indented_line_kind(next_line, normalization) {
555 if next_line[indent + 1..].starts_with("--> ") {
556 let mut lines_in_block = 1;
557 let mut least_indent = indent;
558 while let Some(line) = ahead.next() {
559 match indented_line_kind(line, normalization) {
560 IndentedLineKind::Heading => break,
561 IndentedLineKind::Code(indent) => {
562 lines_in_block += 1;
563 least_indent = cmp::min(least_indent, indent);
564 }
565 IndentedLineKind::Note => lines_in_block += 1,
566 IndentedLineKind::Other(spaces) => {
567 if spaces > 10 {
568 lines_in_block += 1;
569 } else {
570 break;
571 }
572 }
573 }
574 }
575 for _ in 0..lines_in_block {
576 let line = lines.next().unwrap();
577 if let IndentedLineKind::Code(_) | IndentedLineKind::Other(_) =
578 indented_line_kind(line, normalization)
579 {
580 let space = line.find(' ').unwrap();
581 normalized.push_str(&line[..space]);
582 normalized.push_str(&line[space + least_indent..]);
583 } else {
584 normalized.push_str(line);
585 }
586 normalized.push('\n');
587 }
588 }
589 }
590 }
591
592 normalized
593}
594
595fn indented_line_kind(line: &str, normalization: Normalization) -> IndentedLineKind {
596 if let Some(heading_len) = if line.starts_with("error") {
597 Some("error".len())
598 } else if line.starts_with("warning") {
599 Some("warning".len())
600 } else {
601 None
602 } {
603 if line[heading_len..].starts_with(&[':', '['][..]) {
604 return IndentedLineKind::Heading;
605 }
606 }
607
608 if line.starts_with("note:")
609 || line == "..."
610 || normalization >= UnindentAfterHelp && line.starts_with("help:")
611 {
612 return IndentedLineKind::Note;
613 }
614
615 let is_space = |b: &u8| *b == b' ';
616 if let Some(rest) = line.strip_prefix("... ") {
617 let spaces = rest.bytes().take_while(is_space).count();
618 return IndentedLineKind::Code(spaces);
619 }
620
621 let digits = line.bytes().take_while(u8::is_ascii_digit).count();
622 let spaces = line[digits..].bytes().take_while(|b| *b == b' ').count();
623 let rest = &line[digits + spaces..];
624 if spaces > 0
625 && (rest == "|"
626 || rest.starts_with("| ")
627 || digits == 0
628 && (rest.starts_with("--> ") || rest.starts_with("::: ") || rest.starts_with("= ")))
629 {
630 return IndentedLineKind::Code(spaces - 1);
631 }
632
633 IndentedLineKind::Other(if digits == 0 { spaces } else { 0 })
634}