| /* Support for -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 options -fdiagnostics-add-output=, |
| -fdiagnostics-set-output=, and their domain-specific language. */ |
| |
| #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-text.h" |
| #include "diagnostic-format-sarif.h" |
| #include "selftest.h" |
| #include "selftest-diagnostic.h" |
| #include "pretty-print-markup.h" |
| #include "opts.h" |
| #include "options.h" |
| #include "make-unique.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 |
| { |
| 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) |
| {} |
| |
| void |
| report_error (const char *gmsgid, ...) const |
| ATTRIBUTE_GCC_DIAG(2,3); |
| |
| void |
| report_unknown_key (const char *unparsed_arg, |
| const std::string &key, |
| const std::string &scheme_name, |
| auto_vec<const char *> &known_keys) const; |
| |
| void |
| report_missing_key (const char *unparsed_arg, |
| const std::string &key, |
| const std::string &scheme_name, |
| const char *metavar) const; |
| |
| diagnostic_output_file |
| open_output_file (label_text &&filename) const; |
| |
| const gcc_options &m_opts; |
| diagnostic_context &m_dc; |
| line_maps *m_location_mgr; |
| location_t m_loc; |
| const char *m_option_name; |
| }; |
| |
| struct scheme_name_and_params |
| { |
| std::string m_scheme_name; |
| std::vector<std::pair<std::string, std::string>> m_kvs; |
| }; |
| |
| static std::unique_ptr<scheme_name_and_params> |
| parse (const context &ctxt, const char *unparsed_arg); |
| |
| /* Class for parsing the arguments of -fdiagnostics-add-output= and |
| -fdiagnostics-set-output=, and making diagnostic_output_format |
| instances (or issuing errors). */ |
| |
| class output_factory |
| { |
| public: |
| class scheme_handler |
| { |
| 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; |
| }; |
| |
| /* 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 ::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 (::make_unique<text_scheme_handler> ()); |
| m_scheme_handlers.push_back (::make_unique<sarif_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 = ::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_version version = sarif_version::v2_1_0; |
| 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 == "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; |
| } |
| |
| /* Key not found. */ |
| auto_vec<const char *> known_keys; |
| known_keys.safe_push ("file"); |
| known_keys.safe_push ("version"); |
| 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); |
| } |
| if (!output_file) |
| return nullptr; |
| |
| auto sink = make_sarif_sink (ctxt.m_dc, |
| *line_table, |
| ctxt.m_opts.x_main_input_filename, |
| version, |
| std::move (output_file)); |
| return sink; |
| } |
| |
| } // namespace diagnostics_output_spec |
| } // namespace gcc |
| |
| void |
| handle_OPT_fdiagnostics_add_output_ (const gcc_options &opts, |
| diagnostic_context &dc, |
| const char *arg, |
| location_t loc) |
| { |
| gcc_assert (arg); |
| 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); |
| if (!sink) |
| return; |
| |
| dc.add_sink (std::move (sink)); |
| } |
| |
| void |
| handle_OPT_fdiagnostics_set_output_ (const gcc_options &opts, |
| diagnostic_context &dc, |
| const char *arg, |
| location_t loc) |
| { |
| gcc_assert (arg); |
| 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); |
| if (!sink) |
| return; |
| |
| 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 */ |