From: David Malcolm Date: Tue, 27 May 2025 16:22:26 +0000 (-0400) Subject: diagnostics: rework experimental-html output [PR116792] X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=04871e748862fc48fac739f0ce9769e566ca7cc3;p=thirdparty%2Fgcc.git diagnostics: rework experimental-html output [PR116792] This patch reworks the HTML output from the the option -fdiagnostics-add-output=experimental-html so that for source quoting and path printing, rather than simply adding the textual output inside
 elements, it breaks up the output
into HTML tags reflecting the structure of the output, using CSS, SVG
and a little javascript to help the user navigate the diagnostics and
the events within any paths.

This uses ideas from the patch I posted in:
  https://gcc.gnu.org/pipermail/gcc-patches/2020-November/558603.html
but reworks the above patch so that:
* rather than printing source to a pretty_printer, the HTML is created
  by building a DOM tree in memory, using a new xml::printer class.  This
  should be less error-prone than the pretty_printer approach, since it
  ought to solve escaping issues.  Instead of a text vs html boolean,
  the code is generalized via templates with to_text vs to_html sinks.
  This templatization applies both to path-printing and
  diagnostic-show-locus.cc.
* the HTML output can have multiple diagnostics and multiple paths rather
  than just a single path.  The javascript keyboard controls now cycle
  through all diagnostics and all events within them

An example of the output can be seen at:
  https://dmalcolm.fedorapeople.org/gcc/2025-05-27/malloc-1.c.html
where the keys "j" and "k" cycle through diagnostics and events
within them.

gcc/ChangeLog:
	PR other/116792
	* diagnostic-format-html.cc: Define INCLUDE_STRING.
	Include "xml.h", "xml-printer.h", and "json.h".
	(html_generation_options::html_generation_options): New.
	(namespace xml): Move decls to xml.h and convert from using
	label_text to std::string.
	(xml::text::write_as_xml): Reimplement indentation so it is done
	by this node, rather than the parent.
	(xml::node_with_children::add_text): Convert from label_text to
	std::string.  Consolidate runs of text into a single node.
	(xml::document::write_as_xml): Reimplement indentation.
	(xml::element::write_as_xml): Reimplement indentation so it is
	done by this node, rather than the parent.  Convert from
	label_text to std::string.  Update attribute-printing to new
	representation to preserve insertion order.
	(xml::element::set_attr): Convert from label_text to std::string.
	Record insertion order.
	(xml::raw::write_as_xml): New.
	(xml::printer::printer): New.
	(xml::printer::push_tag): New.
	(xml::printer::push_tag_with_class): New.
	(xml::printer::pop_tag): New.
	(xml::printer::set_attr): New.
	(xml::printer::add_text): New.
	(xml::printer::add_raw): New.
	(xml::printer::push_element): New.
	(xml::printer::append): New.
	(xml::printer::get_insertion_point): New.
	(html_builder::add_focus_id): New.
	(html_builder::m_html_gen_opts): New field.
	(html_builder::m_head_element): New field.
	(html_builder::m_next_diag_id): New field.
	(html_builder::m_ui_focus_ids): New field.
	(make_div): Convert from label_text to std::string.
	(make_span): Likewise.
	(HTML_STYLE): New.
	(HTML_SCRIPT): New.
	(html_builder::html_builder): Fix indentation.  Add
	"html_gen_opts" param.  Initialize new fields.  Reimplement
	using xml::printer.  Optionally add style and script tags.
	(class html_path_label_writer): New.
	(html_builder::make_element_for_diagnostic): Convert from
	label_text to std::string. Set "id" on "gcc-diagnostic" and
	"gcc-message" 
elements; add the latter to the focus ids. Use diagnostic_context::maybe_show_locus_as_html rather than html_builder::make_element_for_source. Use print_path_as_html rather than html_builder::make_element_for_path. (html_builder::make_element_for_source): Drop. (html_builder::make_element_for_path): Drop. (html_builder::make_element_for_patch): Convert from label_text to std::string. (html_builder::make_metadata_element): Likewise. Use xml::printer. (html_builder::make_element_for_metadata): Convert from label_text to std::string. (html_builder::emit_diagram): Expand comment. (html_builder::flush_to_file): Write out initializer for "focus_ids" into javascript. (html_output_format::html_output_format): Add param "html_gen_opts" and use it to initialize m_builder. (html_file_output_format::html_file_output_format): Likewise, to initialize base class. (make_html_sink): Likewise, to pass to ctor. (selftest::test_html_diagnostic_context::test_html_diagnostic_context): Set up html_generation_options. (selftest::html_buffered_output_format::html_buffered_output_format): Add html_gen_opts param. (selftest::test_simple_log): Add id attributes to expected text for "gcc-diagnostic" and "gcc-message" elements. Update whitespace for indentation fixes. (selftest::test_metadata): Update whitespace for indentation fixes. (selftest::test_printer): New selftest. (selftest::test_attribute_ordering): New selftest. (selftest::diagnostic_format_html_cc_tests): Call the new selftests. * diagnostic-format-html.h (struct html_generation_options): New. (make_html_sink): Add "html_gen_opts" param. (print_path_as_html): New decl. * diagnostic-path-output.cc: Define INCLUDE_MAP. Add includes of "diagnostic-format-html.h", "xml.h", and "xml-printer.h". (path_print_policy::path_print_policy): Add ctor. (path_print_policy::get_diagram_theme): Fix whitespace. (struct stack_frame): New. (begin_html_stack_frame): New function. (end_html_stack_frame): New function. (emit_svg_arrow): New function. (event_range::print): Rename to... (event_range::print_as_text): ...this. Update call to diagnostic_start_span. (event_range::print_as_html): New, based on the above, but ported from pretty_printer to xml::printer. (thread_event_printer::print_swimlane_for_event_range): Rename to... (thread_event_printer::print_swimlane_for_event_range_as_text): ...this. Update for renaming of event_range::print to event_range::print_as_text. (thread_event_printer::print_swimlane_for_event_range_as_html): New. (print_path_summary_as_text): Update for "_as_text" renaming. (print_path_summary_as_html): New. (print_path_as_html): New. * diagnostic-show-locus.cc: Add defines of INCLUDE_MAP and INCLUDE_STRING. Add includes of "xml.h" and "xml-printer.h". (struct char_display_policy): Replace "m_print_cb" with "m_print_text_cb" and "m_print_html_cb". (struct to_text): New. (struct to_html): New. (get_printer): New. (default_diagnostic_start_span_fn): New. (default_diagnostic_start_span_fn): New. (class layout): Update "friend class layout_printer;" for template. (enum class margin_kind): New. (class layout_printer): Convert into a template. (layout_printer::m_pp): Replace field with... (layout_printer::m_sink): ...this. (layout_printer::m_colorizer): Drop field in favor of a pointer in the "to_text" sink. (default_print_decoded_ch): Convert into a template. (escape_as_bytes_print): Likewise. (escape_as_unicode_print): Likewise. (make_char_policy): Update to use both text and html callbacks. (layout_printer::print_gap_in_line_numbering): Replace with... (layout_printer::print_gap_in_line_numbering): ...this (layout_printer::print_gap_in_line_numbering): ...and this. (layout_printer::print_source_line): Convert to template, using m_sink. (layout_printer::print_leftmost_column): Likewise. (layout_printer::start_annotation_line): Likewise. (layout_printer::end_line): New. (layout_printer::end_line): New. (layout_printer::print_annotation_line): Convert to template, using m_sink. (class line_label): Add field m_original_range_idx. (layout_printer::begin_label): New. (layout_printer::begin_label): New. (layout_printer::end_label): New. (layout_printer::end_label): New. (layout_printer::print_any_labels): Convert to template, using m_sink. (layout_printer::print_leading_fixits): Likewise. (layout_printer::print_trailing_fixits): Likewise. (layout_printer::print_newline): Drop. (layout_printer::move_to_column): Convert to template, using m_sink. (layout_printer::show_ruler): Likewise. (layout_printer::print_line): Likewise. (layout_printer::print_any_right_to_left_edge_lines): Likewise. (layout_printer::layout_printer): Likewise. (diagnostic_context::maybe_show_locus_as_html): New. (diagnostic_source_print_policy::diagnostic_source_print_policy): Update for split of start_span_cb into text vs html variants. (diagnostic_source_print_policy::print): Update for use of templates; use to_text. (diagnostic_source_print_policy::print_as_html): New. (layout_printer::print): Convert to template, using m_sink. (selftest::make_element_for_locus): New. (selftest::make_raw_html_for_locus): New. (selftest::test_layout_x_offset_display_utf8): Update for use of templates. (selftest::test_layout_x_offset_display_tab): Likewise. (selftest::test_one_liner_caret_and_range): Add test coverage of HTML output. (selftest::test_one_liner_labels): Likewise. * diagnostic.cc (diagnostic_context::initialize): Update for split of start_span_cb into text vs html variants. (default_diagnostic_start_span_fn): Move to diagnostic-show-locus.cc, converting to template. * diagnostic.h (class xml::printer): New forward decl. (diagnostic_start_span_fn): Replace typedef with "using", converting to a template. (struct to_text): New forward decl. (struct to_html): New forward decl. (get_printer): New decl. (diagnostic_location_print_policy::print_text_span_start): New decl. (diagnostic_location_print_policy::print_html_span_start): New decl. (class html_label_writer): New. (diagnostic_source_print_policy::print_as_html): New decl. (diagnostic_source_print_policy::get_start_span_fn): Replace with... (diagnostic_source_print_policy::get_text_start_span_fn): ...this (diagnostic_source_print_policy::get_html_start_span_fn): ...and this (diagnostic_source_print_policy::m_start_span_cb): Replace with... (diagnostic_source_print_policy::m_text_start_span_cb): ...this (diagnostic_source_print_policy::m_html_start_span_cb): ...and this. (diagnostic_context::maybe_show_locus_as_html): New decl. (diagnostic_context::m_text_callbacks::m_start_span): Replace with... (diagnostic_context::m_text_callbacks::m_text_start_span): ...this (diagnostic_context::m_text_callbacks::m_html_start_span): ...and this. (diagnostic_start_span): Update for template change. (diagnostic_show_locus_as_html): New inline function. (default_diagnostic_start_span_fn): Convert to template. * doc/invoke.texi (experimental-html): Add "css" and "javascript" keys. * opts-diagnostic.cc (html_scheme_handler::make_sink): Likewise. * selftest-diagnostic.cc (selftest::test_diagnostic_context::start_span_cb): Update for template changes. * selftest-diagnostic.h (selftest::test_diagnostic_context::start_span_cb): Likewise. * xml-printer.h: New file. * xml.h: New file, based on material in diagnostic-format-html.cc, but using std::string rather than label_text. (xml::element::m_key_insertion_order): New field. (struct xml::raw): New. gcc/fortran/ChangeLog PR other/116792 * error.cc (gfc_diagnostic_start_span): Update for diagnostic.h changes. gcc/testsuite/ChangeLog: PR other/116792 * gcc.dg/html-output/missing-semicolon.c: Add ":javascript=no" to html output. * gcc.dg/html-output/missing-semicolon.py: Move repeated definitions into lib/htmltest.py. * gcc.dg/plugin/diagnostic_group_plugin.cc: Update for template changes. * gcc.dg/plugin/diagnostic-test-metadata-html.c: Add ":javascript=no" to html output. Add "-fdiagnostics-show-line-numbers". * gcc.dg/plugin/diagnostic-test-metadata-html.py: Move repeated definitions into lib/htmltest.py. Add checks of annotated source. * gcc.dg/plugin/diagnostic-test-paths-2.c: Add ":javascript=no" to html output. * gcc.dg/plugin/diagnostic-test-paths-2.py: Move repeated definitions into lib/htmltest.py. Add checks of execution path. * gcc.dg/plugin/diagnostic-test-paths-4.c: Add -fdiagnostics-add-output=experimental-html:javascript=no. Add invocation ot diagnostic-test-paths-4.py. * gcc.dg/plugin/diagnostic-test-paths-4.py: New test script. * gcc.dg/plugin/diagnostic-test-show-locus-bw-line-numbers.c: Add -fdiagnostics-add-output=experimental-html:javascript=no. Add invocation of diagnostic-test-show-locus.py. * gcc.dg/plugin/diagnostic-test-show-locus.py: New test script. * lib/htmltest.py: New test support script. Signed-off-by: David Malcolm --- diff --git a/gcc/diagnostic-format-html.cc b/gcc/diagnostic-format-html.cc index 6bb1caf41d1..f2b255bf9cd 100644 --- a/gcc/diagnostic-format-html.cc +++ b/gcc/diagnostic-format-html.cc @@ -20,6 +20,7 @@ along with GCC; see the file COPYING3. If not see #include "config.h" #define INCLUDE_MAP +#define INCLUDE_STRING #define INCLUDE_VECTOR #include "system.h" #include "coretypes.h" @@ -36,6 +37,17 @@ along with GCC; see the file COPYING3. If not see #include "pretty-print-urlifier.h" #include "edit-context.h" #include "intl.h" +#include "xml.h" +#include "xml-printer.h" +#include "json.h" + +// struct html_generation_options + +html_generation_options::html_generation_options () +: m_css (true), + m_javascript (true) +{ +} namespace xml { @@ -46,57 +58,6 @@ namespace xml { # pragma GCC diagnostic ignored "-Wformat-diag" #endif -struct node -{ - virtual ~node () {} - virtual void write_as_xml (pretty_printer *pp, - int depth, bool indent) const = 0; - void dump (FILE *out) const; - void DEBUG_FUNCTION dump () const { dump (stderr); } -}; - -struct text : public node -{ - text (label_text str) - : m_str (std::move (str)) - {} - - void write_as_xml (pretty_printer *pp, - int depth, bool indent) const final override; - - label_text m_str; -}; - -struct node_with_children : public node -{ - void add_child (std::unique_ptr node); - void add_text (label_text str); - - std::vector> m_children; -}; - -struct document : public node_with_children -{ - void write_as_xml (pretty_printer *pp, - int depth, bool indent) const final override; -}; - -struct element : public node_with_children -{ - element (const char *kind, bool preserve_whitespace) - : m_kind (kind), - m_preserve_whitespace (preserve_whitespace) - {} - - void write_as_xml (pretty_printer *pp, - int depth, bool indent) const final override; - - void set_attr (const char *name, label_text value); - - const char *m_kind; - bool m_preserve_whitespace; - std::map m_attributes; -}; /* Implementation. */ @@ -146,9 +107,16 @@ node::dump (FILE *out) const /* struct text : public node. */ void -text::write_as_xml (pretty_printer *pp, int /*depth*/, bool /*indent*/) const +text::write_as_xml (pretty_printer *pp, int depth, bool indent) const { - write_escaped_text (pp, m_str.get ()); + if (indent) + { + for (int i = 0; i < depth; ++i) + pp_string (pp, " "); + } + write_escaped_text (pp, m_str.c_str ()); + if (indent) + pp_newline (pp); } /* struct node_with_children : public node. */ @@ -161,9 +129,15 @@ node_with_children::add_child (std::unique_ptr node) } void -node_with_children::add_text (label_text str) +node_with_children::add_text (std::string str) { - gcc_assert (str.get ()); + // Consolidate runs of text + if (!m_children.empty ()) + if (text *t = m_children.back ()->dyn_cast_text ()) + { + t->m_str += std::move (str); + return; + } add_child (std::make_unique (std::move (str))); } @@ -177,6 +151,8 @@ document::write_as_xml (pretty_printer *pp, int depth, bool indent) const pp_string (pp, ""); + if (indent) + pp_newline (pp); for (auto &iter : m_children) iter->write_as_xml (pp, depth, indent); } @@ -188,48 +164,139 @@ element::write_as_xml (pretty_printer *pp, int depth, bool indent) const { if (indent) { - pp_newline (pp); for (int i = 0; i < depth; ++i) pp_string (pp, " "); } - if (m_preserve_whitespace) - indent = false; - - pp_printf (pp, "<%s", m_kind); - for (auto &attr : m_attributes) + pp_printf (pp, "<%s", m_kind.c_str ()); + for (auto &key : m_key_insertion_order) { - pp_printf (pp, " %s=\"", attr.first); - write_escaped_text (pp, attr.second.get ()); - pp_string (pp, "\""); + auto iter = m_attributes.find (key); + if (iter != m_attributes.end ()) + { + pp_printf (pp, " %s=\"", key.c_str ()); + write_escaped_text (pp, iter->second.c_str ()); + pp_string (pp, "\""); + } } if (m_children.empty ()) - pp_string (pp, " />"); + pp_string (pp, "/>"); else { + const bool indent_children = m_preserve_whitespace ? false : indent; pp_string (pp, ">"); + if (indent_children) + pp_newline (pp); for (auto &child : m_children) - child->write_as_xml (pp, depth + 1, indent); - if (indent) + child->write_as_xml (pp, depth + 1, indent_children); + if (indent_children) { - pp_newline (pp); for (int i = 0; i < depth; ++i) pp_string (pp, " "); } - pp_printf (pp, "", m_kind); + pp_printf (pp, "", m_kind.c_str ()); } + + if (indent) + pp_newline (pp); } void -element::set_attr (const char *name, label_text value) +element::set_attr (const char *name, std::string value) { + auto iter = m_attributes.find (name); + if (iter == m_attributes.end ()) + m_key_insertion_order.push_back (name); m_attributes[name] = std::move (value); } +// struct raw : public node + +void +raw::write_as_xml (pretty_printer *pp, + int /*depth*/, bool /*indent*/) const +{ + pp_string (pp, m_xml_src.c_str ()); +} + #if __GNUC__ >= 10 # pragma GCC diagnostic pop #endif +// class printer + +printer::printer (element &insertion_point) +{ + m_open_tags.push_back (&insertion_point); +} + +void +printer::push_tag (std::string name, + bool preserve_whitespace) +{ + push_element + (std::make_unique (std::move (name), + preserve_whitespace)); +} + +void +printer::push_tag_with_class (std::string name, std::string class_, + bool preserve_whitespace) +{ + auto new_element + = std::make_unique (std::move (name), + preserve_whitespace); + new_element->set_attr ("class", class_); + push_element (std::move (new_element)); +} + +void +printer::pop_tag () +{ + m_open_tags.pop_back (); +} + +void +printer::set_attr (const char *name, std::string value) +{ + m_open_tags.back ()->set_attr (name, value); +} + +void +printer::add_text (std::string text) +{ + element *parent = m_open_tags.back (); + parent->add_text (std::move (text)); +} + +void +printer::add_raw (std::string text) +{ + element *parent = m_open_tags.back (); + parent->add_child (std::make_unique (std::move (text))); +} + +void +printer::push_element (std::unique_ptr new_element) +{ + element *parent = m_open_tags.back (); + m_open_tags.push_back (new_element.get ()); + parent->add_child (std::move (new_element)); +} + +void +printer::append (std::unique_ptr new_node) +{ + element *parent = m_open_tags.back (); + parent->add_child (std::move (new_node)); +} + +element * +printer::get_insertion_point () const +{ + return m_open_tags.back (); +} + } // namespace xml class html_builder; @@ -284,7 +351,8 @@ public: html_builder (diagnostic_context &context, pretty_printer &pp, - const line_maps *line_maps); + const line_maps *line_maps, + const html_generation_options &html_gen_opts); void on_report_diagnostic (const diagnostic_info &diagnostic, diagnostic_t orig_diag_kind, @@ -309,15 +377,14 @@ public: std::unique_ptr make_element_for_metadata (const diagnostic_metadata &metadata); - std::unique_ptr - make_element_for_source (const diagnostic_info &diagnostic); - - std::unique_ptr - make_element_for_path (const diagnostic_path &path); - std::unique_ptr 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 ()); + } + private: std::unique_ptr make_element_for_diagnostic (const diagnostic_info &diagnostic, @@ -330,14 +397,18 @@ private: diagnostic_context &m_context; pretty_printer *m_printer; const line_maps *m_line_maps; + html_generation_options m_html_gen_opts; std::unique_ptr m_document; + xml::element *m_head_element; xml::element *m_diagnostics_element; std::unique_ptr m_cur_diagnostic_element; + int m_next_diag_id; // for handing out unique IDs + json::array m_ui_focus_ids; }; static std::unique_ptr -make_div (label_text class_) +make_div (std::string class_) { auto div = std::make_unique ("div", false); div->set_attr ("class", std::move (class_)); @@ -345,7 +416,7 @@ make_div (label_text class_) } static std::unique_ptr -make_span (label_text class_) +make_span (std::string class_) { auto span = std::make_unique ("span", true); span->set_attr ("class", std::move (class_)); @@ -400,45 +471,151 @@ diagnostic_html_format_buffer::flush () /* class html_builder. */ +/* Style information for writing out HTML paths. + Colors taken from https://www.patternfly.org/v3/styles/color-palette/ */ + +static const char * const HTML_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