Repository: rust-lang/annotate-snippets-rs (rust)

Issue:
Provide a way to 'anonymize' line numbers when building a Snippet
In the rust test suite there's the option to replace line numbers in the diagnostics output with letters, so that changes in the output produce less noisy diffs.

With that flag enabled, the following

```
error: cannot concatenate a byte string literal
  --> $DIR/concat.rs:2:13
   |
 2 |     concat!(b'f');
   |             ^^^^
```

is turned into

```
error: cannot concatenate a byte string literal
  --> $DIR/concat.rs:2:13
   |
LL |     concat!(b'f');
   |             ^^^^
```

Any line number is replaced by `LL`, no matter the digits of that number.

AFAICT, rustc would currently have to implement its own `Snippet -> DisplayList` conversion, which would duplicate a lot of the conversion code just to anonymize the line numbers.

I think this would be nice to have inside `annotate-snippets` too, maybe as an option when constructing a Snippet? I believe other users of this library could also benefit from this option.


Pull request description:
Add option to anonymize line numbers
With the current diagnostics emitter of Rust, the `-Z ui-testing` flag
allows to 'anonymize' line numbers in the UI test output.

This means that a line such as:

     2 |     concat!(b'f');

is turned into:

    LL |     concat!(b'f');

This is done because it makes the diff of UI test output changes much
less noisy.

To support this with `annotate-snippet`, we add a second parameter to
`DisplayListFormatter` that, when true, replaces the line numbers in the
left column with the text `LL`. The replacement text is always `LL` and
it does not affect the initial location line number.

In the new `annotate-snippet` emitter in rustc, we can then use this
parameter depending on whether the `-Z ui-testing` flag is set or not.

Closes #2 
cc https://github.com/rust-lang/rust/issues/59346

Patch:
diff --git a/examples/expected_type.rs b/examples/expected_type.rs
index 6c6ed01..6ad3c36 100644
--- a/examples/expected_type.rs
+++ b/examples/expected_type.rs
@@ -37,6 +37,6 @@ fn main() {
     };
 
     let dl = DisplayList::from(snippet);
-    let dlf = DisplayListFormatter::new(true);
+    let dlf = DisplayListFormatter::new(true, false);
     println!("{}", dlf.format(&dl));
 }
diff --git a/examples/footer.rs b/examples/footer.rs
index 7cbc949..1814be1 100644
--- a/examples/footer.rs
+++ b/examples/footer.rs
@@ -34,6 +34,6 @@ fn main() {
     };
 
     let dl = DisplayList::from(snippet);
-    let dlf = DisplayListFormatter::new(true);
+    let dlf = DisplayListFormatter::new(true, false);
     println!("{}", dlf.format(&dl));
 }
diff --git a/examples/format.rs b/examples/format.rs
index 30d86b5..d6a77ae 100644
--- a/examples/format.rs
+++ b/examples/format.rs
@@ -55,6 +55,6 @@ fn main() {
     };
 
     let dl = DisplayList::from(snippet);
-    let dlf = DisplayListFormatter::new(true);
+    let dlf = DisplayListFormatter::new(true, false);
     println!("{}", dlf.format(&dl));
 }
diff --git a/examples/multislice.rs b/examples/multislice.rs
index e1f2946..47981b7 100644
--- a/examples/multislice.rs
+++ b/examples/multislice.rs
@@ -31,6 +31,6 @@ fn main() {
     };
 
     let dl = DisplayList::from(snippet);
-    let dlf = DisplayListFormatter::new(true);
+    let dlf = DisplayListFormatter::new(true, false);
     println!("{}", dlf.format(&dl));
 }
diff --git a/src/display_list/structs.rs b/src/display_list/structs.rs
index ecd9eb4..0d3e0bc 100644
--- a/src/display_list/structs.rs
+++ b/src/display_list/structs.rs
@@ -131,7 +131,7 @@ pub enum DisplayMarkType {
     /// use annotate_snippets::display_list::*;
     /// use annotate_snippets::formatter::DisplayListFormatter;
     ///
-    /// let dlf = DisplayListFormatter::new(false); // Don't use colors
+    /// let dlf = DisplayListFormatter::new(false, false); // Don't use colors
     ///
     /// let dl = DisplayList {
     ///     body: vec![
@@ -161,7 +161,7 @@ pub enum DisplayMarkType {
     /// use annotate_snippets::display_list::*;
     /// use annotate_snippets::formatter::DisplayListFormatter;
     ///
-    /// let dlf = DisplayListFormatter::new(false); // Don't use colors
+    /// let dlf = DisplayListFormatter::new(false, false); // Don't use colors
     ///
     /// let dl = DisplayList {
     ///     body: vec![
@@ -214,7 +214,7 @@ pub enum DisplayHeaderType {
     /// use annotate_snippets::display_list::*;
     /// use annotate_snippets::formatter::DisplayListFormatter;
     ///
-    /// let dlf = DisplayListFormatter::new(false); // Don't use colors
+    /// let dlf = DisplayListFormatter::new(false, false); // Don't use colors
     ///
     /// let dl = DisplayList {
     ///     body: vec![
@@ -236,7 +236,7 @@ pub enum DisplayHeaderType {
     /// use annotate_snippets::display_list::*;
     /// use annotate_snippets::formatter::DisplayListFormatter;
     ///
-    /// let dlf = DisplayListFormatter::new(false); // Don't use colors
+    /// let dlf = DisplayListFormatter::new(false, false); // Don't use colors
     ///
     /// let dl = DisplayList {
     ///     body: vec![
diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs
index 189c90d..eb1392d 100644
--- a/src/formatter/mod.rs
+++ b/src/formatter/mod.rs
@@ -20,9 +20,11 @@ fn repeat_char(c: char, n: usize) -> String {
     s.repeat(n)
 }
 
-/// DisplayListFormatter' constructor accepts a single argument which
-/// allows the formatter to optionally apply colors and emphasis
-/// using `ansi_term` crate.
+/// DisplayListFormatter' constructor accepts two arguments:
+///
+/// * `color` allows the formatter to optionally apply colors and emphasis
+/// using the `ansi_term` crate.
+/// * `anonymized_line_numbers` will replace line numbers in the left column with the text `LL`.
 ///
 /// Example:
 ///
@@ -30,7 +32,7 @@ fn repeat_char(c: char, n: usize) -> String {
 /// use annotate_snippets::formatter::DisplayListFormatter;
 /// use annotate_snippets::display_list::{DisplayList, DisplayLine, DisplaySourceLine};
 ///
-/// let dlf = DisplayListFormatter::new(false); // Don't use colors
+/// let dlf = DisplayListFormatter::new(false, false); // Don't use colors, Don't anonymize line numbers
 ///
 /// let dl = DisplayList {
 ///     body: vec![
@@ -48,23 +50,33 @@ fn repeat_char(c: char, n: usize) -> String {
 /// ```
 pub struct DisplayListFormatter {
     stylesheet: Box<dyn Stylesheet>,
+    anonymized_line_numbers: bool,
 }
 
 impl DisplayListFormatter {
-    /// Constructor for the struct. The argument `color` selects
-    /// the stylesheet depending on the user preferences and `ansi_term`
-    /// crate availability.
-    pub fn new(color: bool) -> Self {
+    const ANONYMIZED_LINE_NUM: &'static str = "LL";
+
+    /// Constructor for the struct.
+    ///
+    /// The argument `color` selects the stylesheet depending on the user preferences and
+    /// `ansi_term` crate availability.
+    ///
+    /// The argument `anonymized_line_numbers` will replace line numbers in the left column with
+    /// the text `LL`. This can be useful to enable when running UI tests, such as in the Rust
+    /// test suite.
+    pub fn new(color: bool, anonymized_line_numbers: bool) -> Self {
         if color {
             Self {
                 #[cfg(feature = "ansi_term")]
                 stylesheet: Box::new(AnsiTermStylesheet {}),
                 #[cfg(not(feature = "ansi_term"))]
                 stylesheet: Box::new(NoColorStylesheet {}),
+                anonymized_line_numbers,
             }
         } else {
             Self {
                 stylesheet: Box::new(NoColorStylesheet {}),
+                anonymized_line_numbers,
             }
         }
     }
@@ -75,7 +87,13 @@ impl DisplayListFormatter {
             DisplayLine::Source {
                 lineno: Some(lineno),
                 ..
-            } => cmp::max(lineno.to_string().len(), max),
+            } => {
+                if self.anonymized_line_numbers {
+                    Self::ANONYMIZED_LINE_NUM.len()
+                } else {
+                    cmp::max(lineno.to_string().len(), max)
+                }
+            },
             _ => max,
         });
         let inline_marks_width = dl.body.iter().fold(0, |max, line| match line {
@@ -285,7 +303,11 @@ impl DisplayListFormatter {
                 inline_marks,
                 line,
             } => {
-                let lineno = self.format_lineno(*lineno, lineno_width);
+                let lineno = if self.anonymized_line_numbers {
+                    Self::ANONYMIZED_LINE_NUM.to_string()
+                } else {
+                    self.format_lineno(*lineno, lineno_width)
+                };
                 let marks = self.format_inline_marks(inline_marks, inline_marks_width);
                 let lf = self.format_source_line(line);
                 let lineno_color = self.stylesheet.get_style(StyleClass::LineNo);


Test patch:
diff --git a/tests/fixtures.rs b/tests/fixtures.rs
index 869ca2e..b6fc147 100644
--- a/tests/fixtures.rs
+++ b/tests/fixtures.rs
@@ -46,7 +46,7 @@ fn test_fixtures() {
         let expected_out = read_file(&path_out).expect("Failed to read file");
 
         let dl = DisplayList::from(snippet);
-        let dlf = DisplayListFormatter::new(true);
+        let dlf = DisplayListFormatter::new(true, false);
         let actual_out = dlf.format(&dl);
         println!("{}", expected_out);
         println!("{}", actual_out.trim_end());
diff --git a/tests/formatter.rs b/tests/formatter.rs
index 2f5dee6..c0bdc11 100644
--- a/tests/formatter.rs
+++ b/tests/formatter.rs
@@ -11,7 +11,7 @@ fn test_source_empty() {
         line: DisplaySourceLine::Empty,
     }]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), " |");
 }
@@ -37,7 +37,7 @@ fn test_source_content() {
         },
     ]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(
         dlf.format(&dl),
@@ -65,7 +65,7 @@ fn test_source_annotation_standalone_singleline() {
         },
     }]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), " | ^^^^^ Example string");
 }
@@ -109,7 +109,7 @@ fn test_source_annotation_standalone_multiline() {
         },
     ]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(
         dlf.format(&dl),
@@ -241,7 +241,7 @@ fn test_source_annotation_standalone_multi_annotation() {
         },
     ]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), " | ----- info: Example string\n |             Second line\n |       warning: This is a note\n |                Second line of the warning\n | ----- info: This is an info\n | ----- help: This is help\n |  This is an annotation of type none");
 }
@@ -270,7 +270,7 @@ fn test_fold_line() {
         },
     ]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(
         dlf.format(&dl),
@@ -286,7 +286,7 @@ fn test_raw_origin_initial_nopos() {
         header_type: DisplayHeaderType::Initial,
     })]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), "--> src/test.rs");
 }
@@ -299,7 +299,7 @@ fn test_raw_origin_initial_pos() {
         header_type: DisplayHeaderType::Initial,
     })]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), "--> src/test.rs:23:15");
 }
@@ -312,7 +312,7 @@ fn test_raw_origin_continuation() {
         header_type: DisplayHeaderType::Continuation,
     })]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), "::: src/test.rs:23:15");
 }
@@ -332,7 +332,7 @@ fn test_raw_annotation_unaligned() {
         continuation: false,
     })]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), "error[E0001]: This is an error");
 }
@@ -366,7 +366,7 @@ fn test_raw_annotation_unaligned_multiline() {
         }),
     ]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(
         dlf.format(&dl),
@@ -389,7 +389,7 @@ fn test_raw_annotation_aligned() {
         continuation: false,
     })]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), " = error[E0001]: This is an error");
 }
@@ -423,7 +423,7 @@ fn test_raw_annotation_aligned_multiline() {
         }),
     ]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(
         dlf.format(&dl),
@@ -472,7 +472,7 @@ fn test_different_annotation_types() {
         }),
     ]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(
         dlf.format(&dl),
@@ -491,7 +491,50 @@ fn test_inline_marks_empty_line() {
         line: DisplaySourceLine::Empty,
     }]);
 
-    let dlf = DisplayListFormatter::new(false);
+    let dlf = DisplayListFormatter::new(false, false);
 
     assert_eq!(dlf.format(&dl), " | |",);
 }
+
+#[test]
+fn test_anon_lines() {
+    let dl = DisplayList::from(vec![
+        DisplayLine::Source {
+            lineno: Some(56),
+            inline_marks: vec![],
+            line: DisplaySourceLine::Content {
+                text: "This is an example".to_string(),
+                range: (0, 19),
+            },
+        },
+        DisplayLine::Source {
+            lineno: Some(57),
+            inline_marks: vec![],
+            line: DisplaySourceLine::Content {
+                text: "of content lines".to_string(),
+                range: (0, 19),
+            },
+        },
+    ]);
+
+    let dlf = DisplayListFormatter::new(false, true);
+
+    assert_eq!(
+        dlf.format(&dl),
+        "LL | This is an example\nLL | of content lines"
+    );
+}
+
+#[test]
+fn test_raw_origin_initial_pos_anon_lines() {
+    let dl = DisplayList::from(vec![DisplayLine::Raw(DisplayRawLine::Origin {
+        path: "src/test.rs".to_string(),
+        pos: Some((23, 15)),
+        header_type: DisplayHeaderType::Initial,
+    })]);
+
+    let dlf = DisplayListFormatter::new(false, true);
+
+    // Using anonymized_line_numbers should not affect the inital position
+    assert_eq!(dlf.format(&dl), "--> src/test.rs:23:15");
+}