blob: f7cce0acce845e281d713b1a053f4d9d5a5f63fc [file] [log] [blame]
/* 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 "diagnostics/color.h"
#include "diagnostics/sink.h"
#include "diagnostics/html-sink.h"
#include "diagnostics/text-sink.h"
#include "diagnostics/sarif-sink.h"
#include "selftest.h"
#include "diagnostics/selftest-context.h"
#include "pretty-print-markup.h"
#include "diagnostics/output-spec.h"
/* A namespace for handling the DSL of the arguments of
-fdiagnostics-add-output= and -fdiagnostics-set-output=
which look like:
SCHEME[:KEY=VALUE(,KEY=VALUE)*]
We call this an output spec. */
namespace diagnostics {
namespace output_spec {
class scheme_handler;
/* Decls. */
/* A class for the result of the first stage of parsing an output spec,
where values are represented as untyped strings.
The scheme might not exist.
The keys have not been validated against the scheme.
The values have not been validated against their keys. */
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 sink
instances (or issuing errors). */
class output_factory
{
public:
output_factory (diagnostics::context &dc);
std::unique_ptr<sink>
make_sink (const context &ctxt,
diagnostics::context &dc,
const scheme_name_and_params &scheme_and_kvs);
scheme_handler *get_scheme_handler (const std::string &scheme_name);
private:
std::vector<std::unique_ptr<scheme_handler>> m_scheme_handlers;
};
enum key_handler::result
key_handler::parse_bool_value (const context &ctxt,
const std::string &key,
const std::string &value,
bool &out) const
{
if (value == "yes")
{
out = true;
return result::ok;
}
else if (value == "no")
{
out = false;
return result::ok;
}
else
{
ctxt.report_error
("%<%s%s%>:"
" unexpected value %qs for key %qs; expected %qs or %qs",
ctxt.get_option_name (), ctxt.get_unparsed_spec (),
value.c_str (),
key.c_str (),
"yes", "no");
return result::malformed_value;
}
}
template <typename EnumType, size_t NumValues>
key_handler::result
key_handler::parse_enum_value (const context &ctxt,
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 result::ok;
}
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 (), ctxt.get_unparsed_spec (),
value.c_str (),
key.c_str (),
&e);
return result::malformed_value;
}
class text_scheme_handler : public scheme_handler
{
public:
text_scheme_handler (diagnostics::context &dc)
: scheme_handler ("text"),
m_show_color (pp_show_color (dc.get_reference_printer ())),
m_show_nesting (true),
m_show_locations_in_nesting (true),
m_show_levels (false)
{
}
std::unique_ptr<sink>
make_sink (const context &ctxt,
diagnostics::context &dc) final override;
enum result
maybe_handle_kv (const context &ctxt,
const std::string &key,
const std::string &value) final override;
void
get_keys (auto_vec<const char *> &out) const final override;
private:
bool m_show_color;
bool m_show_nesting;
bool m_show_locations_in_nesting;
bool m_show_levels;
};
class sarif_scheme_handler : public scheme_handler
{
public:
sarif_scheme_handler ()
: scheme_handler ("sarif"),
m_serialization_kind (sarif_serialization_kind::json)
{
}
std::unique_ptr<sink>
make_sink (const context &ctxt,
diagnostics::context &dc) final override;
enum result
maybe_handle_kv (const context &ctxt,
const std::string &key,
const std::string &value) final override;
void
get_keys (auto_vec<const char *> &out) const final override;
private:
static std::unique_ptr<sarif_serialization_format>
make_sarif_serialization_object (enum sarif_serialization_kind);
label_text m_filename;
enum sarif_serialization_kind m_serialization_kind;
sarif_generation_options m_generation_opts;
};
class html_scheme_handler : public scheme_handler
{
public:
html_scheme_handler () : scheme_handler ("experimental-html") {}
std::unique_ptr<sink>
make_sink (const context &ctxt,
diagnostics::context &dc) final override;
enum result
maybe_handle_kv (const context &ctxt,
const std::string &key,
const std::string &value) final override;
void
get_keys (auto_vec<const char *> &out) const final override;
private:
label_text m_filename;
html_generation_options m_html_gen_opts;
};
/* 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 std::string &key,
const scheme_handler &scheme) const
{
auto_vec<const char *> scheme_key_vec;
scheme.get_keys (scheme_key_vec);
pp_markup::comma_separated_quoted_strings e_scheme_keys (scheme_key_vec);
const char *scheme_name = scheme.get_scheme_name ().c_str ();
if (m_client_keys)
{
auto_vec<const char *> client_key_vec;
m_client_keys->get_keys (client_key_vec);
if (!client_key_vec.is_empty ())
{
pp_markup::comma_separated_quoted_strings e_client_keys
(client_key_vec);
report_error
("%<%s%s%>:"
" unknown key %qs for output scheme %qs;"
" scheme keys: %e; client keys: %e",
get_option_name (), get_unparsed_spec (),
key.c_str (), scheme_name,
&e_scheme_keys, &e_client_keys);
}
}
report_error
("%<%s%s%>:"
" unknown key %qs for output scheme %qs; scheme keys: %e",
get_option_name (), get_unparsed_spec (),
key.c_str (), scheme_name, &e_scheme_keys);
}
void
context::report_missing_key (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 (), get_unparsed_spec (),
key.c_str (), scheme_name.c_str (),
get_option_name (), scheme_name.c_str (), key.c_str (), metavar);
}
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 output_file (nullptr, false, std::move (filename));
}
return output_file (outf, true, std::move (filename));
}
static std::unique_ptr<scheme_name_and_params>
parse (const context &ctxt)
{
scheme_name_and_params result;
const char *const unparsed_spec = ctxt.get_unparsed_spec ();
if (const char *const colon = strchr (unparsed_spec, ':'))
{
result.m_scheme_name = std::string (unparsed_spec, colon - unparsed_spec);
/* 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 (), ctxt.get_unparsed_spec (),
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_spec;
return std::make_unique<scheme_name_and_params> (std::move (result));
}
std::unique_ptr<sink>
context::parse_and_make_sink (diagnostics::context &dc)
{
auto parsed_arg = parse (*this);
if (!parsed_arg)
return nullptr;
output_factory factory (dc);
return factory.make_sink (*this, dc, *parsed_arg);
}
/* class scheme_handler. */
/* class output_factory. */
output_factory::output_factory (diagnostics::context &dc)
{
m_scheme_handlers.push_back (std::make_unique<text_scheme_handler> (dc));
m_scheme_handlers.push_back (std::make_unique<sarif_scheme_handler> ());
m_scheme_handlers.push_back (std::make_unique<html_scheme_handler> ());
}
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<sink>
output_factory::make_sink (const context &ctxt,
diagnostics::context &dc,
const scheme_name_and_params &scheme_and_kvs)
{
auto scheme_handler = get_scheme_handler (scheme_and_kvs.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 (), ctxt.get_unparsed_spec (),
scheme_and_kvs.m_scheme_name.c_str (), &e);
return nullptr;
}
/* Parse key/value pairs. */
for (auto& iter : scheme_and_kvs.m_kvs)
{
const std::string &key = iter.first;
const std::string &value = iter.second;
if (!ctxt.handle_kv (key, value, *scheme_handler))
return nullptr;
}
return scheme_handler->make_sink (ctxt, dc);
}
bool
context::handle_kv (const std::string &key,
const std::string &value,
scheme_handler &scheme) const
{
auto result = scheme.maybe_handle_kv (*this, key, value);
switch (result)
{
default: gcc_unreachable ();
case key_handler::result::ok:
return true;
case key_handler::result::malformed_value:
return false;
case key_handler::result::unrecognized:
/* Key recognized by the scheme; try the client keys. */
if (m_client_keys)
{
result = m_client_keys->maybe_handle_kv (*this, key, value);
switch (result)
{
default: gcc_unreachable ();
case key_handler::result::ok:
return true;
case key_handler::result::malformed_value:
return false;
case key_handler::result::unrecognized:
break;
}
}
report_unknown_key (key, scheme);
return false;
}
}
/* class text_scheme_handler : public scheme_handler. */
std::unique_ptr<sink>
text_scheme_handler::make_sink (const context &,
diagnostics::context &dc)
{
auto sink = std::make_unique<diagnostics::text_sink> (dc);
sink->set_show_nesting (m_show_nesting);
sink->set_show_locations_in_nesting (m_show_locations_in_nesting);
sink->set_show_nesting_levels (m_show_levels);
pp_show_color (sink->get_printer ()) = m_show_color;
return sink;
}
enum key_handler::result
text_scheme_handler::maybe_handle_kv (const context &ctxt,
const std::string &key,
const std::string &value)
{
if (key == "color")
return parse_bool_value (ctxt, key, value, m_show_color);
if (key == "show-nesting")
return parse_bool_value (ctxt, key, value, m_show_nesting);
if (key == "show-nesting-locations")
return parse_bool_value (ctxt, key, value,
m_show_locations_in_nesting);
if (key == "show-nesting-levels")
return parse_bool_value (ctxt, key, value, m_show_levels);
return result::unrecognized;
}
void
text_scheme_handler::get_keys (auto_vec<const char *> &out) const
{
out.safe_push ("color");
out.safe_push ("show-nesting");
out.safe_push ("show-nesting-locations");
out.safe_push ("show-nesting-levels");
}
/* class sarif_scheme_handler : public scheme_handler. */
std::unique_ptr<sink>
sarif_scheme_handler::
make_sink (const context &ctxt,
diagnostics::context &dc)
{
output_file output_file_;
if (m_filename.get ())
output_file_ = ctxt.open_output_file (std::move (m_filename));
else
// Default filename
{
const char *basename = ctxt.get_base_filename ();
if (!basename)
{
ctxt.report_missing_key ("file",
get_scheme_name (),
"FILENAME");
return nullptr;
}
output_file_
= open_sarif_output_file (dc,
ctxt.get_affected_location_mgr (),
basename,
m_serialization_kind);
}
if (!output_file_)
return nullptr;
auto serialization_obj
= make_sarif_serialization_object (m_serialization_kind);
auto sink = make_sarif_sink (dc,
*ctxt.get_affected_location_mgr (),
std::move (serialization_obj),
m_generation_opts,
std::move (output_file_));
return sink;
}
enum key_handler::result
sarif_scheme_handler::maybe_handle_kv (const context &ctxt,
const std::string &key,
const std::string &value)
{
if (key == "file")
{
m_filename = label_text::take (xstrdup (value.c_str ()));
return result::ok;
}
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}}};
return parse_enum_value<enum sarif_serialization_kind>
(ctxt,
key, value,
value_names,
m_serialization_kind);
}
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}}};
return parse_enum_value<enum sarif_version>
(ctxt,
key, value,
value_names,
m_generation_opts.m_version);
}
if (key == "state-graphs")
return parse_bool_value (ctxt, key, value,
m_generation_opts.m_state_graph);
return result::unrecognized;
}
void
sarif_scheme_handler::get_keys (auto_vec<const char *> &out) const
{
out.safe_push ("file");
out.safe_push ("serialization");
out.safe_push ("state-graphs");
out.safe_push ("version");
}
std::unique_ptr<sarif_serialization_format>
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<sarif_serialization_format_json> (true);
break;
}
}
/* class html_scheme_handler : public scheme_handler. */
std::unique_ptr<sink>
html_scheme_handler::
make_sink (const context &ctxt,
diagnostics::context &dc)
{
output_file output_file_;
if (m_filename.get ())
output_file_ = ctxt.open_output_file (std::move (m_filename));
else
// Default filename
{
const char *basename = ctxt.get_base_filename ();
if (!basename)
{
ctxt.report_missing_key ("file",
get_scheme_name (),
"FILENAME");
return nullptr;
}
output_file_
= open_html_output_file
(dc,
ctxt.get_affected_location_mgr (),
basename);
}
if (!output_file_)
return nullptr;
auto sink = make_html_sink (dc,
*ctxt.get_affected_location_mgr (),
m_html_gen_opts,
std::move (output_file_));
return sink;
}
enum key_handler::result
html_scheme_handler::maybe_handle_kv (const context &ctxt,
const std::string &key,
const std::string &value)
{
if (key == "css")
return parse_bool_value (ctxt, key, value, m_html_gen_opts.m_css);
if (key == "file")
{
m_filename = label_text::take (xstrdup (value.c_str ()));
return result::ok;
}
if (key == "javascript")
return parse_bool_value (ctxt, key, value,
m_html_gen_opts.m_javascript);
if (key == "show-state-diagrams")
return parse_bool_value (ctxt, key, value,
m_html_gen_opts.m_show_state_diagrams);
if (key == "show-graph-dot-src")
return parse_bool_value (ctxt, key, value,
m_html_gen_opts.m_show_graph_dot_src);
if (key == "show-graph-sarif")
return parse_bool_value (ctxt, key, value,
m_html_gen_opts.m_show_graph_sarif);
return result::unrecognized;
}
void
html_scheme_handler::get_keys (auto_vec<const char *> &out) const
{
out.safe_push ("css");
out.safe_push ("file");
out.safe_push ("javascript");
out.safe_push ("show-state-diagrams");
out.safe_push ("show-graph-dot-src");
out.safe_push ("show-graph-sarif");
}
} // namespace output_spec
#if CHECKING_P
namespace selftest {
using auto_fix_quotes = ::selftest::auto_fix_quotes;
/* 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::dc_spec_context
{
public:
test_spec_context (const char *option_name,
const char *unparsed_spec,
diagnostics::output_spec::key_handler *client_keys,
line_maps *location_mgr,
diagnostics::context &dc,
location_t loc)
: dc_spec_context (option_name,
unparsed_spec,
client_keys,
location_mgr,
dc,
location_mgr,
loc)
{
}
const char *
get_base_filename () const final override
{
return "BASE_FILENAME";
}
};
parser_test (const char *unparsed_spec,
diagnostics::output_spec::key_handler *client_keys = nullptr)
: m_dc (),
m_ctxt ("-fOPTION=",
unparsed_spec,
client_keys,
line_table,
m_dc,
UNKNOWN_LOCATION),
m_fmt (m_dc.get_sink (0))
{
pp_buffer (m_fmt.get_printer ())->m_flush_p = false;
}
std::unique_ptr<diagnostics::output_spec::scheme_name_and_params>
parse ()
{
return diagnostics::output_spec::parse (m_ctxt);
}
std::unique_ptr<diagnostics::sink>
parse_and_make_sink ()
{
return m_ctxt.parse_and_make_sink (m_dc);
}
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:
diagnostics::selftest::test_context m_dc;
test_spec_context m_ctxt;
diagnostics::sink &m_fmt;
};
/* Selftests. */
static void
test_output_arg_parsing ()
{
/* Minimal correct example. */
{
parser_test pt ("foo");
auto result = pt.parse ();
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 ("foo:");
auto result = pt.parse ();
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 ("foo:=");
auto result = pt.parse ();
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 ("foo:key,");
auto result = pt.parse ();
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 ("foo:key=value");
auto result = pt.parse ();
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 ("foo:key=value,");
auto result = pt.parse ();
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 ("foo:color=red,shape=circle");
auto result = pt.parse ();
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 ());
}
}
class test_key_handler : public diagnostics::output_spec::key_handler
{
public:
test_key_handler ()
: m_verbose (false),
m_strict (false)
{
}
enum result
maybe_handle_kv (const diagnostics::output_spec::context &ctxt,
const std::string &key,
const std::string &value) final override
{
if (key == "verbose")
return parse_bool_value (ctxt, key, value, m_verbose);
if (key == "strict")
return parse_bool_value (ctxt, key, value, m_strict);
return result::unrecognized;
}
void
get_keys (auto_vec<const char *> &out_known_keys) const final override
{
out_known_keys.safe_push ("verbose");
out_known_keys.safe_push ("strict");
}
bool m_verbose;
bool m_strict;
};
static void
test_client_arg_parsing ()
{
test_key_handler client_keys;
parser_test pt ("text:verbose=yes,strict=no", &client_keys);
auto result = pt.parse_and_make_sink ();
ASSERT_TRUE (result.get ());
ASSERT_TRUE (client_keys.m_verbose);
ASSERT_FALSE (client_keys.m_strict);
}
/* Run all of the selftests within this file. */
void
output_spec_cc_tests ()
{
auto_fix_quotes fix_quotes;
auto_fix_progname fix_progname;
test_output_arg_parsing ();
test_client_arg_parsing ();
}
} // namespace diagnostics::selftest
#endif /* #if CHECKING_P */
} // namespace diagnostics