/* 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
. */
/* 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> 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
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
bool
parse_enum_value (const context &ctxt,
const char *unparsed_arg,
const std::string &key,
const std::string &value,
const std::array, NumValues> &value_names,
EnumType &out) const
{
for (auto &iter : value_names)
if (value == iter.first)
{
out = iter.second;
return true;
}
auto_vec 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
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> m_scheme_handlers;
};
class text_scheme_handler : public output_factory::scheme_handler
{
public:
text_scheme_handler () : scheme_handler ("text") {}
std::unique_ptr
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
make_sink (const context &ctxt,
diagnostic_context &dc,
const char *unparsed_arg,
const scheme_name_and_params &parsed_arg) const final override;
private:
static sarif_generation_options
make_sarif_gen_opts (enum sarif_version version,
bool xml_state);
static std::unique_ptr
make_sarif_serialization_object (enum sarif_serialization_kind);
};
class html_scheme_handler : public output_factory::scheme_handler
{
public:
html_scheme_handler () : scheme_handler ("experimental-html") {}
std::unique_ptr
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 &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
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 (std::move (result));
}
std::unique_ptr
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 ());
m_scheme_handlers.push_back (std::make_unique ());
m_scheme_handlers.push_back (std::make_unique ());
}
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
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 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
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 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 (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
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,
(size_t)sarif_serialization_kind::num_values> value_names
{{{"json", sarif_serialization_kind::json}}};
if (!parse_enum_value
(ctxt, unparsed_arg,
key, value,
value_names,
serialization_kind))
return nullptr;
continue;
}
if (key == "version")
{
static const std::array,
(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 (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 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;
auto sarif_gen_opts = make_sarif_gen_opts (version, xml_state);
auto serialization_obj = make_sarif_serialization_object (serialization_kind);
auto sink = make_sarif_sink (dc,
*ctxt.get_affected_location_mgr (),
std::move (serialization_obj),
sarif_gen_opts,
std::move (output_file));
return sink;
}
sarif_generation_options
sarif_scheme_handler::make_sarif_gen_opts (enum sarif_version version,
bool xml_state)
{
sarif_generation_options sarif_gen_opts;
sarif_gen_opts.m_version = version;
sarif_gen_opts.m_xml_state = xml_state;
return sarif_gen_opts;
}
std::unique_ptr
sarif_scheme_handler::
make_sarif_serialization_object (enum sarif_serialization_kind kind)
{
switch (kind)
{
default:
gcc_unreachable ();
case sarif_serialization_kind::json:
return std::make_unique (true);
break;
}
}
/* class html_scheme_handler : public output_factory::scheme_handler. */
std::unique_ptr
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 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
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 */