Add tint::diag namespace for diagnostics

Diagnostics will be used for printing parser / validator error mesasges.
Diagnostics are collected into a `diag::List`, and can then be formatted into a human readable message with `diag::Formatter`.

Bug: tint:282
Change-Id: I8bbef3db22b72d62cb9467c878d9a346890589ad
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/31480
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 51aade8..20c1ae8 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -377,6 +377,10 @@
     "src/ast/workgroup_decoration.h",
     "src/context.cc",
     "src/context.h",
+    "src/diagnostic/diagnostic.cc",
+    "src/diagnostic/diagnostic.h",
+    "src/diagnostic/formatter.cc",
+    "src/diagnostic/formatter.h",
     "src/inspector/entry_point.cc",
     "src/inspector/entry_point.h",
     "src/inspector/inspector.cc",
@@ -767,6 +771,7 @@
     "src/ast/variable_decl_statement_test.cc",
     "src/ast/variable_test.cc",
     "src/ast/workgroup_decoration_test.cc",
+    "src/diagnostic/formatter_test.cc",
     "src/inspector/inspector_test.cc",
     "src/scope_stack_test.cc",
     "src/transform/bound_array_accessors_transform_test.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c90d0ca..e2281b8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -198,6 +198,10 @@
   ast/workgroup_decoration.h
   context.cc
   context.h
+  diagnostic/diagnostic.cc
+  diagnostic/diagnostic.h
+  diagnostic/formatter.cc
+  diagnostic/formatter.h
   inspector/entry_point.cc
   inspector/entry_point.h
   inspector/inspector.cc
@@ -377,6 +381,7 @@
   ast/variable_decl_statement_test.cc
   ast/variable_test.cc
   ast/workgroup_decoration_test.cc
+  diagnostic/formatter_test.cc
   inspector/inspector_test.cc
   scope_stack_test.cc
   transform/bound_array_accessors_transform_test.cc
diff --git a/src/diagnostic/diagnostic.cc b/src/diagnostic/diagnostic.cc
new file mode 100644
index 0000000..c48b73e
--- /dev/null
+++ b/src/diagnostic/diagnostic.cc
@@ -0,0 +1,25 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/diagnostic/diagnostic.h"
+
+namespace tint {
+namespace diag {
+
+List::List() = default;
+List::List(std::initializer_list<Diagnostic> list) : entries_(list) {}
+List::~List() = default;
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/diagnostic/diagnostic.h b/src/diagnostic/diagnostic.h
new file mode 100644
index 0000000..843a006
--- /dev/null
+++ b/src/diagnostic/diagnostic.h
@@ -0,0 +1,79 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_DIAGNOSTIC_DIAGNOSTIC_H_
+#define SRC_DIAGNOSTIC_DIAGNOSTIC_H_
+
+#include <initializer_list>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/source.h"
+
+namespace tint {
+namespace diag {
+
+/// Severity is an enumerator of diagnostic severities.
+enum class Severity { Info, Warning, Error, Fatal };
+
+/// @return true iff |a| is more than, or of equal severity to |b|.
+inline bool operator>=(Severity a, Severity b) {
+  return static_cast<int>(a) >= static_cast<int>(b);
+}
+
+/// Diagnostic holds all the information for a single compiler diagnostic
+/// message.
+class Diagnostic {
+ public:
+  Severity severity = Severity::Error;
+  Source source;
+  std::string message;
+};
+
+/// List is a container of Diagnostic messages.
+class List {
+ public:
+  using iterator = std::vector<Diagnostic>::const_iterator;
+
+  List();
+  List(std::initializer_list<Diagnostic> list);
+  ~List();
+
+  void add(Diagnostic&& diag) {
+    entries_.emplace_back(std::move(diag));
+    if (diag.severity >= Severity::Error) {
+      contains_errors_ = true;
+    }
+  }
+
+  /// @returns true iff the diagnostic list contains errors diagnostics (or of
+  /// higher severity).
+  bool contains_errors() const { return contains_errors_; }
+  /// @returns the number of entries in the list.
+  size_t count() const { return entries_.size(); }
+  /// @returns the first diagnostic in the list.
+  iterator begin() const { return entries_.begin(); }
+  /// @returns the last diagnostic in the list.
+  iterator end() const { return entries_.end(); }
+
+ private:
+  std::vector<Diagnostic> entries_;
+  bool contains_errors_ = false;
+};
+
+}  // namespace diag
+}  // namespace tint
+
+#endif  // SRC_DIAGNOSTIC_DIAGNOSTIC_H_
diff --git a/src/diagnostic/formatter.cc b/src/diagnostic/formatter.cc
new file mode 100644
index 0000000..737e09d
--- /dev/null
+++ b/src/diagnostic/formatter.cc
@@ -0,0 +1,152 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/diagnostic/formatter.h"
+
+#include <algorithm>
+#include <sstream>
+
+#include "src/diagnostic/diagnostic.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+template <typename CharT, typename Traits>
+std::basic_ostream<CharT, Traits>& operator<<(
+    std::basic_ostream<CharT, Traits>& stream,
+    Severity severity) {
+  switch (severity) {
+    case Severity::Info:
+      stream << "info";
+      break;
+    case Severity::Warning:
+      stream << "warning";
+      break;
+    case Severity::Error:
+      stream << "error";
+      break;
+    case Severity::Fatal:
+      stream << "fatal";
+      break;
+  }
+  return stream;
+}
+
+template <typename CharT, typename Traits>
+std::basic_ostream<CharT, Traits>& operator<<(
+    std::basic_ostream<CharT, Traits>& stream,
+    const Source::Location& location) {
+  if (location.line > 0) {
+    stream << location.line;
+    if (location.column > 0) {
+      stream << ":" << location.column;
+    }
+  }
+  return stream;
+}
+
+class BasicFormatter : public Formatter {
+ public:
+  BasicFormatter(bool print_file, bool print_severity, bool print_line)
+      : print_file_(print_file),
+        print_severity_(print_severity),
+        print_line_(print_line) {}
+
+  std::string format(const List& list) const override {
+    bool first = true;
+    std::stringstream ss;
+    for (auto diag : list) {
+      if (!first) {
+        ss << std::endl;
+      }
+      format(diag, ss);
+      first = false;
+    }
+    return ss.str();
+  }
+
+ private:
+  void format(const Diagnostic& diag, std::stringstream& ss) const {
+    auto const& src = diag.source;
+    auto const& rng = src.range;
+
+    if (print_file_ && src.file != nullptr && !src.file->path.empty()) {
+      ss << src.file->path;
+      if (rng.begin.line > 0) {
+        ss << ":" << rng.begin;
+      }
+    } else {
+      ss << rng.begin;
+    }
+    if (print_severity_) {
+      ss << " " << diag.severity;
+    }
+    ss << ": " << diag.message;
+
+    if (print_line_ && src.file != nullptr && rng.begin.line > 0) {
+      ss << std::endl;
+      for (size_t line = rng.begin.line; line <= rng.end.line; line++) {
+        if (line < src.file->lines.size() + 1) {
+          auto len = src.file->lines[line - 1].size();
+
+          ss << src.file->lines[line - 1];
+          ss << std::endl;
+
+          if (line == rng.begin.line && line == rng.end.line) {
+            // Single line
+            repeat(' ', rng.begin.column - 1, ss);
+            repeat('^', std::max<size_t>(rng.end.column - rng.begin.column, 1),
+                   ss);
+          } else if (line == rng.begin.line) {
+            // Start of multi-line
+            repeat(' ', rng.begin.column - 1, ss);
+            repeat('^', len - (rng.begin.column - 1), ss);
+          } else if (line == rng.end.line) {
+            // End of multi-line
+            repeat('^', rng.end.column - 1, ss);
+          } else {
+            // Middle of multi-line
+            repeat('^', len, ss);
+          }
+          ss << std::endl;
+        }
+      }
+    }
+  }
+
+  void repeat(char c, size_t n, std::stringstream& ss) const {
+    while (n-- > 0) {
+      ss << c;
+    }
+  }
+
+  const bool print_file_ = false;
+  const bool print_severity_ = false;
+  const bool print_line_ = false;
+};
+
+}  // namespace
+
+std::unique_ptr<Formatter> Formatter::create(bool print_file,
+                                             bool print_severity,
+                                             bool print_line) {
+  return std::make_unique<BasicFormatter>(print_file, print_severity,
+                                          print_line);
+}
+
+Formatter::~Formatter() = default;
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/diagnostic/formatter.h b/src/diagnostic/formatter.h
new file mode 100644
index 0000000..1288877
--- /dev/null
+++ b/src/diagnostic/formatter.h
@@ -0,0 +1,47 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_DIAGNOSTIC_FORMATTER_H_
+#define SRC_DIAGNOSTIC_FORMATTER_H_
+
+#include <memory>
+#include <string>
+
+namespace tint {
+namespace diag {
+
+class List;
+
+/// Formatters are used to format a list of diagnostics into a human readable
+/// string.
+class Formatter {
+ public:
+  /// @returns a basic diagnostic formatter
+  /// @param print_file include the file path for each diagnostic
+  /// @param print_severity include the severity for each diagnostic
+  /// @param print_line include the source line(s) for the diagnostic
+  static std::unique_ptr<Formatter> create(bool print_file,
+                                           bool print_severity,
+                                           bool print_line);
+
+  virtual ~Formatter();
+
+  /// @return the human readable list of diagnostics formatted to a string.
+  virtual std::string format(const List&) const = 0;
+};
+
+}  // namespace diag
+}  // namespace tint
+
+#endif  // SRC_DIAGNOSTIC_FORMATTER_H_
diff --git a/src/diagnostic/formatter_test.cc b/src/diagnostic/formatter_test.cc
new file mode 100644
index 0000000..ae52b54
--- /dev/null
+++ b/src/diagnostic/formatter_test.cc
@@ -0,0 +1,140 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/diagnostic/formatter.h"
+#include "src/diagnostic/diagnostic.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+constexpr const char* content =
+    R"(the cat says meow
+the dog says woof
+the snake says quack
+the snail says ???
+)";
+
+class DiagFormatterTest : public testing::Test {
+ public:
+  Source::File file{"file.name", content};
+  Diagnostic diag_info{Severity::Info,
+                       Source{Source::Range{Source::Location{1, 14}}, &file},
+                       "purr"};
+  Diagnostic diag_warn{Severity::Warning,
+                       Source{Source::Range{{2, 14}, {2, 18}}, &file}, "grrr"};
+  Diagnostic diag_err{Severity::Error,
+                      Source{Source::Range{{3, 16}, {3, 21}}, &file}, "hiss"};
+  Diagnostic diag_fatal{Severity::Fatal,
+                        Source{Source::Range{{4, 16}, {4, 19}}, &file},
+                        "nothing"};
+};
+
+TEST_F(DiagFormatterTest, Simple) {
+  auto fmt = Formatter::create(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
+3:16: hiss
+4:16: nothing)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, WithFile) {
+  auto fmt = Formatter::create(true, 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
+file.name:3:16: hiss
+file.name:4:16: nothing)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, WithSeverity) {
+  auto fmt = Formatter::create(false, true, 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
+3:16 error: hiss
+4:16 fatal: nothing)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, WithLine) {
+  auto fmt = Formatter::create(false, false, true);
+  auto got = fmt->format(List{diag_info, diag_warn, diag_err, diag_fatal});
+  auto* expect = R"(1:14: purr
+the cat says meow
+             ^
+
+2:14: grrr
+the dog says woof
+             ^^^^
+
+3:16: hiss
+the snake says quack
+               ^^^^^
+
+4:16: nothing
+the snail says ???
+               ^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithFileSeverityLine) {
+  auto fmt = Formatter::create(true, true, true);
+  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
+             ^
+
+file.name:2:14 warning: grrr
+the dog says woof
+             ^^^^
+
+file.name:3:16 error: hiss
+the snake says quack
+               ^^^^^
+
+file.name:4:16 fatal: nothing
+the snail says ???
+               ^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithMultiLine) {
+  Diagnostic multiline{Severity::Warning,
+                       Source{Source::Range{{2, 9}, {4, 15}}, &file},
+                       "multiline"};
+
+  auto fmt = Formatter::create(false, false, true);
+  auto got = fmt->format(List{multiline});
+  auto* expect = R"(2:9: multiline
+the dog says woof
+        ^^^^^^^^^
+the snake says quack
+^^^^^^^^^^^^^^^^^^^^
+the snail says ???
+^^^^^^^^^^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+}  // namespace
+}  // namespace diag
+}  // namespace tint