libgdiagnostics: sarif-replay: add extra sinks via -fdiagnostics-add-output= [PR116792,PR116163]

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).

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.

Signed-off-by: David Malcolm <dmalcolm@redhat.com>
This commit is contained in:
David Malcolm 2025-06-23 18:46:51 -04:00
parent e6406aefd1
commit d0142e1474
22 changed files with 1304 additions and 793 deletions

View File

@ -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):

View File

@ -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

View File

@ -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 \

View File

@ -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)

View File

@ -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 */

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -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 ());
}

View File

@ -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

View File

@ -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;

View File

@ -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");

View File

@ -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 char *
get_base_filename () const final override
{
return (m_opts.x_dump_base_name
? m_opts.x_dump_base_name
: m_opts.x_main_input_basename);
}
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
{
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") {}
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;
};
/* 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 */

View File

@ -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,

View File

@ -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. */

View File

@ -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 ();

View File

@ -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 "

View File

@ -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")

View File

@ -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" } } */