diff options
Diffstat (limited to 'gcc/diagnostic-format-html.cc')
-rw-r--r-- | gcc/diagnostic-format-html.cc | 1642 |
1 files changed, 1642 insertions, 0 deletions
diff --git a/gcc/diagnostic-format-html.cc b/gcc/diagnostic-format-html.cc new file mode 100644 index 0000000..473880f --- /dev/null +++ b/gcc/diagnostic-format-html.cc @@ -0,0 +1,1642 @@ +/* HTML output for diagnostics. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +This file is part of GCC. + +GCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +GCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with GCC; see the file COPYING3. If not see +<http://www.gnu.org/licenses/>. */ + +#include "config.h" +#define INCLUDE_MAP +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostic-metadata.h" +#include "diagnostic-format.h" +#include "diagnostic-format-html.h" +#include "diagnostic-format-text.h" +#include "diagnostic-output-file.h" +#include "diagnostic-buffer.h" +#include "diagnostic-path.h" +#include "diagnostic-client-data-hooks.h" +#include "selftest.h" +#include "selftest-diagnostic.h" +#include "pretty-print-format-impl.h" +#include "pretty-print-urlifier.h" +#include "edit-context.h" +#include "intl.h" +#include "xml.h" +#include "xml-printer.h" +#include "diagnostic-state.h" +#include "graphviz.h" +#include "json.h" +#include "selftest-xml.h" + +// struct html_generation_options + +html_generation_options::html_generation_options () +: m_css (true), + m_javascript (true), + m_show_state_diagrams (false), + m_show_state_diagram_xml (false), + m_show_state_diagram_dot_src (false) +{ +} + +class html_builder; + +/* Concrete buffering implementation subclass for HTML output. */ + +class diagnostic_html_format_buffer : public diagnostic_per_format_buffer +{ +public: + friend class html_builder; + friend class html_output_format; + + diagnostic_html_format_buffer (html_builder &builder) + : m_builder (builder) + {} + + void dump (FILE *out, int indent) const final override; + bool empty_p () const final override; + void move_to (diagnostic_per_format_buffer &dest) final override; + void clear () final override; + void flush () final override; + + void add_result (std::unique_ptr<xml::element> result) + { + m_results.push_back (std::move (result)); + } + +private: + html_builder &m_builder; + std::vector<std::unique_ptr<xml::element>> m_results; +}; + +/* A class for managing HTML output of diagnostics. + + Implemented: + - message text + + Known limitations/missing functionality: + - title for page + - file/line/column + - error vs warning + - CWEs + - rules + - fix-it hints + - paths +*/ + +class html_builder +{ +public: + friend class diagnostic_html_format_buffer; + + html_builder (diagnostic_context &context, + pretty_printer &pp, + const line_maps *line_maps, + const html_generation_options &html_gen_opts); + + void + set_main_input_filename (const char *name); + + void on_report_diagnostic (const diagnostic_info &diagnostic, + diagnostic_t orig_diag_kind, + diagnostic_html_format_buffer *buffer); + void emit_diagram (const diagnostic_diagram &diagram); + void end_group (); + + std::unique_ptr<xml::element> take_current_diagnostic () + { + return std::move (m_cur_diagnostic_element); + } + + void flush_to_file (FILE *outf); + + const xml::document &get_document () const { return *m_document; } + + void set_printer (pretty_printer &pp) + { + m_printer = &pp; + } + + std::unique_ptr<xml::element> + make_element_for_metadata (const diagnostic_metadata &metadata); + + std::unique_ptr<xml::element> + make_element_for_patch (const diagnostic_info &diagnostic); + + void add_focus_id (std::string focus_id) + { + m_ui_focus_ids.append_string (focus_id.c_str ()); + } + + std::unique_ptr<xml::node> + maybe_make_state_diagram (const diagnostic_event &event); + +private: + void + add_stylesheet (std::string url); + + std::unique_ptr<xml::element> + make_element_for_diagnostic (const diagnostic_info &diagnostic, + diagnostic_t orig_diag_kind, + bool alert); + + std::unique_ptr<xml::element> + make_metadata_element (label_text label, + label_text url); + + void + add_at_nesting_level (size_t nesting_level, + std::unique_ptr<xml::element> child_diag_element); + + void + push_nesting_level (); + + void + pop_nesting_level (); + + diagnostic_context &m_context; + pretty_printer *m_printer; + const line_maps *m_line_maps; + html_generation_options m_html_gen_opts; + const logical_location_manager *m_logical_loc_mgr; + + std::unique_ptr<xml::document> m_document; + xml::element *m_head_element; + xml::element *m_title_element; + xml::element *m_diagnostics_element; + std::unique_ptr<xml::element> m_cur_diagnostic_element; + std::vector<xml::element *> m_cur_nesting_levels; + int m_next_diag_id; // for handing out unique IDs + json::array m_ui_focus_ids; + logical_location m_last_logical_location; + location_t m_last_location; + expanded_location m_last_expanded_location; +}; + +static std::unique_ptr<xml::element> +make_div (std::string class_) +{ + auto div = std::make_unique<xml::element> ("div", false); + div->set_attr ("class", std::move (class_)); + return div; +} + +static std::unique_ptr<xml::element> +make_span (std::string class_) +{ + auto span = std::make_unique<xml::element> ("span", true); + span->set_attr ("class", std::move (class_)); + return span; +} + +/* class diagnostic_html_format_buffer : public diagnostic_per_format_buffer. */ + +void +diagnostic_html_format_buffer::dump (FILE *out, int indent) const +{ + fprintf (out, "%*sdiagnostic_html_format_buffer:\n", indent, ""); + int idx = 0; + for (auto &result : m_results) + { + fprintf (out, "%*sresult[%i]:\n", indent + 2, "", idx); + result->dump (out); + fprintf (out, "\n"); + ++idx; + } +} + +bool +diagnostic_html_format_buffer::empty_p () const +{ + return m_results.empty (); +} + +void +diagnostic_html_format_buffer::move_to (diagnostic_per_format_buffer &base) +{ + diagnostic_html_format_buffer &dest + = static_cast<diagnostic_html_format_buffer &> (base); + for (auto &&result : m_results) + dest.m_results.push_back (std::move (result)); + m_results.clear (); +} + +void +diagnostic_html_format_buffer::clear () +{ + m_results.clear (); +} + +void +diagnostic_html_format_buffer::flush () +{ + for (auto &&result : m_results) + m_builder.m_diagnostics_element->add_child (std::move (result)); + m_results.clear (); +} + +/* class html_builder. */ + +/* Style information for writing out HTML paths. + Colors taken from https://pf3.patternfly.org/v3/styles/color-palette/ */ + +static const char * const HTML_STYLE + = (" <style>\n" + " .linenum { color: white;\n" + " background-color: #0088ce;\n" + " white-space: pre;\n" + " border-right: 1px solid black; }\n" + " .ruler { color: red;\n" + " white-space: pre; }\n" + " .source { color: blue;\n" + " background-color: white;\n" + " white-space: pre; }\n" + " .annotation { color: green;\n" + " background-color: white;\n" + " white-space: pre; }\n" + " .linenum-gap { text-align: center;\n" + " border-top: 1px solid black;\n" + " border-right: 1px solid black;\n" + " background-color: #ededed; }\n" + " .source-gap { border-bottom: 1px dashed black;\n" + " border-top: 1px dashed black;\n" + " background-color: #ededed; }\n" + " .no-locus-event { font-family: monospace;\n" + " color: green;\n" + " white-space: pre; }\n" + " .funcname { font-weight: bold; }\n" + " .events-hdr { color: white;\n" + " background-color: #030303; }\n" + " .event-range { border: 1px solid black;\n" + " padding: 0px; }\n" + " .event-range-with-margin { border-spacing: 0; }\n" + " .locus { font-family: monospace;\n" + " border-spacing: 0px; }\n" + " .selected { color: white;\n" + " background-color: #0088ce; }\n" + " .stack-frame-with-margin { border-spacing: 0; }\n" + " .stack-frame { padding: 5px;\n" + " box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.5); }\n" + " .frame-funcname { text-align: right;\n" + " font-style: italic; } \n" + " .highlight-a { color: #703fec;\n" // pf-purple-400 + " font-weight: bold; }\n" + " .highlight-b { color: #3f9c35;\n" // pf-green-400 + " font-weight: bold; }\n" + " .gcc-quoted-text { font-weight: bold;\n" + " font-family: mono; }\n" + " </style>\n"); + +/* A little JavaScript for ease of navigation. + Keys j/k move forward and backward cyclically through a list + of focus ids (written out in another <script> tag as the HTML + is flushed). */ + +const char * const HTML_SCRIPT + = (" var current_focus_idx = 0;\n" + "\n" + " function get_focus_span (focus_idx)\n" + " {\n" + " const element_id = focus_ids[focus_idx];\n" + " return document.getElementById(element_id);\n" + " }\n" + " function get_any_state_diagram (focus_idx)\n" + " {\n" + " const element_id = focus_ids[focus_idx];\n" + " return document.getElementById(element_id + \"-state-diagram\");\n" + " }\n" + " function unhighlight_current_focus_idx ()\n" + " {\n" + " get_focus_span (current_focus_idx).classList.remove ('selected');\n" + " state_diagram = get_any_state_diagram (current_focus_idx);\n" + " if (state_diagram) {\n" + " state_diagram.style.visibility = \"hidden\";\n" + " }\n" + " }\n" + " function highlight_current_focus_idx ()\n" + " {\n" + " const el = get_focus_span (current_focus_idx);\n" + " el.classList.add ('selected');\n" + " state_diagram = get_any_state_diagram (current_focus_idx);\n" + " if (state_diagram) {\n" + " state_diagram.style.visibility = \"visible\";\n" + " }\n" + " // Center the element on the screen\n" + " const top_y = el.getBoundingClientRect ().top + window.pageYOffset;\n" + " const middle = top_y - (window.innerHeight / 2);\n" + " window.scrollTo (0, middle);\n" + " }\n" + " function select_prev_focus_idx ()\n" + " {\n" + " unhighlight_current_focus_idx ();\n" + " if (current_focus_idx > 0)\n" + " current_focus_idx -= 1;\n" + " else\n" + " current_focus_idx = focus_ids.length - 1;\n" + " highlight_current_focus_idx ();\n" + " }\n" + " function select_next_focus_idx ()\n" + " {\n" + " unhighlight_current_focus_idx ();\n" + " if (current_focus_idx < focus_ids.length - 1)\n" + " current_focus_idx += 1;\n" + " else\n" + " current_focus_idx = 0;\n" + " highlight_current_focus_idx ();\n" + " }\n" + " document.addEventListener('keydown', function (ev) {\n" + " if (ev.key == 'j')\n" + " select_next_focus_idx ();\n" + " else if (ev.key == 'k')\n" + " select_prev_focus_idx ();\n" + " });\n" + " highlight_current_focus_idx ();\n"); + +struct html_doctypedecl : public xml::doctypedecl +{ + void write_as_xml (pretty_printer *pp, + int depth, bool indent) const final override + { + if (indent) + { + for (int i = 0; i < depth; ++i) + pp_string (pp, " "); + } + pp_string (pp, "<!DOCTYPE html\n" + " PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n" + " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"); + if (indent) + pp_newline (pp); + } +}; + +/* html_builder's ctor. */ + +html_builder::html_builder (diagnostic_context &context, + pretty_printer &pp, + const line_maps *line_maps, + const html_generation_options &html_gen_opts) +: m_context (context), + m_printer (&pp), + m_line_maps (line_maps), + m_html_gen_opts (html_gen_opts), + m_logical_loc_mgr (nullptr), + m_head_element (nullptr), + m_title_element (nullptr), + m_diagnostics_element (nullptr), + m_next_diag_id (0), + m_last_location (UNKNOWN_LOCATION), + m_last_expanded_location ({}) +{ + gcc_assert (m_line_maps); + + if (auto client_data_hooks = context.get_client_data_hooks ()) + m_logical_loc_mgr = client_data_hooks->get_logical_location_manager (); + + m_document = std::make_unique<xml::document> (); + m_document->m_doctypedecl = std::make_unique<html_doctypedecl> (); + { + auto html_element = std::make_unique<xml::element> ("html", false); + html_element->set_attr ("xmlns", + "http://www.w3.org/1999/xhtml"); + xml::printer xp (*html_element.get ()); + m_document->add_child (std::move (html_element)); + + { + xml::auto_print_element head (xp, "head"); + m_head_element = xp.get_insertion_point (); + { + xml::auto_print_element title (xp, "title", true); + m_title_element = xp.get_insertion_point (); + m_title_element->add_text (" "); + } + + if (m_html_gen_opts.m_css) + { + add_stylesheet ("https://cdnjs.cloudflare.com/ajax/libs/patternfly/3.24.0/css/patternfly.min.css"); + add_stylesheet ("https://cdnjs.cloudflare.com/ajax/libs/patternfly/3.24.0/css/patternfly-additions.min.css"); + xp.add_raw (HTML_STYLE); + } + if (m_html_gen_opts.m_javascript) + { + xp.push_tag ("script"); + /* Escaping rules are different for HTML <script> elements, + so add the script "raw" for now. */ + xp.add_raw (HTML_SCRIPT); + xp.pop_tag ("script"); + } + } + + { + xml::auto_print_element body (xp, "body"); + { + auto diagnostics_element = make_div ("gcc-diagnostic-list"); + m_diagnostics_element = diagnostics_element.get (); + xp.append (std::move (diagnostics_element)); + } + } + } +} + +void +html_builder::set_main_input_filename (const char *name) +{ + gcc_assert (m_title_element); + if (name) + { + m_title_element->m_children.clear (); + m_title_element->add_text (name); + } +} + +void +html_builder::add_stylesheet (std::string url) +{ + gcc_assert (m_head_element); + + xml::printer xp (*m_head_element); + xp.push_tag ("link", false); + xp.set_attr ("rel", "stylesheet"); + xp.set_attr ("type", "text/css"); + xp.set_attr ("href", std::move (url)); +} + +/* Implementation of "on_report_diagnostic" for HTML output. */ + +void +html_builder::on_report_diagnostic (const diagnostic_info &diagnostic, + diagnostic_t orig_diag_kind, + diagnostic_html_format_buffer *buffer) +{ + if (diagnostic.kind == DK_ICE || diagnostic.kind == DK_ICE_NOBT) + { + /* Print a header for the remaining output to stderr, and + return, attempting to print the usual ICE messages to + stderr. Hopefully this will be helpful to the user in + indicating what's gone wrong (also for DejaGnu, for pruning + those messages). */ + fnotice (stderr, "Internal compiler error:\n"); + } + + const int nesting_level = m_context.get_diagnostic_nesting_level (); + bool alert = true; + if (m_cur_diagnostic_element && nesting_level > 0) + alert = false; + if (!m_cur_diagnostic_element) + m_last_logical_location = logical_location (); + auto diag_element + = make_element_for_diagnostic (diagnostic, orig_diag_kind, alert); + if (buffer) + { + gcc_assert (!m_cur_diagnostic_element); + buffer->m_results.push_back (std::move (diag_element)); + } + else + { + if (m_cur_diagnostic_element) + { + /* Nested diagnostic. */ + gcc_assert (nesting_level >= 0); + add_at_nesting_level (nesting_level, std::move (diag_element)); + } + else + /* Top-level diagnostic. */ + { + m_cur_diagnostic_element = std::move (diag_element); + m_cur_nesting_levels.clear (); + } + } +} + +// For ease of comparison with experimental-nesting-show-levels=yes + +static void +add_nesting_level_attr (xml::element &element, + int nesting_level) +{ + element.set_attr ("nesting-level", std::to_string (nesting_level)); +} + +void +html_builder:: +add_at_nesting_level (size_t nesting_level, + std::unique_ptr<xml::element> child_diag_element) +{ + gcc_assert (m_cur_diagnostic_element); + while (nesting_level > m_cur_nesting_levels.size ()) + push_nesting_level (); + while (nesting_level < m_cur_nesting_levels.size ()) + pop_nesting_level (); + + if (nesting_level > 0) + { + gcc_assert (!m_cur_nesting_levels.empty ()); + auto current_nesting_level = m_cur_nesting_levels.back (); + xml::printer xp (*current_nesting_level); + xp.push_tag ("li"); + add_nesting_level_attr (*xp.get_insertion_point (), + m_cur_nesting_levels.size ()); + xp.append (std::move (child_diag_element)); + xp.pop_tag ("li"); + } + else + m_cur_diagnostic_element->add_child (std::move (child_diag_element)); +} + +void +html_builder::push_nesting_level () +{ + gcc_assert (m_cur_diagnostic_element); + auto new_nesting_level = std::make_unique<xml::element> ("ul", false); + add_nesting_level_attr (*new_nesting_level, + m_cur_nesting_levels.size () + 1); + xml::element *current_nesting_level = nullptr; + if (!m_cur_nesting_levels.empty ()) + current_nesting_level = m_cur_nesting_levels.back (); + m_cur_nesting_levels.push_back (new_nesting_level.get ()); + if (current_nesting_level) + current_nesting_level->add_child (std::move (new_nesting_level)); + else + m_cur_diagnostic_element->add_child (std::move (new_nesting_level)); +} + +void +html_builder::pop_nesting_level () +{ + gcc_assert (m_cur_diagnostic_element); + m_cur_nesting_levels.pop_back (); +} + +static void +print_pre_source (xml::printer &xp, const char *text) +{ + xp.push_tag_with_class ("pre", "source", true); + xp.add_text (text); + xp.pop_tag ("pre"); +} + +std::unique_ptr<xml::node> +html_builder::maybe_make_state_diagram (const diagnostic_event &event) +{ + if (!m_html_gen_opts.m_show_state_diagrams) + return nullptr; + + /* Get XML state document; if we're going to print it later, also request + the debug version. */ + auto xml_state + = event.maybe_make_xml_state (m_html_gen_opts.m_show_state_diagram_xml); + if (!xml_state) + return nullptr; + + // Convert it to .dot AST + auto graph = make_dot_graph_from_xml_state (*xml_state); + gcc_assert (graph); + + auto wrapper = std::make_unique<xml::element> ("div", false); + xml::printer xp (*wrapper); + + if (m_html_gen_opts.m_show_state_diagram_xml) + { + // For debugging, show the XML src inline: + pretty_printer pp; + xml_state->write_as_xml (&pp, 0, true); + print_pre_source (xp, pp_formatted_text (&pp)); + } + + if (m_html_gen_opts.m_show_state_diagram_dot_src) + { + // For debugging, show the dot src inline: + pretty_printer pp; + dot::writer w (pp); + graph->print (w); + print_pre_source (xp, pp_formatted_text (&pp)); + } + + // Turn the .dot into SVG and splice into place + auto svg = dot::make_svg_from_graph (*graph); + if (svg) + xp.append (std::move (svg)); + + return wrapper; +} + +/* Custom subclass of html_label_writer. + Wrap labels within a <span> element, supplying them with event IDs. + Add the IDs to the list of focus IDs. */ + +class html_path_label_writer : public html_label_writer +{ +public: + html_path_label_writer (xml::printer &xp, + html_builder &builder, + const diagnostic_path &path, + const std::string &event_id_prefix) + : m_xp (xp), + m_html_builder (builder), + m_path (path), + m_event_id_prefix (event_id_prefix), + m_next_event_idx (0), + m_curr_event_id () + { + } + + void begin_label () final override + { + m_curr_event_id = m_next_event_idx++; + m_xp.push_tag_with_class ("span", "event", true); + m_xp.set_attr ("id", get_element_id ()); + m_html_builder.add_focus_id (get_element_id ()); + } + + void end_label () final override + { + const diagnostic_event &event + = m_path.get_event (m_curr_event_id.zero_based ()); + if (auto state_doc = m_html_builder.maybe_make_state_diagram (event)) + { + m_xp.push_tag_with_class ("div", "state-diagram", false); + m_xp.set_attr ("id", get_element_id () + "-state-diagram"); + m_xp.set_attr ("style", + ("position: absolute;" + " z-index: 1;" + " visibility: hidden;")); + m_xp.append (std::move (state_doc)); + m_xp.pop_tag ("div"); + } + + m_xp.pop_tag ("span"); // from begin_label + } + +private: + std::string + get_element_id () const + { + gcc_assert (m_curr_event_id.known_p ()); + return (m_event_id_prefix + + std::to_string (m_curr_event_id.zero_based ())); + } + + xml::printer &m_xp; + html_builder &m_html_builder; + const diagnostic_path &m_path; + const std::string &m_event_id_prefix; + int m_next_event_idx; + diagnostic_event_id_t m_curr_event_id; +}; + +/* See https://pf3.patternfly.org/v3/pattern-library/widgets/#alerts */ +static const char * +get_pf_class_for_alert_div (diagnostic_t diag_kind) +{ + switch (diag_kind) + { + case DK_DEBUG: + case DK_NOTE: + return "alert alert-info"; + + case DK_ANACHRONISM: + case DK_WARNING: + return "alert alert-warning"; + + case DK_ERROR: + case DK_SORRY: + case DK_ICE: + case DK_ICE_NOBT: + case DK_FATAL: + return "alert alert-danger"; + + default: + gcc_unreachable (); + } +} + +static const char * +get_pf_class_for_alert_icon (diagnostic_t diag_kind) +{ + switch (diag_kind) + { + case DK_DEBUG: + case DK_NOTE: + return "pficon pficon-info"; + + case DK_ANACHRONISM: + case DK_WARNING: + return "pficon pficon-warning-triangle-o"; + + case DK_ERROR: + case DK_SORRY: + case DK_ICE: + case DK_ICE_NOBT: + case DK_FATAL: + return "pficon pficon-error-circle-o"; + + default: + gcc_unreachable (); + } +} + +static const char * +get_label_for_logical_location_kind (enum logical_location_kind kind) +{ + switch (kind) + { + default: + gcc_unreachable (); + case logical_location_kind::unknown: + return nullptr; + + /* Kinds within executable code. */ + case logical_location_kind::function: + return "Function"; + case logical_location_kind::member: + return "Member"; + case logical_location_kind::module_: + return "Module"; + case logical_location_kind::namespace_: + return "Namespace"; + case logical_location_kind::type: + return "Type"; + case logical_location_kind::return_type: + return "Return type"; + case logical_location_kind::parameter: + return "Parameter"; + case logical_location_kind::variable: + return "Variable"; + + /* Kinds within XML or HTML documents. */ + case logical_location_kind::element: + return "Element"; + case logical_location_kind::attribute: + return "Attribute"; + case logical_location_kind::text: + return "Text"; + case logical_location_kind::comment: + return "Comment"; + case logical_location_kind::processing_instruction: + return "Processing Instruction"; + case logical_location_kind::dtd: + return "DTD"; + case logical_location_kind::declaration: + return "Declaration"; + + /* Kinds within JSON documents. */ + case logical_location_kind::object: + return "Object"; + case logical_location_kind::array: + return "Array"; + case logical_location_kind::property: + return "Property"; + case logical_location_kind::value: + return "Value"; + } +} + +static void +add_labelled_value (xml::printer &xp, + std::string id, + std::string label, + std::string value, + bool quote_value) +{ + xp.push_tag ("div", true); + xp.set_attr ("id", id); + xp.push_tag ("span"); + xp.add_text (label); + xp.add_text (" "); + xp.pop_tag ("span"); + xp.push_tag ("span"); + if (quote_value) + xp.set_attr ("class", "gcc-quoted-text"); + xp.add_text (std::move (value)); + xp.pop_tag ("span"); + xp.pop_tag ("div"); +} + +class html_token_printer : public token_printer +{ +public: + html_token_printer (xml::element &parent_element) + /* Ideally pp_token_lists that reach a token_printer should be + "balanced", but for now they can have mismatching pp_tokens + e.g. a begin_color without an end_color (PR other/120610). + Give html_token_printer its own xml::printer as a firewall to + limit the scope of the mismatches in the HTML. */ + : m_xp (parent_element, + /* Similarly we don't check that the popped tags match. */ + false) + { + } + void print_tokens (pretty_printer */*pp*/, + const pp_token_list &tokens) final override + { + /* Implement print_tokens by adding child elements to + m_parent_element. */ + for (auto iter = tokens.m_first; iter; iter = iter->m_next) + switch (iter->m_kind) + { + default: + gcc_unreachable (); + + case pp_token::kind::text: + { + pp_token_text *sub = as_a <pp_token_text *> (iter); + /* The value might be in the obstack, so we may need to + copy it. */ + m_xp.add_text (sub->m_value.get ()); + } + break; + + case pp_token::kind::begin_color: + { + pp_token_begin_color *sub = as_a <pp_token_begin_color *> (iter); + gcc_assert (sub->m_value.get ()); + m_xp.push_tag_with_class ("span", sub->m_value.get ()); + } + break; + + case pp_token::kind::end_color: + m_xp.pop_tag ("span"); + break; + + case pp_token::kind::begin_quote: + { + m_xp.add_text (open_quote); + m_xp.push_tag_with_class ("span", "gcc-quoted-text"); + } + break; + case pp_token::kind::end_quote: + { + m_xp.pop_tag ("span"); + m_xp.add_text (close_quote); + } + break; + + case pp_token::kind::begin_url: + { + pp_token_begin_url *sub = as_a <pp_token_begin_url *> (iter); + m_xp.push_tag ("a", true); + m_xp.set_attr ("href", sub->m_value.get ()); + } + break; + case pp_token::kind::end_url: + m_xp.pop_tag ("a"); + break; + + case pp_token::kind::event_id: + { + pp_token_event_id *sub = as_a <pp_token_event_id *> (iter); + gcc_assert (sub->m_event_id.known_p ()); + m_xp.add_text ("("); + m_xp.add_text (std::to_string (sub->m_event_id.one_based ())); + m_xp.add_text (")"); + } + break; + } + } + +private: + xml::printer m_xp; +}; + +/* Make a <div class="gcc-diagnostic"> for DIAGNOSTIC. + + If ALERT is true, make it be a PatternFly alert (see + https://pf3.patternfly.org/v3/pattern-library/widgets/#alerts) and + show severity text (e.g. "error: "). + + If ALERT is false, don't show the severity text and don't show + the filename if it's the same as the previous diagnostic within the + diagnostic group. */ + +std::unique_ptr<xml::element> +html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic, + diagnostic_t orig_diag_kind, + bool alert) +{ + const int diag_idx = m_next_diag_id++; + std::string diag_id; + { + pretty_printer pp; + pp_printf (&pp, "gcc-diag-%i", diag_idx); + diag_id = pp_formatted_text (&pp); + } + + // TODO: might be nice to emulate the text output format, but colorize it + + /* See https://pf3.patternfly.org/v3/pattern-library/widgets/#alerts + which has this example: +<div class="alert alert-danger"> + <span class="pficon pficon-error-circle-o"></span> + <strong>Hey there is a problem!</strong> Yeah this is really messed up and you should <a href="#" class="alert-link">know about it</a>. +</div> + */ + auto diag_element = make_div ("gcc-diagnostic"); + diag_element->set_attr ("id", diag_id); + if (alert) + diag_element->set_attr ("class", + get_pf_class_for_alert_div (diagnostic.kind)); + + xml::printer xp (*diag_element.get ()); + const size_t depth_within_alert_div = 1; + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + if (alert) + { + xp.push_tag_with_class ("span", + get_pf_class_for_alert_icon (diagnostic.kind), + true); + xp.add_text (" "); + xp.pop_tag ("span"); + } + + // The rest goes in the <div>... + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + xp.push_tag_with_class ("div", "gcc-message", true); + std::string message_alert_id (diag_id + "-message"); + xp.set_attr ("id", message_alert_id); + add_focus_id (message_alert_id); + + const size_t depth_within_message_div = depth_within_alert_div + 1; + gcc_assert (xp.get_num_open_tags () == depth_within_message_div); + + // Severity e.g. "warning: " + bool show_severity = true; + if (!alert) + show_severity = false; + if (show_severity) + { + xp.push_tag ("strong"); + xp.add_text (_(get_diagnostic_kind_text (diagnostic.kind))); + xp.pop_tag ("strong"); + xp.add_text (" "); + } + + // Add the message itself: + html_token_printer tok_printer (*xp.get_insertion_point ()); + m_printer->set_token_printer (&tok_printer); + pp_output_formatted_text (m_printer, m_context.get_urlifier ()); + m_printer->set_token_printer (nullptr); + pp_clear_output_area (m_printer); + + // Add any metadata as a suffix to the message + if (diagnostic.metadata) + { + xp.add_text (" "); + xp.append (make_element_for_metadata (*diagnostic.metadata)); + } + + // Add any option as a suffix to the message + + label_text option_text = label_text::take + (m_context.make_option_name (diagnostic.option_id, + orig_diag_kind, diagnostic.kind)); + if (option_text.get ()) + { + label_text option_url = label_text::take + (m_context.make_option_url (diagnostic.option_id)); + + xp.add_text (" "); + auto option_span = make_span ("gcc-option"); + option_span->add_text ("["); + { + if (option_url.get ()) + { + auto anchor = std::make_unique<xml::element> ("a", true); + anchor->set_attr ("href", option_url.get ()); + anchor->add_text (option_text.get ()); + option_span->add_child (std::move (anchor)); + } + else + option_span->add_text (option_text.get ()); + option_span->add_text ("]"); + } + xp.append (std::move (option_span)); + } + + gcc_assert (xp.get_num_open_tags () == depth_within_message_div); + + xp.pop_tag ("div"); + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + /* Show any logical location. */ + if (m_logical_loc_mgr) + if (auto client_data_hooks = m_context.get_client_data_hooks ()) + if (auto logical_loc = client_data_hooks->get_current_logical_location ()) + if (logical_loc != m_last_logical_location) + { + enum logical_location_kind kind + = m_logical_loc_mgr->get_kind (logical_loc);; + if (const char *label = get_label_for_logical_location_kind (kind)) + if (const char *name_with_scope + = m_logical_loc_mgr->get_name_with_scope (logical_loc)) + add_labelled_value (xp, "logical-location", + label, name_with_scope, true); + m_last_logical_location = logical_loc; + } + + /* Show any physical location. */ + const expanded_location s + = diagnostic_expand_location (&diagnostic); + if (s != m_last_expanded_location + || alert) + { + if (s.file + && (s.file != m_last_expanded_location.file + || alert)) + add_labelled_value (xp, "file", "File", s.file, false); + if (s.line) + { + add_labelled_value (xp, "line", "Line", std::to_string (s.line), false); + diagnostic_column_policy column_policy (m_context); + int converted_column = column_policy.converted_column (s); + if (converted_column >= 0) + add_labelled_value (xp, "column", "Column", + std::to_string (converted_column), + false); + } + if (s.file) + m_last_expanded_location = s; + } + + /* Source (and fix-it hints). */ + { + // TODO: m_context.m_last_location should be moved into the sink + location_t saved = m_context.m_last_location; + m_context.m_last_location = m_last_location; + m_context.maybe_show_locus_as_html (*diagnostic.richloc, + m_context.m_source_printing, + diagnostic.kind, + xp, + nullptr, + nullptr); + m_context.m_last_location = saved; + m_last_location = m_context.m_last_location; + } + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + /* Execution path. */ + if (auto path = diagnostic.richloc->get_path ()) + { + xp.push_tag ("div"); + xp.set_attr ("id", "execution-path"); + + xp.push_tag ("label", true); + const int num_events = path->num_events (); + pretty_printer pp; + pp_printf_n (&pp, num_events, + "Execution path with %i event", + "Execution path with %i events", + num_events); + xp.add_text_from_pp (pp); + xp.pop_tag ("label"); + + std::string event_id_prefix (diag_id + "-event-"); + html_path_label_writer event_label_writer (xp, *this, *path, + event_id_prefix); + + diagnostic_source_print_policy dspp (m_context); + print_path_as_html (xp, *path, m_context, &event_label_writer, + dspp); + + xp.pop_tag ("div"); + } + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + if (auto patch_element = make_element_for_patch (diagnostic)) + { + xp.push_tag ("div"); + xp.set_attr ("id", "suggested-fix"); + xp.push_tag ("label", true); + xp.add_text ("Suggested fix"); + xp.pop_tag ("label"); + xp.append (std::move (patch_element)); + xp.pop_tag ("div"); + } + + return diag_element; +} + +std::unique_ptr<xml::element> +html_builder::make_element_for_patch (const diagnostic_info &diagnostic) +{ + edit_context ec (m_context.get_file_cache ()); + ec.add_fixits (diagnostic.richloc); + if (char *diff = ec.generate_diff (true)) + { + if (strlen (diff) > 0) + { + auto element = std::make_unique<xml::element> ("pre", true); + element->set_attr ("class", "gcc-generated-patch"); + element->add_text (diff); + free (diff); + return element; + } + else + free (diff); + } + return nullptr; +} + +std::unique_ptr<xml::element> +html_builder::make_metadata_element (label_text label, + label_text url) +{ + auto item = make_span ("gcc-metadata-item"); + xml::printer xp (*item.get ()); + xp.add_text ("["); + { + if (url.get ()) + { + xp.push_tag ("a", true); + xp.set_attr ("href", url.get ()); + } + xp.add_text (label.get ()); + if (url.get ()) + xp.pop_tag ("a"); + } + xp.add_text ("]"); + return item; +} + +std::unique_ptr<xml::element> +html_builder::make_element_for_metadata (const diagnostic_metadata &metadata) +{ + auto span_metadata = make_span ("gcc-metadata"); + + int cwe = metadata.get_cwe (); + if (cwe) + { + pretty_printer pp; + pp_printf (&pp, "CWE-%i", cwe); + label_text label = label_text::take (xstrdup (pp_formatted_text (&pp))); + label_text url = label_text::take (get_cwe_url (cwe)); + span_metadata->add_child + (make_metadata_element (std::move (label), std::move (url))); + } + + for (unsigned idx = 0; idx < metadata.get_num_rules (); ++idx) + { + auto &rule = metadata.get_rule (idx); + label_text label = label_text::take (rule.make_description ()); + label_text url = label_text::take (rule.make_url ()); + span_metadata->add_child + (make_metadata_element (std::move (label), std::move (url))); + } + + return span_metadata; +} + +/* Implementation of diagnostic_context::m_diagrams.m_emission_cb + for HTML output. */ + +void +html_builder::emit_diagram (const diagnostic_diagram &/*diagram*/) +{ + /* We must be within the emission of a top-level diagnostic. */ + gcc_assert (m_cur_diagnostic_element); + + // TODO: currently a no-op +} + +/* Implementation of "end_group_cb" for HTML output. */ + +void +html_builder::end_group () +{ + if (m_cur_diagnostic_element) + m_diagnostics_element->add_child (std::move (m_cur_diagnostic_element)); +} + +/* Create a top-level object, and add it to all the results + (and other entities) we've seen so far. + + Flush it all to OUTF. */ + +void +html_builder::flush_to_file (FILE *outf) +{ + if (m_html_gen_opts.m_javascript) + { + gcc_assert (m_head_element); + xml::printer xp (*m_head_element); + /* Add an initialization of the global js variable "focus_ids" + using the array of IDs we saved as we went. */ + xp.push_tag ("script"); + pretty_printer pp; + pp_string (&pp, "focus_ids = "); + m_ui_focus_ids.print (&pp, true); + pp_string (&pp, ";\n"); + xp.add_raw (pp_formatted_text (&pp)); + xp.pop_tag ("script"); + } + auto top = m_document.get (); + top->dump (outf); + fprintf (outf, "\n"); +} + +class html_output_format : public diagnostic_output_format +{ +public: + ~html_output_format () + { + /* Any diagnostics should have been handled by now. + If not, then something's gone wrong with diagnostic + groupings. */ + std::unique_ptr<xml::element> pending_diag + = m_builder.take_current_diagnostic (); + gcc_assert (!pending_diag); + } + + void dump (FILE *out, int indent) const override + { + fprintf (out, "%*shtml_output_format\n", indent, ""); + diagnostic_output_format::dump (out, indent); + } + + void + set_main_input_filename (const char *name) final override + { + m_builder.set_main_input_filename (name); + } + + std::unique_ptr<diagnostic_per_format_buffer> + make_per_format_buffer () final override + { + return std::make_unique<diagnostic_html_format_buffer> (m_builder); + } + void set_buffer (diagnostic_per_format_buffer *base_buffer) final override + { + diagnostic_html_format_buffer *buffer + = static_cast<diagnostic_html_format_buffer *> (base_buffer); + m_buffer = buffer; + } + + void on_begin_group () final override + { + /* No-op, */ + } + void on_end_group () final override + { + m_builder.end_group (); + } + void + on_report_diagnostic (const diagnostic_info &diagnostic, + diagnostic_t orig_diag_kind) final override + { + m_builder.on_report_diagnostic (diagnostic, orig_diag_kind, m_buffer); + } + void on_diagram (const diagnostic_diagram &diagram) final override + { + m_builder.emit_diagram (diagram); + } + void after_diagnostic (const diagnostic_info &) final override + { + /* No-op, but perhaps could show paths here. */ + } + bool follows_reference_printer_p () const final override + { + return false; + } + void update_printer () final override + { + m_printer = m_context.clone_printer (); + + /* Don't colorize the text. */ + pp_show_color (m_printer.get ()) = false; + + /* No textual URLs. */ + m_printer->set_url_format (URL_FORMAT_NONE); + + /* Update the builder to use the new printer. */ + m_builder.set_printer (*get_printer ()); + } + + const xml::document &get_document () const + { + return m_builder.get_document (); + } + + html_builder &get_builder () { return m_builder; } + +protected: + html_output_format (diagnostic_context &context, + const line_maps *line_maps, + const html_generation_options &html_gen_opts) + : diagnostic_output_format (context), + m_builder (context, *get_printer (), line_maps, html_gen_opts), + m_buffer (nullptr) + {} + + html_builder m_builder; + diagnostic_html_format_buffer *m_buffer; +}; + +class html_file_output_format : public html_output_format +{ +public: + html_file_output_format (diagnostic_context &context, + const line_maps *line_maps, + const html_generation_options &html_gen_opts, + diagnostic_output_file output_file) + : html_output_format (context, line_maps, html_gen_opts), + m_output_file (std::move (output_file)) + { + gcc_assert (m_output_file.get_open_file ()); + gcc_assert (m_output_file.get_filename ()); + } + ~html_file_output_format () + { + m_builder.flush_to_file (m_output_file.get_open_file ()); + } + void dump (FILE *out, int indent) const override + { + fprintf (out, "%*shtml_file_output_format: %s\n", + indent, "", + m_output_file.get_filename ()); + diagnostic_output_format::dump (out, indent); + } + bool machine_readable_stderr_p () const final override + { + return false; + } + +private: + diagnostic_output_file m_output_file; +}; + +/* Attempt to open BASE_FILE_NAME.html for writing. + Return a non-null diagnostic_output_file, + or return a null diagnostic_output_file and complain to CONTEXT + using LINE_MAPS. */ + +diagnostic_output_file +diagnostic_output_format_open_html_file (diagnostic_context &context, + line_maps *line_maps, + const char *base_file_name) +{ + if (!base_file_name) + { + rich_location richloc (line_maps, UNKNOWN_LOCATION); + context.emit_diagnostic_with_group + (DK_ERROR, richloc, nullptr, 0, + "unable to determine filename for HTML output"); + return diagnostic_output_file (); + } + + label_text filename = label_text::take (concat (base_file_name, + ".html", + nullptr)); + FILE *outf = fopen (filename.get (), "w"); + if (!outf) + { + rich_location richloc (line_maps, UNKNOWN_LOCATION); + context.emit_diagnostic_with_group + (DK_ERROR, richloc, nullptr, 0, + "unable to open %qs for HTML output: %m", + filename.get ()); + return diagnostic_output_file (); + } + return diagnostic_output_file (outf, true, std::move (filename)); +} + +std::unique_ptr<diagnostic_output_format> +make_html_sink (diagnostic_context &context, + const line_maps &line_maps, + const html_generation_options &html_gen_opts, + diagnostic_output_file output_file) +{ + auto sink + = std::make_unique<html_file_output_format> (context, + &line_maps, + html_gen_opts, + std::move (output_file)); + sink->update_printer (); + return sink; +} + +#if CHECKING_P + +namespace selftest { + +/* Helper for writing tests of html_token_printer. + Printing to m_pp will appear as HTML within m_top_element, a <div>. */ + +struct token_printer_test +{ + token_printer_test () + : m_top_element ("div", true), + m_tok_printer (m_top_element) + { + m_pp.set_token_printer (&m_tok_printer); + } + + xml::element m_top_element; + html_token_printer m_tok_printer; + pretty_printer m_pp; +}; + +static void +test_token_printer () +{ + { + token_printer_test t; + pp_printf (&t.m_pp, "hello world"); + ASSERT_XML_PRINT_EQ + (t.m_top_element, + "<div>hello world</div>\n"); + } + + { + token_printer_test t; + pp_printf (&t.m_pp, "%qs: %qs", "foo", "bar"); + ASSERT_XML_PRINT_EQ + (t.m_top_element, + "<div>" + "`" + "<span class=\"gcc-quoted-text\">" + "foo" + "</span>" + "': `" + "<span class=\"gcc-quoted-text\">" + "bar" + "</span>" + "'" + "</div>\n"); + } + + { + token_printer_test t; + diagnostic_event_id_t event_id (0); + pp_printf (&t.m_pp, "foo %@ bar", &event_id); + ASSERT_XML_PRINT_EQ + (t.m_top_element, + "<div>foo (1) bar</div>\n"); + } +} + +/* A subclass of html_output_format for writing selftests. + The XML output is cached internally, rather than written + out to a file. */ + +class test_html_diagnostic_context : public test_diagnostic_context +{ +public: + test_html_diagnostic_context () + { + html_generation_options html_gen_opts; + html_gen_opts.m_css = false; + html_gen_opts.m_javascript = false; + auto sink = std::make_unique<html_buffered_output_format> (*this, + line_table, + html_gen_opts); + sink->update_printer (); + sink->set_main_input_filename ("(main input filename)"); + m_format = sink.get (); // borrowed + + set_output_format (std::move (sink)); + } + + const xml::document &get_document () const + { + return m_format->get_document (); + } + + html_builder &get_builder () const + { + return m_format->get_builder (); + } + +private: + class html_buffered_output_format : public html_output_format + { + public: + html_buffered_output_format (diagnostic_context &context, + const line_maps *line_maps, + const html_generation_options &html_gen_opts) + : html_output_format (context, line_maps, html_gen_opts) + { + } + bool machine_readable_stderr_p () const final override + { + return true; + } + }; + + html_output_format *m_format; // borrowed +}; + +/* Test of reporting a diagnostic at UNKNOWN_LOCATION to a + diagnostic_context and examining the generated XML document. + Verify various basic properties. */ + +static void +test_simple_log () +{ + test_html_diagnostic_context dc; + + rich_location richloc (line_table, UNKNOWN_LOCATION); + dc.report (DK_ERROR, richloc, nullptr, 0, "this is a test: %qs", "foo"); + + const xml::document &doc = dc.get_document (); + + ASSERT_XML_PRINT_EQ + (doc, + ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE html\n" + " PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n" + " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n" + "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" + " <head>\n" + " <title>(main input filename)</title>\n" + " </head>\n" + " <body>\n" + " <div class=\"gcc-diagnostic-list\">\n" + " <div class=\"alert alert-danger\" id=\"gcc-diag-0\">\n" + " <span class=\"pficon pficon-error-circle-o\"> </span>\n" + " <div class=\"gcc-message\" id=\"gcc-diag-0-message\"><strong>error: </strong> this is a test: `<span class=\"gcc-quoted-text\">foo</span>'</div>\n" + " </div>\n" + " </div>\n" + " </body>\n" + "</html>\n")); +} + +static void +test_metadata () +{ + test_html_diagnostic_context dc; + html_builder &b = dc.get_builder (); + + { + diagnostic_metadata metadata; + metadata.add_cwe (415); + auto element = b.make_element_for_metadata (metadata); + ASSERT_XML_PRINT_EQ + (*element, + "<span class=\"gcc-metadata\">" + "<span class=\"gcc-metadata-item\">" + "[" + "<a href=\"https://cwe.mitre.org/data/definitions/415.html\">" + "CWE-415" + "</a>" + "]" + "</span>" + "</span>\n"); + } + + { + diagnostic_metadata metadata; + diagnostic_metadata::precanned_rule rule ("MISC-42", + "http://example.com"); + metadata.add_rule (rule); + auto element = b.make_element_for_metadata (metadata); + ASSERT_XML_PRINT_EQ + (*element, + "<span class=\"gcc-metadata\">" + "<span class=\"gcc-metadata-item\">" + "[" + "<a href=\"http://example.com\">" + "MISC-42" + "</a>" + "]" + "</span>" + "</span>\n"); + } +} + +/* Run all of the selftests within this file. */ + +void +diagnostic_format_html_cc_tests () +{ + auto_fix_quotes fix_quotes; + test_token_printer (); + test_simple_log (); + test_metadata (); +} + +} // namespace selftest + +#endif /* CHECKING_P */ |