diff --git a/BUILD.gn b/BUILD.gn index 20c1ae879c..2165569747 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -381,6 +381,8 @@ source_set("libtint_core_src") { "src/diagnostic/diagnostic.h", "src/diagnostic/formatter.cc", "src/diagnostic/formatter.h", + "src/diagnostic/printer.cc", + "src/diagnostic/printer.h", "src/inspector/entry_point.cc", "src/inspector/entry_point.h", "src/inspector/inspector.cc", @@ -418,6 +420,14 @@ source_set("libtint_core_src") { "src/writer/writer.h", ] + if (is_linux) { + sources += [ "src/diagnostic/printer_linux.cc" ] + } else if (is_win) { + sources += [ "src/diagnostic/printer_windows.cc" ] + } else { + sources += [ "src/diagnostic/printer_other.cc" ] + } + public_deps = [ ":tint_core_enums_unified1", ":tint_core_tables_unified1", @@ -772,6 +782,7 @@ source_set("tint_unittests_core_src") { "src/ast/variable_test.cc", "src/ast/workgroup_decoration_test.cc", "src/diagnostic/formatter_test.cc", + "src/diagnostic/printer_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 e2281b8385..0eff8bd059 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -202,6 +202,8 @@ set(TINT_LIB_SRCS diagnostic/diagnostic.h diagnostic/formatter.cc diagnostic/formatter.h + diagnostic/printer.cc + diagnostic/printer.h inspector/entry_point.cc inspector/entry_point.h inspector/inspector.cc @@ -239,6 +241,14 @@ set(TINT_LIB_SRCS writer/writer.h ) +if(UNIX) + list(APPEND TINT_LIB_SRCS diagnostic/printer_linux.cc) +elseif(WIN32) + list(APPEND TINT_LIB_SRCS diagnostic/printer_windows.cc) +else() + list(APPEND TINT_LIB_SRCS diagnostic/printer_other.cc) +endif() + if(${TINT_BUILD_SPV_READER}) list(APPEND TINT_LIB_SRCS reader/spirv/construct.h @@ -382,6 +392,7 @@ set(TINT_TEST_SRCS ast/variable_test.cc ast/workgroup_decoration_test.cc diagnostic/formatter_test.cc + diagnostic/printer_test.cc inspector/inspector_test.cc scope_stack_test.cc transform/bound_array_accessors_transform_test.cc diff --git a/src/diagnostic/formatter.cc b/src/diagnostic/formatter.cc index 737e09dfc8..abb72329fa 100644 --- a/src/diagnostic/formatter.cc +++ b/src/diagnostic/formatter.cc @@ -59,76 +59,137 @@ std::basic_ostream& operator<<( class BasicFormatter : public Formatter { public: + struct State { + explicit State(Printer* p) : printer(p) {} + ~State() { flush(); } + + // set_style() sets the current style to new_style, flushing any pending + // messages to the printer if the style changed. + void set_style(const Style& new_style) { + if (style.color != new_style.color || style.bold != new_style.bold) { + flush(); + style = new_style; + } + } + + // flush() writes any pending messages to the printer, clearing the buffer. + void flush() { + auto str = stream.str(); + if (str.length() > 0) { + printer->write(str, style); + std::stringstream reset; + stream.swap(reset); + } + } + + // operator<<() queues msg to be written to the printer. + template + State& operator<<(const T& msg) { + stream << msg; + return *this; + } + + // newline() queues a newline to be written to the printer. + void newline() { stream << std::endl; } + + // repeat() queues the character c to be writen to the printer n times. + void repeat(char c, size_t n) { + while (n-- > 0) { + stream << c; + } + } + + private: + Printer* printer; + Style style; + std::stringstream stream; + }; + 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 { + void format(const List& list, Printer* printer) const override { + State state{printer}; + bool first = true; - std::stringstream ss; for (auto diag : list) { + state.set_style({}); if (!first) { - ss << std::endl; + state.newline(); } - format(diag, ss); + format(diag, &state); first = false; } - return ss.str(); } private: - void format(const Diagnostic& diag, std::stringstream& ss) const { + void format(const Diagnostic& diag, State* state) const { auto const& src = diag.source; auto const& rng = src.range; + state->set_style({Color::kDefault, true}); + if (print_file_ && src.file != nullptr && !src.file->path.empty()) { - ss << src.file->path; + (*state) << src.file->path; if (rng.begin.line > 0) { - ss << ":" << rng.begin; + (*state) << ":" << rng.begin; } } else { - ss << rng.begin; + (*state) << rng.begin; } if (print_severity_) { - ss << " " << diag.severity; + switch (diag.severity) { + case Severity::Warning: + state->set_style({Color::kYellow, true}); + break; + case Severity::Error: + case Severity::Fatal: + state->set_style({Color::kRed, true}); + break; + default: + break; + } + (*state) << " " << diag.severity; } - ss << ": " << diag.message; + + state->set_style({Color::kDefault, true}); + (*state) << ": " << diag.message; if (print_line_ && src.file != nullptr && rng.begin.line > 0) { - ss << std::endl; + state->newline(); + state->set_style({Color::kDefault, false}); + 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; + (*state) << src.file->lines[line - 1]; + state->newline(); + state->set_style({Color::kCyan, false}); 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); + state->repeat(' ', rng.begin.column - 1); + state->repeat( + '^', std::max(rng.end.column - rng.begin.column, 1)); } else if (line == rng.begin.line) { // Start of multi-line - repeat(' ', rng.begin.column - 1, ss); - repeat('^', len - (rng.begin.column - 1), ss); + state->repeat(' ', rng.begin.column - 1); + state->repeat('^', len - (rng.begin.column - 1)); } else if (line == rng.end.line) { // End of multi-line - repeat('^', rng.end.column - 1, ss); + state->repeat('^', rng.end.column - 1); } else { // Middle of multi-line - repeat('^', len, ss); + state->repeat('^', len); } - ss << std::endl; + state->newline(); } } - } - } - void repeat(char c, size_t n, std::stringstream& ss) const { - while (n-- > 0) { - ss << c; + state->set_style({}); } } diff --git a/src/diagnostic/formatter.h b/src/diagnostic/formatter.h index 12888772fd..43061dd9f3 100644 --- a/src/diagnostic/formatter.h +++ b/src/diagnostic/formatter.h @@ -18,6 +18,8 @@ #include #include +#include "src/diagnostic/printer.h" + namespace tint { namespace diag { @@ -37,8 +39,18 @@ class Formatter { virtual ~Formatter(); - /// @return the human readable list of diagnostics formatted to a string. - virtual std::string format(const List&) const = 0; + /// format prints the formatted diagnostic list |list| to |printer|. + /// @param list the list of diagnostic messages to format + /// @param printer the printer used to display the formatted diagnostics + virtual void format(const List& list, Printer* printer) const = 0; + + /// @return the list of diagnostics |list| formatted to a string. + /// @param list the list of diagnostic messages to format + inline std::string format(const List& list) const { + StringPrinter printer; + format(list, &printer); + return printer.str(); + } }; } // namespace diag diff --git a/src/diagnostic/printer.cc b/src/diagnostic/printer.cc new file mode 100644 index 0000000000..47faa58946 --- /dev/null +++ b/src/diagnostic/printer.cc @@ -0,0 +1,36 @@ +// 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/printer.h" + +#include + +namespace tint { +namespace diag { + +Printer::~Printer() = default; + +StringPrinter::StringPrinter() = default; +StringPrinter::~StringPrinter() = default; + +std::string StringPrinter::str() const { + return stream.str(); +} + +void StringPrinter::write(const std::string& str, const Style&) { + stream << str; +} + +} // namespace diag +} // namespace tint diff --git a/src/diagnostic/printer.h b/src/diagnostic/printer.h new file mode 100644 index 0000000000..fa49c1fca9 --- /dev/null +++ b/src/diagnostic/printer.h @@ -0,0 +1,83 @@ +// 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_PRINTER_H_ +#define SRC_DIAGNOSTIC_PRINTER_H_ + +#include +#include +#include + +namespace tint { +namespace diag { + +class List; + +/// Color is an enumerator of colors used by Style. +enum class Color { + kDefault, + kBlack, + kRed, + kGreen, + kYellow, + kBlue, + kMagenta, + kCyan, + kWhite, +}; + +/// Style describes how a diagnostic message should be printed. +struct Style { + /// The foreground text color + Color color = Color::kDefault; + /// If true the text will be displayed with a strong weight + bool bold = false; +}; + +/// Printers are used to print formatted diagnostic messages to a terminal. +class Printer { + public: + /// @returns a diagnostic Printer + /// @param out the file to print to. + /// @param use_colors if true, the printer will use colors if |out| is a + /// terminal and supports them. + static std::unique_ptr create(FILE* out, bool use_colors); + + virtual ~Printer(); + + /// writes the string str to the printer with the given style. + /// @param str the string to write to the printer + /// @param style the style used to print |str| + virtual void write(const std::string& str, const Style& style) = 0; +}; + +/// StringPrinter is an implementation of Printer that writes to a std::string. +class StringPrinter : public Printer { + public: + StringPrinter(); + ~StringPrinter() override; + + /// @returns the printed string. + std::string str() const; + + void write(const std::string& str, const Style&) override; + + private: + std::stringstream stream; +}; + +} // namespace diag +} // namespace tint + +#endif // SRC_DIAGNOSTIC_PRINTER_H_ diff --git a/src/diagnostic/printer_linux.cc b/src/diagnostic/printer_linux.cc new file mode 100644 index 0000000000..840b13355d --- /dev/null +++ b/src/diagnostic/printer_linux.cc @@ -0,0 +1,100 @@ +// 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/printer.h" + +#include + +#include + +namespace tint { +namespace diag { +namespace { + +bool supports_colors(FILE* f) { + if (!isatty(fileno(f))) { + return false; + } + + const char* cterm = getenv("TERM"); + if (cterm == nullptr) { + return false; + } + + std::string term = getenv("TERM"); + if (term != "cygwin" && term != "linux" && term != "rxvt-unicode-256color" && + term != "rxvt-unicode" && term != "screen-256color" && term != "screen" && + term != "tmux-256color" && term != "tmux" && term != "xterm-256color" && + term != "xterm-color" && term != "xterm") { + return false; + } + + return true; +} + +class PrinterLinux : public Printer { + public: + PrinterLinux(FILE* f, bool colors) + : file(f), use_colors(colors && supports_colors(f)) {} + + void write(const std::string& str, const Style& style) override { + write_color(style.color, style.bold); + fwrite(str.data(), 1, str.size(), file); + write_color(Color::kDefault, false); + } + + private: + constexpr const char* color_code(Color color, bool bold) { + switch (color) { + case Color::kDefault: + return bold ? "\u001b[1m" : "\u001b[0m"; + case Color::kBlack: + return bold ? "\u001b[30;1m" : "\u001b[30m"; + case Color::kRed: + return bold ? "\u001b[31;1m" : "\u001b[31m"; + case Color::kGreen: + return bold ? "\u001b[32;1m" : "\u001b[32m"; + case Color::kYellow: + return bold ? "\u001b[33;1m" : "\u001b[33m"; + case Color::kBlue: + return bold ? "\u001b[34;1m" : "\u001b[34m"; + case Color::kMagenta: + return bold ? "\u001b[35;1m" : "\u001b[35m"; + case Color::kCyan: + return bold ? "\u001b[36;1m" : "\u001b[36m"; + case Color::kWhite: + return bold ? "\u001b[37;1m" : "\u001b[37m"; + } + return ""; // unreachable + } + + void write_color(Color color, bool bold) { + if (use_colors) { + auto* code = color_code(color, bold); + fwrite(code, 1, strlen(code), file); + } + } + + FILE* const file; + bool const use_colors; +}; + +} // namespace + +std::unique_ptr Printer::create(FILE* out, bool use_colors) { + return std::make_unique(out, use_colors); +} + +} // namespace diag +} // namespace tint diff --git a/src/diagnostic/printer_other.cc b/src/diagnostic/printer_other.cc new file mode 100644 index 0000000000..8d15c380ef --- /dev/null +++ b/src/diagnostic/printer_other.cc @@ -0,0 +1,42 @@ +// 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/printer.h" + +#include + +namespace tint { +namespace diag { +namespace { + +class PrinterOther : public Printer { + public: + explicit PrinterOther(FILE* f) : file(f) {} + + void write(const std::string& str, const Style&) override { + fwrite(str.data(), 1, str.size(), file); + } + + private: + FILE* file; +}; + +} // namespace + +std::unique_ptr Printer::create(FILE* out, bool) { + return std::make_unique(out); +} + +} // namespace diag +} // namespace tint diff --git a/src/diagnostic/printer_test.cc b/src/diagnostic/printer_test.cc new file mode 100644 index 0000000000..662b8215d5 --- /dev/null +++ b/src/diagnostic/printer_test.cc @@ -0,0 +1,90 @@ +// 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/printer.h" + +#include "gtest/gtest.h" + +namespace tint { +namespace diag { +namespace { + +using PrinterTest = testing::Test; + +// Actually verifying that the expected colors are printed is exceptionally +// difficult as: +// a) The color emission varies by OS. +// b) The logic checks to see if the printer is writing to a terminal, making +// mocking hard. +// c) Actually probing what gets written to a FILE* is notoriously tricky. +// +// The least we can do is to exersice the code - which is what we do here. +// The test will print each of the colors, and can be examined with human +// eyeballs. + +TEST_F(PrinterTest, WithColors) { + auto printer = Printer::create(stdout, true); + printer->write("Default", Style{Color::kDefault, false}); + printer->write("Black", Style{Color::kBlack, false}); + printer->write("Red", Style{Color::kRed, false}); + printer->write("Green", Style{Color::kGreen, false}); + printer->write("Yellow", Style{Color::kYellow, false}); + printer->write("Blue", Style{Color::kBlue, false}); + printer->write("Magenta", Style{Color::kMagenta, false}); + printer->write("Cyan", Style{Color::kCyan, false}); + printer->write("White", Style{Color::kWhite, false}); +} + +TEST_F(PrinterTest, BoldWithColors) { + auto printer = Printer::create(stdout, true); + printer->write("Default", Style{Color::kDefault, true}); + printer->write("Black", Style{Color::kBlack, true}); + printer->write("Red", Style{Color::kRed, true}); + printer->write("Green", Style{Color::kGreen, true}); + printer->write("Yellow", Style{Color::kYellow, true}); + printer->write("Blue", Style{Color::kBlue, true}); + printer->write("Magenta", Style{Color::kMagenta, true}); + printer->write("Cyan", Style{Color::kCyan, true}); + printer->write("White", Style{Color::kWhite, true}); +} + +TEST_F(PrinterTest, WithoutColors) { + auto printer = Printer::create(stdout, false); + printer->write("Default", Style{Color::kDefault, false}); + printer->write("Black", Style{Color::kBlack, false}); + printer->write("Red", Style{Color::kRed, false}); + printer->write("Green", Style{Color::kGreen, false}); + printer->write("Yellow", Style{Color::kYellow, false}); + printer->write("Blue", Style{Color::kBlue, false}); + printer->write("Magenta", Style{Color::kMagenta, false}); + printer->write("Cyan", Style{Color::kCyan, false}); + printer->write("White", Style{Color::kWhite, false}); +} + +TEST_F(PrinterTest, BoldWithoutColors) { + auto printer = Printer::create(stdout, false); + printer->write("Default", Style{Color::kDefault, true}); + printer->write("Black", Style{Color::kBlack, true}); + printer->write("Red", Style{Color::kRed, true}); + printer->write("Green", Style{Color::kGreen, true}); + printer->write("Yellow", Style{Color::kYellow, true}); + printer->write("Blue", Style{Color::kBlue, true}); + printer->write("Magenta", Style{Color::kMagenta, true}); + printer->write("Cyan", Style{Color::kCyan, true}); + printer->write("White", Style{Color::kWhite, true}); +} + +} // namespace +} // namespace diag +} // namespace tint diff --git a/src/diagnostic/printer_windows.cc b/src/diagnostic/printer_windows.cc new file mode 100644 index 0000000000..efcd60d72e --- /dev/null +++ b/src/diagnostic/printer_windows.cc @@ -0,0 +1,113 @@ +// 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/printer.h" + +#include + +#define WIN32_LEAN_AND_MEAN 1 +#include + +namespace tint { +namespace diag { +namespace { + +struct ConsoleInfo { + HANDLE handle = INVALID_HANDLE_VALUE; + WORD default_attributes = 0; + operator bool() const { return handle != INVALID_HANDLE_VALUE; } +}; + +ConsoleInfo console_info(FILE* file) { + if (file == nullptr) { + return {}; + } + + ConsoleInfo console{}; + if (file == stdout) { + console.handle = GetStdHandle(STD_OUTPUT_HANDLE); + } else if (file == stderr) { + console.handle = GetStdHandle(STD_ERROR_HANDLE); + } else { + return {}; + } + + CONSOLE_SCREEN_BUFFER_INFO info{}; + if (GetConsoleScreenBufferInfo(console.handle, &info) == 0) { + return {}; + } + + console.default_attributes = info.wAttributes; + return console; +} + +class PrinterWindows : public Printer { + public: + PrinterWindows(FILE* f, bool use_colors) + : file(f), console(console_info(use_colors ? f : nullptr)) {} + + void write(const std::string& str, const Style& style) override { + write_color(style.color, style.bold); + fwrite(str.data(), 1, str.size(), file); + write_color(Color::kDefault, false); + } + + private: + WORD attributes(Color color, bool bold) { + switch (color) { + case Color::kDefault: + return console.default_attributes; + case Color::kBlack: + return 0; + case Color::kRed: + return FOREGROUND_RED | (bold ? FOREGROUND_INTENSITY : 0); + case Color::kGreen: + return FOREGROUND_GREEN | (bold ? FOREGROUND_INTENSITY : 0); + case Color::kYellow: + return FOREGROUND_RED | FOREGROUND_GREEN | + (bold ? FOREGROUND_INTENSITY : 0); + case Color::kBlue: + return FOREGROUND_BLUE | (bold ? FOREGROUND_INTENSITY : 0); + case Color::kMagenta: + return FOREGROUND_RED | FOREGROUND_BLUE | + (bold ? FOREGROUND_INTENSITY : 0); + case Color::kCyan: + return FOREGROUND_GREEN | FOREGROUND_BLUE | + (bold ? FOREGROUND_INTENSITY : 0); + case Color::kWhite: + return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | + (bold ? FOREGROUND_INTENSITY : 0); + } + return 0; // unreachable + } + + void write_color(Color color, bool bold) { + if (console) { + SetConsoleTextAttribute(console.handle, attributes(color, bold)); + fflush(file); + } + } + + FILE* const file; + ConsoleInfo const console; +}; + +} // namespace + +std::unique_ptr Printer::create(FILE* out, bool use_colors) { + return std::make_unique(out, use_colors); +} + +} // namespace diag +} // namespace tint