[pushed:,r15-3752] diagnostics: add HTML output format as a plugin [PR116792]

Message ID 20240920230601.1850431-1-dmalcolm@redhat.com
State Committed
Commit 48261bd26df624f0b906600fb0ef6864c7a7f895
Headers
Series [pushed:,r15-3752] diagnostics: add HTML output format as a plugin [PR116792] |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gcc_build--master-arm warning Patch is already merged
linaro-tcwg-bot/tcwg_gcc_build--master-aarch64 warning Patch is already merged

Commit Message

David Malcolm Sept. 20, 2024, 11:06 p.m. UTC
  This patch adds an experimental diagnostics output format that
writes HTML.  It isn't ready yet for end-users, but seems worth
keeping in the tree as I refactor the diagnostics subsystem, to
ensure that this code still builds, and to verify that it's possible to
implement new diagnostic output formats via GCC plugins. Hence
this patch merely adds it to the testsuite as an example of a GCC
plugin, rather than exposing it as a feature for end-users.

Successfully bootstrapped & regrtested on x86_64-pc-linux-gnu.
Pushed to trunk as r15-3752-g48261bd26df624.

gcc/testsuite/ChangeLog:
	PR other/116792
	* gcc.dg/plugin/diagnostic-test-xhtml-1.c: New test.
	* gcc.dg/plugin/diagnostic_plugin_xhtml_format.c: New test plugin.
	* gcc.dg/plugin/plugin.exp (plugin_test_list): Add the above.

Signed-off-by: David Malcolm <dmalcolm@redhat.com>
---
 .../gcc.dg/plugin/diagnostic-test-xhtml-1.c   |  19 +
 .../plugin/diagnostic_plugin_xhtml_format.c   | 866 ++++++++++++++++++
 gcc/testsuite/gcc.dg/plugin/plugin.exp        |   2 +
 3 files changed, 887 insertions(+)
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-xhtml-1.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_xhtml_format.c
  

Patch

diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-xhtml-1.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-xhtml-1.c
new file mode 100644
index 000000000000..da069ff4789d
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-xhtml-1.c
@@ -0,0 +1,19 @@ 
+/* { dg-do compile } */
+
+int missing_semicolon (void)
+{
+  return 42
+}
+
+/* Verify some properties of the generated HTML.  */
+
+/* { dg-begin-multiline-output "" }
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html
+     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+   { dg-end-multiline-output "" } */
+
+/* { dg-excess-errors "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_xhtml_format.c b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_xhtml_format.c
new file mode 100644
index 000000000000..192288aff1bc
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_xhtml_format.c
@@ -0,0 +1,866 @@ 
+/* Verify that we can write a non-trivial diagnostic output format
+   as a plugin (XHTML).
+   Copyright (C) 2018-2024 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_LIST
+#define INCLUDE_MAP
+#define INCLUDE_MEMORY
+#define INCLUDE_VECTOR
+#include "system.h"
+#include "coretypes.h"
+#include "diagnostic.h"
+#include "diagnostic-metadata.h"
+#include "diagnostic-path.h"
+#include "cpplib.h"
+#include "logical-location.h"
+#include "diagnostic-client-data-hooks.h"
+#include "diagnostic-diagram.h"
+#include "text-art/canvas.h"
+#include "diagnostic-format.h"
+#include "ordered-hash-map.h"
+#include "sbitmap.h"
+#include "make-unique.h"
+#include "selftest.h"
+#include "selftest-diagnostic.h"
+#include "selftest-diagnostic-show-locus.h"
+#include "text-range-label.h"
+#include "pretty-print-format-impl.h"
+#include "pretty-print-urlifier.h"
+#include "intl.h"
+#include "gcc-plugin.h"
+#include "plugin-version.h"
+
+namespace xml {
+
+/* Disable warnings about quoting issues in the pp_xxx calls below
+   that (intentionally) don't follow GCC diagnostic conventions.  */
+#if __GNUC__ >= 10
+#  pragma GCC diagnostic push
+#  pragma GCC diagnostic ignored "-Wformat-diag"
+#endif
+
+static void write_escaped_text (const char *text);
+
+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> node);
+  void add_text (label_text str);
+
+  std::vector<std::unique_ptr<node>> 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<const char *, label_text> m_attributes;
+};
+
+/* Implementation.  */
+
+static void
+write_escaped_text (pretty_printer *pp, const char *text)
+{
+  gcc_assert (text);
+
+  for (const char *p = text; *p; ++p)
+    {
+      char ch = *p;
+      switch (ch)
+	{
+	default:
+	  pp_character (pp, ch);
+	  break;
+	case '\'':
+	  pp_string (pp, "&apos;");
+	  break;
+	case '"':
+	  pp_string (pp, "&quot;");
+	  break;
+	case '&':
+	  pp_string (pp, "&amp;");
+	  break;
+	case '<':
+	  pp_string (pp, "&lt;");
+	  break;
+	case '>':
+	  pp_string (pp, "&gt;");
+	  break;
+	}
+    }
+}
+
+/* struct node.  */
+
+void
+node::dump (FILE *out) const
+{
+  pretty_printer pp;
+  pp.set_output_stream (out);
+  write_as_xml (&pp, 0, true);
+  pp_flush (&pp);
+}
+
+/* struct text : public node.  */
+
+void
+text::write_as_xml (pretty_printer *pp, int /*depth*/, bool /*indent*/) const
+{
+  write_escaped_text (pp, m_str.get ());
+}
+
+/* struct node_with_children : public node.  */
+
+void
+node_with_children::add_child (std::unique_ptr<node> node)
+{
+  gcc_assert (node.get ());
+  m_children.push_back (std::move (node));
+}
+
+void
+node_with_children::add_text (label_text str)
+{
+  gcc_assert (str.get ());
+  add_child (::make_unique <text> (std::move (str)));
+}
+
+
+/* struct document : public node_with_children.  */
+
+void
+document::write_as_xml (pretty_printer *pp, int depth, bool indent) const
+{
+  pp_string (pp, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+  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\">");
+  for (auto &iter : m_children)
+    iter->write_as_xml (pp, depth, indent);
+}
+
+/* struct element : public node_with_children.  */
+
+void
+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=\"", attr.first);
+      write_escaped_text (pp, attr.second.get ());
+      pp_string (pp, "\"");
+    }
+  if (m_children.empty ())
+    pp_string (pp, " />");
+  else
+    {
+      pp_string (pp, ">");
+      for (auto &child : m_children)
+	child->write_as_xml (pp, depth + 1, indent);
+      if (indent)
+	{
+	  pp_newline (pp);
+	  for (int i = 0; i < depth; ++i)
+	    pp_string (pp, "  ");
+	}
+      pp_printf (pp, "</%s>", m_kind);
+    }
+}
+
+void
+element::set_attr (const char *name, label_text value)
+{
+  m_attributes[name] = std::move (value);
+}
+
+#if __GNUC__ >= 10
+#  pragma GCC diagnostic pop
+#endif
+
+} // namespace xml
+
+/* A class for managing XHTML 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 xhtml_builder
+{
+public:
+  xhtml_builder (diagnostic_context &context,
+		 const line_maps *line_maps);
+
+  void on_report_diagnostic (diagnostic_context &context,
+			     const diagnostic_info &diagnostic,
+			     diagnostic_t orig_diag_kind);
+  void emit_diagram (diagnostic_context &context,
+		     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; }
+
+private:
+  std::unique_ptr<xml::element>
+  make_element_for_diagnostic (diagnostic_context &context,
+			       const diagnostic_info &diagnostic,
+			       diagnostic_t orig_diag_kind);
+
+  diagnostic_context &m_context;
+  const line_maps *m_line_maps;
+
+  std::unique_ptr<xml::document> m_document;
+  xml::element *m_diagnostics_element;
+  std::unique_ptr<xml::element> m_cur_diagnostic_element;
+};
+
+static std::unique_ptr<xml::element>
+make_div (label_text class_)
+{
+  auto div = ::make_unique<xml::element> ("div", false);
+  div->set_attr ("class", std::move (class_));
+  return div;
+}
+
+static std::unique_ptr<xml::element>
+make_span (label_text class_)
+{
+  auto span = ::make_unique<xml::element> ("span", true);
+  span->set_attr ("class", std::move (class_));
+  return span;
+}
+
+/* class xhtml_builder.  */
+
+/* xhtml_builder's ctor.  */
+
+xhtml_builder::xhtml_builder (diagnostic_context &context,
+			      const line_maps *line_maps)
+: m_context (context),
+  m_line_maps (line_maps)
+{
+  gcc_assert (m_line_maps);
+
+  m_document = ::make_unique<xml::document> ();
+  {
+    auto html_element = ::make_unique<xml::element> ("html", false);
+    html_element->set_attr
+      ("xmlns",
+       label_text::borrow ("http://www.w3.org/1999/xhtml"));
+    {
+      {
+	auto head_element = ::make_unique<xml::element> ("head", false);
+	{
+	  auto title_element = ::make_unique<xml::element> ("title", true);
+	  label_text title (label_text::borrow ("Title goes here")); // TODO
+	  title_element->add_text (std::move (title));
+	  head_element->add_child (std::move (title_element));
+	}
+	html_element->add_child (std::move (head_element));
+
+	auto body_element = ::make_unique<xml::element> ("body", false);
+	{
+	  auto diagnostics_element
+	    = make_div (label_text::borrow ("gcc-diagnostic-list"));
+	  m_diagnostics_element = diagnostics_element.get ();
+	  body_element->add_child (std::move (diagnostics_element));
+	}
+	html_element->add_child (std::move (body_element));
+      }
+    }
+    m_document->add_child (std::move (html_element));
+  }
+}
+
+/* Implementation of "on_report_diagnostic" for XHTML output.  */
+
+void
+xhtml_builder::on_report_diagnostic (diagnostic_context &context,
+				     const diagnostic_info &diagnostic,
+				     diagnostic_t orig_diag_kind)
+{
+  // TODO: handle (diagnostic.kind == DK_ICE || diagnostic.kind == DK_ICE_NOBT)
+
+  auto diag_element
+    = make_element_for_diagnostic (context, diagnostic, orig_diag_kind);
+  if (m_cur_diagnostic_element)
+    /* Nested diagnostic.  */
+    m_cur_diagnostic_element->add_child (std::move (diag_element));
+  else
+    /* Top-level diagnostic.  */
+    m_cur_diagnostic_element = std::move (diag_element);
+}
+
+std::unique_ptr<xml::element>
+xhtml_builder::make_element_for_diagnostic (diagnostic_context &context,
+					    const diagnostic_info &diagnostic,
+					    diagnostic_t orig_diag_kind)
+{
+  class xhtml_token_printer : public token_printer
+  {
+  public:
+    xhtml_token_printer (xhtml_builder &builder,
+			 xml::element &parent_element)
+    : m_builder (builder)
+    {
+      m_open_elements.push_back (&parent_element);
+    }
+    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.  */
+	      insertion_element ().add_text
+		(label_text::take (xstrdup (sub->m_value.get ())));
+	    }
+	    break;
+
+	  case pp_token::kind::begin_color:
+	  case pp_token::kind::end_color:
+	    /* These are no-ops.  */
+	    break;
+
+	  case pp_token::kind::begin_quote:
+	    {
+	      insertion_element ().add_text (label_text::borrow (open_quote));
+	      push_element (make_span (label_text::borrow ("gcc-quoted-text")));
+	    }
+	    break;
+	  case pp_token::kind::end_quote:
+	    {
+	      pop_element ();
+	      insertion_element ().add_text (label_text::borrow (close_quote));
+	    }
+	    break;
+
+	  case pp_token::kind::begin_url:
+	    {
+	      pp_token_begin_url *sub = as_a <pp_token_begin_url *> (iter);
+	      auto anchor = ::make_unique<xml::element> ("a", true);
+	      anchor->set_attr ("href", std::move (sub->m_value));
+	      push_element (std::move (anchor));
+	    }
+	    break;
+	  case pp_token::kind::end_url:
+	    pop_element ();
+	    break;
+	  }
+    }
+
+  private:
+    xml::element &insertion_element () const
+    {
+      return *m_open_elements.back ();
+    }
+    void push_element (std::unique_ptr<xml::element> new_element)
+    {
+      xml::element &current_top = insertion_element ();
+      m_open_elements.push_back (new_element.get ());
+      current_top.add_child (std::move (new_element));
+    }
+    void pop_element ()
+    {
+      m_open_elements.pop_back ();
+    }
+
+    xhtml_builder &m_builder;
+    /* We maintain a stack of currently "open" elements.
+       Children are added to the topmost open element.  */
+    std::vector<xml::element *> m_open_elements;
+  };
+
+  auto diag_element = make_div (label_text::borrow ("gcc-diagnostic"));
+
+  // TODO: might be nice to emulate the text output format, but colorize it
+
+  auto message_span = make_span (label_text::borrow ("gcc-message"));
+  xhtml_token_printer tok_printer (*this, *message_span.get ());
+  context.m_printer->set_token_printer (&tok_printer);
+  pp_output_formatted_text (context.m_printer, context.get_urlifier ());
+  context.m_printer->set_token_printer (nullptr);
+  pp_clear_output_area (context.m_printer);
+  diag_element->add_child (std::move (message_span));
+
+  if (diagnostic.metadata)
+    {
+      int cwe = diagnostic.metadata->get_cwe ();
+      if (cwe)
+	{
+	  diag_element->add_text (label_text::borrow (" "));
+	  auto cwe_span = make_span (label_text::borrow ("gcc-cwe-metadata"));
+	  cwe_span->add_text (label_text::borrow ("["));
+	  {
+	    auto anchor = ::make_unique<xml::element> ("a", true);
+	    anchor->set_attr ("href", label_text::take (get_cwe_url (cwe)));
+	    pretty_printer pp;
+	    pp_printf (&pp, "CWE-%i", cwe);
+	    anchor->add_text
+	      (label_text::take (xstrdup (pp_formatted_text (&pp))));
+	    cwe_span->add_child (std::move (anchor));
+	  }
+	  cwe_span->add_text (label_text::borrow ("]"));
+	  diag_element->add_child (std::move (cwe_span));
+	}
+    }
+
+  // TODO: show any rules
+
+  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));
+
+      diag_element->add_text (label_text::borrow (" "));
+      auto option_span = make_span (label_text::borrow ("gcc-option"));
+      option_span->add_text (label_text::borrow ("["));
+      {
+	if (option_url.get ())
+	  {
+	    auto anchor = ::make_unique<xml::element> ("a", true);
+	    anchor->set_attr ("href", std::move (option_url));
+	    anchor->add_text (std::move (option_text));
+	    option_span->add_child (std::move (anchor));
+	  }
+	else
+	  option_span->add_text (std::move (option_text));
+	option_span->add_text (label_text::borrow ("]"));
+      }
+      diag_element->add_child (std::move (option_span));
+    }
+
+  {
+    auto pre = ::make_unique<xml::element> ("pre", true);
+    pre->set_attr ("class", label_text::borrow ("gcc-annotated-source"));
+    // TODO: ideally we'd like to capture elements within the following:
+    diagnostic_show_locus (&context, diagnostic.richloc, diagnostic.kind);
+    pre->add_text
+      (label_text::take (xstrdup (pp_formatted_text (context.m_printer))));
+    pp_clear_output_area (context.m_printer);
+    diag_element->add_child (std::move (pre));
+  }
+
+  return diag_element;
+}
+
+/* Implementation of diagnostic_context::m_diagrams.m_emission_cb
+   for XHTML output.  */
+
+void
+xhtml_builder::emit_diagram (diagnostic_context &/*context*/,
+			     const diagnostic_diagram &/*diagram*/)
+{
+  /* We must be within the emission of a top-level diagnostic.  */
+  gcc_assert (m_cur_diagnostic_element);
+
+  // TODO
+}
+
+/* Implementation of "end_group_cb" for XHTML output.  */
+
+void
+xhtml_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
+xhtml_builder::flush_to_file (FILE *outf)
+{
+  auto top = m_document.get ();
+  top->dump (outf);
+  fprintf (outf, "\n");
+}
+
+/* Callback for diagnostic_context::ice_handler_cb for when an ICE
+   occurs.  */
+
+static void
+xhtml_ice_handler (diagnostic_context *context)
+{
+  /* Attempt to ensure that a .xhtml file is written out.  */
+  diagnostic_finish (context);
+
+  /* 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");
+}
+
+class xhtml_output_format : public diagnostic_output_format
+{
+public:
+  ~xhtml_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 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 (m_context, diagnostic, orig_diag_kind);
+  }
+  void on_diagram (const diagnostic_diagram &diagram) final override
+  {
+    m_builder.emit_diagram (m_context, diagram);
+  }
+  void after_diagnostic (const diagnostic_info &)
+  {
+    /* No-op, but perhaps could show paths here.  */
+  }
+
+  const xml::document &get_document () const
+  {
+    return m_builder.get_document ();
+  }
+
+protected:
+  xhtml_output_format (diagnostic_context &context,
+		       const line_maps *line_maps)
+  : diagnostic_output_format (context),
+    m_builder (context, line_maps)
+  {}
+
+  xhtml_builder m_builder;
+};
+
+class xhtml_stream_output_format : public xhtml_output_format
+{
+public:
+  xhtml_stream_output_format (diagnostic_context &context,
+			      const line_maps *line_maps,
+			      FILE *stream)
+  : xhtml_output_format (context, line_maps),
+    m_stream (stream)
+  {
+  }
+  ~xhtml_stream_output_format ()
+  {
+    m_builder.flush_to_file (m_stream);
+  }
+  bool machine_readable_stderr_p () const final override
+  {
+    return m_stream == stderr;
+  }
+private:
+  FILE *m_stream;
+};
+
+class xhtml_file_output_format : public xhtml_output_format
+{
+public:
+  xhtml_file_output_format (diagnostic_context &context,
+			    const line_maps *line_maps,
+			    const char *base_file_name)
+  : xhtml_output_format (context, line_maps),
+    m_base_file_name (xstrdup (base_file_name))
+  {
+  }
+  ~xhtml_file_output_format ()
+  {
+    char *filename = concat (m_base_file_name, ".xhtml", nullptr);
+    free (m_base_file_name);
+    m_base_file_name = nullptr;
+    FILE *outf = fopen (filename, "w");
+    if (!outf)
+      {
+	const char *errstr = xstrerror (errno);
+	fnotice (stderr, "error: unable to open '%s' for writing: %s\n",
+		 filename, errstr);
+	free (filename);
+	return;
+      }
+    m_builder.flush_to_file (outf);
+    fclose (outf);
+    free (filename);
+  }
+  bool machine_readable_stderr_p () const final override
+  {
+    return false;
+  }
+
+private:
+  char *m_base_file_name;
+};
+
+/* Populate CONTEXT in preparation for XHTML output (either to stderr, or
+   to a file).  */
+
+static void
+diagnostic_output_format_init_xhtml (diagnostic_context &context,
+				     std::unique_ptr<xhtml_output_format> fmt)
+{
+  /* Override callbacks.  */
+  context.set_ice_handler_callback (xhtml_ice_handler);
+
+  /* Don't colorize the text.  */
+  pp_show_color (context.m_printer) = false;
+  context.set_show_highlight_colors (false);
+
+  context.set_output_format (fmt.release ());
+}
+
+/* Populate CONTEXT in preparation for XHTML output to stderr.  */
+
+void
+diagnostic_output_format_init_xhtml_stderr (diagnostic_context &context,
+					    const line_maps *line_maps)
+{
+  gcc_assert (line_maps);
+  auto format = ::make_unique<xhtml_stream_output_format> (context,
+							   line_maps,
+							   stderr);
+  diagnostic_output_format_init_xhtml (context, std::move (format));
+}
+
+/* Populate CONTEXT in preparation for XHTML output to a file named
+   BASE_FILE_NAME.xhtml.  */
+
+void
+diagnostic_output_format_init_xhtml_file (diagnostic_context &context,
+					  const line_maps *line_maps,
+					  const char *base_file_name)
+{
+  gcc_assert (line_maps);
+  auto format = ::make_unique<xhtml_file_output_format> (context,
+							 line_maps,
+							 base_file_name);
+  diagnostic_output_format_init_xhtml (context, std::move (format));
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* A subclass of xhtml_output_format for writing selftests.
+   The JSON output is cached internally, rather than written
+   out to a file.  */
+
+class test_xhtml_diagnostic_context : public test_diagnostic_context
+{
+public:
+  test_xhtml_diagnostic_context ()
+  {
+    auto format = ::make_unique<xhtml_buffered_output_format> (*this,
+							       line_table);
+    m_format = format.get (); // borrowed
+    diagnostic_output_format_init_xhtml (*this, std::move (format));
+  }
+
+  const xml::document &get_document () const
+  {
+    return m_format->get_document ();
+  }
+
+private:
+  class xhtml_buffered_output_format : public xhtml_output_format
+  {
+  public:
+    xhtml_buffered_output_format (diagnostic_context &context,
+				  const line_maps *line_maps)
+      : xhtml_output_format (context, line_maps)
+    {
+    }
+    bool machine_readable_stderr_p () const final override
+    {
+      return true;
+    }
+  };
+
+  xhtml_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_xhtml_diagnostic_context dc;
+
+  rich_location richloc (line_table, UNKNOWN_LOCATION);
+  dc.report (DK_ERROR, richloc, nullptr, 0, "this is a test: %i", 42);
+
+  const xml::document &doc  = dc.get_document ();
+
+  pretty_printer pp;
+  doc.write_as_xml (&pp, 0, true);
+  ASSERT_STREQ
+    (pp_formatted_text (&pp),
+     ("<?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>Title goes here</title>\n"
+      "  </head>\n"
+      "  <body>\n"
+      "    <div class=\"gcc-diagnostic-list\">\n"
+      "      <div class=\"gcc-diagnostic\">\n"
+      "        <span class=\"gcc-message\">this is a test: 42</span>\n"
+      "        <pre class=\"gcc-annotated-source\"></pre>\n"
+      "      </div>\n"
+      "    </div>\n"
+      "  </body>\n"
+      "</html>"));
+}
+
+/* Run all of the selftests within this file.  */
+
+static void
+xhtml_format_selftests ()
+{
+  test_simple_log ();
+}
+
+} // namespace selftest
+
+#endif /* CHECKING_P */
+
+/* Plugin hooks.  */
+
+int plugin_is_GPL_compatible;
+
+/* Entrypoint for the plugin.  */
+
+int
+plugin_init (struct plugin_name_args *plugin_info,
+	     struct plugin_gcc_version *version)
+{
+  const char *plugin_name = plugin_info->base_name;
+  int argc = plugin_info->argc;
+  struct plugin_argument *argv = plugin_info->argv;
+
+  if (!plugin_default_version_check (version, &gcc_version))
+    return 1;
+
+  global_dc->set_output_format (new xhtml_stream_output_format (*global_dc,
+								line_table,
+								stderr));
+
+#if CHECKING_P
+  selftest::xhtml_format_selftests ();
+#endif
+
+  return 0;
+}
diff --git a/gcc/testsuite/gcc.dg/plugin/plugin.exp b/gcc/testsuite/gcc.dg/plugin/plugin.exp
index 2c2d919eddf0..8ac1bfa7f77b 100644
--- a/gcc/testsuite/gcc.dg/plugin/plugin.exp
+++ b/gcc/testsuite/gcc.dg/plugin/plugin.exp
@@ -74,6 +74,8 @@  set plugin_test_list [list \
 	  crash-test-write-though-null-stderr.c \
 	  crash-test-ice-sarif.c \
 	  crash-test-write-though-null-sarif.c } \
+    { diagnostic_plugin_xhtml_format.c \
+	  diagnostic-test-xhtml-1.c } \
     { diagnostic_group_plugin.c \
 	  diagnostic-group-test-1.c } \
     { diagnostic_plugin_test_show_locus.c \