Add diag::Formatter::Style::print_newline_at_end

Automatically prints a newline at the end of the last diagnostic in a list. Defaults to true.

Disabled for many tests that assume no newline at end of string.

Change-Id: Id1c2f7771f03f22d926fafc2bebebcef056ac5e8
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/37260
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/diagnostic/formatter.cc b/src/diagnostic/formatter.cc
index 94affb6..6529eec 100644
--- a/src/diagnostic/formatter.cc
+++ b/src/diagnostic/formatter.cc
@@ -121,6 +121,9 @@
     format(diag, state);
     first = false;
   }
+  if (style_.print_newline_at_end) {
+    state.newline();
+  }
 }
 
 void Formatter::format(const Diagnostic& diag, State& state) const {
diff --git a/src/diagnostic/formatter.h b/src/diagnostic/formatter.h
index 2df249f..8ed0639 100644
--- a/src/diagnostic/formatter.h
+++ b/src/diagnostic/formatter.h
@@ -36,6 +36,8 @@
     bool print_severity = true;
     /// include the source line(s) for the diagnostic
     bool print_line = true;
+    /// print a newline at the end of a diagnostic list
+    bool print_newline_at_end = true;
   };
 
   /// Constructor for the formatter using a default style.
diff --git a/src/diagnostic/formatter_test.cc b/src/diagnostic/formatter_test.cc
index 45b8994..6c4518f 100644
--- a/src/diagnostic/formatter_test.cc
+++ b/src/diagnostic/formatter_test.cc
@@ -45,7 +45,7 @@
 };
 
 TEST_F(DiagFormatterTest, Simple) {
-  Formatter fmt{{false, false, false}};
+  Formatter fmt{{false, false, false, false}};
   auto got = fmt.format(List{diag_info, diag_warn, diag_err, diag_fatal});
   auto* expect = R"(1:14: purr
 2:14: grrr
@@ -54,8 +54,19 @@
   ASSERT_EQ(expect, got);
 }
 
+TEST_F(DiagFormatterTest, SimpleNewlineAtEnd) {
+  Formatter fmt{{false, false, false, true}};
+  auto got = fmt.format(List{diag_info, diag_warn, diag_err, diag_fatal});
+  auto* expect = R"(1:14: purr
+2:14: grrr
+3:16 abc123: hiss
+4:16: nothing
+)";
+  ASSERT_EQ(expect, got);
+}
+
 TEST_F(DiagFormatterTest, SimpleNoSource) {
-  Formatter fmt{{false, false, false}};
+  Formatter fmt{{false, false, false, false}};
   Diagnostic diag{Severity::Info, Source{}, "no source!"};
   auto got = fmt.format(List{diag});
   auto* expect = "no source!";
@@ -63,7 +74,7 @@
 }
 
 TEST_F(DiagFormatterTest, WithFile) {
-  Formatter fmt{{true, false, false}};
+  Formatter fmt{{true, false, false, false}};
   auto got = fmt.format(List{diag_info, diag_warn, diag_err, diag_fatal});
   auto* expect = R"(file.name:1:14: purr
 file.name:2:14: grrr
@@ -73,7 +84,7 @@
 }
 
 TEST_F(DiagFormatterTest, WithSeverity) {
-  Formatter fmt{{false, true, false}};
+  Formatter fmt{{false, true, false, false}};
   auto got = fmt.format(List{diag_info, diag_warn, diag_err, diag_fatal});
   auto* expect = R"(1:14 info: purr
 2:14 warning: grrr
@@ -83,7 +94,7 @@
 }
 
 TEST_F(DiagFormatterTest, WithLine) {
-  Formatter fmt{{false, false, true}};
+  Formatter fmt{{false, false, true, false}};
   auto got = fmt.format(List{diag_info, diag_warn, diag_err, diag_fatal});
   auto* expect = R"(1:14: purr
 the cat says meow
@@ -105,7 +116,7 @@
 }
 
 TEST_F(DiagFormatterTest, BasicWithFileSeverityLine) {
-  Formatter fmt{{true, true, true}};
+  Formatter fmt{{true, true, true, false}};
   auto got = fmt.format(List{diag_info, diag_warn, diag_err, diag_fatal});
   auto* expect = R"(file.name:1:14 info: purr
 the cat says meow
@@ -130,7 +141,7 @@
   Diagnostic multiline{Severity::Warning,
                        Source{Source::Range{{2, 9}, {4, 15}}, &file},
                        "multiline"};
-  Formatter fmt{{false, false, true}};
+  Formatter fmt{{false, false, true, false}};
   auto got = fmt.format(List{multiline});
   auto* expect = R"(2:9: multiline
 the dog says woof
diff --git a/src/reader/reader.h b/src/reader/reader.h
index 8fa4c92..9c71184 100644
--- a/src/reader/reader.h
+++ b/src/reader/reader.h
@@ -39,7 +39,7 @@
 
   /// @returns the parser error string
   std::string error() const {
-    diag::Formatter formatter{{false, false, false}};
+    diag::Formatter formatter{{false, false, false, false}};
     return formatter.format(diags_);
   }
 
diff --git a/src/reader/wgsl/parser_impl.h b/src/reader/wgsl/parser_impl.h
index c523d06..9a4e478 100644
--- a/src/reader/wgsl/parser_impl.h
+++ b/src/reader/wgsl/parser_impl.h
@@ -297,7 +297,7 @@
 
   /// @returns the parser error string
   std::string error() const {
-    diag::Formatter formatter{{false, false, false}};
+    diag::Formatter formatter{{false, false, false, false}};
     return formatter.format(diags_);
   }
 
diff --git a/src/reader/wgsl/parser_impl_error_msg_test.cc b/src/reader/wgsl/parser_impl_error_msg_test.cc
index d43d60f..d969e72 100644
--- a/src/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/reader/wgsl/parser_impl_error_msg_test.cc
@@ -21,17 +21,22 @@
 namespace wgsl {
 namespace {
 
+const diag::Formatter::Style formatter_style{
+    /* print_file: */ true, /* print_severity: */ true,
+    /* print_line: */ true, /* print_newline_at_end: */ false};
+
 class ParserImplErrorTest : public ParserImplTest {};
 
-#define EXPECT(SOURCE, EXPECTED)                                     \
-  do {                                                               \
-    std::string source = SOURCE;                                     \
-    std::string expected = EXPECTED;                                 \
-    auto p = parser(source);                                         \
-    p->set_max_errors(5);                                            \
-    EXPECT_EQ(false, p->Parse());                                    \
-    EXPECT_EQ(true, p->diagnostics().contains_errors());             \
-    EXPECT_EQ(expected, diag::Formatter().format(p->diagnostics())); \
+#define EXPECT(SOURCE, EXPECTED)                                          \
+  do {                                                                    \
+    std::string source = SOURCE;                                          \
+    std::string expected = EXPECTED;                                      \
+    auto p = parser(source);                                              \
+    p->set_max_errors(5);                                                 \
+    EXPECT_EQ(false, p->Parse());                                         \
+    EXPECT_EQ(true, p->diagnostics().contains_errors());                  \
+    EXPECT_EQ(expected,                                                   \
+              diag::Formatter(formatter_style).format(p->diagnostics())); \
   } while (false)
 
 TEST_F(ParserImplErrorTest, AdditiveInvalidExpr) {
diff --git a/src/reader/wgsl/parser_impl_error_resync_test.cc b/src/reader/wgsl/parser_impl_error_resync_test.cc
index e420bcd..be671af 100644
--- a/src/reader/wgsl/parser_impl_error_resync_test.cc
+++ b/src/reader/wgsl/parser_impl_error_resync_test.cc
@@ -21,16 +21,21 @@
 namespace wgsl {
 namespace {
 
+const diag::Formatter::Style formatter_style{
+    /* print_file: */ true, /* print_severity: */ true,
+    /* print_line: */ true, /* print_newline_at_end: */ false};
+
 class ParserImplErrorResyncTest : public ParserImplTest {};
 
-#define EXPECT(SOURCE, EXPECTED)                                     \
-  do {                                                               \
-    std::string source = SOURCE;                                     \
-    std::string expected = EXPECTED;                                 \
-    auto p = parser(source);                                         \
-    EXPECT_EQ(false, p->Parse());                                    \
-    EXPECT_EQ(true, p->diagnostics().contains_errors());             \
-    EXPECT_EQ(expected, diag::Formatter().format(p->diagnostics())); \
+#define EXPECT(SOURCE, EXPECTED)                                          \
+  do {                                                                    \
+    std::string source = SOURCE;                                          \
+    std::string expected = EXPECTED;                                      \
+    auto p = parser(source);                                              \
+    EXPECT_EQ(false, p->Parse());                                         \
+    EXPECT_EQ(true, p->diagnostics().contains_errors());                  \
+    EXPECT_EQ(expected,                                                   \
+              diag::Formatter(formatter_style).format(p->diagnostics())); \
   } while (false)
 
 TEST_F(ParserImplErrorResyncTest, BadFunctionDecls) {
diff --git a/src/transform/test_helper.h b/src/transform/test_helper.h
index a9eb584..3868143 100644
--- a/src/transform/test_helper.h
+++ b/src/transform/test_helper.h
@@ -59,8 +59,10 @@
     auto result = manager.Run(&module);
 
     if (result.diagnostics.contains_errors()) {
+      diag::Formatter::Style style;
+      style.print_newline_at_end = false;
       return "manager().Run() errored:\n" +
-             diag::Formatter().format(result.diagnostics);
+             diag::Formatter(style).format(result.diagnostics);
     }
 
     // Release the source module to ensure there's no uncloned data in result
diff --git a/src/validator/validator.h b/src/validator/validator.h
index 2180de6..8b8f9a9 100644
--- a/src/validator/validator.h
+++ b/src/validator/validator.h
@@ -41,7 +41,7 @@
 
   /// @returns error messages from the validator
   std::string error() {
-    diag::Formatter formatter{{false, false, false}};
+    diag::Formatter formatter{{false, false, false, false}};
     return formatter.format(diags_);
   }
   /// @returns true if an error was encountered
diff --git a/src/validator/validator_impl.h b/src/validator/validator_impl.h
index c0a8d2b..0a77654 100644
--- a/src/validator/validator_impl.h
+++ b/src/validator/validator_impl.h
@@ -54,7 +54,7 @@
 
   /// @returns error messages from the validator
   std::string error() {
-    diag::Formatter formatter{{false, false, false}};
+    diag::Formatter formatter{{false, false, false, false}};
     return formatter.format(diags_);
   }
   /// @returns true if an error was encountered