This patch refactors the support for -fdiagnostics-add-output=SCHEME
from GCC's options parsing so that it is also available to
sarif-replay and to other clients of libgdiagnostics.

With this users of sarif-replay and other such tools can generate HTML
or SARIF as well as text output, using the same
  -fdiagnostics-add-output=SCHEME
as GCC.

As a test, the patch adds support for this option to the dg-lint
script below "contrib".  For example dg-lint can now generate text,
html, and sarif output via:

  LD_LIBRARY_PATH=../build/gcc/ \
    ./contrib/dg-lint/dg-lint \
        contrib/dg-lint/test-*.c \
        -fdiagnostics-add-output=experimental-html:file=dg-lint-tests.html \
        -fdiagnostics-add-output=sarif:file=dg-lint-tests.sarif

where the HTML output from dg-lint can be seen here:
  https://dmalcolm.fedorapeople.org/gcc/2025-06-20/dg-lint-tests.html
the sarif output here:
  https://dmalcolm.fedorapeople.org/gcc/2025-06-23/dg-lint-tests.sarif
and a screenshot of VS Code viewing the sarif output is here:
  
https://dmalcolm.fedorapeople.org/gcc/2025-06-23/vscode-viewing-dg-lint-sarif-output.png

As well as allowing sarif-replay to generate HTML, this patch allows
sarif-replay to also generate SARIF.  Ideally this would faithfully
round-trip all the data, but it's not perfect (which I'm tracking as
PR sarif-replay/120792).

Successfully bootstrapped & regrtested on x86_64-pc-linux-gnu.
Pushed to trunk as r16-1636-gd0142e147486e6.

contrib/ChangeLog:
        PR other/116792
        PR testsuite/116163
        PR sarif-replay/120792
        * dg-lint/dg-lint: Add -fdiagnostics-add-output.
        * dg-lint/libgdiagnostics.py: Add
        diagnostic_manager_add_sink_from_spec.
        (Manager.add_sink_from_spec): New.

gcc/ChangeLog:
        PR other/116792
        PR testsuite/116163
        PR sarif-replay/120792
        * Makefile.in (OBJS-libcommon): Add diagnostic-output-spec.o.
        * diagnostic-format-html.cc (html_builder::html_builder): Ensure
        title is non-empty.
        * diagnostic-output-spec.cc: New file, taken from material in
        opts-diagnostic.cc.
        * diagnostic-output-spec.h: New file.
        * diagnostic.cc (diagnostic_context::set_main_input_filename):
        New.
        * diagnostic.h (diagnostic_context::set_main_input_filename): New
        decl.
        * doc/libgdiagnostics/topics/compatibility.rst
        (LIBGDIAGNOSTICS_ABI_2): New.
        * doc/libgdiagnostics/topics/diagnostic-manager.rst
        (diagnostic_manager_add_sink_from_spec): New.
        (diagnostic_manager_set_analysis_target): New.
        * libgdiagnostics++.h (manager::add_sink_from_spec): New.
        (manager::set_analysis_target): New.
        * libgdiagnostics.cc: Include "diagnostic-output-spec.h".
        (struct spec_context): New.
        (diagnostic_manager_add_sink_from_spec): New.
        (diagnostic_manager_set_analysis_target): New.
        * libgdiagnostics.h
        (LIBDIAGNOSTICS_HAVE_diagnostic_manager_add_sink_from_spec): New
        define.
        (diagnostic_manager_add_sink_from_spec): New decl.
        (LIBDIAGNOSTICS_HAVE_diagnostic_manager_set_analysis_target): New
        define.
        (diagnostic_manager_set_analysis_target): New decl.
        * libgdiagnostics.map (LIBGDIAGNOSTICS_ABI_2): New.
        * libsarifreplay.cc (sarif_replayer::handle_artifact_obj): Looks
        for "analysisTarget" in roles and call set_analysis_target using
        the artifact if found.
        * opts-diagnostic.cc: Refactor, moving material to
        diagnostic-output-spec.cc.
        (struct opt_spec_context): New.
        (handle_OPT_fdiagnostics_add_output_): Use opt_spec_context.
        (handle_OPT_fdiagnostics_set_output_): Likewise.
        * sarif-replay.cc: Define INCLUDE_STRING.
        (struct options): Add m_extra_output_specs.
        (usage_msg): Add -fdiagnostics-add-output=SCHEME.
        (str_starts_with): New.
        (parse_options): Add -fdiagnostics-add-output=SCHEME.
        (main): Likewise.
        * selftest-run-tests.cc (selftest::run_tests): Call
        diagnostic_output_spec_cc_tests rather than
        opts_diagnostic_cc_tests.
        * selftest.h (selftest::diagnostic_output_spec_cc_tests):
        Replace...
        (selftest::opts_diagnostic_cc_tests): ...this.

gcc/testsuite/ChangeLog:
        PR other/116792
        PR testsuite/116163
        PR sarif-replay/120792
        * sarif-replay.dg/2.1.0-valid/signal-1-check-html.py: New test
        script.
        * sarif-replay.dg/2.1.0-valid/signal-1.c.sarif: Add html and sarif
        generation to options.  Invoke the new script to verify that HTML
        and SARIF is generated.
---
 contrib/dg-lint/dg-lint                       |   8 +
 contrib/dg-lint/libgdiagnostics.py            |  17 +
 gcc/Makefile.in                               |   1 +
 gcc/diagnostic-format-html.cc                 |   1 +
 gcc/diagnostic-output-spec.cc                 | 828 ++++++++++++++++++
 gcc/diagnostic-output-spec.h                  | 116 +++
 gcc/diagnostic.cc                             |   7 +
 gcc/diagnostic.h                              |   2 +
 .../libgdiagnostics/topics/compatibility.rst  |   9 +
 .../topics/diagnostic-manager.rst             |  42 +
 gcc/libgdiagnostics++.h                       |  19 +
 gcc/libgdiagnostics.cc                        |  65 ++
 gcc/libgdiagnostics.h                         |  31 +
 gcc/libgdiagnostics.map                       |   7 +
 gcc/libsarifreplay.cc                         |  11 +
 gcc/opts-diagnostic.cc                        | 820 +----------------
 gcc/sarif-replay.cc                           |  32 +
 gcc/selftest-run-tests.cc                     |   2 +-
 gcc/selftest.h                                |   2 +-
 .../2.1.0-valid/signal-1-check-html.py        |  26 +
 .../signal-1-check-sarif-roundtrip.py         |  41 +
 .../2.1.0-valid/signal-1.c.sarif              |  10 +
 22 files changed, 1304 insertions(+), 793 deletions(-)
 create mode 100644 gcc/diagnostic-output-spec.cc
 create mode 100644 gcc/diagnostic-output-spec.h
 create mode 100644 
gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-html.py
 create mode 100644 
gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-sarif-roundtrip.py

diff --git a/contrib/dg-lint/dg-lint b/contrib/dg-lint/dg-lint
index 01d58d7a3e9..4ae0686b975 100755
--- a/contrib/dg-lint/dg-lint
+++ b/contrib/dg-lint/dg-lint
@@ -380,9 +380,17 @@ def skip_file(filename):
 def main(argv):
     parser = argparse.ArgumentParser()#usage=__doc__)
     parser.add_argument('paths', nargs='+', type=pathlib.Path)
+    parser.add_argument('-fdiagnostics-add-output', action='append')
     opts = parser.parse_args(argv[1:])
 
     ctxt = Context()
+    control_mgr = libgdiagnostics.Manager()
+    control_mgr.add_text_sink()
+    for scheme in opts.fdiagnostics_add_output:
+        ctxt.mgr.add_sink_from_spec("-fdiagnostics-add-output=",
+                                    scheme,
+                                    control_mgr)
+
     for path in opts.paths:
         if path.is_dir():
             for dirpath, dirnames, filenames in os.walk(path):
diff --git a/contrib/dg-lint/libgdiagnostics.py 
b/contrib/dg-lint/libgdiagnostics.py
index 03a6440a3e3..8c8cc4887cd 100644
--- a/contrib/dg-lint/libgdiagnostics.py
+++ b/contrib/dg-lint/libgdiagnostics.py
@@ -124,6 +124,13 @@ cdll.diagnostic_add_fix_it_hint_replace.argtypes \
        ctypes.c_char_p]
 cdll.diagnostic_add_fix_it_hint_replace.restype = None
 
+cdll.diagnostic_manager_add_sink_from_spec.argtypes \
+    = [c_diagnostic_manager_ptr,
+       ctypes.c_char_p,
+       ctypes.c_char_p,
+       c_diagnostic_manager_ptr]
+cdll.diagnostic_manager_add_sink_from_spec.restype = ctypes.c_int
+
 # Helper functions
 
 def _to_utf8(s: str):
@@ -156,6 +163,16 @@ class Manager:
                                                c_stderr,
                                                DIAGNOSTIC_COLORIZE_IF_TTY)
 
+    def add_sink_from_spec(self, option_name: str, scheme: str, control_mgr):
+        assert self.c_mgr
+        assert control_mgr.c_mgr
+        res = cdll.diagnostic_manager_add_sink_from_spec (self.c_mgr,
+                                                          
_to_utf8(option_name),
+                                                          _to_utf8(scheme),
+                                                          control_mgr.c_mgr)
+        if res:
+            raise RuntimeError()
+
     def get_file(self, path: str, sarif_lang: str = None):
         assert self.c_mgr
         assert path
diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index f106e833425..9535804f7fb 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1858,6 +1858,7 @@ OBJS-libcommon = diagnostic-spec.o diagnostic.o 
diagnostic-color.o \
        diagnostic-format-text.o \
        diagnostic-global-context.o \
        diagnostic-macro-unwinding.o \
+       diagnostic-output-spec.o \
        diagnostic-path.o \
        diagnostic-path-output.o \
        diagnostic-show-locus.o \
diff --git a/gcc/diagnostic-format-html.cc b/gcc/diagnostic-format-html.cc
index b1b0895d031..c397c9f088d 100644
--- a/gcc/diagnostic-format-html.cc
+++ b/gcc/diagnostic-format-html.cc
@@ -426,6 +426,7 @@ html_builder::html_builder (diagnostic_context &context,
       {
        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)
diff --git a/gcc/diagnostic-output-spec.cc b/gcc/diagnostic-output-spec.cc
new file mode 100644
index 00000000000..e58f0c40fc0
--- /dev/null
+++ b/gcc/diagnostic-output-spec.cc
@@ -0,0 +1,828 @@
+/* Support for the DSL of -fdiagnostics-add-output= and
+   -fdiagnostics-set-output=.
+   Copyright (C) 2024-2025 Free Software Foundation, Inc.
+
+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/>.  */
+
+/* This file implements the domain-specific language for the options
+   -fdiagnostics-add-output= and -fdiagnostics-set-output=, and for
+   the "diagnostic_manager_add_sink_from_spec" entrypoint to
+   libgdiagnostics.  */
+
+#include "config.h"
+#define INCLUDE_ARRAY
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "system.h"
+#include "coretypes.h"
+#include "version.h"
+#include "intl.h"
+#include "diagnostic.h"
+#include "diagnostic-color.h"
+#include "diagnostic-format.h"
+#include "diagnostic-format-html.h"
+#include "diagnostic-format-text.h"
+#include "diagnostic-format-sarif.h"
+#include "selftest.h"
+#include "selftest-diagnostic.h"
+#include "pretty-print-markup.h"
+#include "diagnostic-output-spec.h"
+
+/* A namespace for handling the DSL of the arguments of
+   -fdiagnostics-add-output= and -fdiagnostics-set-output=.  */
+
+namespace diagnostics_output_spec {
+
+/* Decls.  */
+
+struct scheme_name_and_params
+{
+  std::string m_scheme_name;
+  std::vector<std::pair<std::string, std::string>> m_kvs;
+};
+
+/* Class for parsing the arguments of -fdiagnostics-add-output= and
+   -fdiagnostics-set-output=, and making diagnostic_output_format
+   instances (or issuing errors).  */
+
+class output_factory
+{
+public:
+  class scheme_handler
+  {
+  public:
+    scheme_handler (std::string scheme_name)
+    : m_scheme_name (std::move (scheme_name))
+    {}
+    virtual ~scheme_handler () {}
+
+    const std::string &get_scheme_name () const { return m_scheme_name; }
+
+    virtual std::unique_ptr<diagnostic_output_format>
+    make_sink (const context &ctxt,
+              diagnostic_context &dc,
+              const char *unparsed_arg,
+              const scheme_name_and_params &parsed_arg) const = 0;
+
+  protected:
+    bool
+    parse_bool_value (const context &ctxt,
+                     const char *unparsed_arg,
+                     const std::string &key,
+                     const std::string &value,
+                     bool &out) const
+    {
+      if (value == "yes")
+       {
+         out = true;
+         return true;
+       }
+      else if (value == "no")
+       {
+         out = false;
+         return true;
+       }
+      else
+       {
+         ctxt.report_error
+           ("%<%s%s%>:"
+            " unexpected value %qs for key %qs; expected %qs or %qs",
+            ctxt.get_option_name (), unparsed_arg,
+            value.c_str (),
+            key.c_str (),
+            "yes", "no");
+
+         return false;
+       }
+    }
+    template <typename EnumType, size_t NumValues>
+    bool
+    parse_enum_value (const context &ctxt,
+                     const char *unparsed_arg,
+                     const std::string &key,
+                     const std::string &value,
+                     const std::array<std::pair<const char *, EnumType>, 
NumValues> &value_names,
+                     EnumType &out) const
+    {
+      for (auto &iter : value_names)
+       if (value == iter.first)
+         {
+           out = iter.second;
+           return true;
+         }
+
+      auto_vec<const char *> known_values;
+      for (auto iter : value_names)
+       known_values.safe_push (iter.first);
+      pp_markup::comma_separated_quoted_strings e (known_values);
+      ctxt.report_error
+       ("%<%s%s%>:"
+        " unexpected value %qs for key %qs; known values: %e",
+        ctxt.get_option_name (), unparsed_arg,
+        value.c_str (),
+        key.c_str (),
+        &e);
+      return false;
+    }
+
+  private:
+    const std::string m_scheme_name;
+  };
+
+  output_factory ();
+
+  std::unique_ptr<diagnostic_output_format>
+  make_sink (const context &ctxt,
+            diagnostic_context &dc,
+            const char *unparsed_arg,
+            const scheme_name_and_params &parsed_arg);
+
+  const scheme_handler *get_scheme_handler (const std::string &scheme_name);
+
+private:
+  std::vector<std::unique_ptr<scheme_handler>> m_scheme_handlers;
+};
+
+class text_scheme_handler : public output_factory::scheme_handler
+{
+public:
+  text_scheme_handler () : scheme_handler ("text") {}
+
+  std::unique_ptr<diagnostic_output_format>
+  make_sink (const context &ctxt,
+            diagnostic_context &dc,
+            const char *unparsed_arg,
+            const scheme_name_and_params &parsed_arg) const final override;
+};
+
+class sarif_scheme_handler : public output_factory::scheme_handler
+{
+public:
+  sarif_scheme_handler () : scheme_handler ("sarif") {}
+
+  std::unique_ptr<diagnostic_output_format>
+  make_sink (const context &ctxt,
+            diagnostic_context &dc,
+            const char *unparsed_arg,
+            const scheme_name_and_params &parsed_arg) const final override;
+};
+
+class html_scheme_handler : public output_factory::scheme_handler
+{
+public:
+  html_scheme_handler () : scheme_handler ("experimental-html") {}
+
+  std::unique_ptr<diagnostic_output_format>
+  make_sink (const context &ctxt,
+            diagnostic_context &dc,
+            const char *unparsed_arg,
+            const scheme_name_and_params &parsed_arg) const final override;
+};
+
+/* struct context.  */
+
+void
+context::report_error (const char *gmsgid, ...) const
+{
+  va_list ap;
+  va_start (ap, gmsgid);
+  report_error_va (gmsgid, &ap);
+  va_end (ap);
+}
+
+void
+context::report_unknown_key (const char *unparsed_arg,
+                            const std::string &key,
+                            const std::string &scheme_name,
+                            auto_vec<const char *> &known_keys) const
+{
+  pp_markup::comma_separated_quoted_strings e (known_keys);
+  report_error
+    ("%<%s%s%>:"
+     " unknown key %qs for format %qs; known keys: %e",
+     get_option_name (), unparsed_arg,
+     key.c_str (), scheme_name.c_str (), &e);
+}
+
+void
+context::report_missing_key (const char *unparsed_arg,
+                            const std::string &key,
+                            const std::string &scheme_name,
+                            const char *metavar) const
+{
+  report_error
+    ("%<%s%s%>:"
+     " missing required key %qs for format %qs;"
+     " try %<%s%s:%s=%s%>",
+     get_option_name (), unparsed_arg,
+     key.c_str (), scheme_name.c_str (),
+     get_option_name (), scheme_name.c_str (), key.c_str (), metavar);
+}
+
+diagnostic_output_file
+context::open_output_file (label_text &&filename) const
+{
+  FILE *outf = fopen (filename.get (), "w");
+  if (!outf)
+    {
+      report_error ("unable to open %qs: %m", filename.get ());
+      return diagnostic_output_file (nullptr, false, std::move (filename));
+    }
+  return diagnostic_output_file (outf, true, std::move (filename));
+}
+
+static std::unique_ptr<scheme_name_and_params>
+parse (const context &ctxt, const char *unparsed_arg)
+{
+  scheme_name_and_params result;
+  if (const char *const colon = strchr (unparsed_arg, ':'))
+    {
+      result.m_scheme_name = std::string (unparsed_arg, colon - unparsed_arg);
+      /* Expect zero of more of KEY=VALUE,KEY=VALUE, etc  .*/
+      const char *iter = colon + 1;
+      const char *last_separator = ":";
+      while (iter)
+       {
+         /* Look for a non-empty key string followed by '='.  */
+         const char *eq = strchr (iter, '=');
+         if (eq == nullptr || eq == iter)
+           {
+             /* Missing '='.  */
+             ctxt.report_error
+               ("%<%s%s%>:"
+                " expected KEY=VALUE-style parameter for format %qs"
+                " after %qs;"
+                " got %qs",
+                ctxt.get_option_name (), unparsed_arg,
+                result.m_scheme_name.c_str (),
+                last_separator,
+                iter);
+             return nullptr;
+           }
+         std::string key = std::string (iter, eq - iter);
+         std::string value;
+         const char *comma = strchr (iter, ',');
+         if (comma)
+           {
+             value = std::string (eq + 1, comma - (eq + 1));
+             iter = comma + 1;
+             last_separator = ",";
+           }
+         else
+           {
+             value = std::string (eq + 1);
+             iter = nullptr;
+           }
+         result.m_kvs.push_back ({std::move (key), std::move (value)});
+       }
+    }
+  else
+    result.m_scheme_name = unparsed_arg;
+  return std::make_unique<scheme_name_and_params> (std::move (result));
+}
+
+std::unique_ptr<diagnostic_output_format>
+context::parse_and_make_sink (const char *unparsed_arg,
+                             diagnostic_context &dc)
+{
+  auto parsed_arg = diagnostics_output_spec::parse (*this, unparsed_arg);
+  if (!parsed_arg)
+    return nullptr;
+
+  diagnostics_output_spec::output_factory factory;
+  return factory.make_sink (*this, dc, unparsed_arg, *parsed_arg);
+}
+
+/* class output_factory::scheme_handler.  */
+
+/* class output_factory.  */
+
+output_factory::output_factory ()
+{
+  m_scheme_handlers.push_back (std::make_unique<text_scheme_handler> ());
+  m_scheme_handlers.push_back (std::make_unique<sarif_scheme_handler> ());
+  m_scheme_handlers.push_back (std::make_unique<html_scheme_handler> ());
+}
+
+const output_factory::scheme_handler *
+output_factory::get_scheme_handler (const std::string &scheme_name)
+{
+  for (auto &iter : m_scheme_handlers)
+    if (iter->get_scheme_name () == scheme_name)
+      return iter.get ();
+  return nullptr;
+}
+
+std::unique_ptr<diagnostic_output_format>
+output_factory::make_sink (const context &ctxt,
+                          diagnostic_context &dc,
+                          const char *unparsed_arg,
+                          const scheme_name_and_params &parsed_arg)
+{
+  auto scheme_handler = get_scheme_handler (parsed_arg.m_scheme_name);
+  if (!scheme_handler)
+    {
+      auto_vec<const char *> strings;
+      for (auto &iter : m_scheme_handlers)
+       strings.safe_push (iter->get_scheme_name ().c_str ());
+      pp_markup::comma_separated_quoted_strings e (strings);
+      ctxt.report_error ("%<%s%s%>:"
+                        " unrecognized format %qs; known formats: %e",
+                        ctxt.get_option_name (), unparsed_arg,
+                        parsed_arg.m_scheme_name.c_str (), &e);
+      return nullptr;
+    }
+
+  return scheme_handler->make_sink (ctxt, dc, unparsed_arg, parsed_arg);
+}
+
+/* class text_scheme_handler : public output_factory::scheme_handler.  */
+
+std::unique_ptr<diagnostic_output_format>
+text_scheme_handler::make_sink (const context &ctxt,
+                               diagnostic_context &dc,
+                               const char *unparsed_arg,
+                               const scheme_name_and_params &parsed_arg) const
+{
+  bool show_color = pp_show_color (dc.get_reference_printer ());
+  bool show_nesting = false;
+  bool show_locations_in_nesting = true;
+  bool show_levels = false;
+  for (auto& iter : parsed_arg.m_kvs)
+    {
+      const std::string &key = iter.first;
+      const std::string &value = iter.second;
+      if (key == "color")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value, show_color))
+           return nullptr;
+         continue;
+       }
+      if (key == "experimental-nesting")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                show_nesting))
+           return nullptr;
+         continue;
+       }
+      if (key == "experimental-nesting-show-locations")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                show_locations_in_nesting))
+           return nullptr;
+         continue;
+       }
+      if (key == "experimental-nesting-show-levels")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value, show_levels))
+           return nullptr;
+         continue;
+       }
+
+      /* Key not found.  */
+      auto_vec<const char *> known_keys;
+      known_keys.safe_push ("color");
+      known_keys.safe_push ("experimental-nesting");
+      known_keys.safe_push ("experimental-nesting-show-locations");
+      known_keys.safe_push ("experimental-nesting-show-levels");
+      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (),
+                              known_keys);
+      return nullptr;
+    }
+
+  auto sink = std::make_unique<diagnostic_text_output_format> (dc);
+  sink->set_show_nesting (show_nesting);
+  sink->set_show_locations_in_nesting (show_locations_in_nesting);
+  sink->set_show_nesting_levels (show_levels);
+  return sink;
+}
+
+/* class sarif_scheme_handler : public output_factory::scheme_handler.  */
+
+std::unique_ptr<diagnostic_output_format>
+sarif_scheme_handler::make_sink (const context &ctxt,
+                                diagnostic_context &dc,
+                                const char *unparsed_arg,
+                                const scheme_name_and_params &parsed_arg) const
+{
+  label_text filename;
+  enum sarif_serialization_kind serialization_kind
+    = sarif_serialization_kind::json;
+  enum sarif_version version = sarif_version::v2_1_0;
+  bool xml_state = false;
+  for (auto& iter : parsed_arg.m_kvs)
+    {
+      const std::string &key = iter.first;
+      const std::string &value = iter.second;
+      if (key == "file")
+       {
+         filename = label_text::take (xstrdup (value.c_str ()));
+         continue;
+       }
+      if (key == "serialization")
+       {
+         static const std::array<std::pair<const char *, enum 
sarif_serialization_kind>,
+                                 (size_t)sarif_serialization_kind::num_values> 
value_names
+           {{{"json", sarif_serialization_kind::json}}};
+
+         if (!parse_enum_value<enum sarif_serialization_kind>
+                (ctxt, unparsed_arg,
+                 key, value,
+                 value_names,
+                 serialization_kind))
+           return nullptr;
+         continue;
+       }
+      if (key == "version")
+       {
+         static const std::array<std::pair<const char *, enum sarif_version>,
+                                 (size_t)sarif_version::num_versions> 
value_names
+           {{{"2.1", sarif_version::v2_1_0},
+             {"2.2-prerelease", sarif_version::v2_2_prerelease_2024_08_08}}};
+
+           if (!parse_enum_value<enum sarif_version> (ctxt, unparsed_arg,
+                                                      key, value,
+                                                      value_names,
+                                                      version))
+           return nullptr;
+         continue;
+       }
+      if (key == "xml-state")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                xml_state))
+           return nullptr;
+         continue;
+       }
+
+      /* Key not found.  */
+      auto_vec<const char *> known_keys;
+      known_keys.safe_push ("file");
+      known_keys.safe_push ("serialization");
+      known_keys.safe_push ("version");
+      known_keys.safe_push ("xml-state");
+      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (),
+                              known_keys);
+      return nullptr;
+    }
+
+  diagnostic_output_file output_file;
+  if (filename.get ())
+    output_file = ctxt.open_output_file (std::move (filename));
+  else
+    // Default filename
+    {
+      const char *basename = ctxt.get_base_filename ();
+      if (!basename)
+       {
+         ctxt.report_missing_key (unparsed_arg,
+                                  "file",
+                                  get_scheme_name (),
+                                  "FILENAME");
+         return nullptr;
+       }
+      output_file
+       = diagnostic_output_format_open_sarif_file
+           (dc,
+            ctxt.get_affected_location_mgr (),
+            basename,
+            serialization_kind);
+    }
+  if (!output_file)
+    return nullptr;
+
+  sarif_generation_options sarif_gen_opts;
+  sarif_gen_opts.m_version = version;
+  sarif_gen_opts.m_xml_state = xml_state;
+
+  std::unique_ptr<sarif_serialization_format> serialization_obj;
+  switch (serialization_kind)
+    {
+    default:
+      gcc_unreachable ();
+    case sarif_serialization_kind::json:
+      serialization_obj
+       = std::make_unique<sarif_serialization_format_json> (true);
+      break;
+    }
+
+  auto sink = make_sarif_sink (dc,
+                              *ctxt.get_affected_location_mgr (),
+                              std::move (serialization_obj),
+                              sarif_gen_opts,
+                              std::move (output_file));
+  return sink;
+}
+
+/* class html_scheme_handler : public output_factory::scheme_handler.  */
+
+std::unique_ptr<diagnostic_output_format>
+html_scheme_handler::make_sink (const context &ctxt,
+                               diagnostic_context &dc,
+                               const char *unparsed_arg,
+                               const scheme_name_and_params &parsed_arg) const
+{
+  bool css = true;
+  label_text filename;
+  bool javascript = true;
+  bool show_state_diagrams = false;
+  bool show_state_diagram_xml = false;
+  bool show_state_diagram_dot_src = false;
+  for (auto& iter : parsed_arg.m_kvs)
+    {
+      const std::string &key = iter.first;
+      const std::string &value = iter.second;
+      if (key == "css")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                css))
+           return nullptr;
+         continue;
+       }
+      if (key == "file")
+       {
+         filename = label_text::take (xstrdup (value.c_str ()));
+         continue;
+       }
+      if (key == "javascript")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                javascript))
+           return nullptr;
+         continue;
+       }
+      if (key == "show-state-diagrams")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                show_state_diagrams))
+           return nullptr;
+         continue;
+       }
+      if (key == "show-state-diagram-dot-src")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                show_state_diagram_dot_src))
+           return nullptr;
+         continue;
+       }
+      if (key == "show-state-diagram-xml")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                show_state_diagram_xml))
+           return nullptr;
+         continue;
+       }
+
+      /* Key not found.  */
+      auto_vec<const char *> known_keys;
+      known_keys.safe_push ("css");
+      known_keys.safe_push ("file");
+      known_keys.safe_push ("javascript");
+      known_keys.safe_push ("show-state-diagrams");
+      known_keys.safe_push ("show-state-diagram-dot-src");
+      known_keys.safe_push ("show-state-diagram-xml");
+      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (),
+                              known_keys);
+      return nullptr;
+    }
+
+  diagnostic_output_file output_file;
+  if (filename.get ())
+    output_file = ctxt.open_output_file (std::move (filename));
+  else
+    // Default filename
+    {
+      const char *basename = ctxt.get_base_filename ();
+      if (!basename)
+       {
+         ctxt.report_missing_key (unparsed_arg,
+                                  "file",
+                                  get_scheme_name (),
+                                  "FILENAME");
+         return nullptr;
+       }
+      output_file
+       = diagnostic_output_format_open_html_file
+           (dc,
+            ctxt.get_affected_location_mgr (),
+            basename);
+    }
+  if (!output_file)
+    return nullptr;
+
+  html_generation_options html_gen_opts;
+  html_gen_opts.m_css = css;
+  html_gen_opts.m_javascript = javascript;
+  html_gen_opts.m_show_state_diagrams = show_state_diagrams;
+  html_gen_opts.m_show_state_diagram_xml = show_state_diagram_xml;
+  html_gen_opts.m_show_state_diagram_dot_src = show_state_diagram_dot_src;
+
+  auto sink = make_html_sink (dc,
+                             *ctxt.get_affected_location_mgr (),
+                             html_gen_opts,
+                             std::move (output_file));
+  return sink;
+}
+
+} // namespace diagnostics_output_spec
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* RAII class to temporarily override "progname" to the
+   string "PROGNAME".  */
+
+class auto_fix_progname
+{
+public:
+  auto_fix_progname ()
+  {
+    m_old_progname = progname;
+    progname = "PROGNAME";
+  }
+
+  ~auto_fix_progname ()
+  {
+    progname = m_old_progname;
+  }
+
+private:
+  const char *m_old_progname;
+};
+
+struct parser_test
+{
+  class test_spec_context : public diagnostics_output_spec::gcc_spec_context
+  {
+  public:
+    test_spec_context (diagnostic_context &dc,
+                      line_maps *location_mgr,
+                      location_t loc,
+                      const char *option_name)
+    : gcc_spec_context (dc,
+                       location_mgr,
+                       location_mgr,
+                       loc,
+                       option_name)
+    {
+    }
+
+    const char *
+    get_base_filename () const final override
+    {
+      return "BASE_FILENAME";
+    }
+  };
+
+  parser_test ()
+  : m_dc (),
+    m_ctxt (m_dc, line_table, UNKNOWN_LOCATION, "-fOPTION="),
+    m_fmt (m_dc.get_output_format (0))
+  {
+    pp_buffer (m_fmt.get_printer ())->m_flush_p = false;
+  }
+
+  std::unique_ptr<diagnostics_output_spec::scheme_name_and_params>
+  parse (const char *unparsed_arg)
+  {
+    return diagnostics_output_spec::parse (m_ctxt, unparsed_arg);
+  }
+
+  bool execution_failed_p () const
+  {
+    return m_dc.execution_failed_p ();
+  }
+
+  const char *
+  get_diagnostic_text () const
+  {
+    return pp_formatted_text (m_fmt.get_printer ());
+  }
+
+private:
+  test_diagnostic_context m_dc;
+  test_spec_context m_ctxt;
+  diagnostic_output_format &m_fmt;
+};
+
+/* Selftests.  */
+
+static void
+test_output_arg_parsing ()
+{
+  auto_fix_quotes fix_quotes;
+  auto_fix_progname fix_progname;
+
+  /* Minimal correct example.  */
+  {
+    parser_test pt;
+    auto result = pt.parse ("foo");
+    ASSERT_EQ (result->m_scheme_name, "foo");
+    ASSERT_EQ (result->m_kvs.size (), 0);
+    ASSERT_FALSE (pt.execution_failed_p ());
+  }
+
+  /* Stray trailing colon with no key/value pairs.  */
+  {
+    parser_test pt;
+    auto result = pt.parse ("foo:");
+    ASSERT_EQ (result, nullptr);
+    ASSERT_TRUE (pt.execution_failed_p ());
+    ASSERT_STREQ (pt.get_diagnostic_text (),
+                 "PROGNAME: error: `-fOPTION=foo:':"
+                 " expected KEY=VALUE-style parameter for format `foo'"
+                 " after `:';"
+                 " got `'\n");
+  }
+
+  /* No key before '='.  */
+  {
+    parser_test pt;
+    auto result = pt.parse ("foo:=");
+    ASSERT_EQ (result, nullptr);
+    ASSERT_TRUE (pt.execution_failed_p ());
+    ASSERT_STREQ (pt.get_diagnostic_text (),
+                 "PROGNAME: error: `-fOPTION=foo:=':"
+                 " expected KEY=VALUE-style parameter for format `foo'"
+                 " after `:';"
+                 " got `='\n");
+  }
+
+  /* No value for key.  */
+  {
+    parser_test pt;
+    auto result = pt.parse ("foo:key,");
+    ASSERT_EQ (result, nullptr);
+    ASSERT_TRUE (pt.execution_failed_p ());
+    ASSERT_STREQ (pt.get_diagnostic_text (),
+                 "PROGNAME: error: `-fOPTION=foo:key,':"
+                 " expected KEY=VALUE-style parameter for format `foo'"
+                 " after `:';"
+                 " got `key,'\n");
+  }
+
+  /* Correct example, with one key/value pair.  */
+  {
+    parser_test pt;
+    auto result = pt.parse ("foo:key=value");
+    ASSERT_EQ (result->m_scheme_name, "foo");
+    ASSERT_EQ (result->m_kvs.size (), 1);
+    ASSERT_EQ (result->m_kvs[0].first, "key");
+    ASSERT_EQ (result->m_kvs[0].second, "value");
+    ASSERT_FALSE (pt.execution_failed_p ());
+  }
+
+  /* Stray trailing comma.  */
+  {
+    parser_test pt;
+    auto result = pt.parse ("foo:key=value,");
+    ASSERT_EQ (result, nullptr);
+    ASSERT_TRUE (pt.execution_failed_p ());
+    ASSERT_STREQ (pt.get_diagnostic_text (),
+                 "PROGNAME: error: `-fOPTION=foo:key=value,':"
+                 " expected KEY=VALUE-style parameter for format `foo'"
+                 " after `,';"
+                 " got `'\n");
+  }
+
+  /* Correct example, with two key/value pairs.  */
+  {
+    parser_test pt;
+    auto result = pt.parse ("foo:color=red,shape=circle");
+    ASSERT_EQ (result->m_scheme_name, "foo");
+    ASSERT_EQ (result->m_kvs.size (), 2);
+    ASSERT_EQ (result->m_kvs[0].first, "color");
+    ASSERT_EQ (result->m_kvs[0].second, "red");
+    ASSERT_EQ (result->m_kvs[1].first, "shape");
+    ASSERT_EQ (result->m_kvs[1].second, "circle");
+    ASSERT_FALSE (pt.execution_failed_p ());
+  }
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+diagnostic_output_spec_cc_tests ()
+{
+  test_output_arg_parsing ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/diagnostic-output-spec.h b/gcc/diagnostic-output-spec.h
new file mode 100644
index 00000000000..e02cdfe1705
--- /dev/null
+++ b/gcc/diagnostic-output-spec.h
@@ -0,0 +1,116 @@
+/* Support for the DSL of -fdiagnostics-add-output= and
+   -fdiagnostics-set-output=.
+   Copyright (C) 2024-2025 Free Software Foundation, Inc.
+
+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/>.  */
+
+#ifndef GCC_DIAGNOSTIC_OUTPUT_SPEC_H
+#define GCC_DIAGNOSTIC_OUTPUT_SPEC_H
+
+#include "diagnostic-format.h"
+#include "diagnostic-output-file.h"
+
+namespace diagnostics_output_spec {
+
+/* An abstract base class for handling the DSL of -fdiagnostics-add-output=
+   and -fdiagnostics-set-output=.  */
+
+class context
+{
+ public:
+  std::unique_ptr<diagnostic_output_format>
+  parse_and_make_sink (const char *,
+                      diagnostic_context &dc);
+
+  void
+  report_error (const char *gmsgid, ...) const
+    ATTRIBUTE_GCC_DIAG(2,3);
+
+  void
+  report_unknown_key (const char *unparsed_arg,
+                     const std::string &key,
+                     const std::string &scheme_name,
+                     auto_vec<const char *> &known_keys) const;
+
+  void
+  report_missing_key (const char *unparsed_arg,
+                     const std::string &key,
+                     const std::string &scheme_name,
+                     const char *metavar) const;
+
+  diagnostic_output_file
+  open_output_file (label_text &&filename) const;
+
+  const char *
+  get_option_name () const { return m_option_name; }
+
+  line_maps *
+  get_affected_location_mgr () const { return m_affected_location_mgr; }
+
+  virtual ~context () {}
+
+  virtual void
+  report_error_va (const char *gmsgid, va_list *ap) const = 0;
+
+  virtual const char *
+  get_base_filename () const = 0;
+
+protected:
+  context (const char *option_name,
+          line_maps *affected_location_mgr)
+  : m_option_name (option_name),
+    m_affected_location_mgr (affected_location_mgr)
+  {
+  }
+
+  const char *m_option_name;
+  line_maps *m_affected_location_mgr;
+};
+
+/* A subclass that implements reporting errors via a diagnostic_context.  */
+
+struct gcc_spec_context : public diagnostics_output_spec::context
+{
+public:
+  gcc_spec_context (diagnostic_context &dc,
+                   line_maps *affected_location_mgr,
+                   line_maps *control_location_mgr,
+                   location_t loc,
+                   const char *option_name)
+  : context (option_name, affected_location_mgr),
+    m_dc (dc),
+    m_control_location_mgr (control_location_mgr),
+    m_loc (loc)
+  {}
+
+  void report_error_va (const char *gmsgid, va_list *ap) const final override
+    ATTRIBUTE_GCC_DIAG(2, 0)
+  {
+    m_dc.begin_group ();
+    rich_location richloc (m_control_location_mgr, m_loc);
+    m_dc.diagnostic_impl (&richloc, nullptr, -1, gmsgid, ap, DK_ERROR);
+    m_dc.end_group ();
+  }
+
+  diagnostic_context &m_dc;
+  line_maps *m_control_location_mgr;
+  location_t m_loc;
+};
+
+} // namespace diagnostics_output_spec
+
+#endif
diff --git a/gcc/diagnostic.cc b/gcc/diagnostic.cc
index ab52e34e2fb..8547d4882d3 100644
--- a/gcc/diagnostic.cc
+++ b/gcc/diagnostic.cc
@@ -524,6 +524,13 @@ diagnostic_context::supports_fnotice_on_stderr_p () const
   return true;
 }
 
+void
+diagnostic_context::set_main_input_filename (const char *filename)
+{
+  for (auto sink : m_output_sinks)
+    sink->set_main_input_filename (filename);
+}
+
 void
 diagnostic_context::
 set_client_data_hooks (std::unique_ptr<diagnostic_client_data_hooks> hooks)
diff --git a/gcc/diagnostic.h b/gcc/diagnostic.h
index b6cab0d52ed..f9c8253395b 100644
--- a/gcc/diagnostic.h
+++ b/gcc/diagnostic.h
@@ -840,6 +840,8 @@ public:
     return m_option_classifier.m_classification_history;
   }
 
+  void set_main_input_filename (const char *filename);
+
 private:
   void error_recursion () ATTRIBUTE_NORETURN;
 
diff --git a/gcc/doc/libgdiagnostics/topics/compatibility.rst 
b/gcc/doc/libgdiagnostics/topics/compatibility.rst
index 4df685001e6..10adcc516ce 100644
--- a/gcc/doc/libgdiagnostics/topics/compatibility.rst
+++ b/gcc/doc/libgdiagnostics/topics/compatibility.rst
@@ -177,3 +177,12 @@ acccessing values within a 
:type:`diagnostic_logical_location`:
   * :func:`diagnostic_logical_location_get_fully_qualified_name`
 
   * :func:`diagnostic_logical_location_get_decorated_name`
+
+``LIBGDIAGNOSTICS_ABI_2``
+-------------------------
+``LIBGDIAGNOSTICS_ABI_2`` covers the addition of these functions for
+supporting command-line options and SARIF playback:
+
+  * :func:`diagnostic_manager_add_sink_from_spec`
+
+  * :func:`diagnostic_manager_set_analysis_target`
diff --git a/gcc/doc/libgdiagnostics/topics/diagnostic-manager.rst 
b/gcc/doc/libgdiagnostics/topics/diagnostic-manager.rst
index d594b260709..0390704963b 100644
--- a/gcc/doc/libgdiagnostics/topics/diagnostic-manager.rst
+++ b/gcc/doc/libgdiagnostics/topics/diagnostic-manager.rst
@@ -56,3 +56,45 @@ Responsibilities include:
    This will flush output to all of the output sinks, and clean up.
 
    The parameter must be non-NULL.
+
+.. function:: int diagnostic_manager_add_sink_from_spec (diagnostic_manager 
*affected_mgr, \
+                                      const char *option_name, \
+                                      const char *spec, \
+                                      diagnostic_manager *control_mgr)
+
+   This function can be used to support option processing similar to GCC's
+   :option:`-fdiagnostics-add-output=`.  This allows command-line tools to
+   support the same domain-specific language for specifying output sink
+   as GCC does.
+
+   The function will attempt to parse :param:`spec` as if it were
+   an argument to GCC's :option:`-fdiagnostics-add-output=OUTPUT-SPEC`.
+   If successful, it will add an output sink to :param:`affected_mgr` and 
return zero.
+   Otherwise, it will emit an error diagnostic to :param:`control_mgr` and
+   return non-zero.
+
+   :param:`affected_mgr` and :param:`control_mgr` can be the same manager,
+   or be different managers.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_2`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_manager_add_sink_from_spec
+
+
+.. function:: void diagnostic_manager_set_analysis_target (diagnostic_manager 
*mgr, \
+                                                          const 
diagnostic_file *file)
+
+   This function sets the "main input file" of :param:`mgr` to be
+   :param:`file`.
+   This affects the :code:`<title>` of generated HTML and
+   the :code:`role` of the artifact in SARIF output (SARIF v2.1.0 section 
3.24.6).
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_2`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_manager_set_analysis_target
diff --git a/gcc/libgdiagnostics++.h b/gcc/libgdiagnostics++.h
index 18a88a2472c..93b8f90aefb 100644
--- a/gcc/libgdiagnostics++.h
+++ b/gcc/libgdiagnostics++.h
@@ -329,6 +329,17 @@ public:
                                       version);
   }
 
+  bool
+  add_sink_from_spec (const char *option_name,
+                     const char *spec,
+                     manager control_mgr)
+  {
+    return diagnostic_manager_add_sink_from_spec (m_inner,
+                                                 option_name,
+                                                 spec,
+                                                 control_mgr.m_inner);
+  }
+
   void
   write_patch (FILE *dst_stream)
   {
@@ -381,6 +392,8 @@ public:
   diagnostic
   begin_diagnostic (enum diagnostic_level level);
 
+  void
+  set_analysis_target (file f);
 
   diagnostic_manager *m_inner;
   bool m_owned;
@@ -683,6 +696,12 @@ manager::begin_diagnostic (enum diagnostic_level level)
   return diagnostic (diagnostic_begin (m_inner, level));
 }
 
+inline void
+manager::set_analysis_target (file f)
+{
+  diagnostic_manager_set_analysis_target (m_inner, f.m_inner);
+}
+
 } // namespace libgdiagnostics
 
 #endif // #ifndef LIBGDIAGNOSTICSPP_H
diff --git a/gcc/libgdiagnostics.cc b/gcc/libgdiagnostics.cc
index 29f63eb720c..74814c7ef28 100644
--- a/gcc/libgdiagnostics.cc
+++ b/gcc/libgdiagnostics.cc
@@ -31,6 +31,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "diagnostic-client-data-hooks.h"
 #include "diagnostic-format-sarif.h"
 #include "diagnostic-format-text.h"
+#include "diagnostic-output-spec.h"
 #include "logical-location.h"
 #include "edit-context.h"
 #include "libgdiagnostics.h"
@@ -1983,3 +1984,67 @@ diagnostic_logical_location_get_decorated_name (const 
diagnostic_logical_locatio
 
   return loc->m_decorated_name.get_str ();
 }
+
+namespace {
+
+struct spec_context : public diagnostics_output_spec::context
+{
+public:
+  spec_context (const char *option_name,
+               diagnostic_manager &affected_mgr,
+               diagnostic_manager &control_mgr)
+  : context (option_name, affected_mgr.get_line_table ()),
+    m_control_mgr (control_mgr)
+  {}
+
+  void report_error_va (const char *gmsgid, va_list *ap) const final override
+  {
+    diagnostic *diag
+      = diagnostic_begin (&m_control_mgr, DIAGNOSTIC_LEVEL_ERROR);
+    diagnostic_finish_va (diag, gmsgid, ap);
+  }
+
+  const char *
+  get_base_filename () const final override
+  {
+    return nullptr;
+  }
+
+private:
+  diagnostic_manager &m_control_mgr;
+};
+
+} // anon namespace
+
+/* Public entrypoint.  */
+
+int
+diagnostic_manager_add_sink_from_spec (diagnostic_manager *affected_mgr,
+                                      const char *option_name,
+                                      const char *spec,
+                                      diagnostic_manager *control_mgr)
+{
+  FAIL_IF_NULL (affected_mgr);
+  FAIL_IF_NULL (option_name);
+  FAIL_IF_NULL (spec);
+  FAIL_IF_NULL (control_mgr);
+
+  spec_context ctxt (option_name, *affected_mgr, *control_mgr);
+  auto inner_sink = ctxt.parse_and_make_sink (spec, affected_mgr->get_dc ());
+  if (!inner_sink)
+    return -1;
+  affected_mgr->get_dc ().add_sink (std::move (inner_sink));
+  return 0;
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_set_analysis_target (diagnostic_manager *mgr,
+                                       const diagnostic_file *file)
+{
+  FAIL_IF_NULL (mgr);
+  FAIL_IF_NULL (file);
+
+  mgr->get_dc ().set_main_input_filename (file->get_name ());
+}
diff --git a/gcc/libgdiagnostics.h b/gcc/libgdiagnostics.h
index f957779604b..9af2747cb82 100644
--- a/gcc/libgdiagnostics.h
+++ b/gcc/libgdiagnostics.h
@@ -734,6 +734,37 @@ extern diagnostic_file *
 diagnostic_physical_location_get_file (const diagnostic_physical_location 
*physical_loc)
   LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL(0);
 
+/* Attempt to parse SPEC as if an argument to GCC's
+   -fdiagnostics-add-output=OUTPUT-SPEC.
+   If successful, add an output sink to AFFECTED_MGR and return zero.
+   Otherwise, emit a diagnostic to CONTROL_MGR and return non-zero.
+   Added in LIBGDIAGNOSTICS_ABI_2.  */
+#define LIBDIAGNOSTICS_HAVE_diagnostic_manager_add_sink_from_spec
+
+extern int
+diagnostic_manager_add_sink_from_spec (diagnostic_manager *affected_mgr,
+                                      const char *option_name,
+                                      const char *spec,
+                                      diagnostic_manager *control_mgr)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (4);
+
+
+/* Set the main input file of MGR to be FILE.
+   This affects the <title> of generated HTML and
+   the "role" of the artifact in SARIF output (SARIF v2.1.0
+   section 3.24.6).
+   Added in LIBGDIAGNOSTICS_ABI_2.  */
+#define LIBDIAGNOSTICS_HAVE_diagnostic_manager_set_analysis_target
+
+extern void
+diagnostic_manager_set_analysis_target (diagnostic_manager *mgr,
+                                       const diagnostic_file *file)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
 /* DEFERRED:
    - thread-safety
    - plural forms
diff --git a/gcc/libgdiagnostics.map b/gcc/libgdiagnostics.map
index 4233cf86c76..49cabcad0b7 100644
--- a/gcc/libgdiagnostics.map
+++ b/gcc/libgdiagnostics.map
@@ -83,3 +83,10 @@ LIBGDIAGNOSTICS_ABI_1 {
     diagnostic_logical_location_get_fully_qualified_name;
     diagnostic_logical_location_get_decorated_name;
 } LIBGDIAGNOSTICS_ABI_0;
+
+# Add hooks needed for HTML output from sarif-replay
+LIBGDIAGNOSTICS_ABI_2 {
+  global:
+    diagnostic_manager_add_sink_from_spec;
+    diagnostic_manager_set_analysis_target;
+} LIBGDIAGNOSTICS_ABI_1;
diff --git a/gcc/libsarifreplay.cc b/gcc/libsarifreplay.cc
index 6c797624f82..2d6c394e697 100644
--- a/gcc/libsarifreplay.cc
+++ b/gcc/libsarifreplay.cc
@@ -966,6 +966,17 @@ sarif_replayer::handle_artifact_obj (const json::object 
&artifact_obj)
   auto file = m_output_mgr.new_file (artifact_loc_uri->get_string (),
                                     sarif_source_language);
 
+  // 3.24.6 "roles" property
+  const property_spec_ref prop_roles
+    ("artifact", "roles", "3.24.6");
+  if (auto roles_obj
+      = get_optional_property<json::array> (artifact_obj,
+                                           prop_roles))
+    for (auto iter : *roles_obj)
+      if (auto str = require_string (*iter, prop_roles))
+       if (!strcmp (str->get_string (), "analysisTarget"))
+         m_output_mgr.set_analysis_target (file);
+
   // Set contents, if available
   const property_spec_ref prop_contents
     ("artifact", "contents", "3.24.8");
diff --git a/gcc/opts-diagnostic.cc b/gcc/opts-diagnostic.cc
index 1a7d83c0476..d2c3a932e2a 100644
--- a/gcc/opts-diagnostic.cc
+++ b/gcc/opts-diagnostic.cc
@@ -19,7 +19,8 @@ along with GCC; see the file COPYING3.  If not see
 
 
 /* This file implements the options -fdiagnostics-add-output=,
-   -fdiagnostics-set-output=, and their domain-specific language.  */
+   -fdiagnostics-set-output=.  Most of the work is done
+   by diagnostic-output-spec.cc so it can be shared by libgdiagnostics.  */
 
 #include "config.h"
 #define INCLUDE_ARRAY
@@ -30,627 +31,42 @@ along with GCC; see the file COPYING3.  If not see
 #include "version.h"
 #include "intl.h"
 #include "diagnostic.h"
-#include "diagnostic-color.h"
-#include "diagnostic-format.h"
-#include "diagnostic-format-html.h"
-#include "diagnostic-format-text.h"
-#include "diagnostic-format-sarif.h"
-#include "selftest.h"
-#include "selftest-diagnostic.h"
-#include "pretty-print-markup.h"
+#include "diagnostic-output-spec.h"
 #include "opts.h"
 #include "options.h"
 
-/* A namespace for handling the DSL of the arguments of
-   -fdiagnostics-add-output= and -fdiagnostics-set-output=.  */
-
-namespace gcc {
-namespace diagnostics_output_spec {
-
 /* Decls.  */
 
-struct context
+namespace {
+
+struct opt_spec_context : public diagnostics_output_spec::gcc_spec_context
 {
 public:
-  context (const gcc_options &opts,
-          diagnostic_context &dc,
-          line_maps *location_mgr,
-          location_t loc,
-          const char *option_name)
-  : m_opts (opts), m_dc (dc), m_location_mgr (location_mgr), m_loc (loc),
-    m_option_name (option_name)
+  opt_spec_context (const gcc_options &opts,
+                   diagnostic_context &dc,
+                   line_maps *location_mgr,
+                   location_t loc,
+                   const char *option_name)
+  : gcc_spec_context (dc,
+                     location_mgr,
+                     location_mgr,
+                     loc,
+                     option_name),
+    m_opts (opts)
   {}
 
-  void
-  report_error (const char *gmsgid, ...) const
-    ATTRIBUTE_GCC_DIAG(2,3);
-
-  void
-  report_unknown_key (const char *unparsed_arg,
-                     const std::string &key,
-                     const std::string &scheme_name,
-                     auto_vec<const char *> &known_keys) const;
-
-  void
-  report_missing_key (const char *unparsed_arg,
-                     const std::string &key,
-                     const std::string &scheme_name,
-                     const char *metavar) const;
-
-  diagnostic_output_file
-  open_output_file (label_text &&filename) const;
-
-  const gcc_options &m_opts;
-  diagnostic_context &m_dc;
-  line_maps *m_location_mgr;
-  location_t m_loc;
-  const char *m_option_name;
-};
-
-struct scheme_name_and_params
-{
-  std::string m_scheme_name;
-  std::vector<std::pair<std::string, std::string>> m_kvs;
-};
-
-static std::unique_ptr<scheme_name_and_params>
-parse (const context &ctxt, const char *unparsed_arg);
-
-/* Class for parsing the arguments of -fdiagnostics-add-output= and
-   -fdiagnostics-set-output=, and making diagnostic_output_format
-   instances (or issuing errors).  */
-
-class output_factory
-{
-public:
-  class scheme_handler
+  const char *
+  get_base_filename () const final override
   {
-  public:
-    scheme_handler (std::string scheme_name)
-    : m_scheme_name (std::move (scheme_name))
-    {}
-    virtual ~scheme_handler () {}
-
-    const std::string &get_scheme_name () const { return m_scheme_name; }
-
-    virtual std::unique_ptr<diagnostic_output_format>
-    make_sink (const context &ctxt,
-              const char *unparsed_arg,
-              const scheme_name_and_params &parsed_arg) const = 0;
-
-  protected:
-    bool
-    parse_bool_value (const context &ctxt,
-                     const char *unparsed_arg,
-                     const std::string &key,
-                     const std::string &value,
-                     bool &out) const
-    {
-      if (value == "yes")
-       {
-         out = true;
-         return true;
-       }
-      else if (value == "no")
-       {
-         out = false;
-         return true;
-       }
-      else
-       {
-         ctxt.report_error
-           ("%<%s%s%>:"
-            " unexpected value %qs for key %qs; expected %qs or %qs",
-            ctxt.m_option_name, unparsed_arg,
-            value.c_str (),
-            key.c_str (),
-            "yes", "no");
-
-         return false;
-       }
-    }
-    template <typename EnumType, size_t NumValues>
-    bool
-    parse_enum_value (const context &ctxt,
-                     const char *unparsed_arg,
-                     const std::string &key,
-                     const std::string &value,
-                     const std::array<std::pair<const char *, EnumType>, 
NumValues> &value_names,
-                     EnumType &out) const
-    {
-      for (auto &iter : value_names)
-       if (value == iter.first)
-         {
-           out = iter.second;
-           return true;
-         }
-
-      auto_vec<const char *> known_values;
-      for (auto iter : value_names)
-       known_values.safe_push (iter.first);
-      pp_markup::comma_separated_quoted_strings e (known_values);
-      ctxt.report_error
-       ("%<%s%s%>:"
-        " unexpected value %qs for key %qs; known values: %e",
-        ctxt.m_option_name, unparsed_arg,
-        value.c_str (),
-        key.c_str (),
-        &e);
-      return false;
-    }
-
-  private:
-    const std::string m_scheme_name;
-  };
-
-  output_factory ();
-
-  std::unique_ptr<diagnostic_output_format>
-  make_sink (const context &ctxt,
-            const char *unparsed_arg,
-            const scheme_name_and_params &parsed_arg);
-
-  const scheme_handler *get_scheme_handler (const std::string &scheme_name);
-
-private:
-  std::vector<std::unique_ptr<scheme_handler>> m_scheme_handlers;
-};
-
-class text_scheme_handler : public output_factory::scheme_handler
-{
-public:
-  text_scheme_handler () : scheme_handler ("text") {}
-
-  std::unique_ptr<diagnostic_output_format>
-  make_sink (const context &ctxt,
-            const char *unparsed_arg,
-            const scheme_name_and_params &parsed_arg) const final override;
-};
-
-class sarif_scheme_handler : public output_factory::scheme_handler
-{
-public:
-  sarif_scheme_handler () : scheme_handler ("sarif") {}
-
-  std::unique_ptr<diagnostic_output_format>
-  make_sink (const context &ctxt,
-            const char *unparsed_arg,
-            const scheme_name_and_params &parsed_arg) const final override;
-};
-
-class html_scheme_handler : public output_factory::scheme_handler
-{
-public:
-  html_scheme_handler () : scheme_handler ("experimental-html") {}
+    return (m_opts.x_dump_base_name
+           ? m_opts.x_dump_base_name
+           : m_opts.x_main_input_basename);
+  }
 
-  std::unique_ptr<diagnostic_output_format>
-  make_sink (const context &ctxt,
-            const char *unparsed_arg,
-            const scheme_name_and_params &parsed_arg) const final override;
+  const gcc_options &m_opts;
 };
 
-/* struct context.  */
-
-void
-context::report_error (const char *gmsgid, ...) const
-{
-  m_dc.begin_group ();
-  va_list ap;
-  va_start (ap, gmsgid);
-  rich_location richloc (m_location_mgr, m_loc);
-  m_dc.diagnostic_impl (&richloc, nullptr, -1, gmsgid, &ap, DK_ERROR);
-  va_end (ap);
-  m_dc.end_group ();
-}
-
-void
-context::report_unknown_key (const char *unparsed_arg,
-                            const std::string &key,
-                            const std::string &scheme_name,
-                            auto_vec<const char *> &known_keys) const
-{
-  pp_markup::comma_separated_quoted_strings e (known_keys);
-  report_error
-    ("%<%s%s%>:"
-     " unknown key %qs for format %qs; known keys: %e",
-     m_option_name, unparsed_arg,
-     key.c_str (), scheme_name.c_str (), &e);
-}
-
-void
-context::report_missing_key (const char *unparsed_arg,
-                            const std::string &key,
-                            const std::string &scheme_name,
-                            const char *metavar) const
-{
-  report_error
-    ("%<%s%s%>:"
-     " missing required key %qs for format %qs;"
-     " try %<%s%s:%s=%s%>",
-     m_option_name, unparsed_arg,
-     key.c_str (), scheme_name.c_str (),
-     m_option_name, scheme_name.c_str (), key.c_str (), metavar);
-}
-
-std::unique_ptr<scheme_name_and_params>
-parse (const context &ctxt, const char *unparsed_arg)
-{
-  scheme_name_and_params result;
-  if (const char *const colon = strchr (unparsed_arg, ':'))
-    {
-      result.m_scheme_name = std::string (unparsed_arg, colon - unparsed_arg);
-      /* Expect zero of more of KEY=VALUE,KEY=VALUE, etc  .*/
-      const char *iter = colon + 1;
-      const char *last_separator = ":";
-      while (iter)
-       {
-         /* Look for a non-empty key string followed by '='.  */
-         const char *eq = strchr (iter, '=');
-         if (eq == nullptr || eq == iter)
-           {
-             /* Missing '='.  */
-             ctxt.report_error
-               ("%<%s%s%>:"
-                " expected KEY=VALUE-style parameter for format %qs"
-                " after %qs;"
-                " got %qs",
-                ctxt.m_option_name, unparsed_arg,
-                result.m_scheme_name.c_str (),
-                last_separator,
-                iter);
-             return nullptr;
-           }
-         std::string key = std::string (iter, eq - iter);
-         std::string value;
-         const char *comma = strchr (iter, ',');
-         if (comma)
-           {
-             value = std::string (eq + 1, comma - (eq + 1));
-             iter = comma + 1;
-             last_separator = ",";
-           }
-         else
-           {
-             value = std::string (eq + 1);
-             iter = nullptr;
-           }
-         result.m_kvs.push_back ({std::move (key), std::move (value)});
-       }
-    }
-  else
-    result.m_scheme_name = unparsed_arg;
-  return std::make_unique<scheme_name_and_params> (std::move (result));
-}
-
-/* class output_factory::scheme_handler.  */
-
-/* class output_factory.  */
-
-output_factory::output_factory ()
-{
-  m_scheme_handlers.push_back (std::make_unique<text_scheme_handler> ());
-  m_scheme_handlers.push_back (std::make_unique<sarif_scheme_handler> ());
-  m_scheme_handlers.push_back (std::make_unique<html_scheme_handler> ());
-}
-
-const output_factory::scheme_handler *
-output_factory::get_scheme_handler (const std::string &scheme_name)
-{
-  for (auto &iter : m_scheme_handlers)
-    if (iter->get_scheme_name () == scheme_name)
-      return iter.get ();
-  return nullptr;
-}
-
-std::unique_ptr<diagnostic_output_format>
-output_factory::make_sink (const context &ctxt,
-                          const char *unparsed_arg,
-                          const scheme_name_and_params &parsed_arg)
-{
-  auto scheme_handler = get_scheme_handler (parsed_arg.m_scheme_name);
-  if (!scheme_handler)
-    {
-      auto_vec<const char *> strings;
-      for (auto &iter : m_scheme_handlers)
-       strings.safe_push (iter->get_scheme_name ().c_str ());
-      pp_markup::comma_separated_quoted_strings e (strings);
-      ctxt.report_error ("%<%s%s%>:"
-                        " unrecognized format %qs; known formats: %e",
-                        ctxt.m_option_name, unparsed_arg,
-                        parsed_arg.m_scheme_name.c_str (), &e);
-      return nullptr;
-    }
-
-  return scheme_handler->make_sink (ctxt, unparsed_arg, parsed_arg);
-}
-
-/* class text_scheme_handler : public output_factory::scheme_handler.  */
-
-std::unique_ptr<diagnostic_output_format>
-text_scheme_handler::make_sink (const context &ctxt,
-                               const char *unparsed_arg,
-                               const scheme_name_and_params &parsed_arg) const
-{
-  bool show_color = pp_show_color (ctxt.m_dc.get_reference_printer ());
-  bool show_nesting = false;
-  bool show_locations_in_nesting = true;
-  bool show_levels = false;
-  for (auto& iter : parsed_arg.m_kvs)
-    {
-      const std::string &key = iter.first;
-      const std::string &value = iter.second;
-      if (key == "color")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value, show_color))
-           return nullptr;
-         continue;
-       }
-      if (key == "experimental-nesting")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                show_nesting))
-           return nullptr;
-         continue;
-       }
-      if (key == "experimental-nesting-show-locations")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                show_locations_in_nesting))
-           return nullptr;
-         continue;
-       }
-      if (key == "experimental-nesting-show-levels")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value, show_levels))
-           return nullptr;
-         continue;
-       }
-
-      /* Key not found.  */
-      auto_vec<const char *> known_keys;
-      known_keys.safe_push ("color");
-      known_keys.safe_push ("experimental-nesting");
-      known_keys.safe_push ("experimental-nesting-show-locations");
-      known_keys.safe_push ("experimental-nesting-show-levels");
-      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (),
-                              known_keys);
-      return nullptr;
-    }
-
-  auto sink = std::make_unique<diagnostic_text_output_format> (ctxt.m_dc);
-  sink->set_show_nesting (show_nesting);
-  sink->set_show_locations_in_nesting (show_locations_in_nesting);
-  sink->set_show_nesting_levels (show_levels);
-  return sink;
-}
-
-diagnostic_output_file
-context::open_output_file (label_text &&filename) const
-{
-  FILE *outf = fopen (filename.get (), "w");
-  if (!outf)
-    {
-      rich_location richloc (m_location_mgr, m_loc);
-      m_dc.emit_diagnostic_with_group
-       (DK_ERROR, richloc, nullptr, 0,
-        "unable to open %qs: %m", filename.get ());
-      return diagnostic_output_file (nullptr, false, std::move (filename));
-    }
-  return diagnostic_output_file (outf, true, std::move (filename));
-}
-
-/* class sarif_scheme_handler : public output_factory::scheme_handler.  */
-
-std::unique_ptr<diagnostic_output_format>
-sarif_scheme_handler::make_sink (const context &ctxt,
-                                const char *unparsed_arg,
-                                const scheme_name_and_params &parsed_arg) const
-{
-  label_text filename;
-  enum sarif_serialization_kind serialization_kind
-    = sarif_serialization_kind::json;
-  enum sarif_version version = sarif_version::v2_1_0;
-  bool xml_state = false;
-  for (auto& iter : parsed_arg.m_kvs)
-    {
-      const std::string &key = iter.first;
-      const std::string &value = iter.second;
-      if (key == "file")
-       {
-         filename = label_text::take (xstrdup (value.c_str ()));
-         continue;
-       }
-      if (key == "serialization")
-       {
-         static const std::array<std::pair<const char *, enum 
sarif_serialization_kind>,
-                                 (size_t)sarif_serialization_kind::num_values> 
value_names
-           {{{"json", sarif_serialization_kind::json}}};
-
-         if (!parse_enum_value<enum sarif_serialization_kind>
-                (ctxt, unparsed_arg,
-                 key, value,
-                 value_names,
-                 serialization_kind))
-           return nullptr;
-         continue;
-       }
-      if (key == "version")
-       {
-         static const std::array<std::pair<const char *, enum sarif_version>,
-                                 (size_t)sarif_version::num_versions> 
value_names
-           {{{"2.1", sarif_version::v2_1_0},
-             {"2.2-prerelease", sarif_version::v2_2_prerelease_2024_08_08}}};
-
-           if (!parse_enum_value<enum sarif_version> (ctxt, unparsed_arg,
-                                                      key, value,
-                                                      value_names,
-                                                      version))
-           return nullptr;
-         continue;
-       }
-      if (key == "xml-state")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                xml_state))
-           return nullptr;
-         continue;
-       }
-
-      /* Key not found.  */
-      auto_vec<const char *> known_keys;
-      known_keys.safe_push ("file");
-      known_keys.safe_push ("serialization");
-      known_keys.safe_push ("version");
-      known_keys.safe_push ("xml-state");
-      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (),
-                              known_keys);
-      return nullptr;
-    }
-
-  diagnostic_output_file output_file;
-  if (filename.get ())
-    output_file = ctxt.open_output_file (std::move (filename));
-  else
-    // Default filename
-    {
-      const char *basename = (ctxt.m_opts.x_dump_base_name
-                             ? ctxt.m_opts.x_dump_base_name
-                             : ctxt.m_opts.x_main_input_basename);
-      output_file = diagnostic_output_format_open_sarif_file (ctxt.m_dc,
-                                                             line_table,
-                                                             basename,
-                                                             
serialization_kind);
-    }
-  if (!output_file)
-    return nullptr;
-
-  sarif_generation_options sarif_gen_opts;
-  sarif_gen_opts.m_version = version;
-  sarif_gen_opts.m_xml_state = xml_state;
-
-  std::unique_ptr<sarif_serialization_format> serialization_obj;
-  switch (serialization_kind)
-    {
-    default:
-      gcc_unreachable ();
-    case sarif_serialization_kind::json:
-      serialization_obj
-       = std::make_unique<sarif_serialization_format_json> (true);
-      break;
-    }
-
-  auto sink = make_sarif_sink (ctxt.m_dc,
-                              *line_table,
-                              std::move (serialization_obj),
-                              sarif_gen_opts,
-                              std::move (output_file));
-  return sink;
-}
-
-/* class html_scheme_handler : public output_factory::scheme_handler.  */
-
-std::unique_ptr<diagnostic_output_format>
-html_scheme_handler::make_sink (const context &ctxt,
-                               const char *unparsed_arg,
-                               const scheme_name_and_params &parsed_arg) const
-{
-  bool css = true;
-  label_text filename;
-  bool javascript = true;
-  bool show_state_diagrams = false;
-  bool show_state_diagram_xml = false;
-  bool show_state_diagram_dot_src = false;
-
-  for (auto& iter : parsed_arg.m_kvs)
-    {
-      const std::string &key = iter.first;
-      const std::string &value = iter.second;
-      if (key == "css")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                css))
-           return nullptr;
-         continue;
-       }
-      if (key == "file")
-       {
-         filename = label_text::take (xstrdup (value.c_str ()));
-         continue;
-       }
-      if (key == "javascript")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                javascript))
-           return nullptr;
-         continue;
-       }
-      if (key == "show-state-diagrams")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                show_state_diagrams))
-           return nullptr;
-         continue;
-       }
-      if (key == "show-state-diagram-dot-src")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                show_state_diagram_dot_src))
-           return nullptr;
-         continue;
-       }
-      if (key == "show-state-diagram-xml")
-       {
-         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
-                                show_state_diagram_xml))
-           return nullptr;
-         continue;
-       }
-
-      /* Key not found.  */
-      auto_vec<const char *> known_keys;
-      known_keys.safe_push ("css");
-      known_keys.safe_push ("file");
-      known_keys.safe_push ("javascript");
-      known_keys.safe_push ("show-state-diagrams");
-      known_keys.safe_push ("show-state-diagram-dot-src");
-      known_keys.safe_push ("show-state-diagram-xml");
-      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (),
-                              known_keys);
-      return nullptr;
-    }
-
-  diagnostic_output_file output_file;
-  if (filename.get ())
-    output_file = ctxt.open_output_file (std::move (filename));
-  else
-    // Default filename
-    {
-      const char *basename = (ctxt.m_opts.x_dump_base_name
-                             ? ctxt.m_opts.x_dump_base_name
-                             : ctxt.m_opts.x_main_input_basename);
-      output_file = diagnostic_output_format_open_html_file (ctxt.m_dc,
-                                                            line_table,
-                                                            basename);
-    }
-  if (!output_file)
-    return nullptr;
-
-  html_generation_options html_gen_opts;
-  html_gen_opts.m_css = css;
-  html_gen_opts.m_javascript = javascript;
-  html_gen_opts.m_show_state_diagrams = show_state_diagrams;
-  html_gen_opts.m_show_state_diagram_xml = show_state_diagram_xml;
-  html_gen_opts.m_show_state_diagram_dot_src = show_state_diagram_dot_src;
-
-  auto sink = make_html_sink (ctxt.m_dc,
-                             *line_table,
-                             html_gen_opts,
-                             std::move (output_file));
-  return sink;
-}
-
-} // namespace diagnostics_output_spec
-} // namespace gcc
+} // anon namespace
 
 void
 handle_OPT_fdiagnostics_add_output_ (const gcc_options &opts,
@@ -662,14 +78,8 @@ handle_OPT_fdiagnostics_add_output_ (const gcc_options 
&opts,
   gcc_assert (line_table);
 
   const char *const option_name = "-fdiagnostics-add-output=";
-  gcc::diagnostics_output_spec::context ctxt (opts, dc, line_table, loc,
-                                             option_name);
-  auto result = gcc::diagnostics_output_spec::parse (ctxt, arg);
-  if (!result)
-    return;
-
-  gcc::diagnostics_output_spec::output_factory factory;
-  auto sink = factory.make_sink (ctxt, arg, *result);
+  opt_spec_context ctxt (opts, dc, line_table, loc, option_name);
+  auto sink = ctxt.parse_and_make_sink (arg, dc);
   if (!sink)
     return;
 
@@ -687,183 +97,11 @@ handle_OPT_fdiagnostics_set_output_ (const gcc_options 
&opts,
   gcc_assert (line_table);
 
   const char *const option_name = "-fdiagnostics-set-output=";
-  gcc::diagnostics_output_spec::context ctxt (opts, dc, line_table, loc,
-                                             option_name);
-  auto result = gcc::diagnostics_output_spec::parse (ctxt, arg);
-  if (!result)
-    return;
-
-  gcc::diagnostics_output_spec::output_factory factory;
-  auto sink = factory.make_sink (ctxt, arg, *result);
+  opt_spec_context ctxt (opts, dc, line_table, loc, option_name);
+  auto sink = ctxt.parse_and_make_sink (arg, dc);
   if (!sink)
     return;
 
   sink->set_main_input_filename (opts.x_main_input_filename);
   dc.set_output_format (std::move (sink));
 }
-
-#if CHECKING_P
-
-namespace selftest {
-
-/* RAII class to temporarily override "progname" to the
-   string "PROGNAME".  */
-
-class auto_fix_progname
-{
-public:
-  auto_fix_progname ()
-  {
-    m_old_progname = progname;
-    progname = "PROGNAME";
-  }
-
-  ~auto_fix_progname ()
-  {
-    progname = m_old_progname;
-  }
-
-private:
-  const char *m_old_progname;
-};
-
-struct parser_test
-{
-  parser_test ()
-  : m_opts (),
-    m_dc (),
-    m_ctxt (m_opts, m_dc, line_table, UNKNOWN_LOCATION, "-fOPTION="),
-    m_fmt (m_dc.get_output_format (0))
-  {
-    pp_buffer (m_fmt.get_printer ())->m_flush_p = false;
-  }
-
-  std::unique_ptr<gcc::diagnostics_output_spec::scheme_name_and_params>
-  parse (const char *unparsed_arg)
-  {
-    return gcc::diagnostics_output_spec::parse (m_ctxt, unparsed_arg);
-  }
-
-  bool execution_failed_p () const
-  {
-    return m_dc.execution_failed_p ();
-  }
-
-  const char *
-  get_diagnostic_text () const
-  {
-    return pp_formatted_text (m_fmt.get_printer ());
-  }
-
-private:
-  const gcc_options m_opts;
-  test_diagnostic_context m_dc;
-  gcc::diagnostics_output_spec::context m_ctxt;
-  diagnostic_output_format &m_fmt;
-};
-
-/* Selftests.  */
-
-static void
-test_output_arg_parsing ()
-{
-  auto_fix_quotes fix_quotes;
-  auto_fix_progname fix_progname;
-
-  /* Minimal correct example.  */
-  {
-    parser_test pt;
-    auto result = pt.parse ("foo");
-    ASSERT_EQ (result->m_scheme_name, "foo");
-    ASSERT_EQ (result->m_kvs.size (), 0);
-    ASSERT_FALSE (pt.execution_failed_p ());
-  }
-
-  /* Stray trailing colon with no key/value pairs.  */
-  {
-    parser_test pt;
-    auto result = pt.parse ("foo:");
-    ASSERT_EQ (result, nullptr);
-    ASSERT_TRUE (pt.execution_failed_p ());
-    ASSERT_STREQ (pt.get_diagnostic_text (),
-                 "PROGNAME: error: `-fOPTION=foo:':"
-                 " expected KEY=VALUE-style parameter for format `foo'"
-                 " after `:';"
-                 " got `'\n");
-  }
-
-  /* No key before '='.  */
-  {
-    parser_test pt;
-    auto result = pt.parse ("foo:=");
-    ASSERT_EQ (result, nullptr);
-    ASSERT_TRUE (pt.execution_failed_p ());
-    ASSERT_STREQ (pt.get_diagnostic_text (),
-                 "PROGNAME: error: `-fOPTION=foo:=':"
-                 " expected KEY=VALUE-style parameter for format `foo'"
-                 " after `:';"
-                 " got `='\n");
-  }
-
-  /* No value for key.  */
-  {
-    parser_test pt;
-    auto result = pt.parse ("foo:key,");
-    ASSERT_EQ (result, nullptr);
-    ASSERT_TRUE (pt.execution_failed_p ());
-    ASSERT_STREQ (pt.get_diagnostic_text (),
-                 "PROGNAME: error: `-fOPTION=foo:key,':"
-                 " expected KEY=VALUE-style parameter for format `foo'"
-                 " after `:';"
-                 " got `key,'\n");
-  }
-
-  /* Correct example, with one key/value pair.  */
-  {
-    parser_test pt;
-    auto result = pt.parse ("foo:key=value");
-    ASSERT_EQ (result->m_scheme_name, "foo");
-    ASSERT_EQ (result->m_kvs.size (), 1);
-    ASSERT_EQ (result->m_kvs[0].first, "key");
-    ASSERT_EQ (result->m_kvs[0].second, "value");
-    ASSERT_FALSE (pt.execution_failed_p ());
-  }
-
-  /* Stray trailing comma.  */
-  {
-    parser_test pt;
-    auto result = pt.parse ("foo:key=value,");
-    ASSERT_EQ (result, nullptr);
-    ASSERT_TRUE (pt.execution_failed_p ());
-    ASSERT_STREQ (pt.get_diagnostic_text (),
-                 "PROGNAME: error: `-fOPTION=foo:key=value,':"
-                 " expected KEY=VALUE-style parameter for format `foo'"
-                 " after `,';"
-                 " got `'\n");
-  }
-
-  /* Correct example, with two key/value pairs.  */
-  {
-    parser_test pt;
-    auto result = pt.parse ("foo:color=red,shape=circle");
-    ASSERT_EQ (result->m_scheme_name, "foo");
-    ASSERT_EQ (result->m_kvs.size (), 2);
-    ASSERT_EQ (result->m_kvs[0].first, "color");
-    ASSERT_EQ (result->m_kvs[0].second, "red");
-    ASSERT_EQ (result->m_kvs[1].first, "shape");
-    ASSERT_EQ (result->m_kvs[1].second, "circle");
-    ASSERT_FALSE (pt.execution_failed_p ());
-  }
-}
-
-/* Run all of the selftests within this file.  */
-
-void
-opts_diagnostic_cc_tests ()
-{
-  test_output_arg_parsing ();
-}
-
-} // namespace selftest
-
-#endif /* #if CHECKING_P */
diff --git a/gcc/sarif-replay.cc b/gcc/sarif-replay.cc
index 1523d875e90..a96c97bd92b 100644
--- a/gcc/sarif-replay.cc
+++ b/gcc/sarif-replay.cc
@@ -19,6 +19,7 @@ along with GCC; see the file COPYING3.  If not see
 <http://www.gnu.org/licenses/>.  */
 
 #include "config.h"
+#define INCLUDE_STRING
 #define INCLUDE_VECTOR
 #include "system.h"
 #include "coretypes.h"
@@ -48,6 +49,7 @@ struct options
 
   replay_options m_replay_opts;
   std::vector<const char *> m_sarif_filenames;
+  std::vector<std::string> m_extra_output_specs;
 };
 
 static void
@@ -70,6 +72,10 @@ static const char *const usage_msg = (
 "\n"
 "Options:\n"
 "\n"
+"  -fdiagnostics-add-output=SCHEME\n"
+"     Add an additional output sink when replaying diagnostics, as\n"
+"     per the gcc option\n"
+"\n"
 "  -fdiagnostics-color={never|always|auto}\n"
 "     Control colorization of diagnostics.  Default: auto.\n"
 "\n"
@@ -95,6 +101,19 @@ print_usage ()
   fprintf (stderr, usage_msg);
 }
 
+/* If STR starts with PREFIX, return the rest of STR.
+   Otherwise return nullptr.  */
+
+static const char *
+str_starts_with (const char *str, const char *prefix)
+{
+  size_t prefix_len = strlen (prefix);
+  if (0 == strncmp (str, prefix, prefix_len))
+    return str + prefix_len;
+  else
+    return nullptr;
+}
+
 static bool
 parse_options (int argc, char **argv,
               options &opts,
@@ -128,6 +147,13 @@ parse_options (int argc, char **argv,
          opts.m_replay_opts.m_echo_file = true;
          handled = true;
        }
+#define ADD_OUTPUT_OPTION "-fdiagnostics-add-output="
+      else if (const char *arg
+               = str_starts_with (option, ADD_OUTPUT_OPTION))
+       {
+         opts.m_extra_output_specs.push_back (std::string (arg));
+         handled = true;
+       }
       else if (strcmp (option, "-fdiagnostics-color=never") == 0)
        {
          opts.m_replay_opts.m_diagnostics_colorize = DIAGNOSTIC_COLORIZE_NO;
@@ -221,6 +247,12 @@ main (int argc, char **argv)
       libgdiagnostics::manager playback_mgr;
       playback_mgr.add_text_sink (stderr,
                                  opts.m_replay_opts.m_diagnostics_colorize);
+      for (auto spec : opts.m_extra_output_specs)
+       if (playback_mgr.add_sink_from_spec
+             (ADD_OUTPUT_OPTION,
+              spec.c_str (),
+              libgdiagnostics::manager (control_mgr.m_inner, false)))
+         return -1;
 
       int result = sarif_replay_path (filename,
                                      playback_mgr.m_inner,
diff --git a/gcc/selftest-run-tests.cc b/gcc/selftest-run-tests.cc
index 2d8573ce28f..139c7b381b6 100644
--- a/gcc/selftest-run-tests.cc
+++ b/gcc/selftest-run-tests.cc
@@ -103,6 +103,7 @@ selftest::run_tests ()
   diagnostic_format_html_cc_tests ();
   diagnostic_format_json_cc_tests ();
   diagnostic_format_sarif_cc_tests ();
+  diagnostic_output_spec_cc_tests ();
   edit_context_cc_tests ();
   fold_const_cc_tests ();
   spellcheck_cc_tests ();
@@ -112,7 +113,6 @@ selftest::run_tests ()
   simple_diagnostic_path_cc_tests ();
   lazy_diagnostic_path_cc_tests ();
   attribs_cc_tests ();
-  opts_diagnostic_cc_tests ();
   path_coverage_cc_tests ();
 
   /* This one relies on most of the above.  */
diff --git a/gcc/selftest.h b/gcc/selftest.h
index a6c96027ce0..4af647cca9e 100644
--- a/gcc/selftest.h
+++ b/gcc/selftest.h
@@ -225,6 +225,7 @@ extern void diagnostic_color_cc_tests ();
 extern void diagnostic_format_html_cc_tests ();
 extern void diagnostic_format_json_cc_tests ();
 extern void diagnostic_format_sarif_cc_tests ();
+extern void diagnostic_output_spec_cc_tests ();
 extern void diagnostic_path_output_cc_tests ();
 extern void diagnostic_show_locus_cc_tests ();
 extern void digraph_cc_tests ();
@@ -250,7 +251,6 @@ extern void lazy_diagnostic_path_cc_tests ();
 extern void opt_suggestions_cc_tests ();
 extern void optinfo_emit_json_cc_tests ();
 extern void opts_cc_tests ();
-extern void opts_diagnostic_cc_tests ();
 extern void ordered_hash_map_tests_cc_tests ();
 extern void path_coverage_cc_tests ();
 extern void predict_cc_tests ();
diff --git a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-html.py 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-html.py
new file mode 100644
index 00000000000..a0978da048b
--- /dev/null
+++ b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-html.py
@@ -0,0 +1,26 @@
+from htmltest import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def html_tree():
+    return html_tree_from_env()
+
+def test_generated_html(html_tree):
+    root = html_tree.getroot ()
+    assert root.tag == make_tag('html')
+
+    head = root.find('xhtml:head', ns)
+    assert head is not None
+
+    title = head.find('xhtml:title', ns)
+    assert title.text == '../../src/gcc/testsuite/gcc.dg/analyzer/signal-1.c'
+
+    diag = get_diag_by_index(html_tree, 0)
+
+    msg = get_message_within_diag(diag)
+    assert msg is not None
+    
+    assert_tag(msg[0], 'strong')
+    assert msg[0].text == 'warning: '
+    assert msg[0].tail == " call to ‘fprintf’ from within signal handler "
diff --git 
a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-sarif-roundtrip.py 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-sarif-roundtrip.py
new file mode 100644
index 00000000000..ea93b039e8f
--- /dev/null
+++ 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1-check-sarif-roundtrip.py
@@ -0,0 +1,41 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_basics(sarif):
+    schema = sarif['$schema']
+    assert schema == 
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
+
+    version = sarif['version']
+    assert version == "2.1.0"
+
+def test_execution_successful(sarif):
+    runs = sarif['runs']
+    run = runs[0]
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    invocation = invocations[0]
+
+    assert invocation['executionSuccessful'] == True
+
+def test_warning(sarif):
+    result = get_result_by_index(sarif, 0)
+
+    assert result['level'] == 'warning'
+
+    # TODO: this should be "-Wanalyzer-unsafe-call-within-signal-handler" and 
have a URL
+    assert result['ruleId'] == 'warning'
+
+    # TODO: check code flow
+    events = result["codeFlows"][0]["threadFlows"][0]['locations']
+
+    # Event "(1)": "entry to 'main'" (index == 0)
+    assert events[0]['location']['message']['text'] == "entry to ‘main’"
+
+    # Final event:
+    assert events[-1]['location']['message']['text'].startswith("call to 
‘fprintf’ from within signal handler")
diff --git a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1.c.sarif 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1.c.sarif
index 54fa0b83f1d..3b88af95f93 100644
--- a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1.c.sarif
+++ b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/signal-1.c.sarif
@@ -2,6 +2,8 @@
 
    The dg directives were stripped out from the generated .sarif
    to avoid confusing DejaGnu for this test.   */
+/* { dg-additional-options 
"-fdiagnostics-add-output=experimental-html:file=signal-1.c.sarif.html,javascript=no"
 } */
+/* { dg-additional-options 
"-fdiagnostics-add-output=sarif:file=signal-1.c.roundtrip.sarif" } */
 
 {"$schema": 
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";,
  "version": "2.1.0",
@@ -211,3 +213,11 @@ In function 'custom_logger':
                   |      |   (7) call to ‘fprintf’ from within signal handler
                   |
    { dg-end-multiline-output "" } */
+
+/* Use a Python script to verify various properties about the generated
+   .html file:
+   { dg-final { run-html-pytest signal-1.c.sarif 
"2.1.0-valid/signal-1-check-html.py" } } */
+
+/* Use a Python script to verify various properties about the *generated*
+   .sarif file:
+   { dg-final { run-sarif-pytest signal-1.c.roundtrip 
"2.1.0-valid/signal-1-check-sarif-roundtrip.py" } } */
-- 
2.49.0


Reply via email to