diff --git a/BUILD.gn b/BUILD.gn index 51aade8eca..20c1ae879c 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -377,6 +377,10 @@ source_set("libtint_core_src") { "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 @@ source_set("tint_unittests_core_src") { "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 c90d0cacb9..e2281b8385 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -198,6 +198,10 @@ set(TINT_LIB_SRCS 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 @@ set(TINT_TEST_SRCS 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 0000000000..c48b73e1ad --- /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 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 0000000000..843a00616e --- /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 +#include +#include +#include + +#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(a) >= static_cast(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::const_iterator; + + List(); + List(std::initializer_list 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 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 0000000000..737e09dfc8 --- /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 +#include + +#include "src/diagnostic/diagnostic.h" + +namespace tint { +namespace diag { +namespace { + +template +std::basic_ostream& operator<<( + std::basic_ostream& 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 +std::basic_ostream& operator<<( + std::basic_ostream& 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(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::create(bool print_file, + bool print_severity, + bool print_line) { + return std::make_unique(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 0000000000..12888772fd --- /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 +#include + +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 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 0000000000..ae52b54c0a --- /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