diff options
Diffstat (limited to 'gcc/diagnostics')
56 files changed, 31244 insertions, 0 deletions
diff --git a/gcc/diagnostics/buffering.cc b/gcc/diagnostics/buffering.cc new file mode 100644 index 0000000..29f039f --- /dev/null +++ b/gcc/diagnostics/buffering.cc @@ -0,0 +1,199 @@ +/* Support for buffering diagnostics before flushing them to output sinks. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostics/buffering.h" +#include "diagnostics/sink.h" + +namespace diagnostics { + +/* Methods fns of diagnostics::context relating to buffering. */ + +/* If BUFFER_ is non-null, use BUFFER as the active diagnostics::buffer on + this context. BUFFER is borrowed. + + If BUFFER_ is null, stop any buffering on this context until the next call + to this function. */ + +void +context::set_diagnostic_buffer (buffer *buffer_) +{ + /* We don't allow changing buffering within a diagnostic group + (to simplify handling of buffered diagnostics within the + diagnostic_format implementations). */ + gcc_assert (m_diagnostic_groups.m_group_nesting_depth == 0); + + /* Likewise, for simplicity, we only allow changing buffers + at nesting level 0. */ + gcc_assert (m_diagnostic_groups.m_diagnostic_nesting_level == 0); + + m_diagnostic_buffer = buffer_; + + if (buffer_) + { + buffer_->ensure_per_sink_buffers (); + gcc_assert (buffer_->m_per_sink_buffers); + gcc_assert (buffer_->m_per_sink_buffers->length () + == m_sinks.length ()); + for (unsigned idx = 0; idx < m_sinks.length (); ++idx) + { + auto sink_ = m_sinks[idx]; + auto per_sink_buffer = (*buffer_->m_per_sink_buffers)[idx]; + sink_->set_buffer (per_sink_buffer); + } + } + else + for (auto sink_ : m_sinks) + sink_->set_buffer (nullptr); +} + +/* Clear BUFFER_ without flushing it. */ + +void +context::clear_diagnostic_buffer (buffer &buffer_) +{ + if (buffer_.m_per_sink_buffers) + for (auto per_sink_buffer_ : *buffer_.m_per_sink_buffers) + per_sink_buffer_->clear (); + + buffer_.m_diagnostic_counters.clear (); + + /* We need to reset last_location, otherwise we may skip caret lines + when we actually give a diagnostic. */ + m_last_location = UNKNOWN_LOCATION; +} + +/* Flush the diagnostics in BUFFER_ to this context, clearing BUFFER_. */ + +void +context::flush_diagnostic_buffer (buffer &buffer_) +{ + bool had_errors + = (buffer_.diagnostic_count (kind::error) > 0 + || buffer_.diagnostic_count (kind::werror) > 0); + if (buffer_.m_per_sink_buffers) + for (auto per_sink_buffer_ : *buffer_.m_per_sink_buffers) + per_sink_buffer_->flush (); + buffer_.m_diagnostic_counters.move_to (m_diagnostic_counters); + + action_after_output (had_errors ? kind::error : kind::warning); + check_max_errors (true); +} + +/* class diagnostics::buffer. */ + +buffer::buffer (context &ctxt) +: m_ctxt (ctxt), + m_per_sink_buffers (nullptr) +{ +} + +buffer::~buffer () +{ + if (m_per_sink_buffers) + { + for (auto iter : *m_per_sink_buffers) + delete iter; + delete m_per_sink_buffers; + } +} + +void +buffer::dump (FILE *out, int indent) const +{ + m_diagnostic_counters.dump (out, indent + 2); + fprintf (out, "%*sm_per_sink_buffers:\n", indent, ""); + if (m_per_sink_buffers) + for (auto per_sink_buffer_ : *m_per_sink_buffers) + per_sink_buffer_->dump (out, indent + 2); + else + fprintf (out, "%*s(none)\n", indent + 2, ""); +} + +bool +buffer::empty_p () const +{ + if (m_per_sink_buffers) + for (auto per_sink_buffer_ : *m_per_sink_buffers) + /* Query initial buffer. */ + return per_sink_buffer_->empty_p (); + return true; +} + +void +buffer::move_to (buffer &dest) +{ + /* Bail if there's nothing to move. */ + if (!m_per_sink_buffers) + return; + + m_diagnostic_counters.move_to (dest.m_diagnostic_counters); + + if (!dest.m_per_sink_buffers) + { + /* Optimization for the "move to empty" case: + simply move the vec to the dest. */ + dest.m_per_sink_buffers = m_per_sink_buffers; + m_per_sink_buffers = nullptr; + return; + } + + dest.ensure_per_sink_buffers (); + gcc_assert (m_per_sink_buffers); + gcc_assert (m_per_sink_buffers->length () + == m_ctxt.m_sinks.length ()); + gcc_assert (dest.m_per_sink_buffers); + gcc_assert (dest.m_per_sink_buffers->length () + == m_ctxt.m_sinks.length ()); + for (unsigned idx = 0; idx < m_ctxt.m_sinks.length (); ++idx) + { + auto per_sink_buffer_src = (*m_per_sink_buffers)[idx]; + auto per_sink_buffer_dest = (*dest.m_per_sink_buffers)[idx]; + per_sink_buffer_src->move_to (*per_sink_buffer_dest); + } +} + +/* Lazily get the output formats to create their own kind of buffers. + We can't change the output sinks on a context once this has been called + on any diagnostics::buffer instances for that context, since there's no + way to update all diagnostics::buffer instances for that context. */ + +void +buffer::ensure_per_sink_buffers () +{ + if (!m_per_sink_buffers) + { + m_per_sink_buffers = new auto_vec<per_sink_buffer *> (); + for (unsigned idx = 0; idx < m_ctxt.m_sinks.length (); ++idx) + { + auto sink_ = m_ctxt.m_sinks[idx]; + auto per_sink_buffer = sink_->make_per_sink_buffer (); + m_per_sink_buffers->safe_push (per_sink_buffer.release ()); + } + } + gcc_assert (m_per_sink_buffers); + gcc_assert (m_per_sink_buffers->length () + == m_ctxt.m_sinks.length ()); +} + +} // namespace diagnostics diff --git a/gcc/diagnostics/buffering.h b/gcc/diagnostics/buffering.h new file mode 100644 index 0000000..c3ac070 --- /dev/null +++ b/gcc/diagnostics/buffering.h @@ -0,0 +1,113 @@ +/* Support for buffering diagnostics before flushing them to output sinks. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_BUFFERING_H +#define GCC_DIAGNOSTICS_BUFFERING_H + +#include "diagnostic.h" + +namespace diagnostics { + +class per_sink_buffer; +class sink; + class text_sink; + +/* Class representing a buffer of zero or more diagnostics that + have been reported to a diagnostics::context, but which haven't + yet been flushed. + + A diagnostics::buffer can be: + + * flushed to the diagnostics::context, which issues + the diagnostics within the buffer to the output format + and checks for limits such as -fmax-errors=, or + + * moved to another diagnostics::buffer, which moves the diagnostics + within the first buffer to the other buffer, appending them after any + existing diagnostics within the destination buffer, emptying the + source buffer, or + + * cleared, which discards any diagnostics within the buffer + without issuing them to the output format. + + Since a buffer needs to contain output-format-specific data, + it's not possible to change the output format of the + diagnostics::context once any buffers are non-empty. + + To simplify implementing output formats, it's not possible + to change buffering on a diagnostics::context whilst within a + diagnostic group. */ + +class buffer +{ + public: + friend class context; + + buffer (context &ctxt); + ~buffer (); + + void dump (FILE *out, int indent) const; + void DEBUG_FUNCTION dump () const { dump (stderr, 0); } + + int diagnostic_count (enum kind kind) const + { + return m_diagnostic_counters.get_count (kind); + } + + bool empty_p () const; + + void move_to (buffer &dest); + + private: + void ensure_per_sink_buffers (); + + context &m_ctxt; + auto_vec<per_sink_buffer *> *m_per_sink_buffers; + + /* The number of buffered diagnostics of each kind. */ + counters m_diagnostic_counters; +}; + +/* Implementation detail of diagnostics::buffer. + + Abstract base class describing how to represent zero of more + buffered diagnostics for a particular diagnostics::sink + (e.g. text vs SARIF). + + Each diagnostics::sink subclass should implement its own + subclass for handling diagnostics::buffer. */ + +class per_sink_buffer +{ +public: + virtual ~per_sink_buffer () {} + + virtual void dump (FILE *out, int indent) const = 0; + void DEBUG_FUNCTION dump () const { dump (stderr, 0); } + + virtual bool empty_p () const = 0; + virtual void move_to (per_sink_buffer &dest) = 0; + virtual void clear () = 0; + virtual void flush () = 0; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_BUFFERING_H */ diff --git a/gcc/diagnostics/changes.cc b/gcc/diagnostics/changes.cc new file mode 100644 index 0000000..e1caab0 --- /dev/null +++ b/gcc/diagnostics/changes.cc @@ -0,0 +1,1874 @@ +/* Determining the results of applying fix-it hints. + Copyright (C) 2016-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/>. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "line-map.h" +#include "diagnostics/changes.h" +#include "pretty-print.h" +#include "diagnostics/color.h" +#include "diagnostics/file-cache.h" +#include "selftest.h" + +namespace diagnostics { +namespace changes { + +/* This file implements a way to track the effect of fix-its, + via a class change_set; the other classes are support classes for + change_set. + + A complication here is that fix-its are expressed relative to coordinates + in the file when it was parsed, before any changes have been made, and + so if there's more that one fix-it to be applied, we have to adjust + later fix-its to allow for the changes made by earlier ones. This + is done by the various "get_effective_column" methods. + + The "filename" params are required to outlive the change_set (no + copy of the underlying str is taken, just the ptr). */ + +/* Forward decls. class change_set is declared within changes.h. + The other types are declared here. */ +class change_set; +class changed_file; +class changed_line; +class line_event; + +/* A struct to hold the params of a print_diff call. */ + +class diff +{ +public: + diff (pretty_printer *pp, bool show_filenames) + : m_pp (pp), m_show_filenames (show_filenames) {} + + pretty_printer *m_pp; + bool m_show_filenames; +}; + +/* The state of one named file within an change_set: the filename, + and the lines that have been edited so far. */ + +class changed_file +{ + public: + changed_file (change_set &ec, const char *filename); + static void delete_cb (changed_file *file); + + const char *get_filename () const { return m_filename; } + char *get_content (); + + bool apply_fixit (int line, int start_column, + int next_column, + const char *replacement_str, + int replacement_len); + int get_effective_column (int line, int column); + + static int call_print_diff (const char *, changed_file *file, + void *user_data) + { + diff *d = (diff *)user_data; + file->print_diff (d->m_pp, d->m_show_filenames); + return 0; + } + + file_cache &get_file_cache () const + { + return m_change_set.get_file_cache (); + } + + private: + bool print_content (pretty_printer *pp); + void print_diff (pretty_printer *pp, bool show_filenames); + int print_diff_hunk (pretty_printer *pp, int old_start_of_hunk, + int old_end_of_hunk, int new_start_of_hunk); + changed_line *get_line (int line); + changed_line *get_or_insert_line (int line); + int get_num_lines (bool *missing_trailing_newline); + + int get_effective_line_count (int old_start_of_hunk, + int old_end_of_hunk); + + void print_run_of_changed_lines (pretty_printer *pp, + int start_of_run, + int end_of_run); + + change_set &m_change_set; + const char *m_filename; + typed_splay_tree<int, changed_line *> m_changed_lines; + int m_num_lines; +}; + +/* A line added before an changed_line. */ + +class added_line +{ + public: + added_line (const char *content, int len) + : m_content (xstrndup (content, len)), m_len (len) {} + ~added_line () { free (m_content); } + + const char *get_content () const { return m_content; } + int get_len () const { return m_len; } + + private: + char *m_content; + int m_len; +}; + +/* Class for representing edit events that have occurred on one line of + one file: the replacement of some text between some columns + on the line. + + Subsequent events will need their columns adjusting if they're + are on this line and their column is >= the start point. */ + +class line_event +{ + public: + line_event (int start, int next, int len) : m_start (start), + m_delta (len - (next - start)) {} + + int get_effective_column (int orig_column) const + { + if (orig_column >= m_start) + return orig_column += m_delta; + else + return orig_column; + } + + private: + int m_start; + int m_delta; +}; + +/* The state of one edited line within an changed_file. + As well as the current content of the line, it contains a record of + the changes, so that further changes can be applied in the correct + place. + + When handling fix-it hints containing newlines, new lines are added + as added_line predecessors to an changed_line. Hence it's possible + for an "changed_line" to not actually have been changed, but to merely + be a placeholder for the lines added before it. This can be tested + for with actuall_edited_p, and has a slight effect on how diff hunks + are generated. */ + +class changed_line +{ + public: + changed_line (file_cache &fc, const char *filename, int line_num); + ~changed_line (); + static void delete_cb (changed_line *el); + + int get_line_num () const { return m_line_num; } + const char *get_content () const { return m_content; } + int get_len () const { return m_len; } + + int get_effective_column (int orig_column) const; + bool apply_fixit (int start_column, + int next_column, + const char *replacement_str, + int replacement_len); + + int get_effective_line_count () const; + + /* Has the content of this line actually changed, or are we merely + recording predecessor added_lines? */ + bool actually_edited_p () const { return m_line_events.length () > 0; } + + void print_content (pretty_printer *pp) const; + void print_diff_lines (pretty_printer *pp) const; + + private: + void ensure_capacity (int len); + void ensure_terminated (); + + int m_line_num; + char *m_content; + int m_len; + int m_alloc_sz; + auto_vec <line_event> m_line_events; + auto_vec <added_line *> m_predecessors; +}; + +/* Forward decls. */ + +static void +print_diff_line (pretty_printer *pp, char prefix_char, + const char *line, int line_size); + +/* Implementation of class change_set. */ + +/* change_set's ctor. */ + +change_set::change_set (file_cache &fc) +: m_file_cache (fc), + m_valid (true), + m_files (strcmp, NULL, changed_file::delete_cb) +{} + +/* Add any fixits within RICHLOC to this context, recording the + changes that they make. */ + +void +change_set::add_fixits (rich_location *richloc) +{ + if (!m_valid) + return; + if (richloc->seen_impossible_fixit_p ()) + { + m_valid = false; + return; + } + for (unsigned i = 0; i < richloc->get_num_fixit_hints (); i++) + { + const fixit_hint *hint = richloc->get_fixit_hint (i); + if (!apply_fixit (hint)) + m_valid = false; + } +} + +/* Get the content of the given file, with fix-its applied. + If any errors occurred in this change_set, return NULL. + The ptr should be freed by the caller. */ + +char * +change_set::get_content (const char *filename) +{ + if (!m_valid) + return NULL; + changed_file &file = get_or_insert_file (filename); + return file.get_content (); +} + +/* Map a location before the edits to a column number after the edits. + This method is for the selftests. */ + +int +change_set::get_effective_column (const char *filename, int line, + int column) +{ + changed_file *file = get_file (filename); + if (!file) + return column; + return file->get_effective_column (line, column); +} + +/* Generate a unified diff. The resulting string should be freed by the + caller. Primarily for selftests. + If any errors occurred in this change_set, return NULL. */ + +char * +change_set::generate_diff (bool show_filenames) +{ + if (!m_valid) + return NULL; + + pretty_printer pp; + print_diff (&pp, show_filenames); + return xstrdup (pp_formatted_text (&pp)); +} + +/* Print a unified diff to PP, showing the changes made within the + context. */ + +void +change_set::print_diff (pretty_printer *pp, bool show_filenames) +{ + if (!m_valid) + return; + diff d (pp, show_filenames); + m_files.foreach (changed_file::call_print_diff, &d); +} + +/* Attempt to apply the given fixit. Return true if it can be + applied, or false otherwise. */ + +bool +change_set::apply_fixit (const fixit_hint *hint) +{ + expanded_location start = expand_location (hint->get_start_loc ()); + expanded_location next_loc = expand_location (hint->get_next_loc ()); + if (start.file != next_loc.file) + return false; + if (start.line != next_loc.line) + return false; + if (start.column == 0) + return false; + if (next_loc.column == 0) + return false; + + changed_file &file = get_or_insert_file (start.file); + if (!m_valid) + return false; + return file.apply_fixit (start.line, start.column, next_loc.column, + hint->get_string (), + hint->get_length ()); +} + +/* Locate the changed_file * for FILENAME, if any + Return NULL if there isn't one. */ + +changed_file * +change_set::get_file (const char *filename) +{ + gcc_assert (filename); + return m_files.lookup (filename); +} + +/* Locate the changed_file for FILENAME, adding one if there isn't one. */ + +changed_file & +change_set::get_or_insert_file (const char *filename) +{ + gcc_assert (filename); + + changed_file *file = get_file (filename); + if (file) + return *file; + + /* Not found. */ + file = new changed_file (*this, filename); + m_files.insert (filename, file); + return *file; +} + +/* Implementation of class changed_file. */ + +/* Callback for m_changed_lines, for comparing line numbers. */ + +static int line_comparator (int a, int b) +{ + return a - b; +} + +/* changed_file's constructor. */ + +changed_file::changed_file (change_set &ec, const char *filename) +: m_change_set (ec), + m_filename (filename), + m_changed_lines (line_comparator, NULL, changed_line::delete_cb), + m_num_lines (-1) +{ +} + +/* A callback for deleting changed_file *, for use as a + delete_value_fn for change_set::m_files. */ + +void +changed_file::delete_cb (changed_file *file) +{ + delete file; +} + +/* Get the content of the file, with fix-its applied. + The ptr should be freed by the caller. */ + +char * +changed_file::get_content () +{ + pretty_printer pp; + if (!print_content (&pp)) + return NULL; + return xstrdup (pp_formatted_text (&pp)); +} + +/* Attempt to replace columns START_COLUMN up to but not including NEXT_COLUMN + of LINE with the string REPLACEMENT_STR of length REPLACEMENT_LEN, + updating the in-memory copy of the line, and the record of edits to + the line. */ + +bool +changed_file::apply_fixit (int line, int start_column, int next_column, + const char *replacement_str, + int replacement_len) +{ + changed_line *el = get_or_insert_line (line); + if (!el) + return false; + return el->apply_fixit (start_column, next_column, replacement_str, + replacement_len); +} + +/* Given line LINE, map from COLUMN in the input file to its current + column after edits have been applied. */ + +int +changed_file::get_effective_column (int line, int column) +{ + const changed_line *el = get_line (line); + if (!el) + return column; + return el->get_effective_column (column); +} + +/* Attempt to print the content of the file to PP, with edits applied. + Return true if successful, false otherwise. */ + +bool +changed_file::print_content (pretty_printer *pp) +{ + bool missing_trailing_newline; + int line_count = get_num_lines (&missing_trailing_newline); + for (int line_num = 1; line_num <= line_count; line_num++) + { + changed_line *el = get_line (line_num); + if (el) + el->print_content (pp); + else + { + char_span line + = get_file_cache ().get_source_line (m_filename, line_num); + if (!line) + return false; + for (size_t i = 0; i < line.length (); i++) + pp_character (pp, line[i]); + } + if (line_num < line_count) + pp_character (pp, '\n'); + } + + if (!missing_trailing_newline) + pp_character (pp, '\n'); + + return true; +} + +/* Print a unified diff to PP, showing any changes that have occurred + to this file. */ + +void +changed_file::print_diff (pretty_printer *pp, bool show_filenames) +{ + if (show_filenames) + { + pp_string (pp, colorize_start (pp_show_color (pp), "diff-filename")); + /* Avoid -Wformat-diag in non-diagnostic output. */ + pp_string (pp, "--- "); + pp_string (pp, m_filename); + pp_newline (pp); + pp_string (pp, "+++ "); + pp_string (pp, m_filename); + pp_newline (pp); + pp_string (pp, colorize_stop (pp_show_color (pp))); + } + + changed_line *el = m_changed_lines.min (); + + bool missing_trailing_newline; + int line_count = get_num_lines (&missing_trailing_newline); + + const int context_lines = 3; + + /* Track new line numbers minus old line numbers. */ + + int line_delta = 0; + + while (el) + { + int start_of_hunk = el->get_line_num (); + start_of_hunk -= context_lines; + if (start_of_hunk < 1) + start_of_hunk = 1; + + /* Locate end of hunk, merging in changed lines + that are sufficiently close. */ + while (true) + { + changed_line *next_el + = m_changed_lines.successor (el->get_line_num ()); + if (!next_el) + break; + + int end_of_printed_hunk = el->get_line_num () + context_lines; + if (!el->actually_edited_p ()) + end_of_printed_hunk--; + + if (end_of_printed_hunk + >= next_el->get_line_num () - context_lines) + el = next_el; + else + break; + } + + int end_of_hunk = el->get_line_num (); + end_of_hunk += context_lines; + if (!el->actually_edited_p ()) + end_of_hunk--; + if (end_of_hunk > line_count) + end_of_hunk = line_count; + + int new_start_of_hunk = start_of_hunk + line_delta; + line_delta += print_diff_hunk (pp, start_of_hunk, end_of_hunk, + new_start_of_hunk); + el = m_changed_lines.successor (el->get_line_num ()); + } +} + +/* Print one hunk within a unified diff to PP, covering the + given range of lines. OLD_START_OF_HUNK and OLD_END_OF_HUNK are + line numbers in the unedited version of the file. + NEW_START_OF_HUNK is a line number in the edited version of the file. + Return the change in the line count within the hunk. */ + +int +changed_file::print_diff_hunk (pretty_printer *pp, int old_start_of_hunk, + int old_end_of_hunk, int new_start_of_hunk) +{ + int old_num_lines = old_end_of_hunk - old_start_of_hunk + 1; + int new_num_lines + = get_effective_line_count (old_start_of_hunk, old_end_of_hunk); + + pp_string (pp, colorize_start (pp_show_color (pp), "diff-hunk")); + pp_printf (pp, "%s -%i,%i +%i,%i %s", + "@@", old_start_of_hunk, old_num_lines, + new_start_of_hunk, new_num_lines, "@@\n"); + pp_string (pp, colorize_stop (pp_show_color (pp))); + + int line_num = old_start_of_hunk; + while (line_num <= old_end_of_hunk) + { + changed_line *el = get_line (line_num); + if (el) + { + /* We have an edited line. + Consolidate into runs of changed lines. */ + const int first_changed_line_in_run = line_num; + while (get_line (line_num)) + line_num++; + const int last_changed_line_in_run = line_num - 1; + print_run_of_changed_lines (pp, first_changed_line_in_run, + last_changed_line_in_run); + } + else + { + /* Unchanged line. */ + char_span old_line + = get_file_cache ().get_source_line (m_filename, line_num); + print_diff_line (pp, ' ', old_line.get_buffer (), old_line.length ()); + line_num++; + } + } + + return new_num_lines - old_num_lines; +} + +/* Subroutine of changed_file::print_diff_hunk: given a run of lines + from START_OF_RUN to END_OF_RUN that all have changed_line instances, + print the diff to PP. */ + +void +changed_file::print_run_of_changed_lines (pretty_printer *pp, + int start_of_run, + int end_of_run) +{ + /* Show old version of lines. */ + pp_string (pp, colorize_start (pp_show_color (pp), + "diff-delete")); + for (int line_num = start_of_run; + line_num <= end_of_run; + line_num++) + { + changed_line *el_in_run = get_line (line_num); + gcc_assert (el_in_run); + if (el_in_run->actually_edited_p ()) + { + char_span old_line + = get_file_cache ().get_source_line (m_filename, line_num); + print_diff_line (pp, '-', old_line.get_buffer (), + old_line.length ()); + } + } + pp_string (pp, colorize_stop (pp_show_color (pp))); + + /* Show new version of lines. */ + pp_string (pp, colorize_start (pp_show_color (pp), + "diff-insert")); + for (int line_num = start_of_run; + line_num <= end_of_run; + line_num++) + { + changed_line *el_in_run = get_line (line_num); + gcc_assert (el_in_run); + el_in_run->print_diff_lines (pp); + } + pp_string (pp, colorize_stop (pp_show_color (pp))); +} + +/* Print one line within a diff, starting with PREFIX_CHAR, + followed by the LINE of content, of length LEN. LINE is + not necessarily 0-terminated. Print a trailing newline. */ + +static void +print_diff_line (pretty_printer *pp, char prefix_char, + const char *line, int len) +{ + pp_character (pp, prefix_char); + for (int i = 0; i < len; i++) + pp_character (pp, line[i]); + pp_character (pp, '\n'); +} + +/* Determine the number of lines that will be present after + editing for the range of lines from OLD_START_OF_HUNK to + OLD_END_OF_HUNK inclusive. */ + +int +changed_file::get_effective_line_count (int old_start_of_hunk, + int old_end_of_hunk) +{ + int line_count = 0; + for (int old_line_num = old_start_of_hunk; old_line_num <= old_end_of_hunk; + old_line_num++) + { + changed_line *el = get_line (old_line_num); + if (el) + line_count += el->get_effective_line_count (); + else + line_count++; + } + return line_count; +} + +/* Get the state of LINE within the file, or NULL if it is untouched. */ + +changed_line * +changed_file::get_line (int line) +{ + return m_changed_lines.lookup (line); +} + +/* Get the state of LINE within the file, creating a state for it + if necessary. Return NULL if an error occurs. */ + +changed_line * +changed_file::get_or_insert_line (int line) +{ + changed_line *el = get_line (line); + if (el) + return el; + el = new changed_line (get_file_cache (), m_filename, line); + if (el->get_content () == NULL) + { + delete el; + return NULL; + } + m_changed_lines.insert (line, el); + return el; +} + +/* Get the total number of lines in m_content, writing + true to *MISSING_TRAILING_NEWLINE if the final line + if missing a newline, false otherwise. */ + +int +changed_file::get_num_lines (bool *missing_trailing_newline) +{ + gcc_assert (missing_trailing_newline); + if (m_num_lines == -1) + { + m_num_lines = 0; + while (true) + { + char_span line + = get_file_cache ().get_source_line (m_filename, m_num_lines + 1); + if (line) + m_num_lines++; + else + break; + } + } + *missing_trailing_newline + = get_file_cache ().missing_trailing_newline_p (m_filename); + return m_num_lines; +} + +/* Implementation of class changed_line. */ + +/* changed_line's ctor. */ + +changed_line::changed_line (file_cache &fc, const char *filename, int line_num) +: m_line_num (line_num), + m_content (NULL), m_len (0), m_alloc_sz (0), + m_line_events (), + m_predecessors () +{ + char_span line = fc.get_source_line (filename, line_num); + if (!line) + return; + m_len = line.length (); + ensure_capacity (m_len); + memcpy (m_content, line.get_buffer (), m_len); + ensure_terminated (); +} + +/* changed_line's dtor. */ + +changed_line::~changed_line () +{ + unsigned i; + added_line *pred; + + free (m_content); + FOR_EACH_VEC_ELT (m_predecessors, i, pred) + delete pred; +} + +/* A callback for deleting changed_line *, for use as a + delete_value_fn for changed_file::m_changed_lines. */ + +void +changed_line::delete_cb (changed_line *el) +{ + delete el; +} + +/* Map a location before the edits to a column number after the edits, + within a specific line. */ + +int +changed_line::get_effective_column (int orig_column) const +{ + int i; + line_event *event; + FOR_EACH_VEC_ELT (m_line_events, i, event) + orig_column = event->get_effective_column (orig_column); + return orig_column; +} + +/* Attempt to replace columns START_COLUMN up to but not including + NEXT_COLUMN of the line with the string REPLACEMENT_STR of + length REPLACEMENT_LEN, updating the in-memory copy of the line, + and the record of edits to the line. + Return true if successful; false if an error occurred. */ + +bool +changed_line::apply_fixit (int start_column, + int next_column, + const char *replacement_str, + int replacement_len) +{ + /* Handle newlines. They will only ever be at the end of the + replacement text, thanks to the filtering in rich_location. */ + if (replacement_len > 1) + if (replacement_str[replacement_len - 1] == '\n') + { + /* Stash in m_predecessors, stripping off newline. */ + m_predecessors.safe_push (new added_line (replacement_str, + replacement_len - 1)); + return true; + } + + start_column = get_effective_column (start_column); + next_column = get_effective_column (next_column); + + int start_offset = start_column - 1; + int next_offset = next_column - 1; + + gcc_assert (start_offset >= 0); + gcc_assert (next_offset >= 0); + + if (start_column > next_column) + return false; + if (start_offset >= (m_len + 1)) + return false; + if (next_offset >= (m_len + 1)) + return false; + + size_t victim_len = next_offset - start_offset; + + /* Ensure buffer is big enough. */ + size_t new_len = m_len + replacement_len - victim_len; + ensure_capacity (new_len); + + char *suffix = m_content + next_offset; + gcc_assert (suffix <= m_content + m_len); + size_t len_suffix = (m_content + m_len) - suffix; + + /* Move successor content into position. They overlap, so use memmove. */ + memmove (m_content + start_offset + replacement_len, + suffix, len_suffix); + + /* Replace target content. They don't overlap, so use memcpy. */ + memcpy (m_content + start_offset, + replacement_str, + replacement_len); + + m_len = new_len; + + ensure_terminated (); + + /* Record the replacement, so that future changes to the line can have + their column information adjusted accordingly. */ + m_line_events.safe_push (line_event (start_column, next_column, + replacement_len)); + return true; +} + +/* Determine the number of lines that will be present after + editing for this line. Typically this is just 1, but + if newlines have been added before this line, they will + also be counted. */ + +int +changed_line::get_effective_line_count () const +{ + return m_predecessors.length () + 1; +} + +/* Subroutine of changed_file::print_content. + Print this line and any new lines added before it, to PP. */ + +void +changed_line::print_content (pretty_printer *pp) const +{ + unsigned i; + added_line *pred; + FOR_EACH_VEC_ELT (m_predecessors, i, pred) + { + pp_string (pp, pred->get_content ()); + pp_newline (pp); + } + pp_string (pp, m_content); +} + +/* Subroutine of changed_file::print_run_of_changed_lines for + printing diff hunks to PP. + Print the '+' line for this line, and any newlines added + before it. + Note that if this changed_line was actually edited, the '-' + line has already been printed. If it wasn't, then we merely + have a placeholder changed_line for adding newlines to, and + we need to print a ' ' line for the changed_line as we haven't + printed it yet. */ + +void +changed_line::print_diff_lines (pretty_printer *pp) const +{ + unsigned i; + added_line *pred; + FOR_EACH_VEC_ELT (m_predecessors, i, pred) + print_diff_line (pp, '+', pred->get_content (), + pred->get_len ()); + if (actually_edited_p ()) + print_diff_line (pp, '+', m_content, m_len); + else + print_diff_line (pp, ' ', m_content, m_len); +} + +/* Ensure that the buffer for m_content is at least large enough to hold + a string of length LEN and its 0-terminator, doubling on repeated + allocations. */ + +void +changed_line::ensure_capacity (int len) +{ + /* Allow 1 extra byte for 0-termination. */ + if (m_alloc_sz < (len + 1)) + { + size_t new_alloc_sz = (len + 1) * 2; + m_content = (char *)xrealloc (m_content, new_alloc_sz); + m_alloc_sz = new_alloc_sz; + } +} + +/* Ensure that m_content is 0-terminated. */ + +void +changed_line::ensure_terminated () +{ + /* 0-terminate the buffer. */ + gcc_assert (m_len < m_alloc_sz); + m_content[m_len] = '\0'; +} + +#if CHECKING_P + +/* Selftests of code-editing. */ + +namespace selftest { + +using line_table_case = ::selftest::line_table_case; +using line_table_test = ::selftest::line_table_test; +using temp_source_file = ::selftest::temp_source_file; +using named_temp_file = ::selftest::named_temp_file; + +/* A wrapper class for ensuring that the underlying pointer is freed. */ + +template <typename POINTER_T> +class auto_free +{ + public: + auto_free (POINTER_T p) : m_ptr (p) {} + ~auto_free () { free (m_ptr); } + + operator POINTER_T () { return m_ptr; } + + private: + POINTER_T m_ptr; +}; + +/* Verify that change_set::get_content works for unedited files. */ + +static void +test_get_content () +{ + /* Test of empty file. */ + { + const char *content = (""); + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + file_cache fc; + change_set edit (fc); + auto_free <char *> result = edit.get_content (tmp.get_filename ()); + ASSERT_STREQ ("", result); + } + + /* Test of simple content. */ + { + const char *content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + file_cache fc; + change_set edit (fc); + auto_free <char *> result = edit.get_content (tmp.get_filename ()); + ASSERT_STREQ ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n", result); + } + + /* Test of omitting the trailing newline on the final line. */ + { + const char *content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + file_cache fc; + change_set edit (fc); + auto_free <char *> result = edit.get_content (tmp.get_filename ()); + /* We should respect the omitted trailing newline. */ + ASSERT_STREQ ("/* before */\n" + "foo = bar.field;\n" + "/* after */", result); + } +} + +/* Test applying an "insert" fixit, using insert_before. */ + +static void +test_applying_fixits_insert_before (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 2); + + /* Add a comment in front of "bar.field". */ + location_t start = linemap_position_for_column (line_table, 7); + rich_location richloc (line_table, start); + richloc.add_fixit_insert_before ("/* inserted */"); + + if (start > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + if (start <= LINE_MAP_MAX_LOCATION_WITH_COLS) + ASSERT_STREQ ("/* before */\n" + "foo = /* inserted */bar.field;\n" + "/* after */\n", new_content); + + /* Verify that locations on other lines aren't affected by the change. */ + ASSERT_EQ (100, edit.get_effective_column (filename, 1, 100)); + ASSERT_EQ (100, edit.get_effective_column (filename, 3, 100)); + + /* Verify locations on the line before the change. */ + ASSERT_EQ (1, edit.get_effective_column (filename, 2, 1)); + ASSERT_EQ (6, edit.get_effective_column (filename, 2, 6)); + + /* Verify locations on the line at and after the change. */ + ASSERT_EQ (21, edit.get_effective_column (filename, 2, 7)); + ASSERT_EQ (22, edit.get_effective_column (filename, 2, 8)); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,3 +1,3 @@\n" + " /* before */\n" + "-foo = bar.field;\n" + "+foo = /* inserted */bar.field;\n" + " /* after */\n", diff); +} + +/* Test applying an "insert" fixit, using insert_after, with + a range of length > 1 (to ensure that the end-point of + the input range is used). */ + +static void +test_applying_fixits_insert_after (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 2); + + /* Add a comment after "field". */ + location_t start = linemap_position_for_column (line_table, 11); + location_t finish = linemap_position_for_column (line_table, 15); + location_t field = make_location (start, start, finish); + rich_location richloc (line_table, field); + richloc.add_fixit_insert_after ("/* inserted */"); + + if (finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Verify that the text was inserted after the end of "field". */ + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + ASSERT_STREQ ("/* before */\n" + "foo = bar.field/* inserted */;\n" + "/* after */\n", new_content); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,3 +1,3 @@\n" + " /* before */\n" + "-foo = bar.field;\n" + "+foo = bar.field/* inserted */;\n" + " /* after */\n", diff); +} + +/* Test applying an "insert" fixit, using insert_after at the end of + a line (contrast with test_applying_fixits_insert_after_failure + below). */ + +static void +test_applying_fixits_insert_after_at_line_end (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 2); + + /* Add a comment after the semicolon. */ + location_t loc = linemap_position_for_column (line_table, 16); + rich_location richloc (line_table, loc); + richloc.add_fixit_insert_after ("/* inserted */"); + + if (loc > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + ASSERT_STREQ ("/* before */\n" + "foo = bar.field;/* inserted */\n" + "/* after */\n", new_content); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,3 +1,3 @@\n" + " /* before */\n" + "-foo = bar.field;\n" + "+foo = bar.field;/* inserted */\n" + " /* after */\n", diff); +} + +/* Test of a failed attempt to apply an "insert" fixit, using insert_after, + due to the relevant linemap ending. Contrast with + test_applying_fixits_insert_after_at_line_end above. */ + +static void +test_applying_fixits_insert_after_failure (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 2); + + /* Add a comment after the semicolon. */ + location_t loc = linemap_position_for_column (line_table, 16); + rich_location richloc (line_table, loc); + + /* We want a failure of linemap_position_for_loc_and_offset. + We can do this by starting a new linemap at line 3, so that + there is no appropriate location value for the insertion point + within the linemap for line 2. */ + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 3); + + /* The failure fails to happen at the transition point from + packed ranges to unpacked ranges (where there are some "spare" + location_t values). Skip the test there. */ + if (loc >= LINE_MAP_MAX_LOCATION_WITH_PACKED_RANGES) + return; + + /* Offsetting "loc" should now fail (by returning the input loc. */ + ASSERT_EQ (loc, linemap_position_for_loc_and_offset (line_table, loc, 1)); + + /* Hence attempting to use add_fixit_insert_after at the end of the line + should now fail. */ + richloc.add_fixit_insert_after ("/* inserted */"); + ASSERT_TRUE (richloc.seen_impossible_fixit_p ()); + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + ASSERT_FALSE (edit.valid_p ()); + ASSERT_EQ (NULL, edit.get_content (filename)); + ASSERT_EQ (NULL, edit.generate_diff (false)); +} + +/* Test applying an "insert" fixit that adds a newline. */ + +static void +test_applying_fixits_insert_containing_newline (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = (" case 'a':\n" /* line 1. */ + " x = a;\n" /* line 2. */ + " case 'b':\n" /* line 3. */ + " x = b;\n");/* line 4. */ + + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 3); + + /* Add a "break;" on a line by itself before line 3 i.e. before + column 1 of line 3. */ + location_t case_start = linemap_position_for_column (line_table, 5); + location_t case_finish = linemap_position_for_column (line_table, 13); + location_t case_loc = make_location (case_start, case_start, case_finish); + rich_location richloc (line_table, case_loc); + location_t line_start = linemap_position_for_column (line_table, 1); + richloc.add_fixit_insert_before (line_start, " break;\n"); + + if (case_finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + ASSERT_STREQ ((" case 'a':\n" + " x = a;\n" + " break;\n" + " case 'b':\n" + " x = b;\n"), + new_content); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ (("@@ -1,4 +1,5 @@\n" + " case 'a':\n" + " x = a;\n" + "+ break;\n" + " case 'b':\n" + " x = b;\n"), + diff); +} + +/* Test applying a "replace" fixit that grows the affected line. */ + +static void +test_applying_fixits_growing_replace (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 2); + + /* Replace "field" with "m_field". */ + location_t start = linemap_position_for_column (line_table, 11); + location_t finish = linemap_position_for_column (line_table, 15); + location_t field = make_location (start, start, finish); + rich_location richloc (line_table, field); + richloc.add_fixit_replace ("m_field"); + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + if (finish <= LINE_MAP_MAX_LOCATION_WITH_COLS) + { + ASSERT_STREQ ("/* before */\n" + "foo = bar.m_field;\n" + "/* after */\n", new_content); + + /* Verify location of ";" after the change. */ + ASSERT_EQ (18, edit.get_effective_column (filename, 2, 16)); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,3 +1,3 @@\n" + " /* before */\n" + "-foo = bar.field;\n" + "+foo = bar.m_field;\n" + " /* after */\n", diff); + } +} + +/* Test applying a "replace" fixit that shrinks the affected line. */ + +static void +test_applying_fixits_shrinking_replace (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................000000000111111111. + .........................123456789012345678. */ + const char *old_content = ("/* before */\n" + "foo = bar.m_field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 2); + + /* Replace "field" with "m_field". */ + location_t start = linemap_position_for_column (line_table, 11); + location_t finish = linemap_position_for_column (line_table, 17); + location_t m_field = make_location (start, start, finish); + rich_location richloc (line_table, m_field); + richloc.add_fixit_replace ("field"); + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + if (finish <= LINE_MAP_MAX_LOCATION_WITH_COLS) + { + ASSERT_STREQ ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n", new_content); + + /* Verify location of ";" after the change. */ + ASSERT_EQ (16, edit.get_effective_column (filename, 2, 18)); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,3 +1,3 @@\n" + " /* before */\n" + "-foo = bar.m_field;\n" + "+foo = bar.field;\n" + " /* after */\n", diff); + } +} + +/* Replacement fix-it hint containing a newline. */ + +static void +test_applying_fixits_replace_containing_newline (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111. + .........................1234567890123. */ + const char *old_content = "foo = bar ();\n"; + + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 1); + + /* Replace the " = " with "\n = ", as if we were reformatting an + overly long line. */ + location_t start = linemap_position_for_column (line_table, 4); + location_t finish = linemap_position_for_column (line_table, 6); + location_t loc = linemap_position_for_column (line_table, 13); + rich_location richloc (line_table, loc); + source_range range = source_range::from_locations (start, finish); + richloc.add_fixit_replace (range, "\n = "); + + /* Newlines are only supported within fix-it hints that + are at the start of lines (for entirely new lines), hence + this fix-it should not be displayed. */ + ASSERT_TRUE (richloc.seen_impossible_fixit_p ()); + + if (finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + //ASSERT_STREQ ("foo\n = bar ();\n", new_content); +} + +/* Test applying a "remove" fixit. */ + +static void +test_applying_fixits_remove (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................000000000111111111. + .........................123456789012345678. */ + const char *old_content = ("/* before */\n" + "foo = bar.m_field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 2); + + /* Remove ".m_field". */ + location_t start = linemap_position_for_column (line_table, 10); + location_t finish = linemap_position_for_column (line_table, 17); + rich_location richloc (line_table, start); + source_range range; + range.m_start = start; + range.m_finish = finish; + richloc.add_fixit_remove (range); + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (filename); + if (finish <= LINE_MAP_MAX_LOCATION_WITH_COLS) + { + ASSERT_STREQ ("/* before */\n" + "foo = bar;\n" + "/* after */\n", new_content); + + /* Verify location of ";" after the change. */ + ASSERT_EQ (10, edit.get_effective_column (filename, 2, 18)); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,3 +1,3 @@\n" + " /* before */\n" + "-foo = bar.m_field;\n" + "+foo = bar;\n" + " /* after */\n", diff); + } +} + +/* Test applying multiple fixits to one line. */ + +static void +test_applying_fixits_multiple (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................00000000011111111. + .........................12345678901234567. */ + const char *old_content = ("/* before */\n" + "foo = bar.field;\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 2); + + location_t c7 = linemap_position_for_column (line_table, 7); + location_t c9 = linemap_position_for_column (line_table, 9); + location_t c11 = linemap_position_for_column (line_table, 11); + location_t c15 = linemap_position_for_column (line_table, 15); + location_t c17 = linemap_position_for_column (line_table, 17); + + if (c17 > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Add a comment in front of "bar.field". */ + rich_location insert_a (line_table, c7); + insert_a.add_fixit_insert_before (c7, "/* alpha */"); + + /* Add a comment after "bar.field;". */ + rich_location insert_b (line_table, c17); + insert_b.add_fixit_insert_before (c17, "/* beta */"); + + /* Replace "bar" with "pub". */ + rich_location replace_a (line_table, c7); + replace_a.add_fixit_replace (source_range::from_locations (c7, c9), + "pub"); + + /* Replace "field" with "meadow". */ + rich_location replace_b (line_table, c7); + replace_b.add_fixit_replace (source_range::from_locations (c11, c15), + "meadow"); + + file_cache fc; + change_set edit (fc); + edit.add_fixits (&insert_a); + ASSERT_EQ (100, edit.get_effective_column (filename, 1, 100)); + ASSERT_EQ (1, edit.get_effective_column (filename, 2, 1)); + ASSERT_EQ (6, edit.get_effective_column (filename, 2, 6)); + ASSERT_EQ (18, edit.get_effective_column (filename, 2, 7)); + ASSERT_EQ (27, edit.get_effective_column (filename, 2, 16)); + ASSERT_EQ (100, edit.get_effective_column (filename, 3, 100)); + + edit.add_fixits (&insert_b); + edit.add_fixits (&replace_a); + edit.add_fixits (&replace_b); + + if (c17 <= LINE_MAP_MAX_LOCATION_WITH_COLS) + { + auto_free <char *> new_content = edit.get_content (tmp.get_filename ()); + ASSERT_STREQ ("/* before */\n" + "foo = /* alpha */pub.meadow;/* beta */\n" + "/* after */\n", + new_content); + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,3 +1,3 @@\n" + " /* before */\n" + "-foo = bar.field;\n" + "+foo = /* alpha */pub.meadow;/* beta */\n" + " /* after */\n", diff); + } +} + +/* Subroutine of test_applying_fixits_multiple_lines. + Add the text "CHANGED: " to the front of the given line. */ + +static location_t +change_line (change_set &edit, int line_num) +{ + const line_map_ordinary *ord_map + = LINEMAPS_LAST_ORDINARY_MAP (line_table); + const int column = 1; + location_t loc = + linemap_position_for_line_and_column (line_table, ord_map, + line_num, column); + + expanded_location exploc = expand_location (loc); + if (loc <= LINE_MAP_MAX_LOCATION_WITH_COLS) + { + ASSERT_EQ (line_num, exploc.line); + ASSERT_EQ (column, exploc.column); + } + + rich_location insert (line_table, loc); + insert.add_fixit_insert_before ("CHANGED: "); + edit.add_fixits (&insert); + return loc; +} + +/* Subroutine of test_applying_fixits_multiple_lines. + Add the text "INSERTED\n" in front of the given line. */ + +static location_t +insert_line (change_set &edit, int line_num) +{ + const line_map_ordinary *ord_map + = LINEMAPS_LAST_ORDINARY_MAP (line_table); + const int column = 1; + location_t loc = + linemap_position_for_line_and_column (line_table, ord_map, + line_num, column); + + expanded_location exploc = expand_location (loc); + if (loc <= LINE_MAP_MAX_LOCATION_WITH_COLS) + { + ASSERT_EQ (line_num, exploc.line); + ASSERT_EQ (column, exploc.column); + } + + rich_location insert (line_table, loc); + insert.add_fixit_insert_before ("INSERTED\n"); + edit.add_fixits (&insert); + return loc; +} + +/* Test of editing multiple lines within a long file, + to ensure that diffs are generated as expected. */ + +static void +test_applying_fixits_multiple_lines (const line_table_case &case_) +{ + /* Create a tempfile and write many lines of text to it. */ + named_temp_file tmp (".txt"); + const char *filename = tmp.get_filename (); + FILE *f = fopen (filename, "w"); + ASSERT_NE (f, NULL); + for (int i = 1; i <= 1000; i++) + fprintf (f, "line %i\n", i); + fclose (f); + + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 1); + linemap_position_for_column (line_table, 127); + + file_cache fc; + change_set edit (fc); + + /* A run of consecutive lines. */ + change_line (edit, 2); + change_line (edit, 3); + change_line (edit, 4); + insert_line (edit, 5); + + /* A run of nearby lines, within the contextual limit. */ + change_line (edit, 150); + change_line (edit, 151); + location_t last_loc = change_line (edit, 153); + + if (last_loc > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Verify diff. */ + auto_free <char *> diff = edit.generate_diff (false); + ASSERT_STREQ ("@@ -1,7 +1,8 @@\n" + " line 1\n" + "-line 2\n" + "-line 3\n" + "-line 4\n" + "+CHANGED: line 2\n" + "+CHANGED: line 3\n" + "+CHANGED: line 4\n" + "+INSERTED\n" + " line 5\n" + " line 6\n" + " line 7\n" + "@@ -147,10 +148,10 @@\n" + " line 147\n" + " line 148\n" + " line 149\n" + "-line 150\n" + "-line 151\n" + "+CHANGED: line 150\n" + "+CHANGED: line 151\n" + " line 152\n" + "-line 153\n" + "+CHANGED: line 153\n" + " line 154\n" + " line 155\n" + " line 156\n", diff); + + /* Ensure tmp stays alive until this point, so that the tempfile + persists until after the generate_diff call. */ + tmp.get_filename (); +} + +/* Test of converting an initializer for a named field from + the old GCC extension to C99 syntax. + Exercises a shrinking replacement followed by a growing + replacement on the same line. */ + +static void +test_applying_fixits_modernize_named_init (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................00000000011111111. + .........................12345678901234567. */ + const char *old_content = ("/* before */\n" + "bar : 1,\n" + "/* after */\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 2); + + location_t c1 = linemap_position_for_column (line_table, 1); + location_t c3 = linemap_position_for_column (line_table, 3); + location_t c8 = linemap_position_for_column (line_table, 8); + + if (c8 > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Replace "bar" with ".". */ + rich_location r1 (line_table, c8); + r1.add_fixit_replace (source_range::from_locations (c1, c3), + "."); + + /* Replace ":" with "bar =". */ + rich_location r2 (line_table, c8); + r2.add_fixit_replace (source_range::from_locations (c8, c8), + "bar ="); + + /* The order should not matter. Do r1 then r2. */ + { + file_cache fc; + change_set edit (fc); + edit.add_fixits (&r1); + + /* Verify state after first replacement. */ + { + auto_free <char *> new_content = edit.get_content (tmp.get_filename ()); + /* We should now have: + ............00000000011. + ............12345678901. */ + ASSERT_STREQ ("/* before */\n" + ". : 1,\n" + "/* after */\n", + new_content); + /* Location of the "1". */ + ASSERT_EQ (6, edit.get_effective_column (filename, 2, 8)); + /* Location of the ",". */ + ASSERT_EQ (9, edit.get_effective_column (filename, 2, 11)); + } + + edit.add_fixits (&r2); + + auto_free <char *> new_content = edit.get_content (tmp.get_filename ()); + /* Verify state after second replacement. + ............00000000011111111. + ............12345678901234567. */ + ASSERT_STREQ ("/* before */\n" + ". bar = 1,\n" + "/* after */\n", + new_content); + } + + /* Try again, doing r2 then r1; the new_content should be the same. */ + { + file_cache fc; + change_set edit (fc); + edit.add_fixits (&r2); + edit.add_fixits (&r1); + auto_free <char *> new_content = edit.get_content (tmp.get_filename ()); + /*.............00000000011111111. + .............12345678901234567. */ + ASSERT_STREQ ("/* before */\n" + ". bar = 1,\n" + "/* after */\n", + new_content); + } +} + +/* Test of a fixit affecting a file that can't be read. */ + +static void +test_applying_fixits_unreadable_file () +{ + const char *filename = "this-does-not-exist.txt"; + line_table_test ltt; + linemap_add (line_table, LC_ENTER, false, filename, 1); + + location_t loc = linemap_position_for_column (line_table, 1); + + rich_location insert (line_table, loc); + insert.add_fixit_insert_before ("change 1"); + insert.add_fixit_insert_before ("change 2"); + + file_cache fc; + change_set edit (fc); + /* Attempting to add the fixits affecting the unreadable file + should transition the edit from valid to invalid. */ + ASSERT_TRUE (edit.valid_p ()); + edit.add_fixits (&insert); + ASSERT_FALSE (edit.valid_p ()); + ASSERT_EQ (NULL, edit.get_content (filename)); + ASSERT_EQ (NULL, edit.generate_diff (false)); +} + +/* Verify that we gracefully handle an attempt to edit a line + that's beyond the end of the file. */ + +static void +test_applying_fixits_line_out_of_range () +{ + /* Create a tempfile and write some text to it. + ........................00000000011111111. + ........................12345678901234567. */ + const char *old_content = "One-liner file\n"; + temp_source_file tmp (SELFTEST_LOCATION, ".txt", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt; + linemap_add (line_table, LC_ENTER, false, filename, 2); + + /* Try to insert a string in line 2. */ + location_t loc = linemap_position_for_column (line_table, 1); + + rich_location insert (line_table, loc); + insert.add_fixit_insert_before ("change"); + + /* Verify that attempting the insertion puts an change_set + into an invalid state. */ + file_cache fc; + change_set edit (fc); + ASSERT_TRUE (edit.valid_p ()); + edit.add_fixits (&insert); + ASSERT_FALSE (edit.valid_p ()); + ASSERT_EQ (NULL, edit.get_content (filename)); + ASSERT_EQ (NULL, edit.generate_diff (false)); +} + +/* Verify the boundary conditions of column values in fix-it + hints applied to change_set instances. */ + +static void +test_applying_fixits_column_validation (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + ........................00000000011111111. + ........................12345678901234567. */ + const char *old_content = "One-liner file\n"; + temp_source_file tmp (SELFTEST_LOCATION, ".txt", old_content); + const char *filename = tmp.get_filename (); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, filename, 1); + + location_t c11 = linemap_position_for_column (line_table, 11); + location_t c14 = linemap_position_for_column (line_table, 14); + location_t c15 = linemap_position_for_column (line_table, 15); + location_t c16 = linemap_position_for_column (line_table, 16); + + /* Verify limits of valid columns in insertion fixits. */ + + /* Verify inserting at the end of the line. */ + { + rich_location richloc (line_table, c11); + richloc.add_fixit_insert_before (c15, " change"); + + /* Col 15 is at the end of the line, so the insertion + should succeed. */ + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (tmp.get_filename ()); + if (c15 <= LINE_MAP_MAX_LOCATION_WITH_COLS) + ASSERT_STREQ ("One-liner file change\n", new_content); + else + ASSERT_EQ (NULL, new_content); + } + + /* Verify inserting beyond the end of the line. */ + { + rich_location richloc (line_table, c11); + richloc.add_fixit_insert_before (c16, " change"); + + /* Col 16 is beyond the end of the line, so the insertion + should fail gracefully. */ + file_cache fc; + change_set edit (fc); + ASSERT_TRUE (edit.valid_p ()); + edit.add_fixits (&richloc); + ASSERT_FALSE (edit.valid_p ()); + ASSERT_EQ (NULL, edit.get_content (filename)); + ASSERT_EQ (NULL, edit.generate_diff (false)); + } + + /* Verify limits of valid columns in replacement fixits. */ + + /* Verify replacing the end of the line. */ + { + rich_location richloc (line_table, c11); + source_range range = source_range::from_locations (c11, c14); + richloc.add_fixit_replace (range, "change"); + + /* Col 14 is at the end of the line, so the replacement + should succeed. */ + file_cache fc; + change_set edit (fc); + edit.add_fixits (&richloc); + auto_free <char *> new_content = edit.get_content (tmp.get_filename ()); + if (c14 <= LINE_MAP_MAX_LOCATION_WITH_COLS) + ASSERT_STREQ ("One-liner change\n", new_content); + else + ASSERT_EQ (NULL, new_content); + } + + /* Verify going beyond the end of the line. */ + { + rich_location richloc (line_table, c11); + source_range range = source_range::from_locations (c11, c15); + richloc.add_fixit_replace (range, "change"); + + /* Col 15 is after the end of the line, so the replacement + should fail; verify that the attempt fails gracefully. */ + file_cache fc; + change_set edit (fc); + ASSERT_TRUE (edit.valid_p ()); + edit.add_fixits (&richloc); + ASSERT_FALSE (edit.valid_p ()); + ASSERT_EQ (NULL, edit.get_content (filename)); + ASSERT_EQ (NULL, edit.generate_diff (false)); + } +} + +static void +run_all_tests () +{ + test_get_content (); + for_each_line_table_case (test_applying_fixits_insert_before); + for_each_line_table_case (test_applying_fixits_insert_after); + for_each_line_table_case (test_applying_fixits_insert_after_at_line_end); + for_each_line_table_case (test_applying_fixits_insert_after_failure); + for_each_line_table_case (test_applying_fixits_insert_containing_newline); + for_each_line_table_case (test_applying_fixits_growing_replace); + for_each_line_table_case (test_applying_fixits_shrinking_replace); + for_each_line_table_case (test_applying_fixits_replace_containing_newline); + for_each_line_table_case (test_applying_fixits_remove); + for_each_line_table_case (test_applying_fixits_multiple); + for_each_line_table_case (test_applying_fixits_multiple_lines); + for_each_line_table_case (test_applying_fixits_modernize_named_init); + test_applying_fixits_unreadable_file (); + test_applying_fixits_line_out_of_range (); + for_each_line_table_case (test_applying_fixits_column_validation); +} + +} // namespace diagnostics::changes::selftest + +#endif /* CHECKING_P */ + +} // namespace diagnostics::changes + +#if CHECKING_P + +namespace selftest { // diagnostics::selftest + +/* Run all of the selftests within this file. */ + +void +changes_cc_tests () +{ + diagnostics::changes::selftest::run_all_tests (); +} + +} // namespace selftest + +#endif /* CHECKING_P */ + +} // namespace diagnostics diff --git a/gcc/diagnostics/changes.h b/gcc/diagnostics/changes.h new file mode 100644 index 0000000..a5fe4b0 --- /dev/null +++ b/gcc/diagnostics/changes.h @@ -0,0 +1,78 @@ +/* Determining the results of applying fix-it hints. + Copyright (C) 2016-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_DIAGNOSTICS_CHANGES_H +#define GCC_DIAGNOSTICS_CHANGES_H + +#include "typed-splay-tree.h" + +class fixit_hint; + +namespace diagnostics { +namespace changes { + +class change_set; +class changed_file; + +/* A set of changes to the source code. + + The changes are "atomic" - if any changes can't be applied, + none of them can be (tracked by the m_valid flag). + Similarly, attempts to add the changes from a rich_location flagged + as containing invalid changes mean that the whole of the change_set + is flagged as invalid. + + A complication here is that fix-its are expressed relative to coordinates + in the files when they were parsed, before any changes have been made, and + so if there's more that one fix-it to be applied, we have to adjust + later fix-its to allow for the changes made by earlier ones. This + is done by the various "get_effective_column" methods. */ + +class change_set +{ + public: + change_set (file_cache &); + + bool valid_p () const { return m_valid; } + + void add_fixits (rich_location *richloc); + + char *get_content (const char *filename); + + int get_effective_column (const char *filename, int line, int column); + + char *generate_diff (bool show_filenames); + void print_diff (pretty_printer *pp, bool show_filenames); + + file_cache &get_file_cache () const { return m_file_cache; } + + private: + bool apply_fixit (const fixit_hint *hint); + changed_file *get_file (const char *filename); + changed_file &get_or_insert_file (const char *filename); + + file_cache &m_file_cache; + bool m_valid; + typed_splay_tree<const char *, changed_file *> m_files; +}; + +} // namespace diagnostics::changes +} // namespace diagnostics + +#endif /* GCC_DIAGNOSTICS_CHANGES_H. */ diff --git a/gcc/diagnostics/client-data-hooks.h b/gcc/diagnostics/client-data-hooks.h new file mode 100644 index 0000000..94c51b2 --- /dev/null +++ b/gcc/diagnostics/client-data-hooks.h @@ -0,0 +1,125 @@ +/* Additional metadata about a client for a diagnostic context. + Copyright (C) 2022-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_CLIENT_DATA_HOOKS_H +#define GCC_DIAGNOSTICS_CLIENT_DATA_HOOKS_H + +#include "diagnostics/logical-locations.h" + +namespace diagnostics { + +class sarif_object; +class client_version_info; + +/* A bundle of additional metadata, owned by the diagnostics::context, + for querying things about the client, like version data. */ + +class client_data_hooks +{ + public: + virtual ~client_data_hooks () {} + + /* Get version info for this client, or NULL. */ + virtual const client_version_info *get_any_version_info () const = 0; + + /* Get the current logical_locations::manager for this client, or null. */ + virtual const logical_locations::manager * + get_logical_location_manager () const = 0; + + /* Get the current logical location, or null. + If this returns a non-null logical location, then + get_logical_location_manager must return non-null. */ + virtual logical_locations::key + get_current_logical_location () const = 0; + + /* Get a sourceLanguage value for FILENAME, or return NULL. + See SARIF v2.1.0 Appendix J for suggested values. */ + virtual const char * + maybe_get_sarif_source_language (const char *filename) const = 0; + + /* Hook to allow client to populate a SARIF "invocation" object with + a custom property bag (see SARIF v2.1.0 section 3.8). */ + virtual void + add_sarif_invocation_properties (sarif_object &invocation_obj) const = 0; +}; + +class client_plugin_info; + +/* Abstract base class for a diagnostics::context to get at + version information about the client. */ + +class client_version_info +{ +public: + class plugin_visitor + { + public: + virtual void on_plugin (const client_plugin_info &) = 0; + }; + + virtual ~client_version_info () {} + + /* Get a string suitable for use as the value of the "name" property + (SARIF v2.1.0 section 3.19.8). */ + virtual const char *get_tool_name () const = 0; + + /* Create a string suitable for use as the value of the "fullName" property + (SARIF v2.1.0 section 3.19.9). */ + virtual char *maybe_make_full_name () const = 0; + + /* Get a string suitable for use as the value of the "version" property + (SARIF v2.1.0 section 3.19.13). */ + virtual const char *get_version_string () const = 0; + + /* Create a string suitable for use as the value of the "informationUri" + property (SARIF v2.1.0 section 3.19.17). */ + virtual char *maybe_make_version_url () const = 0; + + virtual void for_each_plugin (plugin_visitor &v) const = 0; +}; + +/* Abstract base class for a diagnostics::context to get at + information about a specific plugin within a client. */ + +class client_plugin_info +{ +public: + /* For use e.g. by SARIF "name" property (SARIF v2.1.0 section 3.19.8). */ + virtual const char *get_short_name () const = 0; + + /* For use e.g. by SARIF "fullName" property + (SARIF v2.1.0 section 3.19.9). */ + virtual const char *get_full_name () const = 0; + + /* For use e.g. by SARIF "version" property + (SARIF v2.1.0 section 3.19.13). */ + virtual const char *get_version () const = 0; +}; + +} // namespace diagnostics + +/* Factory function for making an instance of client_data_hooks + for use in the compiler (i.e. with knowledge of "tree", access to + langhooks, etc). */ + +extern std::unique_ptr<diagnostics::client_data_hooks> +make_compiler_data_hooks (); + +#endif /* ! GCC_DIAGNOSTICS_CLIENT_DATA_HOOKS_H */ diff --git a/gcc/diagnostics/color.cc b/gcc/diagnostics/color.cc new file mode 100644 index 0000000..7b499fe --- /dev/null +++ b/gcc/diagnostics/color.cc @@ -0,0 +1,536 @@ +/* Output colorization. + Copyright (C) 2011-2025 Free Software Foundation, Inc. + + This program 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. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA + 02110-1301, USA. */ + +#include "config.h" +#define INCLUDE_VECTOR +#include "system.h" +#include "diagnostics/color.h" +#include "diagnostics/url.h" +#include "label-text.h" + +#ifdef __MINGW32__ +# define WIN32_LEAN_AND_MEAN +# include <windows.h> +#endif + +#include "color-macros.h" +#include "selftest.h" + +/* The context and logic for choosing default --color screen attributes + (foreground and background colors, etc.) are the following. + -- There are eight basic colors available, each with its own + nominal luminosity to the human eye and foreground/background + codes (black [0 %, 30/40], blue [11 %, 34/44], red [30 %, 31/41], + magenta [41 %, 35/45], green [59 %, 32/42], cyan [70 %, 36/46], + yellow [89 %, 33/43], and white [100 %, 37/47]). + -- Sometimes, white as a background is actually implemented using + a shade of light gray, so that a foreground white can be visible + on top of it (but most often not). + -- Sometimes, black as a foreground is actually implemented using + a shade of dark gray, so that it can be visible on top of a + background black (but most often not). + -- Sometimes, more colors are available, as extensions. + -- Other attributes can be selected/deselected (bold [1/22], + underline [4/24], standout/inverse [7/27], blink [5/25], and + invisible/hidden [8/28]). They are sometimes implemented by + using colors instead of what their names imply; e.g., bold is + often achieved by using brighter colors. In practice, only bold + is really available to us, underline sometimes being mapped by + the terminal to some strange color choice, and standout best + being left for use by downstream programs such as less(1). + -- We cannot assume that any of the extensions or special features + are available for the purpose of choosing defaults for everyone. + -- The most prevalent default terminal backgrounds are pure black + and pure white, and are not necessarily the same shades of + those as if they were selected explicitly with SGR sequences. + Some terminals use dark or light pictures as default background, + but those are covered over by an explicit selection of background + color with an SGR sequence; their users will appreciate their + background pictures not be covered like this, if possible. + -- Some uses of colors attributes is to make some output items + more understated (e.g., context lines); this cannot be achieved + by changing the background color. + -- For these reasons, the GCC color defaults should strive not + to change the background color from its default, unless it's + for a short item that should be highlighted, not understated. + -- The GCC foreground color defaults (without an explicitly set + background) should provide enough contrast to be readable on any + terminal with either a black (dark) or white (light) background. + This only leaves red, magenta, green, and cyan (and their bold + counterparts) and possibly bold blue. */ +/* Default colors. The user can overwrite them using environment + variable GCC_COLORS. */ +struct color_default +{ + const char *m_name; + const char *m_val; +}; + +/* For GCC_COLORS. */ +static const color_default gcc_color_defaults[] = +{ + { "error", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_RED) }, + { "warning", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_MAGENTA) }, + { "note", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_CYAN) }, + { "range1", SGR_SEQ (COLOR_FG_GREEN) }, + { "range2", SGR_SEQ (COLOR_FG_BLUE) }, + { "locus", SGR_SEQ (COLOR_BOLD) }, + { "quote", SGR_SEQ (COLOR_BOLD) }, + { "path", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_CYAN) }, + { "fnname", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) }, + { "targs", SGR_SEQ (COLOR_FG_MAGENTA) }, + { "fixit-insert", SGR_SEQ (COLOR_FG_GREEN) }, + { "fixit-delete", SGR_SEQ (COLOR_FG_RED) }, + { "diff-filename", SGR_SEQ (COLOR_BOLD) }, + { "diff-hunk", SGR_SEQ (COLOR_FG_CYAN) }, + { "diff-delete", SGR_SEQ (COLOR_FG_RED) }, + { "diff-insert", SGR_SEQ (COLOR_FG_GREEN) }, + { "type-diff", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) }, + { "valid", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) }, + { "invalid", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_RED) }, + { "highlight-a", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) }, + { "highlight-b", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_BLUE) } +}; + +class diagnostic_color_dict +{ +public: + diagnostic_color_dict (const color_default *default_values, + size_t num_default_values); + + bool parse_envvar_value (const char *const envvar_value); + + const char *get_start_by_name (const char *name, size_t name_len) const; + const char *get_start_by_name (const char *name) const + { + return get_start_by_name (name, strlen (name)); + } + +private: + struct entry + { + entry (const color_default &d) + : m_name (d.m_name), + m_name_len (strlen (d.m_name)), + m_val (label_text::borrow (d.m_val)) + { + } + + const char *m_name; + size_t m_name_len; + label_text m_val; + }; + + const entry *get_entry_by_name (const char *name, size_t name_len) const; + entry *get_entry_by_name (const char *name, size_t name_len); + + std::vector<entry> m_entries; +}; + +static diagnostic_color_dict *g_color_dict; + +const char * +colorize_start (bool show_color, const char *name, size_t name_len) +{ + if (!show_color) + return ""; + + if (!g_color_dict) + return ""; + + return g_color_dict->get_start_by_name (name, name_len); +} + +/* Look for an entry named NAME of length NAME_LEN within this + diagnostic_color_dict, or nullptr if there isn't one. */ + +const diagnostic_color_dict::entry * +diagnostic_color_dict::get_entry_by_name (const char *name, + size_t name_len) const +{ + for (auto &iter : m_entries) + if (iter.m_name_len == name_len + && memcmp (iter.m_name, name, name_len) == 0) + return &iter; + return nullptr; +} + +/* Non-const version of the above. */ + +diagnostic_color_dict::entry * +diagnostic_color_dict::get_entry_by_name (const char *name, + size_t name_len) +{ + for (auto &iter : m_entries) + if (iter.m_name_len == name_len + && memcmp (iter.m_name, name, name_len) == 0) + return &iter; + return nullptr; +} + +/* Return the SGR codes to start a color entry named NAME of length + NAME_LEN within this diagnostic_color_dict, or the empty string if + there isn't one. */ + +const char * +diagnostic_color_dict::get_start_by_name (const char *name, + size_t name_len) const +{ + if (const entry *e = get_entry_by_name (name, name_len)) + return e->m_val.get (); + + return ""; +} + +const char * +colorize_stop (bool show_color) +{ + return show_color ? SGR_RESET : ""; +} + +/* diagnostic_color_dict's ctor. Initialize it from the given array + of color_default values. */ + +diagnostic_color_dict:: +diagnostic_color_dict (const color_default *default_values, + size_t num_default_values) +{ + m_entries.reserve (num_default_values); + for (size_t idx = 0; idx < num_default_values; idx++) + m_entries.push_back (entry (default_values[idx])); +} + +/* Parse a list of color definitions from an environment variable + value (such as that of GCC_COLORS). + No character escaping is needed or supported. */ + +bool +diagnostic_color_dict::parse_envvar_value (const char *const envvar_value) +{ + /* envvar not set: use the default colors. */ + if (envvar_value == nullptr) + return true; + + /* envvar set to empty string: disable colorization. */ + if (*envvar_value == '\0') + return false; + + const char *q, *name, *val; + size_t name_len = 0, val_len = 0; + + name = q = envvar_value; + val = nullptr; + /* From now on, be well-formed or you're gone. */ + for (;;) + if (*q == ':' || *q == '\0') + { + if (val) + val_len = q - val; + else + name_len = q - name; + /* Empty name without val (empty cap) + won't match and will be ignored. */ + entry *e = get_entry_by_name (name, name_len); + /* If name unknown, go on for forward compatibility. */ + if (e && val) + { + char *b = XNEWVEC (char, val_len + sizeof (SGR_SEQ (""))); + memcpy (b, SGR_START, strlen (SGR_START)); + memcpy (b + strlen (SGR_START), val, val_len); + memcpy (b + strlen (SGR_START) + val_len, SGR_END, + sizeof (SGR_END)); + e->m_val = label_text::take (b); + } + if (*q == '\0') + return true; + name = ++q; + val = nullptr; + } + else if (*q == '=') + { + if (q == name || val) + return true; + + name_len = q - name; + val = ++q; /* Can be the empty string. */ + } + else if (val == nullptr) + q++; /* Accumulate name. */ + else if (*q == ';' || (*q >= '0' && *q <= '9')) + q++; /* Accumulate val. Protect the terminal from being sent + garbage. */ + else + return true; +} + +/* Parse GCC_COLORS. The default would look like: + GCC_COLORS='error=01;31:warning=01;35:note=01;36:\ + range1=32:range2=34:locus=01:quote=01:path=01;36:\ + fixit-insert=32:fixit-delete=31:'\ + diff-filename=01:diff-hunk=32:diff-delete=31:diff-insert=32:\ + type-diff=01;32'. */ +static bool +parse_gcc_colors () +{ + if (!g_color_dict) + return false; + return g_color_dict->parse_envvar_value (getenv ("GCC_COLORS")); /* Plural! */ +} + +/* Return true if we should use color when in auto mode, false otherwise. */ +static bool +should_colorize (void) +{ +#ifdef __MINGW32__ + /* For consistency reasons, one should check the handle returned by + _get_osfhandle(_fileno(stderr)) because the function + pp_write_text_to_stream() in pretty-print.cc calls fputs() on + that stream. However, the code below for non-Windows doesn't seem + to care about it either... */ + HANDLE handle; + DWORD mode; + BOOL isconsole = false; + + handle = GetStdHandle (STD_ERROR_HANDLE); + + if ((handle != INVALID_HANDLE_VALUE) && (handle != nullptr)) + isconsole = GetConsoleMode (handle, &mode); + +#ifdef ENABLE_VIRTUAL_TERMINAL_PROCESSING + if (isconsole) + { + /* Try to enable processing of VT100 escape sequences */ + mode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + SetConsoleMode (handle, mode); + } +#endif + + return isconsole; +#else + char const *t = getenv ("TERM"); + /* emacs M-x shell sets TERM="dumb". */ + return t && strcmp (t, "dumb") != 0 && isatty (STDERR_FILENO); +#endif +} + +bool +colorize_init (diagnostic_color_rule_t rule) +{ + if (!g_color_dict) + g_color_dict = new diagnostic_color_dict (gcc_color_defaults, + ARRAY_SIZE (gcc_color_defaults)); + + switch (rule) + { + case DIAGNOSTICS_COLOR_NO: + return false; + case DIAGNOSTICS_COLOR_YES: + return parse_gcc_colors (); + case DIAGNOSTICS_COLOR_AUTO: + if (should_colorize ()) + return parse_gcc_colors (); + else + return false; + default: + gcc_unreachable (); + } +} + +/* Return URL_FORMAT_XXX which tells how we should emit urls + when in always mode. + We use GCC_URLS and if that is not defined TERM_URLS. + If neither is defined the feature is enabled by default. */ + +static diagnostic_url_format +parse_env_vars_for_urls () +{ + const char *p; + + p = getenv ("GCC_URLS"); /* Plural! */ + if (p == nullptr) + p = getenv ("TERM_URLS"); + + if (p == nullptr) + return URL_FORMAT_DEFAULT; + + if (*p == '\0') + return URL_FORMAT_NONE; + + if (!strcmp (p, "no")) + return URL_FORMAT_NONE; + + if (!strcmp (p, "st")) + return URL_FORMAT_ST; + + if (!strcmp (p, "bel")) + return URL_FORMAT_BEL; + + return URL_FORMAT_DEFAULT; +} + +/* Return true if we should use urls when in auto mode, false otherwise. */ + +static bool +auto_enable_urls () +{ + const char *term, *colorterm; + + /* First check the terminal is capable of printing color escapes, + if not URLs won't work either. */ + if (!should_colorize ()) + return false; + +#ifdef __MINGW32__ + HANDLE handle; + DWORD mode; + + handle = GetStdHandle (STD_ERROR_HANDLE); + if ((handle == INVALID_HANDLE_VALUE) || (handle == nullptr)) + return false; + + /* If ansi escape sequences aren't supported by the console, then URLs will + print mangled from mingw_ansi_fputs's console API translation. It wouldn't + be useful even if this weren't the case. */ + if (GetConsoleMode (handle, &mode) +#ifdef ENABLE_VIRTUAL_TERMINAL_PROCESSING + && !(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) +#endif + ) + return false; +#endif + + /* xfce4-terminal is known to not implement URLs at this time. + Recently new installations (0.8) will safely ignore the URL escape + sequences, but a large number of legacy installations (0.6.3) print + garbage when URLs are printed. Therefore we lose nothing by + disabling this feature for that specific terminal type. */ + colorterm = getenv ("COLORTERM"); + if (colorterm && !strcmp (colorterm, "xfce4-terminal")) + return false; + + /* Old versions of gnome-terminal where URL escapes cause screen + corruptions set COLORTERM="gnome-terminal", recent versions + with working URL support set this to "truecolor". */ + if (colorterm && !strcmp (colorterm, "gnome-terminal")) + return false; + + /* Since the following checks are less specific than the ones + above, let GCC_URLS and TERM_URLS override the decision. */ + if (getenv ("GCC_URLS") || getenv ("TERM_URLS")) + return true; + + /* In an ssh session the COLORTERM is not there, but TERM=xterm + can be used as an indication of a incompatible terminal while + TERM=xterm-256color appears to be a working terminal. */ + term = getenv ("TERM"); + if (!colorterm && term && !strcmp (term, "xterm")) + return false; + + /* When logging in a linux over serial line, we see TERM=linux + and no COLORTERM, it is unlikely that the URL escapes will + work in that environmen either. */ + if (!colorterm && term && !strcmp (term, "linux")) + return false; + + return true; +} + +/* Determine if URLs should be enabled, based on RULE, + and, if so, which format to use. + This reuses the logic for colorization. */ + +diagnostic_url_format +determine_url_format (diagnostic_url_rule_t rule) +{ + switch (rule) + { + case DIAGNOSTICS_URL_NO: + return URL_FORMAT_NONE; + case DIAGNOSTICS_URL_YES: + return parse_env_vars_for_urls (); + case DIAGNOSTICS_URL_AUTO: + if (auto_enable_urls ()) + return parse_env_vars_for_urls (); + else + return URL_FORMAT_NONE; + default: + gcc_unreachable (); + } +} + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +/* Test of an empty diagnostic_color_dict. */ + +static void +test_empty_color_dict () +{ + diagnostic_color_dict d (nullptr, 0); + ASSERT_STREQ (d.get_start_by_name ("warning"), ""); + ASSERT_STREQ (d.get_start_by_name ("should-not-be-found"), ""); +} + +/* Test of a diagnostic_color_dict with GCC's defaults. */ + +static void +test_default_color_dict () +{ + diagnostic_color_dict d (gcc_color_defaults, + ARRAY_SIZE (gcc_color_defaults)); + ASSERT_STREQ (d.get_start_by_name ("warning"), + SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_MAGENTA)); + ASSERT_STREQ (d.get_start_by_name ("should-not-be-found"), ""); +} + +/* Test of a diagnostic_color_dict with GCC's defaults plus overrides from + an environment variable. */ + +static void +test_color_dict_envvar_parsing () +{ + diagnostic_color_dict d (gcc_color_defaults, + ARRAY_SIZE (gcc_color_defaults)); + + d.parse_envvar_value ("error=01;37:warning=01;42:unknown-value=01;36"); + + ASSERT_STREQ (d.get_start_by_name ("error"), + SGR_SEQ ("01;37")); + ASSERT_STREQ (d.get_start_by_name ("warning"), + SGR_SEQ ("01;42")); + ASSERT_STREQ (d.get_start_by_name ("unknown-value"), ""); + ASSERT_STREQ (d.get_start_by_name ("should-not-be-found"), ""); +} + + +/* Run all of the selftests within this file. */ + +void +color_cc_tests () +{ + test_empty_color_dict (); + test_default_color_dict (); + test_color_dict_envvar_parsing (); +} + +} // namespace selftest +} // namespace diagnostics + +#endif /* #if CHECKING_P */ diff --git a/gcc/diagnostics/color.h b/gcc/diagnostics/color.h new file mode 100644 index 0000000..42b67eb --- /dev/null +++ b/gcc/diagnostics/color.h @@ -0,0 +1,65 @@ +/* Copyright (C) 2013-2025 Free Software Foundation, Inc. + Contributed by Manuel Lopez-Ibanez <manu@gcc.gnu.org> + +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/>. */ + +/* Based on code from: */ +/* grep.c - main driver file for grep. + Copyright (C) 1992-2025 Free Software Foundation, Inc. + + This program 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. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA + 02110-1301, USA. + + Written July 1992 by Mike Haertel. */ + +#ifndef GCC_DIAGNOSTICS_COLOR_H +#define GCC_DIAGNOSTICS_COLOR_H + +/* Whether to add color to diagnostics: + o DIAGNOSTICS_COLOR_NO: never + o DIAGNOSTICS_COLOR_YES: always + o DIAGNOSTICS_COLOR_AUTO: depending on the output stream. */ +typedef enum +{ + DIAGNOSTICS_COLOR_NO = 0, + DIAGNOSTICS_COLOR_YES = 1, + DIAGNOSTICS_COLOR_AUTO = 2 +} diagnostic_color_rule_t; + +const char *colorize_start (bool, const char *, size_t); +const char *colorize_stop (bool); +bool colorize_init (diagnostic_color_rule_t); + +inline const char * +colorize_start (bool show_color, const char *name) +{ + return colorize_start (show_color, name, strlen (name)); +} + +#endif /* ! GCC_DIAGNOSTICS_COLOR_H */ diff --git a/gcc/diagnostics/context-options.h b/gcc/diagnostics/context-options.h new file mode 100644 index 0000000..c4e92eb --- /dev/null +++ b/gcc/diagnostics/context-options.h @@ -0,0 +1,116 @@ +/* Declare enums for diagnostics::context and related types. + Copyright (C) 2000-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_DIAGNOSTICS_CONTEXT_OPTIONS_H +#define GCC_DIAGNOSTICS_CONTEXT_OPTIONS_H + +/* An enum for controlling what units to use for the column number + when diagnostics are output, used by the -fdiagnostics-column-unit option. + Tabs will be expanded or not according to the value of -ftabstop. The origin + (default 1) is controlled by -fdiagnostics-column-origin. */ + +enum diagnostics_column_unit +{ + /* The default from GCC 11 onwards: display columns. */ + DIAGNOSTICS_COLUMN_UNIT_DISPLAY, + + /* The behavior in GCC 10 and earlier: simple bytes. */ + DIAGNOSTICS_COLUMN_UNIT_BYTE +}; + +/* An enum for controlling how to print non-ASCII characters/bytes when + a diagnostic suggests escaping the source code on output. */ + +enum diagnostics_escape_format +{ + /* Escape non-ASCII Unicode characters in the form <U+XXXX> and + non-UTF-8 bytes in the form <XX>. */ + DIAGNOSTICS_ESCAPE_FORMAT_UNICODE, + + /* Escape non-ASCII bytes in the form <XX> (thus showing the underlying + encoding of non-ASCII Unicode characters). */ + DIAGNOSTICS_ESCAPE_FORMAT_BYTES +}; + +/* Enum for overriding the standard output format. */ + +enum diagnostics_output_format +{ + /* The default: textual output. */ + DIAGNOSTICS_OUTPUT_FORMAT_TEXT, + + /* SARIF-based output, as JSON to stderr. */ + DIAGNOSTICS_OUTPUT_FORMAT_SARIF_STDERR, + + /* SARIF-based output, to a JSON file. */ + DIAGNOSTICS_OUTPUT_FORMAT_SARIF_FILE +}; + +/* An enum for controlling how diagnostic paths should be printed. */ +enum diagnostic_path_format +{ + /* Don't print diagnostic paths. */ + DPF_NONE, + + /* Print diagnostic paths by emitting a separate "note" for every event + in the path. */ + DPF_SEPARATE_EVENTS, + + /* Print diagnostic paths by consolidating events together where they + are close enough, and printing such runs of events with multiple + calls to diagnostic_show_locus, showing the individual events in + each run via labels in the source. */ + DPF_INLINE_EVENTS +}; + +/* An enum for capturing values of GCC_EXTRA_DIAGNOSTIC_OUTPUT, + and for -fdiagnostics-parseable-fixits. */ + +enum diagnostics_extra_output_kind +{ + /* No extra output, or an unrecognized value. */ + EXTRA_DIAGNOSTIC_OUTPUT_none, + + /* Emit fix-it hints using the "fixits-v1" format, equivalent to + -fdiagnostics-parseable-fixits. */ + EXTRA_DIAGNOSTIC_OUTPUT_fixits_v1, + + /* Emit fix-it hints using the "fixits-v2" format. */ + EXTRA_DIAGNOSTIC_OUTPUT_fixits_v2 +}; + +/* Values for -fdiagnostics-text-art-charset=. */ + +enum diagnostic_text_art_charset +{ + /* No text art diagrams shall be emitted. */ + DIAGNOSTICS_TEXT_ART_CHARSET_NONE, + + /* Use pure ASCII for text art diagrams. */ + DIAGNOSTICS_TEXT_ART_CHARSET_ASCII, + + /* Use ASCII + conservative use of other unicode characters + in text art diagrams. */ + DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE, + + /* Use Emoji. */ + DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI +}; + +#endif /* ! GCC_DIAGNOSTICS_CONTEXT_OPTIONS_H */ diff --git a/gcc/diagnostics/context.cc b/gcc/diagnostics/context.cc new file mode 100644 index 0000000..85f7d2a --- /dev/null +++ b/gcc/diagnostics/context.cc @@ -0,0 +1,2152 @@ +/* Language-independent diagnostic subroutines for the GNU Compiler Collection + Copyright (C) 1999-2025 Free Software Foundation, Inc. + Contributed by Gabriel Dos Reis <gdr@codesourcery.com> + +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 language independent aspect of diagnostic + message module. */ + +#include "config.h" +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "version.h" +#include "demangle.h" +#include "intl.h" +#include "backtrace.h" +#include "diagnostic.h" +#include "diagnostics/color.h" +#include "diagnostics/url.h" +#include "diagnostics/metadata.h" +#include "diagnostics/paths.h" +#include "diagnostics/client-data-hooks.h" +#include "diagnostics/diagram.h" +#include "diagnostics/sink.h" +#include "diagnostics/sarif-sink.h" +#include "diagnostics/text-sink.h" +#include "diagnostics/changes.h" +#include "selftest.h" +#include "diagnostics/selftest-context.h" +#include "opts.h" +#include "cpplib.h" +#include "text-art/theme.h" +#include "pretty-print-urlifier.h" +#include "diagnostics/logical-locations.h" +#include "diagnostics/buffering.h" +#include "diagnostics/file-cache.h" + +#ifdef HAVE_TERMIOS_H +# include <termios.h> +#endif + +#ifdef GWINSZ_IN_SYS_IOCTL +# include <sys/ioctl.h> +#endif + +/* Disable warnings about quoting issues in the pp_xxx calls below + that (intentionally) don't follow GCC diagnostic conventions. */ +#if __GNUC__ >= 10 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-diag" +#endif + +static void real_abort (void) ATTRIBUTE_NORETURN; + +/* Name of program invoked, sans directories. */ + +const char *progname; + + +/* Return a malloc'd string containing MSG formatted a la printf. The + caller is responsible for freeing the memory. */ +char * +build_message_string (const char *msg, ...) +{ + char *str; + va_list ap; + + va_start (ap, msg); + str = xvasprintf (msg, ap); + va_end (ap); + + return str; +} + + +/* Return the value of the getenv("COLUMNS") as an integer. If the + value is not set to a positive integer, use ioctl to get the + terminal width. If it fails, return INT_MAX. */ +int +get_terminal_width (void) +{ + const char * s = getenv ("COLUMNS"); + if (s != nullptr) { + int n = atoi (s); + if (n > 0) + return n; + } + +#ifdef TIOCGWINSZ + struct winsize w; + w.ws_col = 0; + if (ioctl (0, TIOCGWINSZ, &w) == 0 && w.ws_col > 0) + return w.ws_col; +#endif + + return INT_MAX; +} + +namespace diagnostics { + +/* Set caret_max_width to value. */ + +void +context::set_caret_max_width (int value) +{ + /* One minus to account for the leading empty space. */ + value = value ? value - 1 + : (isatty (fileno (pp_buffer (get_reference_printer ())->m_stream)) + ? get_terminal_width () - 1 : INT_MAX); + + if (value <= 0) + value = INT_MAX; + + m_source_printing.max_width = value; +} + +/* Initialize the diagnostic message outputting machinery. */ + +void +context::initialize (int n_opts) +{ + /* Allocate a basic pretty-printer. Clients will replace this a + much more elaborated pretty-printer if they wish. */ + m_reference_printer = std::make_unique<pretty_printer> ().release (); + + m_file_cache = new file_cache (); + m_diagnostic_counters.clear (); + m_warning_as_error_requested = false; + m_n_opts = n_opts; + m_option_classifier.init (n_opts); + m_source_printing.enabled = false; + set_caret_max_width (pp_line_cutoff (get_reference_printer ())); + for (int i = 0; i < rich_location::STATICALLY_ALLOCATED_RANGES; i++) + m_source_printing.caret_chars[i] = '^'; + m_show_cwe = false; + m_show_rules = false; + m_path_format = DPF_NONE; + m_show_path_depths = false; + m_show_option_requested = false; + m_abort_on_error = false; + m_show_column = false; + m_pedantic_errors = false; + m_permissive = false; + m_opt_permissive = 0; + m_fatal_errors = false; + m_inhibit_warnings = false; + m_warn_system_headers = false; + m_max_errors = 0; + m_internal_error = nullptr; + m_adjust_diagnostic_info = nullptr; + m_text_callbacks.m_begin_diagnostic = default_text_starter; + m_text_callbacks.m_text_start_span + = default_start_span_fn<to_text>; + m_text_callbacks.m_html_start_span + = default_start_span_fn<to_html>; + m_text_callbacks.m_end_diagnostic = default_text_finalizer; + m_option_mgr = nullptr; + m_urlifier_stack = new auto_vec<urlifier_stack_node> (); + m_last_location = UNKNOWN_LOCATION; + m_client_aux_data = nullptr; + m_lock = 0; + m_inhibit_notes_p = false; + m_source_printing.colorize_source_p = false; + m_source_printing.show_labels_p = false; + m_source_printing.show_line_numbers_p = false; + m_source_printing.min_margin_width = 0; + m_source_printing.show_ruler_p = false; + m_source_printing.show_event_links_p = false; + m_report_bug = false; + m_extra_output_kind = EXTRA_DIAGNOSTIC_OUTPUT_none; + if (const char *var = getenv ("GCC_EXTRA_DIAGNOSTIC_OUTPUT")) + { + if (!strcmp (var, "fixits-v1")) + m_extra_output_kind = EXTRA_DIAGNOSTIC_OUTPUT_fixits_v1; + else if (!strcmp (var, "fixits-v2")) + m_extra_output_kind = EXTRA_DIAGNOSTIC_OUTPUT_fixits_v2; + /* Silently ignore unrecognized values. */ + } + m_column_unit = DIAGNOSTICS_COLUMN_UNIT_DISPLAY; + m_column_origin = 1; + m_tabstop = 8; + m_escape_format = DIAGNOSTICS_ESCAPE_FORMAT_UNICODE; + m_fixits_change_set = nullptr; + m_diagnostic_groups.m_group_nesting_depth = 0; + m_diagnostic_groups.m_diagnostic_nesting_level = 0; + m_diagnostic_groups.m_emission_count = 0; + m_diagnostic_groups.m_inhibiting_notes_from = 0; + m_sinks.safe_push (new text_sink (*this, nullptr, true)); + m_set_locations_cb = nullptr; + m_client_data_hooks = nullptr; + m_diagrams.m_theme = nullptr; + m_original_argv = nullptr; + m_diagnostic_buffer = nullptr; + + enum diagnostic_text_art_charset text_art_charset + = DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI; + if (const char *lang = getenv ("LANG")) + { + /* For LANG=C, don't assume the terminal supports anything + other than ASCII. */ + if (!strcmp (lang, "C")) + text_art_charset = DIAGNOSTICS_TEXT_ART_CHARSET_ASCII; + } + set_text_art_charset (text_art_charset); +} + +/* Maybe initialize the color support. We require clients to do this + explicitly, since most clients don't want color. When called + without a VALUE, it initializes with DIAGNOSTICS_COLOR_DEFAULT. */ + +void +context::color_init (int value) +{ + /* value == -1 is the default value. */ + if (value < 0) + { + /* If DIAGNOSTICS_COLOR_DEFAULT is -1, default to + -fdiagnostics-color=auto if GCC_COLORS is in the environment, + otherwise default to -fdiagnostics-color=never, for other + values default to that + -fdiagnostics-color={never,auto,always}. */ + if (DIAGNOSTICS_COLOR_DEFAULT == -1) + { + if (!getenv ("GCC_COLORS")) + return; + value = DIAGNOSTICS_COLOR_AUTO; + } + else + value = DIAGNOSTICS_COLOR_DEFAULT; + } + pp_show_color (m_reference_printer) + = colorize_init ((diagnostic_color_rule_t) value); + for (auto sink_ : m_sinks) + if (sink_->follows_reference_printer_p ()) + pp_show_color (sink_->get_printer ()) + = pp_show_color (m_reference_printer); +} + +/* Initialize URL support within this context based on VALUE, + handling "auto". */ + +void +context::urls_init (int value) +{ + /* value == -1 is the default value. */ + if (value < 0) + { + /* If DIAGNOSTICS_URLS_DEFAULT is -1, default to + -fdiagnostics-urls=auto if GCC_URLS or TERM_URLS is in the + environment, otherwise default to -fdiagnostics-urls=never, + for other values default to that + -fdiagnostics-urls={never,auto,always}. */ + if (DIAGNOSTICS_URLS_DEFAULT == -1) + { + if (!getenv ("GCC_URLS") && !getenv ("TERM_URLS")) + return; + value = DIAGNOSTICS_URL_AUTO; + } + else + value = DIAGNOSTICS_URLS_DEFAULT; + } + + m_reference_printer->set_url_format + (determine_url_format ((diagnostic_url_rule_t) value)); + for (auto sink_ : m_sinks) + if (sink_->follows_reference_printer_p ()) + sink_->get_printer ()->set_url_format + (m_reference_printer->get_url_format ()); +} + +/* Create the file_cache, if not already created, and tell it how to + translate files on input. */ +void +context::initialize_input_context (diagnostic_input_charset_callback ccb, + bool should_skip_bom) +{ + m_file_cache->initialize_input_context (ccb, should_skip_bom); +} + +/* Do any cleaning up required after the last diagnostic is emitted. */ + +void +context::finish () +{ + /* We might be handling a fatal error. + Close any active diagnostic groups, which may trigger flushing + sinks. */ + while (m_diagnostic_groups.m_group_nesting_depth > 0) + end_group (); + + set_diagnostic_buffer (nullptr); + + /* Clean ups. */ + + while (!m_sinks.is_empty ()) + delete m_sinks.pop (); + + if (m_diagrams.m_theme) + { + delete m_diagrams.m_theme; + m_diagrams.m_theme = nullptr; + } + + delete m_file_cache; + m_file_cache = nullptr; + + m_option_classifier.fini (); + + delete m_reference_printer; + m_reference_printer = nullptr; + + if (m_fixits_change_set) + { + delete m_fixits_change_set; + m_fixits_change_set = nullptr; + } + + if (m_client_data_hooks) + { + delete m_client_data_hooks; + m_client_data_hooks = nullptr; + } + + delete m_option_mgr; + m_option_mgr = nullptr; + + if (m_urlifier_stack) + { + while (!m_urlifier_stack->is_empty ()) + pop_urlifier (); + delete m_urlifier_stack; + m_urlifier_stack = nullptr; + } + + freeargv (m_original_argv); + m_original_argv = nullptr; +} + +/* Dump state of this diagnostics::context to OUT, for debugging. */ + +void +context::dump (FILE *out) const +{ + fprintf (out, "diagnostics::context:\n"); + m_diagnostic_counters.dump (out, 2); + fprintf (out, " reference printer:\n"); + m_reference_printer->dump (out, 4); + fprintf (out, " output sinks:\n"); + if (m_sinks.length () > 0) + { + for (unsigned i = 0; i < m_sinks.length (); ++i) + { + fprintf (out, " sink %i:\n", i); + m_sinks[i]->dump (out, 4); + } + } + else + fprintf (out, " (none):\n"); + fprintf (out, " diagnostic buffer:\n"); + if (m_diagnostic_buffer) + m_diagnostic_buffer->dump (out, 4); + else + fprintf (out, " (none):\n"); + fprintf (out, " file cache:\n"); + if (m_file_cache) + m_file_cache->dump (out, 4); + else + fprintf (out, " (none):\n"); +} + +/* Return true if sufficiently severe diagnostics have been seen that + we ought to exit with a non-zero exit code. */ + +bool +context::execution_failed_p () const +{ + /* Equivalent to (seen_error () || werrorcount), but on + this context, rather than global_dc. */ + return (diagnostic_count (kind::error) + || diagnostic_count (kind::sorry) + || diagnostic_count (kind::werror)); +} + +void +context::remove_all_output_sinks () +{ + while (!m_sinks.is_empty ()) + delete m_sinks.pop (); +} + +void +context::set_sink (std::unique_ptr<sink> sink_) +{ + remove_all_output_sinks (); + m_sinks.safe_push (sink_.release ()); +} + +sink & +context::get_sink (size_t idx) const +{ + gcc_assert (idx < m_sinks.length ()); + gcc_assert (m_sinks[idx]); + return *m_sinks[idx]; +} + +void +context::add_sink (std::unique_ptr<sink> sink_) +{ + m_sinks.safe_push (sink_.release ()); +} + +/* Return true if there are no machine-readable formats writing to stderr. */ + +bool +context::supports_fnotice_on_stderr_p () const +{ + for (auto sink_ : m_sinks) + if (sink_->machine_readable_stderr_p ()) + return false; + return true; +} + +void +context::set_main_input_filename (const char *filename) +{ + for (auto sink_ : m_sinks) + sink_->set_main_input_filename (filename); +} + +void +context::set_client_data_hooks (std::unique_ptr<client_data_hooks> hooks) +{ + delete m_client_data_hooks; + /* Ideally the field would be a std::unique_ptr here. */ + m_client_data_hooks = hooks.release (); +} + +void +context::set_original_argv (unique_argv original_argv) +{ + /* Ideally we'd use a unique_argv for m_original_argv, but + diagnostics::context doesn't yet have a ctor/dtor pair. */ + + // Ensure any old value is freed + freeargv (m_original_argv); + + // Take ownership of the new value + m_original_argv = original_argv.release (); +} + +void +context::set_option_manager (std::unique_ptr<option_manager> mgr, + unsigned lang_mask) +{ + delete m_option_mgr; + m_option_mgr = mgr.release (); + m_lang_mask = lang_mask; +} + +void +context::push_owned_urlifier (std::unique_ptr<urlifier> ptr) +{ + gcc_assert (m_urlifier_stack); + const urlifier_stack_node node = { ptr.release (), true }; + m_urlifier_stack->safe_push (node); +} + +void +context::push_borrowed_urlifier (const urlifier &loan) +{ + gcc_assert (m_urlifier_stack); + const urlifier_stack_node node = { const_cast <urlifier *> (&loan), false }; + m_urlifier_stack->safe_push (node); +} + +void +context::pop_urlifier () +{ + gcc_assert (m_urlifier_stack); + gcc_assert (m_urlifier_stack->length () > 0); + + const urlifier_stack_node node = m_urlifier_stack->pop (); + if (node.m_owned) + delete node.m_urlifier; +} + +const logical_locations::manager * +context::get_logical_location_manager () const +{ + if (!m_client_data_hooks) + return nullptr; + return m_client_data_hooks->get_logical_location_manager (); +} + +const urlifier * +context::get_urlifier () const +{ + if (!m_urlifier_stack) + return nullptr; + if (m_urlifier_stack->is_empty ()) + return nullptr; + return m_urlifier_stack->last ().m_urlifier; +} + + +/* Set PP as the reference printer for this context. + Refresh all output sinks. */ + +void +context::set_pretty_printer (std::unique_ptr<pretty_printer> pp) +{ + delete m_reference_printer; + m_reference_printer = pp.release (); + refresh_output_sinks (); +} + +/* Give all output sinks a chance to rebuild their pretty_printer. */ + +void +context::refresh_output_sinks () +{ + for (auto sink_ : m_sinks) + sink_->update_printer (); +} + +/* Set FORMAT_DECODER on the reference printer and on the pretty_printer + of all output sinks. */ + +void +context::set_format_decoder (printer_fn format_decoder) +{ + pp_format_decoder (m_reference_printer) = format_decoder; + for (auto sink_ : m_sinks) + pp_format_decoder (sink_->get_printer ()) = format_decoder; +} + +void +context::set_show_highlight_colors (bool val) +{ + pp_show_highlight_colors (m_reference_printer) = val; + for (auto sink_ : m_sinks) + if (sink_->follows_reference_printer_p ()) + pp_show_highlight_colors (sink_->get_printer ()) = val; +} + +void +context::set_prefixing_rule (diagnostic_prefixing_rule_t rule) +{ + pp_prefixing_rule (m_reference_printer) = rule; + for (auto sink_ : m_sinks) + if (sink_->follows_reference_printer_p ()) + pp_prefixing_rule (sink_->get_printer ()) = rule; +} + +void +context::initialize_fixits_change_set () +{ + delete m_fixits_change_set; + gcc_assert (m_file_cache); + m_fixits_change_set = new changes::change_set (*m_file_cache); +} + +} // namespace diagnostics + +/* Initialize DIAGNOSTIC, where the message MSG has already been + translated. */ +void +diagnostic_set_info_translated (diagnostics::diagnostic_info *diagnostic, + const char *msg, va_list *args, + rich_location *richloc, + enum diagnostics::kind kind) +{ + gcc_assert (richloc); + diagnostic->m_message.m_err_no = errno; + diagnostic->m_message.m_args_ptr = args; + diagnostic->m_message.m_format_spec = msg; + diagnostic->m_message.m_richloc = richloc; + diagnostic->m_richloc = richloc; + diagnostic->m_metadata = nullptr; + diagnostic->m_kind = kind; + diagnostic->m_option_id = 0; +} + +/* Initialize DIAGNOSTIC, where the message GMSGID has not yet been + translated. */ +void +diagnostic_set_info (diagnostics::diagnostic_info *diagnostic, + const char *gmsgid, va_list *args, + rich_location *richloc, + enum diagnostics::kind kind) +{ + gcc_assert (richloc); + diagnostic_set_info_translated (diagnostic, _(gmsgid), args, richloc, kind); +} + +namespace diagnostics { + +static const char *const diagnostic_kind_text[] = { +#define DEFINE_DIAGNOSTIC_KIND(K, T, C) (T), +#include "diagnostics/kinds.def" +#undef DEFINE_DIAGNOSTIC_KIND + "must-not-happen" +}; + +/* Get unlocalized string describing KIND. */ + +const char * +get_text_for_kind (enum kind kind) +{ + return diagnostic_kind_text[static_cast<int> (kind)]; +} + +static const char *const diagnostic_kind_color[] = { +#define DEFINE_DIAGNOSTIC_KIND(K, T, C) (C), +#include "diagnostics/kinds.def" +#undef DEFINE_DIAGNOSTIC_KIND + nullptr +}; + +/* Get a color name for diagnostics of type KIND + Result could be nullptr. */ + +const char * +get_color_for_kind (enum kind kind) +{ + return diagnostic_kind_color[static_cast<int> (kind)]; +} + +/* Given an expanded_location, convert the column (which is in 1-based bytes) + to the requested units, without converting the origin. + Return -1 if the column is invalid (<= 0). */ + +static int +convert_column_unit (file_cache &fc, + enum diagnostics_column_unit column_unit, + int tabstop, + expanded_location s) +{ + if (s.column <= 0) + return -1; + + switch (column_unit) + { + default: + gcc_unreachable (); + + case DIAGNOSTICS_COLUMN_UNIT_DISPLAY: + { + cpp_char_column_policy policy (tabstop, cpp_wcwidth); + return location_compute_display_column (fc, s, policy); + } + + case DIAGNOSTICS_COLUMN_UNIT_BYTE: + return s.column; + } +} + +column_policy::column_policy (const context &dc) +: m_file_cache (dc.get_file_cache ()), + m_column_unit (dc.m_column_unit), + m_column_origin (dc.m_column_origin), + m_tabstop (dc.m_tabstop) +{ +} + +/* Given an expanded_location, convert the column (which is in 1-based bytes) + to the requested units and origin. Return -1 if the column is + invalid (<= 0). */ +int +column_policy::converted_column (expanded_location s) const +{ + int one_based_col = convert_column_unit (m_file_cache, + m_column_unit, m_tabstop, s); + if (one_based_col <= 0) + return -1; + return one_based_col + (m_column_origin - 1); +} + +/* Return a string describing a location e.g. "foo.c:42:10". */ + +label_text +column_policy::get_location_text (const expanded_location &s, + bool show_column, + bool colorize) const +{ + const char *locus_cs = colorize_start (colorize, "locus"); + const char *locus_ce = colorize_stop (colorize); + const char *file = s.file ? s.file : progname; + int line = 0; + int col = -1; + if (strcmp (file, special_fname_builtin ())) + { + line = s.line; + if (show_column) + col = converted_column (s); + } + + const char *line_col = maybe_line_and_column (line, col); + return label_text::take (build_message_string ("%s%s%s:%s", locus_cs, file, + line_col, locus_ce)); +} + +location_print_policy:: +location_print_policy (const context &dc) +: m_column_policy (dc), + m_show_column (dc.m_show_column) +{ +} + +location_print_policy:: +location_print_policy (const text_sink &text_output) +: + m_column_policy (text_output.get_context ()), + m_show_column (text_output.get_context ().m_show_column) +{ +} + +} // namespace diagnostics + +/* Functions at which to stop the backtrace print. It's not + particularly helpful to print the callers of these functions. */ + +static const char * const bt_stop[] = +{ + "main", + "toplev::main", + "execute_one_pass", + "compile_file", +}; + +/* A callback function passed to the backtrace_full function. */ + +static int +bt_callback (void *data, uintptr_t pc, const char *filename, int lineno, + const char *function) +{ + int *pcount = (int *) data; + + /* If we don't have any useful information, don't print + anything. */ + if (filename == nullptr && function == nullptr) + return 0; + + /* Skip functions in context.cc. */ + if (*pcount == 0 + && filename != nullptr + && strcmp (lbasename (filename), "context.cc") == 0) + return 0; + + /* Print up to 20 functions. We could make this a --param, but + since this is only for debugging just use a constant for now. */ + if (*pcount >= 20) + { + /* Returning a non-zero value stops the backtrace. */ + return 1; + } + ++*pcount; + + char *alc = nullptr; + if (function != nullptr) + { + char *str = cplus_demangle_v3 (function, + (DMGL_VERBOSE | DMGL_ANSI + | DMGL_GNU_V3 | DMGL_PARAMS)); + if (str != nullptr) + { + alc = str; + function = str; + } + + for (size_t i = 0; i < ARRAY_SIZE (bt_stop); ++i) + { + size_t len = strlen (bt_stop[i]); + if (strncmp (function, bt_stop[i], len) == 0 + && (function[len] == '\0' || function[len] == '(')) + { + if (alc != nullptr) + free (alc); + /* Returning a non-zero value stops the backtrace. */ + return 1; + } + } + } + + fprintf (stderr, "0x%lx %s\n\t%s:%d\n", + (unsigned long) pc, + function == nullptr ? "???" : function, + filename == nullptr ? "???" : filename, + lineno); + + if (alc != nullptr) + free (alc); + + return 0; +} + +/* A callback function passed to the backtrace_full function. This is + called if backtrace_full has an error. */ + +static void +bt_err_callback (void *data ATTRIBUTE_UNUSED, const char *msg, int errnum) +{ + if (errnum < 0) + { + /* This means that no debug info was available. Just quietly + skip printing backtrace info. */ + return; + } + fprintf (stderr, "%s%s%s\n", msg, errnum == 0 ? "" : ": ", + errnum == 0 ? "" : xstrerror (errnum)); +} + +namespace diagnostics { + +/* Check if we've met the maximum error limit, and if so fatally exit + with a message. + FLUSH indicates whether a diagnostics::context::finish call is needed. */ + +void +context::check_max_errors (bool flush) +{ + if (!m_max_errors) + return; + + int count = (diagnostic_count (kind::error) + + diagnostic_count (kind::sorry) + + diagnostic_count (kind::werror)); + + if (count >= m_max_errors) + { + fnotice (stderr, + "compilation terminated due to -fmax-errors=%u.\n", + m_max_errors); + if (flush) + finish (); + exit (FATAL_EXIT_CODE); + } +} + +/* Take any action which is expected to happen after the diagnostic + is written out. This function does not always return. */ + +void +context::action_after_output (enum kind diag_kind) +{ + switch (diag_kind) + { + case kind::debug: + case kind::note: + case kind::anachronism: + case kind::warning: + break; + + case kind::error: + case kind::sorry: + if (m_abort_on_error) + real_abort (); + if (m_fatal_errors) + { + fnotice (stderr, "compilation terminated due to -Wfatal-errors.\n"); + finish (); + exit (FATAL_EXIT_CODE); + } + break; + + case kind::ice: + case kind::ice_nobt: + { + /* Attempt to ensure that any outputs are flushed e.g. that .sarif + files are written out. + Only do it once. */ + static bool finishing_due_to_ice = false; + if (!finishing_due_to_ice) + { + finishing_due_to_ice = true; + finish (); + } + + struct backtrace_state *state = nullptr; + if (diag_kind == kind::ice) + state = backtrace_create_state (nullptr, 0, bt_err_callback, nullptr); + int count = 0; + if (state != nullptr) + backtrace_full (state, 2, bt_callback, bt_err_callback, + (void *) &count); + + if (m_abort_on_error) + real_abort (); + + if (m_report_bug) + fnotice (stderr, "Please submit a full bug report, " + "with preprocessed source.\n"); + else + fnotice (stderr, "Please submit a full bug report, " + "with preprocessed source (by using -freport-bug).\n"); + + if (count > 0) + fnotice (stderr, "Please include the complete backtrace " + "with any bug report.\n"); + fnotice (stderr, "See %s for instructions.\n", bug_report_url); + + exit (ICE_EXIT_CODE); + } + + case kind::fatal: + if (m_abort_on_error) + real_abort (); + fnotice (stderr, "compilation terminated.\n"); + finish (); + exit (FATAL_EXIT_CODE); + + default: + gcc_unreachable (); + } +} + +/* State whether we should inhibit notes in the current diagnostic_group and + its future children if any. */ + +void +context::inhibit_notes_in_group (bool inhibit) +{ + int curr_depth = (m_diagnostic_groups.m_group_nesting_depth + + m_diagnostic_groups.m_diagnostic_nesting_level); + + if (inhibit) + { + /* If we're already inhibiting, there's nothing to do. */ + if (m_diagnostic_groups.m_inhibiting_notes_from) + return; + + /* Since we're called via warning/error/... that all have their own + diagnostic_group, we must consider that we started inhibiting in their + parent. */ + gcc_assert (m_diagnostic_groups.m_group_nesting_depth > 0); + m_diagnostic_groups.m_inhibiting_notes_from = curr_depth - 1; + } + else if (m_diagnostic_groups.m_inhibiting_notes_from) + { + /* Only cancel inhibition at the depth that set it up. */ + if (curr_depth >= m_diagnostic_groups.m_inhibiting_notes_from) + return; + + m_diagnostic_groups.m_inhibiting_notes_from = 0; + } +} + +/* Return whether notes must be inhibited in the current diagnostic_group. */ + +bool +context::notes_inhibited_in_group () const +{ + if (m_diagnostic_groups.m_inhibiting_notes_from + && (m_diagnostic_groups.m_group_nesting_depth + + m_diagnostic_groups.m_diagnostic_nesting_level + >= m_diagnostic_groups.m_inhibiting_notes_from)) + return true; + return false; +} + +/* class diagnostics::logical_locations::manager. */ + +/* Return true iff this is a function or method. */ + +bool +logical_locations::manager::function_p (key k) const +{ + switch (get_kind (k)) + { + default: + gcc_unreachable (); + case kind::unknown: + case kind::module_: + case kind::namespace_: + case kind::type: + case kind::return_type: + case kind::parameter: + case kind::variable: + return false; + + case kind::function: + case kind::member: + return true; + } +} + +/* Helper function for print_parseable_fixits. Print TEXT to PP, obeying the + escaping rules for -fdiagnostics-parseable-fixits. */ + +static void +print_escaped_string (pretty_printer *pp, const char *text) +{ + gcc_assert (pp); + gcc_assert (text); + + pp_character (pp, '"'); + for (const char *ch = text; *ch; ch++) + { + switch (*ch) + { + case '\\': + /* Escape backslash as two backslashes. */ + pp_string (pp, "\\\\"); + break; + case '\t': + /* Escape tab as "\t". */ + pp_string (pp, "\\t"); + break; + case '\n': + /* Escape newline as "\n". */ + pp_string (pp, "\\n"); + break; + case '"': + /* Escape doublequotes as \". */ + pp_string (pp, "\\\""); + break; + default: + if (ISPRINT (*ch)) + pp_character (pp, *ch); + else + /* Use octal for non-printable chars. */ + { + unsigned char c = (*ch & 0xff); + pp_printf (pp, "\\%o%o%o", (c / 64), (c / 8) & 007, c & 007); + } + break; + } + } + pp_character (pp, '"'); +} + +/* Implementation of -fdiagnostics-parseable-fixits and + GCC_EXTRA_DIAGNOSTIC_OUTPUT. + Print a machine-parseable version of all fixits in RICHLOC to PP, + using COLUMN_UNIT to express columns. + Use TABSTOP when handling DIAGNOSTICS_COLUMN_UNIT_DISPLAY. */ + +static void +print_parseable_fixits (file_cache &fc, + pretty_printer *pp, rich_location *richloc, + enum diagnostics_column_unit column_unit, + int tabstop) +{ + gcc_assert (pp); + gcc_assert (richloc); + + char *saved_prefix = pp_take_prefix (pp); + pp_set_prefix (pp, nullptr); + + for (unsigned i = 0; i < richloc->get_num_fixit_hints (); i++) + { + const fixit_hint *hint = richloc->get_fixit_hint (i); + location_t start_loc = hint->get_start_loc (); + expanded_location start_exploc = expand_location (start_loc); + pp_string (pp, "fix-it:"); + print_escaped_string (pp, start_exploc.file); + /* For compatibility with clang, print as a half-open range. */ + location_t next_loc = hint->get_next_loc (); + expanded_location next_exploc = expand_location (next_loc); + int start_col + = convert_column_unit (fc, column_unit, tabstop, start_exploc); + int next_col + = convert_column_unit (fc, column_unit, tabstop, next_exploc); + pp_printf (pp, ":{%i:%i-%i:%i}:", + start_exploc.line, start_col, + next_exploc.line, next_col); + print_escaped_string (pp, hint->get_string ()); + pp_newline (pp); + } + + pp_set_prefix (pp, saved_prefix); +} + +/* Update the inlining info in this context for a DIAGNOSTIC. */ + +void +context::get_any_inlining_info (diagnostic_info *diagnostic) +{ + auto &ilocs = diagnostic->m_iinfo.m_ilocs; + + if (m_set_locations_cb) + /* Retrieve the locations into which the expression about to be + diagnosed has been inlined, including those of all the callers + all the way down the inlining stack. */ + m_set_locations_cb (this, diagnostic); + else + { + /* When there's no callback use just the one location provided + by the caller of the diagnostic function. */ + location_t loc = diagnostic_location (diagnostic); + ilocs.safe_push (loc); + diagnostic->m_iinfo.m_allsyslocs = in_system_header_at (loc); + } +} + +/* Generate a URL string describing CWE. The caller is responsible for + freeing the string. */ + +char * +get_cwe_url (int cwe) +{ + return xasprintf ("https://cwe.mitre.org/data/definitions/%i.html", cwe); +} + +/* Returns whether a DIAGNOSTIC should be printed, and adjusts diagnostic->kind + as appropriate for #pragma GCC diagnostic and -Werror=foo. */ + +bool +context::diagnostic_enabled (diagnostic_info *diagnostic) +{ + /* Update the inlining stack for this diagnostic. */ + get_any_inlining_info (diagnostic); + + /* Diagnostics with no option or -fpermissive are always enabled. */ + if (!diagnostic->m_option_id.m_idx + || diagnostic->m_option_id == m_opt_permissive) + return true; + + /* This tests if the user provided the appropriate -Wfoo or + -Wno-foo option. */ + if (!option_enabled_p (diagnostic->m_option_id)) + return false; + + /* This tests for #pragma diagnostic changes. */ + enum kind diag_class + = m_option_classifier.update_effective_level_from_pragmas (diagnostic); + + /* This tests if the user provided the appropriate -Werror=foo + option. */ + if (diag_class == kind::unspecified + && !option_unspecified_p (diagnostic->m_option_id)) + { + const enum kind new_kind + = m_option_classifier.get_current_override (diagnostic->m_option_id); + if (new_kind != kind::any) + /* kind::any means the diagnostic is not to be ignored, but we don't want + to change it specifically to kind::error or kind::warning; we want to + preserve whatever the caller has specified. */ + diagnostic->m_kind = new_kind; + } + + /* This allows for future extensions, like temporarily disabling + warnings for ranges of source code. */ + if (diagnostic->m_kind == kind::ignored) + return false; + + return true; +} + +/* Returns whether warning OPT_ID is enabled at LOC. */ + +bool +context::warning_enabled_at (location_t loc, + option_id opt_id) +{ + if (!diagnostic_report_warnings_p (this, loc)) + return false; + + rich_location richloc (line_table, loc); + diagnostic_info diagnostic = {}; + diagnostic.m_option_id = opt_id; + diagnostic.m_richloc = &richloc; + diagnostic.m_message.m_richloc = &richloc; + diagnostic.m_kind = kind::warning; + return diagnostic_enabled (&diagnostic); +} + +/* Emit a diagnostic within a diagnostic group on this context. */ + +bool +context::emit_diagnostic_with_group (enum kind kind, + rich_location &richloc, + const metadata *metadata, + option_id opt_id, + const char *gmsgid, ...) +{ + begin_group (); + + va_list ap; + va_start (ap, gmsgid); + bool ret = emit_diagnostic_with_group_va (kind, richloc, metadata, opt_id, + gmsgid, &ap); + va_end (ap); + + end_group (); + + return ret; +} + +/* As above, but taking a va_list *. */ + +bool +context::emit_diagnostic_with_group_va (enum kind kind, + rich_location &richloc, + const metadata *metadata, + option_id opt_id, + const char *gmsgid, va_list *ap) +{ + begin_group (); + + bool ret = diagnostic_impl (&richloc, metadata, opt_id, + gmsgid, ap, kind); + + end_group (); + + return ret; +} + +/* Report a diagnostic message (an error or a warning) as specified by + this diagnostics::context. + front-end independent format specifiers are exactly those described + in the documentation of output_format. + Return true if a diagnostic was printed, false otherwise. */ + +bool +context::report_diagnostic (diagnostic_info *diagnostic) +{ + enum kind orig_diag_kind = diagnostic->m_kind; + + /* Every call to report_diagnostic should be within a + begin_group/end_group pair so that output formats can reliably + flush diagnostics with on_end_group when the topmost group is ended. */ + gcc_assert (m_diagnostic_groups.m_group_nesting_depth > 0); + + /* Give preference to being able to inhibit warnings, before they + get reclassified to something else. */ + bool was_warning = (diagnostic->m_kind == kind::warning + || diagnostic->m_kind == kind::pedwarn); + if (was_warning && m_inhibit_warnings) + { + inhibit_notes_in_group (); + return false; + } + + if (m_adjust_diagnostic_info) + m_adjust_diagnostic_info (this, diagnostic); + + if (diagnostic->m_kind == kind::pedwarn) + { + diagnostic->m_kind = m_pedantic_errors ? kind::error : kind::warning; + + /* We do this to avoid giving the message for -pedantic-errors. */ + orig_diag_kind = diagnostic->m_kind; + } + + if (diagnostic->m_kind == kind::note && m_inhibit_notes_p) + return false; + + /* If the user requested that warnings be treated as errors, so be + it. Note that we do this before the next block so that + individual warnings can be overridden back to warnings with + -Wno-error=*. */ + if (m_warning_as_error_requested + && diagnostic->m_kind == kind::warning) + diagnostic->m_kind = kind::error; + + diagnostic->m_message.m_data = &diagnostic->m_x_data; + + /* Check to see if the diagnostic is enabled at the location and + not disabled by #pragma GCC diagnostic anywhere along the inlining + stack. . */ + if (!diagnostic_enabled (diagnostic)) + { + inhibit_notes_in_group (); + return false; + } + + if ((was_warning || diagnostic->m_kind == kind::warning) + && ((!m_warn_system_headers + && diagnostic->m_iinfo.m_allsyslocs) + || m_inhibit_warnings)) + /* Bail if the warning is not to be reported because all locations in the + inlining stack (if there is one) are in system headers. */ + return false; + + if (diagnostic->m_kind == kind::note && notes_inhibited_in_group ()) + /* Bail for all the notes in the diagnostic_group that started to inhibit notes. */ + return false; + + if (diagnostic->m_kind != kind::note && diagnostic->m_kind != kind::ice) + check_max_errors (false); + + if (m_lock > 0) + { + /* If we're reporting an ICE in the middle of some other error, + try to flush out the previous error, then let this one + through. Don't do this more than once. */ + if ((diagnostic->m_kind == kind::ice + || diagnostic->m_kind == kind::ice_nobt) + && m_lock == 1) + pp_newline_and_flush (m_reference_printer); + else + error_recursion (); + } + + /* We are accepting the diagnostic, so should stop inhibiting notes. */ + inhibit_notes_in_group (/*inhibit=*/false); + + m_lock++; + + if (diagnostic->m_kind == kind::ice || diagnostic->m_kind == kind::ice_nobt) + { + /* When not checking, ICEs are converted to fatal errors when an + error has already occurred. This is counteracted by + abort_on_error. */ + if (!CHECKING_P + && (diagnostic_count (kind::error) > 0 + || diagnostic_count (kind::sorry) > 0) + && !m_abort_on_error) + { + expanded_location s + = expand_location (diagnostic_location (diagnostic)); + fnotice (stderr, "%s:%d: confused by earlier errors, bailing out\n", + s.file, s.line); + exit (ICE_EXIT_CODE); + } + if (m_internal_error) + (*m_internal_error) (this, + diagnostic->m_message.m_format_spec, + diagnostic->m_message.m_args_ptr); + } + + /* Increment the counter for the appropriate diagnostic kind, either + within this context, or within the diagnostic_buffer. */ + { + const enum kind kind_for_count = + ((diagnostic->m_kind == kind::error && orig_diag_kind == kind::warning) + ? kind::werror + : diagnostic->m_kind); + counters &cs + = (m_diagnostic_buffer + ? m_diagnostic_buffer->m_diagnostic_counters + : m_diagnostic_counters); + ++cs.m_count_for_kind[static_cast<size_t> (kind_for_count)]; + } + + /* Is this the initial diagnostic within the stack of groups? */ + if (m_diagnostic_groups.m_emission_count == 0) + for (auto sink_ : m_sinks) + sink_->on_begin_group (); + m_diagnostic_groups.m_emission_count++; + + va_list *orig_args = diagnostic->m_message.m_args_ptr; + for (auto sink_ : m_sinks) + { + /* Formatting the message is done per-output-format, + so that each output format gets its own set of pp_token_lists + to work with. + + Run phases 1 and 2 of formatting the message before calling + the format's on_report_diagnostic. + In particular, some format codes may have side-effects here which + need to happen before sending the diagnostic to the output format. + For example, Fortran's %C and %L formatting codes populate the + rich_location. + Such side-effects must be idempotent, since they are run per + output-format. + + Make a duplicate of the varargs for each call to pp_format, + so that each has its own set to consume. */ + va_list copied_args; + va_copy (copied_args, *orig_args); + diagnostic->m_message.m_args_ptr = &copied_args; + pp_format (sink_->get_printer (), &diagnostic->m_message); + va_end (copied_args); + + /* Call vfunc in the output format. This is responsible for + phase 3 of formatting, and for printing the result. */ + sink_->on_report_diagnostic (*diagnostic, orig_diag_kind); + } + + switch (m_extra_output_kind) + { + default: + break; + case EXTRA_DIAGNOSTIC_OUTPUT_fixits_v1: + print_parseable_fixits (get_file_cache (), + m_reference_printer, diagnostic->m_richloc, + DIAGNOSTICS_COLUMN_UNIT_BYTE, + m_tabstop); + pp_flush (m_reference_printer); + break; + case EXTRA_DIAGNOSTIC_OUTPUT_fixits_v2: + print_parseable_fixits (get_file_cache (), + m_reference_printer, diagnostic->m_richloc, + DIAGNOSTICS_COLUMN_UNIT_DISPLAY, + m_tabstop); + pp_flush (m_reference_printer); + break; + } + if (m_diagnostic_buffer == nullptr + || diagnostic->m_kind == kind::ice + || diagnostic->m_kind == kind::ice_nobt) + action_after_output (diagnostic->m_kind); + diagnostic->m_x_data = nullptr; + + if (m_fixits_change_set) + if (diagnostic->m_richloc->fixits_can_be_auto_applied_p ()) + if (!m_diagnostic_buffer) + m_fixits_change_set->add_fixits (diagnostic->m_richloc); + + m_lock--; + + if (!m_diagnostic_buffer) + for (auto sink_ : m_sinks) + sink_->after_diagnostic (*diagnostic); + + return true; +} + +void +context::report_verbatim (text_info &text) +{ + va_list *orig_args = text.m_args_ptr; + for (auto sink_ : m_sinks) + { + va_list copied_args; + va_copy (copied_args, *orig_args); + text.m_args_ptr = &copied_args; + sink_->on_report_verbatim (text); + va_end (copied_args); + } +} + +void +context::report_global_digraph (const lazily_created<digraphs::digraph> &ldg) +{ + for (auto sink_ : m_sinks) + sink_->report_global_digraph (ldg); +} + +/* Get the number of digits in the decimal representation of VALUE. */ + +int +num_digits (int value) +{ + /* Perhaps simpler to use log10 for this, but doing it this way avoids + using floating point. */ + gcc_assert (value >= 0); + + if (value == 0) + return 1; + + int digits = 0; + while (value > 0) + { + digits++; + value /= 10; + } + return digits; +} + +} // namespace diagnostics + +/* Given a partial pathname as input, return another pathname that + shares no directory elements with the pathname of __FILE__. This + is used by fancy_abort() to print `internal compiler error in expr.cc' + instead of `internal compiler error in ../../GCC/gcc/expr.cc'. */ + +const char * +trim_filename (const char *name) +{ + static const char this_file[] = __FILE__; + const char *p = name, *q = this_file; + + /* First skip any "../" in each filename. This allows us to give a proper + reference to a file in a subdirectory. */ + while (p[0] == '.' && p[1] == '.' && IS_DIR_SEPARATOR (p[2])) + p += 3; + + while (q[0] == '.' && q[1] == '.' && IS_DIR_SEPARATOR (q[2])) + q += 3; + + /* Now skip any parts the two filenames have in common. */ + while (*p == *q && *p != 0 && *q != 0) + p++, q++; + + /* Now go backwards until the previous directory separator. */ + while (p > name && !IS_DIR_SEPARATOR (p[-1])) + p--; + + return p; +} + +namespace diagnostics { + +/* Implement emit_diagnostic, inform, warning, warning_at, pedwarn, + permerror, error, error_at, error_at, sorry, fatal_error, internal_error, + and internal_error_no_backtrace, as documented and defined below. */ +bool +context::diagnostic_impl (rich_location *richloc, + const metadata *metadata, + option_id opt_id, + const char *gmsgid, + va_list *ap, enum kind kind) +{ + diagnostic_info diagnostic; + if (kind == diagnostics::kind::permerror) + { + diagnostic_set_info (&diagnostic, gmsgid, ap, richloc, + (m_permissive + ? diagnostics::kind::warning + : diagnostics::kind::error)); + diagnostic.m_option_id = (opt_id.m_idx != -1 ? opt_id : m_opt_permissive); + } + else + { + diagnostic_set_info (&diagnostic, gmsgid, ap, richloc, kind); + if (kind == diagnostics::kind::warning + || kind == diagnostics::kind::pedwarn) + diagnostic.m_option_id = opt_id; + } + diagnostic.m_metadata = metadata; + return report_diagnostic (&diagnostic); +} + +/* Implement inform_n, warning_n, and error_n, as documented and + defined below. */ +bool +context::diagnostic_n_impl (rich_location *richloc, + const metadata *metadata, + option_id opt_id, + unsigned HOST_WIDE_INT n, + const char *singular_gmsgid, + const char *plural_gmsgid, + va_list *ap, enum kind kind) +{ + diagnostic_info diagnostic; + unsigned long gtn; + + if (sizeof n <= sizeof gtn) + gtn = n; + else + /* Use the largest number ngettext can handle, otherwise + preserve the six least significant decimal digits for + languages where the plural form depends on them. */ + gtn = n <= ULONG_MAX ? n : n % 1000000LU + 1000000LU; + + const char *text = ngettext (singular_gmsgid, plural_gmsgid, gtn); + diagnostic_set_info_translated (&diagnostic, text, ap, richloc, kind); + if (kind == diagnostics::kind::warning) + diagnostic.m_option_id = opt_id; + diagnostic.m_metadata = metadata; + return report_diagnostic (&diagnostic); +} + + +/* Emit DIAGRAM to this context, respecting the output format. */ + +void +context::emit_diagram (const diagram &diag) +{ + if (m_diagrams.m_theme == nullptr) + return; + + for (auto sink_ : m_sinks) + sink_->on_diagram (diag); +} + +/* Inform the user that an error occurred while trying to report some + other error. This indicates catastrophic internal inconsistencies, + so give up now. But do try to flush out the previous error. + This mustn't use internal_error, that will cause infinite recursion. */ + +void +context::error_recursion () +{ + if (m_lock < 3) + pp_newline_and_flush (m_reference_printer); + + fnotice (stderr, + "internal compiler error: error reporting routines re-entered.\n"); + + /* Call action_after_output to get the "please submit a bug report" + message. */ + action_after_output (kind::ice); + + /* Do not use gcc_unreachable here; that goes through internal_error + and therefore would cause infinite recursion. */ + real_abort (); +} + +} // namespace diagnostics + +/* Report an internal compiler error in a friendly manner. This is + the function that gets called upon use of abort() in the source + code generally, thanks to a special macro. */ + +void +fancy_abort (const char *file, int line, const char *function) +{ + /* If fancy_abort is called before the diagnostic subsystem is initialized, + internal_error will crash internally in a way that prevents a + useful message reaching the user. + This can happen with libgccjit in the case of gcc_assert failures + that occur outside of the libgccjit mutex that guards the rest of + gcc's state, including global_dc (when global_dc may not be + initialized yet, or might be in use by another thread). + Handle such cases as gracefully as possible by falling back to a + minimal abort handler that only relies on i18n. */ + if (global_dc->get_reference_printer () == nullptr) + { + /* Print the error message. */ + fnotice (stderr, diagnostics::get_text_for_kind (diagnostics::kind::ice)); + fnotice (stderr, "in %s, at %s:%d", function, trim_filename (file), line); + fputc ('\n', stderr); + + /* Attempt to print a backtrace. */ + struct backtrace_state *state + = backtrace_create_state (nullptr, 0, bt_err_callback, nullptr); + int count = 0; + if (state != nullptr) + backtrace_full (state, 2, bt_callback, bt_err_callback, + (void *) &count); + + /* We can't call warn_if_plugins or emergency_dump_function as these + rely on GCC state that might not be initialized, or might be in + use by another thread. */ + + /* Abort the process. */ + real_abort (); + } + + internal_error ("in %s, at %s:%d", function, trim_filename (file), line); +} + +namespace diagnostics { + +/* class diagnostics::context. */ + +void +context::begin_group () +{ + m_diagnostic_groups.m_group_nesting_depth++; +} + +void +context::end_group () +{ + if (--m_diagnostic_groups.m_group_nesting_depth == 0) + { + /* Handle the case where we've popped the final diagnostic group. + If any diagnostics were emitted, give the context a chance + to do something. */ + if (m_diagnostic_groups.m_emission_count > 0) + for (auto sink_ : m_sinks) + sink_->on_end_group (); + m_diagnostic_groups.m_emission_count = 0; + } + /* We're popping one level, so might need to stop inhibiting notes. */ + inhibit_notes_in_group (/*inhibit=*/false); +} + +void +context::push_nesting_level () +{ + ++m_diagnostic_groups.m_diagnostic_nesting_level; +} + +void +context::pop_nesting_level () +{ + --m_diagnostic_groups.m_diagnostic_nesting_level; + /* We're popping one level, so might need to stop inhibiting notes. */ + inhibit_notes_in_group (/*inhibit=*/false); +} + +void +sink::dump (FILE *out, int indent) const +{ + fprintf (out, "%*sprinter:\n", indent, ""); + m_printer->dump (out, indent + 2); +} + +void +sink::on_report_verbatim (text_info &) +{ + /* No-op. */ +} + +/* Set the output format for DC to FORMAT, using BASE_FILE_NAME for + file-based output formats. */ + +void +output_format_init (context &dc, + const char *main_input_filename_, + const char *base_file_name, + enum diagnostics_output_format format, + bool json_formatting) +{ + sink *new_sink = nullptr; + switch (format) + { + default: + gcc_unreachable (); + case DIAGNOSTICS_OUTPUT_FORMAT_TEXT: + /* The default; do nothing. */ + break; + + case DIAGNOSTICS_OUTPUT_FORMAT_SARIF_STDERR: + new_sink = &init_sarif_stderr (dc, + line_table, + json_formatting); + break; + + case DIAGNOSTICS_OUTPUT_FORMAT_SARIF_FILE: + new_sink = &init_sarif_file (dc, + line_table, + json_formatting, + base_file_name); + break; + } + if (new_sink) + new_sink->set_main_input_filename (main_input_filename_); +} + +/* Initialize this context's m_diagrams based on CHARSET. + Specifically, make a text_art::theme object for m_diagrams.m_theme, + (or nullptr for "no diagrams"). */ + +void +context::set_text_art_charset (enum diagnostic_text_art_charset charset) +{ + delete m_diagrams.m_theme; + switch (charset) + { + default: + gcc_unreachable (); + + case DIAGNOSTICS_TEXT_ART_CHARSET_NONE: + m_diagrams.m_theme = nullptr; + break; + + case DIAGNOSTICS_TEXT_ART_CHARSET_ASCII: + m_diagrams.m_theme = new text_art::ascii_theme (); + break; + + case DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE: + m_diagrams.m_theme = new text_art::unicode_theme (); + break; + + case DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI: + m_diagrams.m_theme = new text_art::emoji_theme (); + break; + } +} + +/* struct diagnostics::counters. */ + +counters::counters () +{ + clear (); +} + +void +counters::dump (FILE *out, int indent) const +{ + fprintf (out, "%*scounts:\n", indent, ""); + bool none = true; + for (int i = 0; i < static_cast<int> (kind::last_diagnostic_kind); i++) + if (m_count_for_kind[i] > 0) + { + fprintf (out, "%*s%s%i\n", + indent + 2, "", + get_text_for_kind (static_cast<enum kind> (i)), + m_count_for_kind[i]); + none = false; + } + if (none) + fprintf (out, "%*s(none)\n", indent + 2, ""); +} + +void +counters::move_to (counters &dest) +{ + for (int i = 0; i < static_cast<int> (kind::last_diagnostic_kind); i++) + dest.m_count_for_kind[i] += m_count_for_kind[i]; + clear (); +} + +void +counters::clear () +{ + memset (&m_count_for_kind, 0, sizeof m_count_for_kind); +} + +#if CHECKING_P + +namespace selftest { + +using line_table_test = ::selftest::line_table_test; +using temp_source_file = ::selftest::temp_source_file; + +/* Helper function for test_print_escaped_string. */ + +static void +assert_print_escaped_string (const ::selftest::location &loc, + const char *expected_output, + const char *input) +{ + pretty_printer pp; + print_escaped_string (&pp, input); + ASSERT_STREQ_AT (loc, expected_output, pp_formatted_text (&pp)); +} + +#define ASSERT_PRINT_ESCAPED_STRING_STREQ(EXPECTED_OUTPUT, INPUT) \ + assert_print_escaped_string (SELFTEST_LOCATION, EXPECTED_OUTPUT, INPUT) + +/* Tests of print_escaped_string. */ + +static void +test_print_escaped_string () +{ + /* Empty string. */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"\"", ""); + + /* Non-empty string. */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"hello world\"", "hello world"); + + /* Various things that need to be escaped: */ + /* Backslash. */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"before\\\\after\"", + "before\\after"); + /* Tab. */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"before\\tafter\"", + "before\tafter"); + /* Newline. */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"before\\nafter\"", + "before\nafter"); + /* Double quote. */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"before\\\"after\"", + "before\"after"); + + /* Non-printable characters: BEL: '\a': 0x07 */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"before\\007after\"", + "before\aafter"); + /* Non-printable characters: vertical tab: '\v': 0x0b */ + ASSERT_PRINT_ESCAPED_STRING_STREQ ("\"before\\013after\"", + "before\vafter"); +} + +/* Tests of print_parseable_fixits. */ + +/* Verify that print_parseable_fixits emits the empty string if there + are no fixits. */ + +static void +test_print_parseable_fixits_none () +{ + pretty_printer pp; + file_cache fc; + rich_location richloc (line_table, UNKNOWN_LOCATION); + + print_parseable_fixits (fc, &pp, &richloc, DIAGNOSTICS_COLUMN_UNIT_BYTE, 8); + ASSERT_STREQ ("", pp_formatted_text (&pp)); +} + +/* Verify that print_parseable_fixits does the right thing if there + is an insertion fixit hint. */ + +static void +test_print_parseable_fixits_insert () +{ + pretty_printer pp; + file_cache fc; + rich_location richloc (line_table, UNKNOWN_LOCATION); + + linemap_add (line_table, LC_ENTER, false, "test.c", 0); + linemap_line_start (line_table, 5, 100); + linemap_add (line_table, LC_LEAVE, false, nullptr, 0); + location_t where = linemap_position_for_column (line_table, 10); + richloc.add_fixit_insert_before (where, "added content"); + + print_parseable_fixits (fc, &pp, &richloc, DIAGNOSTICS_COLUMN_UNIT_BYTE, 8); + ASSERT_STREQ ("fix-it:\"test.c\":{5:10-5:10}:\"added content\"\n", + pp_formatted_text (&pp)); +} + +/* Verify that print_parseable_fixits does the right thing if there + is an removal fixit hint. */ + +static void +test_print_parseable_fixits_remove () +{ + pretty_printer pp; + file_cache fc; + rich_location richloc (line_table, UNKNOWN_LOCATION); + + linemap_add (line_table, LC_ENTER, false, "test.c", 0); + linemap_line_start (line_table, 5, 100); + linemap_add (line_table, LC_LEAVE, false, nullptr, 0); + source_range where; + where.m_start = linemap_position_for_column (line_table, 10); + where.m_finish = linemap_position_for_column (line_table, 20); + richloc.add_fixit_remove (where); + + print_parseable_fixits (fc, &pp, &richloc, DIAGNOSTICS_COLUMN_UNIT_BYTE, 8); + ASSERT_STREQ ("fix-it:\"test.c\":{5:10-5:21}:\"\"\n", + pp_formatted_text (&pp)); +} + +/* Verify that print_parseable_fixits does the right thing if there + is an replacement fixit hint. */ + +static void +test_print_parseable_fixits_replace () +{ + pretty_printer pp; + file_cache fc; + rich_location richloc (line_table, UNKNOWN_LOCATION); + + linemap_add (line_table, LC_ENTER, false, "test.c", 0); + linemap_line_start (line_table, 5, 100); + linemap_add (line_table, LC_LEAVE, false, nullptr, 0); + source_range where; + where.m_start = linemap_position_for_column (line_table, 10); + where.m_finish = linemap_position_for_column (line_table, 20); + richloc.add_fixit_replace (where, "replacement"); + + print_parseable_fixits (fc, &pp, &richloc, DIAGNOSTICS_COLUMN_UNIT_BYTE, 8); + ASSERT_STREQ ("fix-it:\"test.c\":{5:10-5:21}:\"replacement\"\n", + pp_formatted_text (&pp)); +} + +/* Verify that print_parseable_fixits correctly handles + DIAGNOSTICS_COLUMN_UNIT_BYTE vs DIAGNOSTICS_COLUMN_UNIT_COLUMN. */ + +static void +test_print_parseable_fixits_bytes_vs_display_columns () +{ + line_table_test ltt; + rich_location richloc (line_table, UNKNOWN_LOCATION); + + /* 1-based byte offsets: 12345677778888999900001234567. */ + const char *const content = "smile \xf0\x9f\x98\x82 colour\n"; + /* 1-based display cols: 123456[......7-8.....]9012345. */ + const int tabstop = 8; + + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + file_cache fc; + const char *const fname = tmp.get_filename (); + + linemap_add (line_table, LC_ENTER, false, fname, 0); + linemap_line_start (line_table, 1, 100); + linemap_add (line_table, LC_LEAVE, false, nullptr, 0); + source_range where; + where.m_start = linemap_position_for_column (line_table, 12); + where.m_finish = linemap_position_for_column (line_table, 17); + richloc.add_fixit_replace (where, "color"); + + /* Escape fname. */ + pretty_printer tmp_pp; + print_escaped_string (&tmp_pp, fname); + char *escaped_fname = xstrdup (pp_formatted_text (&tmp_pp)); + + const int buf_len = strlen (escaped_fname) + 100; + char *const expected = XNEWVEC (char, buf_len); + + { + pretty_printer pp; + print_parseable_fixits (fc, &pp, &richloc, + DIAGNOSTICS_COLUMN_UNIT_BYTE, + tabstop); + snprintf (expected, buf_len, + "fix-it:%s:{1:12-1:18}:\"color\"\n", escaped_fname); + ASSERT_STREQ (expected, pp_formatted_text (&pp)); + } + { + pretty_printer pp; + print_parseable_fixits (fc, &pp, &richloc, + DIAGNOSTICS_COLUMN_UNIT_DISPLAY, + tabstop); + snprintf (expected, buf_len, + "fix-it:%s:{1:10-1:16}:\"color\"\n", escaped_fname); + ASSERT_STREQ (expected, pp_formatted_text (&pp)); + } + + XDELETEVEC (expected); + free (escaped_fname); +} + +/* Verify that + diagnostics::column_policy::get_location_text (..., SHOW_COLUMN, ...) + generates EXPECTED_LOC_TEXT, given FILENAME, LINE, COLUMN, with + colorization disabled. */ + +static void +assert_location_text (const char *expected_loc_text, + const char *filename, int line, int column, + bool show_column, + int origin = 1, + enum diagnostics_column_unit column_unit + = DIAGNOSTICS_COLUMN_UNIT_BYTE) +{ + diagnostics::selftest::test_context dc; + dc.m_column_unit = column_unit; + dc.m_column_origin = origin; + + expanded_location xloc; + xloc.file = filename; + xloc.line = line; + xloc.column = column; + xloc.data = nullptr; + xloc.sysp = false; + + diagnostics::column_policy column_policy (dc); + label_text actual_loc_text + = column_policy.get_location_text (xloc, show_column, false); + ASSERT_STREQ (expected_loc_text, actual_loc_text.get ()); +} + +/* Verify that get_location_text works as expected. */ + +static void +test_get_location_text () +{ + const char *old_progname = progname; + progname = "PROGNAME"; + assert_location_text ("PROGNAME:", nullptr, 0, 0, true); + char *built_in_colon = concat (special_fname_builtin (), ":", (char *) 0); + assert_location_text (built_in_colon, special_fname_builtin (), + 42, 10, true); + free (built_in_colon); + assert_location_text ("foo.c:42:10:", "foo.c", 42, 10, true); + assert_location_text ("foo.c:42:9:", "foo.c", 42, 10, true, 0); + assert_location_text ("foo.c:42:1010:", "foo.c", 42, 10, true, 1001); + for (int origin = 0; origin != 2; ++origin) + assert_location_text ("foo.c:42:", "foo.c", 42, 0, true, origin); + assert_location_text ("foo.c:", "foo.c", 0, 10, true); + assert_location_text ("foo.c:42:", "foo.c", 42, 10, false); + assert_location_text ("foo.c:", "foo.c", 0, 10, false); + + diagnostics::maybe_line_and_column (INT_MAX, INT_MAX); + diagnostics::maybe_line_and_column (INT_MIN, INT_MIN); + + { + /* In order to test display columns vs byte columns, we need to create a + file for location_get_source_line() to read. */ + + const char *const content = "smile \xf0\x9f\x98\x82\n"; + const int line_bytes = strlen (content) - 1; + const int def_tabstop = 8; + const cpp_char_column_policy policy (def_tabstop, cpp_wcwidth); + const int display_width = cpp_display_width (content, line_bytes, policy); + ASSERT_EQ (line_bytes - 2, display_width); + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + const char *const fname = tmp.get_filename (); + const int buf_len = strlen (fname) + 16; + char *const expected = XNEWVEC (char, buf_len); + + snprintf (expected, buf_len, "%s:1:%d:", fname, line_bytes); + assert_location_text (expected, fname, 1, line_bytes, true, + 1, DIAGNOSTICS_COLUMN_UNIT_BYTE); + + snprintf (expected, buf_len, "%s:1:%d:", fname, line_bytes - 1); + assert_location_text (expected, fname, 1, line_bytes, true, + 0, DIAGNOSTICS_COLUMN_UNIT_BYTE); + + snprintf (expected, buf_len, "%s:1:%d:", fname, display_width); + assert_location_text (expected, fname, 1, line_bytes, true, + 1, DIAGNOSTICS_COLUMN_UNIT_DISPLAY); + + snprintf (expected, buf_len, "%s:1:%d:", fname, display_width - 1); + assert_location_text (expected, fname, 1, line_bytes, true, + 0, DIAGNOSTICS_COLUMN_UNIT_DISPLAY); + + XDELETEVEC (expected); + } + + + progname = old_progname; +} + +/* Selftest for num_digits. */ + +static void +test_num_digits () +{ + ASSERT_EQ (1, num_digits (0)); + ASSERT_EQ (1, num_digits (9)); + ASSERT_EQ (2, num_digits (10)); + ASSERT_EQ (2, num_digits (99)); + ASSERT_EQ (3, num_digits (100)); + ASSERT_EQ (3, num_digits (999)); + ASSERT_EQ (4, num_digits (1000)); + ASSERT_EQ (4, num_digits (9999)); + ASSERT_EQ (5, num_digits (10000)); + ASSERT_EQ (5, num_digits (99999)); + ASSERT_EQ (6, num_digits (100000)); + ASSERT_EQ (6, num_digits (999999)); + ASSERT_EQ (7, num_digits (1000000)); + ASSERT_EQ (7, num_digits (9999999)); + ASSERT_EQ (8, num_digits (10000000)); + ASSERT_EQ (8, num_digits (99999999)); +} + +/* Run all of the selftests within this file. + + According to https://gcc.gnu.org/pipermail/gcc/2021-November/237703.html + there are some language-specific assumptions within these tests, so only + run them from C/C++. */ + +void +context_cc_tests () +{ + test_print_escaped_string (); + test_print_parseable_fixits_none (); + test_print_parseable_fixits_insert (); + test_print_parseable_fixits_remove (); + test_print_parseable_fixits_replace (); + test_print_parseable_fixits_bytes_vs_display_columns (); + test_get_location_text (); + test_num_digits (); +} + +} // namespace diagnostics::selftest + +#endif /* #if CHECKING_P */ + +} // namespace diagnostics + +#if __GNUC__ >= 10 +# pragma GCC diagnostic pop +#endif + +static void real_abort (void) ATTRIBUTE_NORETURN; + +/* Really call the system 'abort'. This has to go right at the end of + this file, so that there are no functions after it that call abort + and get the system abort instead of our macro. */ +#undef abort +static void +real_abort (void) +{ + abort (); +} diff --git a/gcc/diagnostics/context.h b/gcc/diagnostics/context.h new file mode 100644 index 0000000..f47370b --- /dev/null +++ b/gcc/diagnostics/context.h @@ -0,0 +1,959 @@ +/* Declare diagnostics::context and related types. + Copyright (C) 2000-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_DIAGNOSTICS_CONTEXT_H +#define GCC_DIAGNOSTICS_CONTEXT_H + +#include "lazily-created.h" +#include "unique-argv.h" +#include "diagnostics/option-classifier.h" +#include "diagnostics/context-options.h" + +namespace diagnostics { + + namespace changes { + class change_set; + } + + namespace digraphs { class digraph; } + + namespace logical_locations { + class manager; + } + + class buffer; + class client_data_hooks; + class diagram; + class sink; + class text_sink; + + class source_effect_info; + +} // namespace diagnostics + +namespace text_art +{ + class theme; +} // namespace text_art + +namespace xml +{ + class printer; +} // namespace xml + +namespace diagnostics { + +/* Forward declarations. */ +class context; +class location_print_policy; +class source_print_policy; + +typedef void (*text_starter_fn) (text_sink &, + const diagnostic_info *); + +struct to_text; +struct to_html; + +extern pretty_printer *get_printer (to_text &); + +template <typename TextOrHtml> +using start_span_fn = void (*) (const location_print_policy &, + TextOrHtml &text_or_html, + expanded_location); + +typedef void (*text_finalizer_fn) (text_sink &, + const diagnostic_info *, + enum kind); + +/* Abstract base class for the diagnostic subsystem to make queries + about command-line options. */ + +class option_manager +{ +public: + virtual ~option_manager () {} + + /* Return 1 if option OPT_ID is enabled, 0 if it is disabled, + or -1 if it isn't a simple on-off switch + (or if the value is unknown, typically set later in target). */ + virtual int option_enabled_p (option_id opt_id) const = 0; + + /* Return malloced memory for the name of the option OPT_ID + which enabled a diagnostic, originally of type ORIG_DIAG_KIND but + possibly converted to DIAG_KIND by options such as -Werror. + May return NULL if no name is to be printed. + May be passed 0 as well as the index of a particular option. */ + virtual char *make_option_name (option_id opt_id, + enum kind orig_diag_kind, + enum kind diag_kind) const = 0; + + /* Return malloced memory for a URL describing the option that controls + a diagnostic. + May return NULL if no URL is available. + May be passed 0 as well as the index of a particular option. */ + virtual char *make_option_url (option_id opt_id) const = 0; +}; + +/* A bundle of options relating to printing the user's source code + (potentially with a margin, underlining, labels, etc). */ + +struct source_printing_options +{ + /* True if we should print the source line with a caret indicating + the location. + Corresponds to -fdiagnostics-show-caret. */ + bool enabled; + + /* Maximum width of the source line printed. */ + int max_width; + + /* Character used at the caret when printing source locations. */ + char caret_chars[rich_location::STATICALLY_ALLOCATED_RANGES]; + + /* When printing source code, should the characters at carets and ranges + be colorized? (assuming colorization is on at all). + This should be true for frontends that generate range information + (so that the ranges of code are colorized), + and false for frontends that merely specify points within the + source code (to avoid e.g. colorizing just the first character in + a token, which would look strange). */ + bool colorize_source_p; + + /* When printing source code, should labelled ranges be printed? + Corresponds to -fdiagnostics-show-labels. */ + bool show_labels_p; + + /* When printing source code, should there be a left-hand margin + showing line numbers? + Corresponds to -fdiagnostics-show-line-numbers. */ + bool show_line_numbers_p; + + /* If printing source code, what should the minimum width of the margin + be? Line numbers will be right-aligned, and padded to this width. + Corresponds to -fdiagnostics-minimum-margin-width=VALUE. */ + int min_margin_width; + + /* Usable by plugins; if true, print a debugging ruler above the + source output. */ + bool show_ruler_p; + + /* When printing events in an inline path, should we print lines + visualizing links between related events (e.g. for CFG paths)? + Corresponds to -fdiagnostics-show-event-links. */ + bool show_event_links_p; +}; + +/* A bundle of state for determining column numbers in diagnostics + (tab stops, whether to start at 0 or 1, etc). + Uses a file_cache to handle tabs. */ + +class column_policy +{ +public: + column_policy (const context &dc); + + int converted_column (expanded_location s) const; + + label_text get_location_text (const expanded_location &s, + bool show_column, + bool colorize) const; + + int get_tabstop () const { return m_tabstop; } + +private: + file_cache &m_file_cache; + enum diagnostics_column_unit m_column_unit; + int m_column_origin; + int m_tabstop; +}; + +/* A bundle of state for printing locations within diagnostics + (e.g. "FILENAME:LINE:COLUMN"), to isolate the interactions between + context and the start_span callbacks. */ + +class location_print_policy +{ +public: + location_print_policy (const context &dc); + location_print_policy (const text_sink &); + + bool show_column_p () const { return m_show_column; } + + const column_policy & + get_column_policy () const { return m_column_policy; } + + void + print_text_span_start (const context &dc, + pretty_printer &pp, + const expanded_location &exploc); + + void + print_html_span_start (const context &dc, + xml::printer &xp, + const expanded_location &exploc); + +private: + column_policy m_column_policy; + bool m_show_column; +}; + +/* Abstract base class for optionally supplying extra tags when writing + out annotation labels in HTML output. */ + +class html_label_writer +{ +public: + virtual ~html_label_writer () {} + virtual void begin_label () = 0; + virtual void end_label () = 0; +}; + +/* A bundle of state for printing source within a diagnostic, + to isolate the interactions between context and the + implementation of diagnostic_show_locus. */ + +class source_print_policy +{ +public: + source_print_policy (const context &); + source_print_policy (const context &, + const source_printing_options &); + + void + print (pretty_printer &pp, + const rich_location &richloc, + enum kind diagnostic_kind, + source_effect_info *effect_info) const; + + void + print_as_html (xml::printer &xp, + const rich_location &richloc, + enum kind diagnostic_kind, + source_effect_info *effect_info, + html_label_writer *label_writer) const; + + const source_printing_options & + get_options () const { return m_options; } + + start_span_fn<to_text> + get_text_start_span_fn () const { return m_text_start_span_cb; } + + start_span_fn<to_html> + get_html_start_span_fn () const { return m_html_start_span_cb; } + + file_cache & + get_file_cache () const { return m_file_cache; } + + enum diagnostics_escape_format + get_escape_format () const + { + return m_escape_format; + } + + text_art::theme * + get_diagram_theme () const { return m_diagram_theme; } + + const column_policy &get_column_policy () const + { + return m_location_policy.get_column_policy (); + } + + const location_print_policy &get_location_policy () const + { + return m_location_policy; + } + +private: + const source_printing_options &m_options; + location_print_policy m_location_policy; + start_span_fn<to_text> m_text_start_span_cb; + start_span_fn<to_html> m_html_start_span_cb; + file_cache &m_file_cache; + + /* Other data copied from context. */ + text_art::theme *m_diagram_theme; + enum diagnostics_escape_format m_escape_format; +}; + +/* A collection of counters of diagnostics, per-kind + (e.g. "3 errors and 1 warning"), for use by both context + and by diagnostics::buffer. */ + +struct counters +{ + counters (); + + void dump (FILE *out, int indent) const; + void DEBUG_FUNCTION dump () const { dump (stderr, 0); } + + int get_count (enum kind kind) const + { + return m_count_for_kind[static_cast<size_t> (kind)]; + } + + void move_to (counters &dest); + void clear (); + + int m_count_for_kind[static_cast<size_t> (kind::last_diagnostic_kind)]; +}; + +/* This class encapsulates the state of the diagnostics subsystem + as a whole (either directly, or via owned objects of other classes, to + avoid global variables). + + It has responsibility for: + - being a central place for clients to report diagnostics + - reporting those diagnostics to zero or more output sinks + (e.g. text vs SARIF) + - providing a "dump" member function for a debug dump of the state of + the diagnostics subsytem + - direct vs buffered diagnostics (see class diagnostics::buffer) + - tracking the original argv of the program (for SARIF output) + - crash-handling + + It delegates responsibility to various other classes: + - the various output sinks (instances of diagnostics::sink + subclasses) + - formatting of messages (class pretty_printer) + - an optional urlifier to inject URLs into formatted messages + - counting the number of diagnostics reported of each kind + (class diagnostics::counters) + - calling out to a option_manager to determine if + a particular warning is enabled or disabled + - tracking pragmas that enable/disable warnings in a range of + source code + - a cache for use when quoting the user's source code (class file_cache) + - a text_art::theme + - a diagnostics::changes::change_set for generating patches from fix-it hints + - diagnostics::client_data_hooks for metadata. + + Try to avoid adding new responsibilities to this class itself, to avoid + the "blob" anti-pattern. */ + +class context +{ +public: + /* Give access to m_text_callbacks. */ + // FIXME: these need updating + friend text_starter_fn & + text_starter (context *dc); + friend start_span_fn<to_text> & + start_span (context *dc); + friend text_finalizer_fn & + text_finalizer (context *dc); + + friend class source_print_policy; + friend class text_sink; + friend class buffer; + + typedef void (*set_locations_callback_t) (context *, + diagnostic_info *); + + void initialize (int n_opts); + void color_init (int value); + void urls_init (int value); + void set_pretty_printer (std::unique_ptr<pretty_printer> pp); + void refresh_output_sinks (); + + void finish (); + + void dump (FILE *out) const; + void DEBUG_FUNCTION dump () const { dump (stderr); } + + bool execution_failed_p () const; + + void set_original_argv (unique_argv original_argv); + const char * const *get_original_argv () + { + return const_cast<const char * const *> (m_original_argv); + } + + void set_set_locations_callback (set_locations_callback_t cb) + { + m_set_locations_cb = cb; + } + + void + initialize_input_context (diagnostic_input_charset_callback ccb, + bool should_skip_bom); + + void begin_group (); + void end_group (); + + void push_nesting_level (); + void pop_nesting_level (); + + bool warning_enabled_at (location_t loc, option_id opt_id); + + bool option_unspecified_p (option_id opt_id) const + { + return m_option_classifier.option_unspecified_p (opt_id); + } + + bool emit_diagnostic_with_group (enum kind kind, + rich_location &richloc, + const metadata *metadata, + option_id opt_id, + const char *gmsgid, ...) + ATTRIBUTE_GCC_DIAG(6,7); + bool emit_diagnostic_with_group_va (enum kind kind, + rich_location &richloc, + const metadata *metadata, + option_id opt_id, + const char *gmsgid, va_list *ap) + ATTRIBUTE_GCC_DIAG(6,0); + + bool report_diagnostic (diagnostic_info *); + void report_verbatim (text_info &); + + /* Report a directed graph associated with the run as a whole + to any sinks that support directed graphs. */ + void + report_global_digraph (const lazily_created<digraphs::digraph> &); + + enum kind + classify_diagnostic (option_id opt_id, + enum kind new_kind, + location_t where) + { + return m_option_classifier.classify_diagnostic (this, + opt_id, + new_kind, + where); + } + + void push_diagnostics (location_t where ATTRIBUTE_UNUSED) + { + m_option_classifier.push (); + } + void pop_diagnostics (location_t where) + { + m_option_classifier.pop (where); + } + + void maybe_show_locus (const rich_location &richloc, + const source_printing_options &opts, + enum kind diagnostic_kind, + pretty_printer &pp, + source_effect_info *effect_info); + void maybe_show_locus_as_html (const rich_location &richloc, + const source_printing_options &opts, + enum kind diagnostic_kind, + xml::printer &xp, + source_effect_info *effect_info, + html_label_writer *label_writer); + + void emit_diagram (const diagram &diag); + + /* Various setters for use by option-handling logic. */ + void set_sink (std::unique_ptr<sink> sink_); + void set_text_art_charset (enum diagnostic_text_art_charset charset); + void set_client_data_hooks (std::unique_ptr<client_data_hooks> hooks); + + void push_owned_urlifier (std::unique_ptr<urlifier>); + void push_borrowed_urlifier (const urlifier &); + void pop_urlifier (); + + void initialize_fixits_change_set (); + void set_warning_as_error_requested (bool val) + { + m_warning_as_error_requested = val; + } + void set_report_bug (bool val) { m_report_bug = val; } + void set_extra_output_kind (enum diagnostics_extra_output_kind kind) + { + m_extra_output_kind = kind; + } + void set_show_cwe (bool val) { m_show_cwe = val; } + void set_show_rules (bool val) { m_show_rules = val; } + void set_show_highlight_colors (bool val); + void set_path_format (enum diagnostic_path_format val) + { + m_path_format = val; + } + void set_show_path_depths (bool val) { m_show_path_depths = val; } + void set_show_option_requested (bool val) { m_show_option_requested = val; } + void set_max_errors (int val) { m_max_errors = val; } + void set_escape_format (enum diagnostics_escape_format val) + { + m_escape_format = val; + } + + void set_format_decoder (printer_fn format_decoder); + void set_prefixing_rule (diagnostic_prefixing_rule_t rule); + + /* Various accessors. */ + bool warning_as_error_requested_p () const + { + return m_warning_as_error_requested; + } + bool show_path_depths_p () const { return m_show_path_depths; } + sink &get_sink (size_t idx) const; + enum diagnostic_path_format get_path_format () const { return m_path_format; } + enum diagnostics_escape_format get_escape_format () const + { + return m_escape_format; + } + + file_cache & + get_file_cache () const + { + gcc_assert (m_file_cache); + return *m_file_cache; + } + + changes::change_set *get_fixits_change_set () const + { + return m_fixits_change_set; + } + const client_data_hooks *get_client_data_hooks () const + { + return m_client_data_hooks; + } + + const logical_locations::manager * + get_logical_location_manager () const; + + const urlifier *get_urlifier () const; + + text_art::theme *get_diagram_theme () const { return m_diagrams.m_theme; } + + int &diagnostic_count (enum kind kind) + { + return m_diagnostic_counters.m_count_for_kind[static_cast<size_t> (kind)]; + } + int diagnostic_count (enum kind kind) const + { + return m_diagnostic_counters.get_count (kind); + } + + /* Option-related member functions. */ + inline bool option_enabled_p (option_id opt_id) const + { + if (!m_option_mgr) + return true; + return m_option_mgr->option_enabled_p (opt_id); + } + + inline char *make_option_name (option_id opt_id, + enum kind orig_diag_kind, + enum kind diag_kind) const + { + if (!m_option_mgr) + return nullptr; + return m_option_mgr->make_option_name (opt_id, + orig_diag_kind, + diag_kind); + } + + inline char *make_option_url (option_id opt_id) const + { + if (!m_option_mgr) + return nullptr; + return m_option_mgr->make_option_url (opt_id); + } + + void + set_option_manager (std::unique_ptr<option_manager> mgr, + unsigned lang_mask); + + unsigned get_lang_mask () const + { + return m_lang_mask; + } + + bool diagnostic_impl (rich_location *, const metadata *, + option_id, const char *, + va_list *, enum kind) ATTRIBUTE_GCC_DIAG(5,0); + bool diagnostic_n_impl (rich_location *, const metadata *, + option_id, unsigned HOST_WIDE_INT, + const char *, const char *, va_list *, + enum kind) ATTRIBUTE_GCC_DIAG(7,0); + + int get_diagnostic_nesting_level () const + { + return m_diagnostic_groups.m_diagnostic_nesting_level; + } + + char *build_indent_prefix () const; + + int + pch_save (FILE *f) + { + return m_option_classifier.pch_save (f); + } + + int + pch_restore (FILE *f) + { + return m_option_classifier.pch_restore (f); + } + + + void set_diagnostic_buffer (buffer *); + buffer *get_diagnostic_buffer () const + { + return m_diagnostic_buffer; + } + void clear_diagnostic_buffer (buffer &); + void flush_diagnostic_buffer (buffer &); + + std::unique_ptr<pretty_printer> clone_printer () const + { + return m_reference_printer->clone (); + } + + pretty_printer *get_reference_printer () const + { + return m_reference_printer; + } + + void + add_sink (std::unique_ptr<sink>); + + void remove_all_output_sinks (); + + bool supports_fnotice_on_stderr_p () const; + + /* Raise SIGABRT on any diagnostic of severity kind::error or higher. */ + void + set_abort_on_error (bool val) + { + m_abort_on_error = val; + } + + /* Accessor for use in serialization, e.g. by C++ modules. */ + auto & + get_classification_history () + { + return m_option_classifier.m_classification_history; + } + + void set_main_input_filename (const char *filename); + + void + set_permissive_option (option_id opt_permissive) + { + m_opt_permissive = opt_permissive; + } + + void + set_fatal_errors (bool fatal_errors) + { + m_fatal_errors = fatal_errors; + } + + void + set_internal_error_callback (void (*cb) (context *, + const char *, + va_list *)) + { + m_internal_error = cb; + } + + void + set_adjust_diagnostic_info_callback (void (*cb) (context *, + diagnostic_info *)) + { + m_adjust_diagnostic_info = cb; + } + + void + inhibit_notes () { m_inhibit_notes_p = true; } + + source_printing_options & + get_source_printing_options () + { + return m_source_printing; + } + const source_printing_options & + get_source_printing_options () const + { + return m_source_printing; + } + + void set_caret_max_width (int value); + +private: + void error_recursion () ATTRIBUTE_NORETURN; + + bool diagnostic_enabled (diagnostic_info *diagnostic); + + void get_any_inlining_info (diagnostic_info *diagnostic); + + void check_max_errors (bool flush); + void action_after_output (enum kind diag_kind); + + /* Data members. + Ideally, all of these would be private. */ + +private: + /* A reference instance of pretty_printer created by the client + and owned by the context. Used for cloning when creating/adding + output formats. + Owned by the context; this would be a std::unique_ptr if + context had a proper ctor. */ + pretty_printer *m_reference_printer; + + /* Cache of source code. + Owned by the context; this would be a std::unique_ptr if + context had a proper ctor. */ + file_cache *m_file_cache; + + /* The number of times we have issued diagnostics. */ + counters m_diagnostic_counters; + + /* True if it has been requested that warnings be treated as errors. */ + bool m_warning_as_error_requested; + + /* The number of option indexes that can be passed to warning() et + al. */ + int m_n_opts; + + /* The stack of sets of overridden diagnostic option severities. */ + option_classifier m_option_classifier; + + /* True if we should print any CWE identifiers associated with + diagnostics. */ + bool m_show_cwe; + + /* True if we should print any rules associated with diagnostics. */ + bool m_show_rules; + + /* How should diagnostics::paths::path objects be printed. */ + enum diagnostic_path_format m_path_format; + + /* True if we should print stack depths when printing diagnostic paths. */ + bool m_show_path_depths; + + /* True if we should print the command line option which controls + each diagnostic, if known. */ + bool m_show_option_requested; + + /* True if we should raise a SIGABRT on errors. */ + bool m_abort_on_error; + +public: + /* True if we should show the column number on diagnostics. */ + bool m_show_column; + + /* True if pedwarns are errors. */ + bool m_pedantic_errors; + + /* True if permerrors are warnings. */ + bool m_permissive; + +private: + /* The option to associate with turning permerrors into warnings, + if any. */ + option_id m_opt_permissive; + + /* True if errors are fatal. */ + bool m_fatal_errors; + +public: + /* True if all warnings should be disabled. */ + bool m_inhibit_warnings; + + /* True if warnings should be given in system headers. */ + bool m_warn_system_headers; + +private: + /* Maximum number of errors to report. */ + int m_max_errors; + + /* Client-supplied callbacks for use in text output. */ + struct { + /* This function is called before any message is printed out. It is + responsible for preparing message prefix and such. For example, it + might say: + In file included from "/usr/local/include/curses.h:5: + from "/home/gdr/src/nifty_printer.h:56: + ... + */ + text_starter_fn m_begin_diagnostic; + + /* This function is called by diagnostic_show_locus in between + disjoint spans of source code, so that the context can print + something to indicate that a new span of source code has begun. */ + start_span_fn<to_text> m_text_start_span; + start_span_fn<to_html> m_html_start_span; + + /* This function is called after the diagnostic message is printed. */ + text_finalizer_fn m_end_diagnostic; + } m_text_callbacks; + + /* Client hook to report an internal error. */ + void (*m_internal_error) (context *, const char *, va_list *); + + /* Client hook to adjust properties of the given diagnostic that we're + about to issue, such as its kind. */ + void (*m_adjust_diagnostic_info)(context *, diagnostic_info *); + + /* Owned by the context; this would be a std::unique_ptr if + context had a proper ctor. */ + option_manager *m_option_mgr; + unsigned m_lang_mask; + + /* A stack of optional hooks for adding URLs to quoted text strings in + diagnostics. Only used for the main diagnostic message. + Typically a single one owner by the context, but can be temporarily + overridden by a borrowed urlifier (e.g. on-stack). */ + struct urlifier_stack_node + { + urlifier *m_urlifier; + bool m_owned; + }; + auto_vec<urlifier_stack_node> *m_urlifier_stack; + +public: + /* Auxiliary data for client. */ + void *m_client_aux_data; + + /* Used to detect that the last caret was printed at the same location. */ + location_t m_last_location; + +private: + int m_lock; + + bool m_inhibit_notes_p; + + source_printing_options m_source_printing; + + /* True if -freport-bug option is used. */ + bool m_report_bug; + + /* Used to specify additional diagnostic output to be emitted after the + rest of the diagnostic. This is for implementing + -fdiagnostics-parseable-fixits and GCC_EXTRA_DIAGNOSTIC_OUTPUT. */ + enum diagnostics_extra_output_kind m_extra_output_kind; + +public: + /* What units to use when outputting the column number. */ + enum diagnostics_column_unit m_column_unit; + + /* The origin for the column number (1-based or 0-based typically). */ + int m_column_origin; + + /* The size of the tabstop for tab expansion. */ + int m_tabstop; + +private: + /* How should non-ASCII/non-printable bytes be escaped when + a diagnostic suggests escaping the source code on output. */ + enum diagnostics_escape_format m_escape_format; + + /* If non-NULL, a diagnostics::changes::change_set to which fix-it hints + should be applied, for generating patches. + Owned by the context; this would be a std::unique_ptr if + context had a proper ctor. */ + changes::change_set *m_fixits_change_set; + + /* Fields relating to diagnostic groups. */ + struct { + /* How many diagnostic_group instances are currently alive. */ + int m_group_nesting_depth; + + /* How many nesting levels have been pushed within this group. */ + int m_diagnostic_nesting_level; + + /* How many diagnostics have been emitted since the bottommost + diagnostic_group was pushed. */ + int m_emission_count; + + /* The "group+diagnostic" nesting depth from which to inhibit notes. */ + int m_inhibiting_notes_from; + } m_diagnostic_groups; + + void inhibit_notes_in_group (bool inhibit = true); + bool notes_inhibited_in_group () const; + + /* The various sinks to which diagnostics are to be outputted + (text vs structured formats such as SARIF). + The sinks are owned by the context; this would be a + std::vector<std::unique_ptr> if context had a + proper ctor. */ + auto_vec<sink *> m_sinks; + + /* Callback to set the locations of call sites along the inlining + stack corresponding to a diagnostic location. Needed to traverse + the BLOCK_SUPERCONTEXT() chain hanging off the LOCATION_BLOCK() + of a diagnostic's location. */ + set_locations_callback_t m_set_locations_cb; + + /* A bundle of hooks for providing data to the context about its client + e.g. version information, plugins, etc. + Used by SARIF output to give metadata about the client that's + producing diagnostics. + Owned by the context; this would be a std::unique_ptr if + context had a proper ctor. */ + client_data_hooks *m_client_data_hooks; + + /* Support for diagrams. */ + struct + { + /* Theme to use when generating diagrams. + Can be NULL (if text art is disabled). + Owned by the context; this would be a std::unique_ptr if + context had a proper ctor. */ + text_art::theme *m_theme; + + } m_diagrams; + + /* Owned by the context. */ + char **m_original_argv; + + /* Borrowed pointer to the active diagnostics::buffer, if any. + If null (the default), then diagnostics that are reported to the + context are immediately issued to the output format. + If non-null, then diagnostics that are reported to the context + are buffered in the buffer, and may be issued to the output format + later (if the buffer is flushed), moved to other buffers, or + discarded (if the buffer is cleared). */ + buffer *m_diagnostic_buffer; +}; + +/* Client supplied function to announce a diagnostic + (for text-based diagnostic output). */ +inline text_starter_fn & +text_starter (context *dc) +{ + return dc->m_text_callbacks.m_begin_diagnostic; +} + +/* Client supplied function called between disjoint spans of source code, + so that the context can print + something to indicate that a new span of source code has begun. */ +inline start_span_fn<to_text> & +start_span (context *dc) +{ + return dc->m_text_callbacks.m_text_start_span; +} + +/* Client supplied function called after a diagnostic message is + displayed (for text-based diagnostic output). */ +inline text_finalizer_fn & +text_finalizer (context *dc) +{ + return dc->m_text_callbacks.m_end_diagnostic; +} + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_CONTEXT_H */ diff --git a/gcc/diagnostics/diagnostic-info.h b/gcc/diagnostics/diagnostic-info.h new file mode 100644 index 0000000..052fef5 --- /dev/null +++ b/gcc/diagnostics/diagnostic-info.h @@ -0,0 +1,75 @@ +/* Various declarations for language-independent diagnostics subroutines. + Copyright (C) 2000-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_DIAGNOSTICS_DIAGNOSTIC_INFO_H +#define GCC_DIAGNOSTICS_DIAGNOSTIC_INFO_H + +namespace diagnostics { + +class metadata; + +/* A diagnostic is described by the MESSAGE to send, the FILE and LINE of + its context and its KIND (ice, error, warning, note, ...) See complete + list in diagnostics/kinds.def. */ + +struct diagnostic_info +{ + diagnostic_info () + : m_message (), + m_richloc (), + m_metadata (), + m_x_data (), + m_kind (), + m_option_id (), + m_iinfo () + { } + + /* Text to be formatted. */ + text_info m_message; + + /* The location at which the diagnostic is to be reported. */ + rich_location *m_richloc; + + /* An optional bundle of metadata associated with the diagnostic + (or NULL). */ + const metadata *m_metadata; + + /* Auxiliary data for client. */ + void *m_x_data; + /* The kind of diagnostic it is about. */ + kind m_kind; + /* Which OPT_* directly controls this diagnostic. */ + option_id m_option_id; + + /* Inlining context containing locations for each call site along + the inlining stack. */ + struct inlining_info + { + /* Locations along the inlining stack. */ + auto_vec<location_t, 8> m_ilocs; + /* The abstract origin of the location. */ + void *m_ao; + /* Set if every M_ILOCS element is in a system header. */ + bool m_allsyslocs; + } m_iinfo; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_DIAGNOSTIC_INFO_H */ diff --git a/gcc/diagnostics/diagnostics-selftests.cc b/gcc/diagnostics/diagnostics-selftests.cc new file mode 100644 index 0000000..94a212a --- /dev/null +++ b/gcc/diagnostics/diagnostics-selftests.cc @@ -0,0 +1,59 @@ +/* Selftest support for diagnostics. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "selftest.h" +#include "diagnostics/diagnostics-selftests.h" + +#if CHECKING_P + +namespace diagnostics { + +namespace selftest { + +/* Run all diagnostics-specific selftests, + apart from context_cc_tests, which according to + https://gcc.gnu.org/pipermail/gcc/2021-November/237703.html + has some language-specific assumptions, and thus is run from + c_family_tests instead. */ + +void +run_diagnostics_selftests () +{ + color_cc_tests (); + file_cache_cc_tests (); + source_printing_cc_tests (); + html_sink_cc_tests (); + sarif_sink_cc_tests (); + digraphs_cc_tests (); + output_spec_cc_tests (); + state_graphs_cc_tests (); + lazy_paths_cc_tests (); + paths_output_cc_tests (); + changes_cc_tests (); +} + +} /* end of namespace diagnostics::selftest. */ + +} // namespace diagnostics + +#endif /* #if CHECKING_P */ diff --git a/gcc/diagnostics/diagnostics-selftests.h b/gcc/diagnostics/diagnostics-selftests.h new file mode 100644 index 0000000..994ebad --- /dev/null +++ b/gcc/diagnostics/diagnostics-selftests.h @@ -0,0 +1,55 @@ +/* Selftests for diagnostics. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_SELFTESTS_H +#define GCC_DIAGNOSTICS_SELFTESTS_H + +#if CHECKING_P + +namespace diagnostics { + +namespace selftest { + +extern void run_diagnostics_selftests (); + +/* Declarations for specific families of tests (by source file within + "diagnostics/"), in alphabetical order. */ + +extern void changes_cc_tests (); +extern void color_cc_tests (); +extern void context_cc_tests (); +extern void digraphs_cc_tests (); +extern void file_cache_cc_tests (); +extern void html_sink_cc_tests (); +extern void lazy_paths_cc_tests (); +extern void output_spec_cc_tests (); +extern void paths_output_cc_tests (); +extern void sarif_sink_cc_tests (); +extern void selftest_logical_locations_cc_tests (); +extern void source_printing_cc_tests (); +extern void state_graphs_cc_tests (); + +} /* end of namespace diagnostics::selftest. */ + +} // namespace diagnostics + +#endif /* #if CHECKING_P */ + +#endif /* GCC_DIAGNOSTICS_SELFTESTS_H */ diff --git a/gcc/diagnostics/diagram.h b/gcc/diagnostics/diagram.h new file mode 100644 index 0000000..2d33b10 --- /dev/null +++ b/gcc/diagnostics/diagram.h @@ -0,0 +1,55 @@ +/* Support for diagrams within diagnostics. + Copyright (C) 2023-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_DIAGRAM_H +#define GCC_DIAGNOSTICS_DIAGRAM_H + +namespace text_art +{ + class canvas; +} // namespace text_art + +namespace diagnostics { + +/* A text art diagram, along with an "alternative text" string + describing it. */ + +class diagram +{ + public: + diagram (const text_art::canvas &canvas, + const char *alt_text) + : m_canvas (canvas), + m_alt_text (alt_text) + { + gcc_assert (alt_text); + } + + const text_art::canvas &get_canvas () const { return m_canvas; } + const char *get_alt_text () const { return m_alt_text; } + + private: + const text_art::canvas &m_canvas; + const char *const m_alt_text; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_DIAGRAM_H */ diff --git a/gcc/diagnostics/digraphs.cc b/gcc/diagnostics/digraphs.cc new file mode 100644 index 0000000..4a2ea4f --- /dev/null +++ b/gcc/diagnostics/digraphs.cc @@ -0,0 +1,464 @@ +/* Directed graphs associated with a diagnostic. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + +#define INCLUDE_ALGORITHM +#define INCLUDE_MAP +#define INCLUDE_SET +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "config.h" +#include "system.h" +#include "coretypes.h" + +#include "graphviz.h" +#include "diagnostics/digraphs.h" +#include "diagnostics/sarif-sink.h" + +#include "selftest.h" + +using digraph = diagnostics::digraphs::digraph; +using digraph_node = diagnostics::digraphs::node; +using digraph_edge = diagnostics::digraphs::edge; + +namespace { + +class conversion_to_dot +{ +public: + std::unique_ptr<dot::graph> + make_dot_graph_from_diagnostic_graph (const digraph &); + + std::unique_ptr<dot::stmt> + make_dot_node_from_digraph_node (const digraph_node &); + + std::unique_ptr<dot::edge_stmt> + make_dot_edge_from_digraph_edge (const digraph_edge &); + + dot::id + get_dot_id_for_node (const digraph_node &); + + bool + has_edges_p (const digraph_node &); + +private: + std::set<const digraph_node *> m_nodes_with_edges; + std::map<const digraph_node *, dot::stmt *> m_node_map; +}; + +} // anonymous namespace + +// class conversion_to_dot + +std::unique_ptr<dot::graph> +conversion_to_dot:: +make_dot_graph_from_diagnostic_graph (const diagnostics::digraphs::digraph &input_graph) +{ + auto output_graph = std::make_unique<dot::graph> (); + + if (const char *description = input_graph.get_description ()) + output_graph->m_stmt_list.add_attr (dot::id ("label"), + dot::id (description)); + + const int num_nodes = input_graph.get_num_nodes (); + const int num_edges = input_graph.get_num_edges (); + + /* Determine which nodes have in-edges and out-edges. */ + for (int i = 0; i < num_edges; ++i) + { + const digraph_edge &input_edge = input_graph.get_edge (i); + m_nodes_with_edges.insert (&input_edge.get_src_node ()); + m_nodes_with_edges.insert (&input_edge.get_dst_node ()); + } + + for (int i = 0; i < num_nodes; ++i) + { + const digraph_node &input_node = input_graph.get_node (i); + auto dot_node_stmt = make_dot_node_from_digraph_node (input_node); + output_graph->m_stmt_list.add_stmt (std::move (dot_node_stmt)); + } + + for (int i = 0; i < num_edges; ++i) + { + const digraph_edge &input_edge = input_graph.get_edge (i); + auto dot_edge_stmt = make_dot_edge_from_digraph_edge (input_edge); + output_graph->m_stmt_list.add_stmt (std::move (dot_edge_stmt)); + } + + return output_graph; +} + +std::unique_ptr<dot::stmt> +conversion_to_dot:: +make_dot_node_from_digraph_node (const diagnostics::digraphs::node &input_node) +{ + dot::id dot_id (get_dot_id_for_node (input_node)); + + /* For now, we can only do either edges or children, not both + ...but see https://graphviz.org/docs/attrs/compound/ */ + + if (has_edges_p (input_node)) + { + auto output_node + = std::make_unique<dot::node_stmt> (std::move (dot_id)); + m_node_map[&input_node] = output_node.get (); + if (const char *label = input_node.get_label ()) + output_node->set_label (dot::id (label)); + return output_node; + } + else + { + auto output_node = std::make_unique<dot::subgraph> (std::move (dot_id)); + m_node_map[&input_node] = output_node.get (); + if (const char *label = input_node.get_label ()) + output_node->add_attr (dot::id ("label"), dot::id (label)); + const int num_children = input_node.get_num_children (); + for (int i = 0; i < num_children; ++i) + { + const digraph_node &input_child = input_node.get_child (i); + auto dot_child_stmt = make_dot_node_from_digraph_node (input_child); + output_node->m_stmt_list.add_stmt (std::move (dot_child_stmt)); + } + return output_node; + } +} + +std::unique_ptr<dot::edge_stmt> +conversion_to_dot:: +make_dot_edge_from_digraph_edge (const digraph_edge &input_edge) +{ + const digraph_node &src_dnode = input_edge.get_src_node (); + const digraph_node &dst_dnode = input_edge.get_dst_node (); + auto output_edge + = std::make_unique<dot::edge_stmt> + (get_dot_id_for_node (src_dnode), + get_dot_id_for_node (dst_dnode)); + if (const char *label = input_edge.get_label ()) + output_edge->set_label (dot::id (label)); + return output_edge; +} + +dot::id +conversion_to_dot::get_dot_id_for_node (const digraph_node &input_node) +{ + if (has_edges_p (input_node)) + return input_node.get_id (); + else + return std::string ("cluster_") + input_node.get_id (); +} + +bool +conversion_to_dot::has_edges_p (const digraph_node &input_node) +{ + return m_nodes_with_edges.find (&input_node) != m_nodes_with_edges.end (); +} + +// class object + +const char * +diagnostics::digraphs::object:: +get_attr (const char *key_prefix, const char *key) const +{ + if (!m_property_bag) + return nullptr; + std::string prefixed_key = std::string (key_prefix) + key; + if (json::value *jv = m_property_bag->get (prefixed_key.c_str ())) + if (json::string *jstr = jv->dyn_cast_string ()) + return jstr->get_string (); + return nullptr; +} + +void +diagnostics::digraphs::object:: +set_attr (const char *key_prefix, const char *key, const char *value) +{ + set_json_attr (key_prefix, key, std::make_unique<json::string> (value)); +} + +void +diagnostics::digraphs::object:: +set_json_attr (const char *key_prefix, const char *key, std::unique_ptr<json::value> value) +{ + std::string prefixed_key = std::string (key_prefix) + key; + if (!m_property_bag) + m_property_bag = std::make_unique<json::object> (); + m_property_bag->set (prefixed_key.c_str (), std::move (value)); +} + +// class digraph + +DEBUG_FUNCTION void +diagnostics::digraphs::digraph::dump () const +{ + make_json_sarif_graph ()->dump (); +} + +std::unique_ptr<json::object> +diagnostics::digraphs::digraph::make_json_sarif_graph () const +{ + return make_sarif_graph (*this, nullptr, nullptr); +} + +std::unique_ptr<dot::graph> +diagnostics::digraphs::digraph::make_dot_graph () const +{ + conversion_to_dot to_dot; + return to_dot.make_dot_graph_from_diagnostic_graph (*this); +} + +std::unique_ptr<diagnostics::digraphs::digraph> +diagnostics::digraphs::digraph::clone () const +{ + auto result = std::make_unique<diagnostics::digraphs::digraph> (); + + if (get_property_bag ()) + result->set_property_bag (get_property_bag ()->clone_as_object ()); + + std::map<diagnostics::digraphs::node *, diagnostics::digraphs::node *> node_mapping; + + for (auto &iter : m_nodes) + result->add_node (iter->clone (*result, node_mapping)); + for (auto &iter : m_edges) + result->add_edge (iter->clone (*result, node_mapping)); + + return result; +} + +void +diagnostics::digraphs::digraph::add_edge (const char *id, + node &src_node, + node &dst_node, + const char *label) +{ + auto e = std::make_unique<digraph_edge> (*this, + id, + src_node, + dst_node); + if (label) + e->set_label (label); + add_edge (std::move (e)); +} + +/* Utility function for edge ids: either use EDGE_ID, or + generate a unique one for when we don't care about the name. + + Edges in SARIF "SHALL" have an id that's unique within the graph + (SARIF 2.1.0 §3.41.2). This is so that graph traversals can refer + to edges by id (SARIF 2.1.0's §3.43.2 edgeId property). */ + +std::string +diagnostics::digraphs::digraph::make_edge_id (const char *edge_id) +{ + /* If we have an id, use it. */ + if (edge_id) + return edge_id; + + /* Otherwise, generate a unique one of the form "edgeN". */ + while (true) + { + auto candidate (std::string ("edge") + + std::to_string (m_next_edge_id_index++)); + auto iter = m_id_to_edge_map.find (candidate); + if (iter != m_id_to_edge_map.end ()) + { + // Try again with the next index... + continue; + } + return candidate; + } +} + +// class node + +DEBUG_FUNCTION void +diagnostics::digraphs::node::dump () const +{ + to_json_sarif_node ()->dump (); +} + +std::unique_ptr<json::object> +diagnostics::digraphs::node::to_json_sarif_node () const +{ + return make_sarif_node (*this, nullptr, nullptr); +} + +std::unique_ptr<diagnostics::digraphs::node> +diagnostics::digraphs::node::clone (digraph &new_graph, + std::map<node *, node *> &node_mapping) const +{ + auto result + = std::make_unique<diagnostics::digraphs::node> (new_graph, + get_id ()); + node_mapping.insert ({const_cast <node *> (this), result.get ()}); + + result->set_logical_loc (m_logical_loc); + + if (get_property_bag ()) + result->set_property_bag (get_property_bag ()->clone_as_object ()); + + for (auto &iter : m_children) + result->add_child (iter->clone (new_graph, node_mapping)); + + return result; +} + +// class edge + +std::unique_ptr<digraph_edge> +digraph_edge::clone (digraph &new_graph, + const std::map<node *, node *> &node_mapping) const +{ + auto iter_new_src = node_mapping.find (&m_src_node); + gcc_assert (iter_new_src != node_mapping.end ()); + auto iter_new_dst = node_mapping.find (&m_dst_node); + gcc_assert (iter_new_dst != node_mapping.end ()); + auto result + = std::make_unique<digraph_edge> (new_graph, + m_id.c_str (), + *iter_new_src->second, + *iter_new_dst->second); + if (get_property_bag ()) + result->set_property_bag (get_property_bag ()->clone_as_object ()); + + return result; +} + +DEBUG_FUNCTION void +diagnostics::digraphs::edge::dump () const +{ + to_json_sarif_edge ()->dump (); +} + +std::unique_ptr<json::object> +diagnostics::digraphs::edge::to_json_sarif_edge () const +{ + return make_sarif_edge (*this, nullptr); +} + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +static void +test_empty_graph () +{ + digraph g; + + { + auto sarif = g.make_json_sarif_graph (); + + pretty_printer pp; + sarif->print (&pp, true); + ASSERT_STREQ + (pp_formatted_text (&pp), + ("{\"nodes\": [],\n" + " \"edges\": []}")); + } + + { + auto dg = g.make_dot_graph (); + + pretty_printer pp; + dot::writer w (pp); + dg->print (w); + ASSERT_STREQ + (pp_formatted_text (&pp), + ("digraph {\n" + "}\n")); + } +} + +static void +test_simple_graph () +{ +#define KEY_PREFIX "/placeholder/" + auto g = std::make_unique<digraph> (); + g->set_description ("test graph"); + g->set_attr (KEY_PREFIX, "date", "1066"); + + auto a = std::make_unique<digraph_node> (*g, "a"); + auto b = std::make_unique<digraph_node> (*g, "b"); + b->set_attr (KEY_PREFIX, "color", "red"); + auto c = std::make_unique<digraph_node> (*g, "c"); + c->set_label ("I am a node label"); + + auto e = std::make_unique<digraph_edge> (*g, nullptr, *a, *c); + e->set_attr (KEY_PREFIX, "status", "copacetic"); + e->set_label ("I am an edge label"); + g->add_edge (std::move (e)); + + g->add_node (std::move (a)); + + b->add_child (std::move (c)); + g->add_node (std::move (b)); +#undef KEY_PREFIX + + { + auto sarif = g->make_json_sarif_graph (); + + pretty_printer pp; + sarif->print (&pp, true); + ASSERT_STREQ + (pp_formatted_text (&pp), + ("{\"properties\": {\"/placeholder/date\": \"1066\"},\n" + " \"nodes\": [{\"id\": \"a\"},\n" + " {\"id\": \"b\",\n" + " \"properties\": {\"/placeholder/color\": \"red\"},\n" + " \"children\": [{\"id\": \"c\"}]}],\n" + " \"edges\": [{\"id\": \"edge0\",\n" + " \"properties\": {\"/placeholder/status\": \"copacetic\"},\n" + " \"sourceNodeId\": \"a\",\n" + " \"targetNodeId\": \"c\"}]}")); + } + + { + auto dg = g->make_dot_graph (); + + pretty_printer pp; + dot::writer w (pp); + dg->print (w); + ASSERT_STREQ + (pp_formatted_text (&pp), + ("digraph {\n" + " label=\"test graph\";\n" + " a;\n" + " \n" + " subgraph cluster_b {\n" + " c [label=\"I am a node label\"];\n" + "\n" + " };\n" + " a -> c [label=\"I am an edge label\"];\n" + "}\n")); + } +} + +/* Run all of the selftests within this file. */ + +void +digraphs_cc_tests () +{ + test_empty_graph (); + test_simple_graph (); +} + +} // namespace diagnostics::selftest +} // namespace diagnostics + +#endif /* CHECKING_P */ diff --git a/gcc/diagnostics/digraphs.h b/gcc/diagnostics/digraphs.h new file mode 100644 index 0000000..7193ee4 --- /dev/null +++ b/gcc/diagnostics/digraphs.h @@ -0,0 +1,379 @@ +/* Directed graphs associated with a diagnostic. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_DIGRAPHS_H +#define GCC_DIAGNOSTICS_DIGRAPHS_H + +#include "json.h" +#include "diagnostics/logical-locations.h" + +class graphviz_out; + +class sarif_graph; +class sarif_node; +class sarif_edge; + +namespace dot { class graph; } + +namespace diagnostics { +namespace digraphs { + +/* A family of classes: digraph, node, and edge, closely related to + SARIF's graph, node, and edge types (SARIF v2.1.0 sections 3.39-3.41). + + Nodes can have child nodes, allowing for arbitrarily deep nesting. + Edges can be between any pair of nodes (potentially at different + nesting levels). + + Digraphs, nodes, and edges also optionally have a JSON property bag, + allowing round-tripping of arbitrary key/value pairs through SARIF. */ + +class digraph; +class node; +class edge; + +/* A base class for digraph, node, and edge to allow them to have + an optional JSON property bag. */ + +class object +{ +public: + const char * + get_attr (const char *key_prefix, + const char *key) const; + + void + set_attr (const char *key_prefix, + const char *key, + const char *value); + + void + set_json_attr (const char *key_prefix, + const char *key, + std::unique_ptr<json::value> value); + + json::object * + get_property_bag () const { return m_property_bag.get (); } + + void + set_property_bag (std::unique_ptr<json::object> property_bag) + { + m_property_bag = std::move (property_bag); + } + +private: + std::unique_ptr<json::object> m_property_bag; +}; + +// A directed graph, corresponding to SARIF v2.1.0 section 3.39. + +class digraph : public object +{ + public: + friend class node; + friend class edge; + + digraph () : m_next_edge_id_index (0) {} + virtual ~digraph () {} + + const char * + get_description () const + { + if (!m_description) + return nullptr; + return m_description->c_str (); + } + + void + set_description (const char *desc) + { + if (desc) + m_description = std::make_unique<std::string> (desc); + else + m_description = nullptr; + } + void + set_description (std::string desc) + { + m_description = std::make_unique<std::string> (std::move (desc)); + } + + node * + get_node_by_id (const char *id) const + { + auto iter = m_id_to_node_map.find (id); + if (iter == m_id_to_node_map.end ()) + return nullptr; + return iter->second; + } + + edge * + get_edge_by_id (const char *id) const + { + auto iter = m_id_to_edge_map.find (id); + if (iter == m_id_to_edge_map.end ()) + return nullptr; + return iter->second; + } + + size_t + get_num_nodes () const + { + return m_nodes.size (); + } + + node & + get_node (size_t idx) const + { + return *m_nodes[idx].get (); + } + + size_t + get_num_edges () const + { + return m_edges.size (); + } + + edge & + get_edge (size_t idx) const + { + return *m_edges[idx].get (); + } + + void + dump () const; + + std::unique_ptr<json::object> + make_json_sarif_graph () const; + + std::unique_ptr<dot::graph> + make_dot_graph () const; + + void + add_node (std::unique_ptr<node> n) + { + gcc_assert (n); + m_nodes.push_back (std::move (n)); + } + + void + add_edge (std::unique_ptr<edge> e) + { + gcc_assert (e); + m_edges.push_back (std::move (e)); + } + + void + add_edge (const char *id, + node &src_node, + node &dst_node, + const char *label = nullptr); + + std::unique_ptr<digraph> clone () const; + + private: + void + add_node_id (std::string node_id, node &new_node) + { + m_id_to_node_map.insert ({std::move (node_id), &new_node}); + } + void + add_edge_id (std::string edge_id, edge &new_edge) + { + m_id_to_edge_map.insert ({std::move (edge_id), &new_edge}); + } + + std::string + make_edge_id (const char *edge_id); + + std::unique_ptr<std::string> m_description; + std::map<std::string, node *> m_id_to_node_map; + std::map<std::string, edge *> m_id_to_edge_map; + std::vector<std::unique_ptr<node>> m_nodes; + std::vector<std::unique_ptr<edge>> m_edges; + size_t m_next_edge_id_index; +}; + +// A node in a directed graph, corresponding to SARIF v2.1.0 section 3.40. + +class node : public object +{ + public: + virtual ~node () {} + + node (digraph &g, std::string id) + : m_id (id), + m_physical_loc (UNKNOWN_LOCATION) + { + g.add_node_id (std::move (id), *this); + } + node (const node &) = delete; + + std::string + get_id () const { return m_id; } + + const char * + get_label () const + { + if (!m_label) + return nullptr; + return m_label->c_str (); + } + + void + set_label (const char *label) + { + if (label) + m_label = std::make_unique<std::string> (label); + else + m_label = nullptr; + } + void + set_label (std::string label) + { + m_label = std::make_unique<std::string> (std::move (label)); + } + + size_t + get_num_children () const { return m_children.size (); } + + node & + get_child (size_t idx) const { return *m_children[idx].get (); } + + void + add_child (std::unique_ptr<node> child) + { + gcc_assert (child); + m_children.push_back (std::move (child)); + } + + location_t + get_physical_loc () const + { + return m_physical_loc; + } + + void + set_physical_loc (location_t physical_loc) + { + m_physical_loc = physical_loc; + } + + logical_locations::key + get_logical_loc () const + { + return m_logical_loc; + } + + void + set_logical_loc (logical_locations::key logical_loc) + { + m_logical_loc = logical_loc; + } + + void print (graphviz_out &gv) const; + + void + dump () const; + + std::unique_ptr<json::object> + to_json_sarif_node () const; + + std::unique_ptr<node> + clone (digraph &new_graph, + std::map<node *, node *> &node_mapping) const; + + private: + std::string m_id; + std::unique_ptr<std::string> m_label; + std::vector<std::unique_ptr<node>> m_children; + location_t m_physical_loc; + logical_locations::key m_logical_loc; +}; + +// An edge in a directed graph, corresponding to SARIF v2.1.0 section 3.41. + +class edge : public object +{ + public: + virtual ~edge () {} + + /* SARIF requires us to provide unique edge IDs within a graph, + but otherwise we don't need them. + Pass in nullptr for the id to get the graph to generate a unique + edge id for us. */ + edge (digraph &g, + const char *id, + node &src_node, + node &dst_node) + : m_id (g.make_edge_id (id)), + m_src_node (src_node), + m_dst_node (dst_node) + { + g.add_edge_id (m_id, *this); + } + + std::string + get_id () const { return m_id; } + + const char * + get_label () const + { + if (!m_label) + return nullptr; + return m_label->c_str (); + } + + void + set_label (const char *label) + { + if (label) + m_label = std::make_unique<std::string> (label); + else + m_label = nullptr; + } + + node & + get_src_node () const { return m_src_node; } + + node & + get_dst_node () const { return m_dst_node; } + + void + dump () const; + + std::unique_ptr<json::object> + to_json_sarif_edge () const; + + std::unique_ptr<edge> + clone (digraph &new_graph, + const std::map<diagnostics::digraphs::node *, diagnostics::digraphs::node *> &node_mapping) const; + +private: + std::string m_id; + std::unique_ptr<std::string> m_label; + node &m_src_node; + node &m_dst_node; +}; + +} // namespace digraphs +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_DIGRAPHS_H */ diff --git a/gcc/diagnostics/event-id.h b/gcc/diagnostics/event-id.h new file mode 100644 index 0000000..167599d --- /dev/null +++ b/gcc/diagnostics/event-id.h @@ -0,0 +1,83 @@ +/* A class for referring to events within a diagnostics::paths::path. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_EVENT_ID_H +#define GCC_DIAGNOSTICS_EVENT_ID_H + +/* A class for referring to events within a diagnostics::paths::path. + + They are stored as 0-based offsets into the events, but + printed (e.g. via %@) as 1-based numbers. + + For example, a 3-event path has event offsets 0, 1, and 2, + which would be shown to the user as "(1)", "(2)" and "(3)". + + This has its own header so that pretty-print.cc can use this + to implement "%@" without bringing in all of diagnostics::paths. + + This has to be in the global namespace for compatibility with + c-format.cc in GCC 10 onwards. */ + +class diagnostic_event_id_t +{ + public: + diagnostic_event_id_t () : m_index (UNKNOWN_EVENT_IDX) {} + diagnostic_event_id_t (int zero_based_idx) : m_index (zero_based_idx) {} + + bool known_p () const { return m_index != UNKNOWN_EVENT_IDX; } + + int zero_based () const + { + gcc_assert (known_p ()); + return m_index; + } + + int one_based () const + { + gcc_assert (known_p ()); + return m_index + 1; + } + + private: + static const int UNKNOWN_EVENT_IDX = -1; + int m_index; // zero-based +}; + +namespace diagnostics { +namespace paths { + +typedef diagnostic_event_id_t event_id_t; + +/* A type for compactly referring to a particular thread within a + diagnostics::paths::path. Typically there is just one thread per path, + with id 0. */ +typedef int thread_id_t; + +} // namespace paths +} // namespace diagnostics + + +/* A pointer to a diagnostic_event_id_t, for use with the "%@" format + code, which will print a 1-based representation for it, with suitable + colorization, e.g. "(1)". + The %@ format code requires that known_p be true for the event ID. */ +typedef diagnostic_event_id_t *diagnostic_event_id_ptr; + +#endif /* ! GCC_DIAGNOSTICS_EVENT_ID_H */ diff --git a/gcc/diagnostics/file-cache.cc b/gcc/diagnostics/file-cache.cc new file mode 100644 index 0000000..febeb03 --- /dev/null +++ b/gcc/diagnostics/file-cache.cc @@ -0,0 +1,1083 @@ +/* Caching input files for use by diagnostics. + Copyright (C) 2004-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/>. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "cpplib.h" +#include "diagnostics/file-cache.h" +#include "selftest.h" + +#ifndef HAVE_ICONV +#define HAVE_ICONV 0 +#endif + +namespace diagnostics { + +/* Input charset configuration. */ +static const char *default_charset_callback (const char *) +{ + return nullptr; +} + +void +file_cache::initialize_input_context (diagnostic_input_charset_callback ccb, + bool should_skip_bom) +{ + m_input_context.ccb = (ccb ? ccb : default_charset_callback); + m_input_context.should_skip_bom = should_skip_bom; +} + +/* This is a cache used by get_next_line to store the content of a + file to be searched for file lines. */ +class file_cache_slot +{ +public: + file_cache_slot (); + ~file_cache_slot (); + + void dump (FILE *out, int indent) const; + void DEBUG_FUNCTION dump () const { dump (stderr, 0); } + + bool read_line_num (size_t line_num, + char ** line, ssize_t *line_len); + + /* Accessors. */ + const char *get_file_path () const { return m_file_path; } + unsigned get_use_count () const { return m_use_count; } + bool missing_trailing_newline_p () const + { + return m_missing_trailing_newline; + } + char_span get_full_file_content (); + + void inc_use_count () { m_use_count++; } + + bool create (const file_cache::input_context &in_context, + const char *file_path, FILE *fp, unsigned highest_use_count); + void evict (); + void set_content (const char *buf, size_t sz); + + static size_t tune (size_t line_record_size_) + { + size_t ret = line_record_size; + line_record_size = line_record_size_; + return ret; + } + + private: + /* These are information used to store a line boundary. */ + class line_info + { + public: + /* The line number. It starts from 1. */ + size_t line_num; + + /* The position (byte count) of the beginning of the line, + relative to the file data pointer. This starts at zero. */ + size_t start_pos; + + /* The position (byte count) of the last byte of the line. This + normally points to the '\n' character, or to one byte after the + last byte of the file, if the file doesn't contain a '\n' + character. */ + size_t end_pos; + + line_info (size_t l, size_t s, size_t e) + : line_num (l), start_pos (s), end_pos (e) + {} + + line_info () + :line_num (0), start_pos (0), end_pos (0) + {} + + static bool less_than(const line_info &a, const line_info &b) + { + return a.line_num < b.line_num; + } + }; + + bool needs_read_p () const; + bool needs_grow_p () const; + void maybe_grow (); + bool read_data (); + bool maybe_read_data (); + bool get_next_line (char **line, ssize_t *line_len); + bool read_next_line (char ** line, ssize_t *line_len); + bool goto_next_line (); + + static const size_t buffer_size = 4 * 1024; + static size_t line_record_size; + static size_t recent_cached_lines_shift; + + /* The number of time this file has been accessed. This is used + to designate which file cache to evict from the cache + array. */ + unsigned m_use_count; + + /* The file_path is the key for identifying a particular file in + the cache. This copy is owned by the slot. */ + char *m_file_path; + + FILE *m_fp; + + /* True when an read error happened. */ + bool m_error; + + /* This points to the content of the file that we've read so + far. */ + char *m_data; + + /* The allocated buffer to be freed may start a little earlier than DATA, + e.g. if a UTF8 BOM was skipped at the beginning. */ + int m_alloc_offset; + + /* The size of the DATA array above.*/ + size_t m_size; + + /* The number of bytes read from the underlying file so far. This + must be less (or equal) than SIZE above. */ + size_t m_nb_read; + + /* The index of the beginning of the current line. */ + size_t m_line_start_idx; + + /* The number of the previous line read. This starts at 1. Zero + means we've read no line so far. */ + size_t m_line_num; + + /* Could this file be missing a trailing newline on its final line? + Initially true (to cope with empty files), set to true/false + as each line is read. */ + bool m_missing_trailing_newline; + + /* This is a record of the beginning and end of the lines we've seen + while reading the file. This is useful to avoid walking the data + from the beginning when we are asked to read a line that is + before LINE_START_IDX above. When the lines exceed line_record_size + this is scaled down dynamically, with the line_info becoming anchors. */ + vec<line_info, va_heap> m_line_record; + + /* A cache of the recently seen lines. This is maintained as a ring + buffer. */ + vec<line_info, va_heap> m_line_recent; + + /* First and last valid entry in m_line_recent. */ + size_t m_line_recent_last, m_line_recent_first; + + void offset_buffer (int offset) + { + gcc_assert (offset < 0 ? m_alloc_offset + offset >= 0 + : (size_t) offset <= m_size); + gcc_assert (m_data); + m_alloc_offset += offset; + m_data += offset; + m_size -= offset; + } + +}; + +size_t file_cache_slot::line_record_size = 0; +size_t file_cache_slot::recent_cached_lines_shift = 8; + +/* Tune file_cache. */ +void +file_cache::tune (size_t num_file_slots, size_t lines) +{ + if (file_cache_slot::tune (lines) != lines + || m_num_file_slots != num_file_slots) + { + delete[] m_file_slots; + m_file_slots = new file_cache_slot[num_file_slots]; + } + m_num_file_slots = num_file_slots; +} + +static const char * +find_end_of_line (const char *s, size_t len); + +/* Lookup the cache used for the content of a given file accessed by + caret diagnostic. Return the found cached file, or NULL if no + cached file was found. */ + +file_cache_slot * +file_cache::lookup_file (const char *file_path) +{ + gcc_assert (file_path); + + /* This will contain the found cached file. */ + file_cache_slot *r = NULL; + for (unsigned i = 0; i < m_num_file_slots; ++i) + { + file_cache_slot *c = &m_file_slots[i]; + if (c->get_file_path () && !strcmp (c->get_file_path (), file_path)) + { + c->inc_use_count (); + r = c; + } + } + + if (r) + r->inc_use_count (); + + return r; +} + +/* Purge any mention of FILENAME from the cache of files used for + printing source code. For use in selftests when working + with tempfiles. */ + +void +file_cache::forcibly_evict_file (const char *file_path) +{ + gcc_assert (file_path); + + file_cache_slot *r = lookup_file (file_path); + if (!r) + /* Not found. */ + return; + + r->evict (); +} + +/* Determine if FILE_PATH missing a trailing newline on its final line. + Only valid to call once all of the file has been loaded, by + requesting a line number beyond the end of the file. */ + +bool +file_cache::missing_trailing_newline_p (const char *file_path) +{ + gcc_assert (file_path); + + file_cache_slot *r = lookup_or_add_file (file_path); + return r->missing_trailing_newline_p (); +} + +void +file_cache::add_buffered_content (const char *file_path, + const char *buffer, + size_t sz) +{ + gcc_assert (file_path); + + file_cache_slot *r = lookup_file (file_path); + if (!r) + { + unsigned highest_use_count = 0; + r = evicted_cache_tab_entry (&highest_use_count); + if (!r->create (m_input_context, file_path, nullptr, highest_use_count)) + return; + } + + r->set_content (buffer, sz); +} + +void +file_cache_slot::evict () +{ + free (m_file_path); + m_file_path = NULL; + if (m_fp) + fclose (m_fp); + m_error = false; + m_fp = NULL; + m_nb_read = 0; + m_line_start_idx = 0; + m_line_num = 0; + m_line_record.truncate (0); + m_line_recent_first = 0; + m_line_recent_last = 0; + m_use_count = 0; + m_missing_trailing_newline = true; +} + +/* Return the file cache that has been less used, recently, or the + first empty one. If HIGHEST_USE_COUNT is non-null, + *HIGHEST_USE_COUNT is set to the highest use count of the entries + in the cache table. */ + +file_cache_slot* +file_cache::evicted_cache_tab_entry (unsigned *highest_use_count) +{ + file_cache_slot *to_evict = &m_file_slots[0]; + unsigned huc = to_evict->get_use_count (); + for (unsigned i = 1; i < m_num_file_slots; ++i) + { + file_cache_slot *c = &m_file_slots[i]; + bool c_is_empty = (c->get_file_path () == NULL); + + if (c->get_use_count () < to_evict->get_use_count () + || (to_evict->get_file_path () && c_is_empty)) + /* We evict C because it's either an entry with a lower use + count or one that is empty. */ + to_evict = c; + + if (huc < c->get_use_count ()) + huc = c->get_use_count (); + + if (c_is_empty) + /* We've reached the end of the cache; subsequent elements are + all empty. */ + break; + } + + if (highest_use_count) + *highest_use_count = huc; + + return to_evict; +} + +/* Create the cache used for the content of a given file to be + accessed by caret diagnostic. This cache is added to an array of + cache and can be retrieved by lookup_file_in_cache_tab. This + function returns the created cache. Note that only the last + m_num_file_slots files are cached. + + This can return nullptr if the FILE_PATH can't be opened for + reading, or if the content can't be converted to the input_charset. */ + +file_cache_slot* +file_cache::add_file (const char *file_path) +{ + + FILE *fp = fopen (file_path, "r"); + if (fp == NULL) + return NULL; + + unsigned highest_use_count = 0; + file_cache_slot *r = evicted_cache_tab_entry (&highest_use_count); + if (!r->create (m_input_context, file_path, fp, highest_use_count)) + return NULL; + return r; +} + +/* Get a borrowed char_span to the full content of this file + as decoded according to the input charset, encoded as UTF-8. */ + +char_span +file_cache_slot::get_full_file_content () +{ + char *line; + ssize_t line_len; + while (get_next_line (&line, &line_len)) + { + } + return char_span (m_data, m_nb_read); +} + +/* Populate this slot for use on FILE_PATH and FP, dropping any + existing cached content within it. */ + +bool +file_cache_slot::create (const file_cache::input_context &in_context, + const char *file_path, FILE *fp, + unsigned highest_use_count) +{ + m_file_path = file_path ? xstrdup (file_path) : nullptr; + if (m_fp) + fclose (m_fp); + m_error = false; + m_fp = fp; + if (m_alloc_offset) + offset_buffer (-m_alloc_offset); + m_nb_read = 0; + m_line_start_idx = 0; + m_line_num = 0; + m_line_recent_first = 0; + m_line_recent_last = 0; + m_line_record.truncate (0); + /* Ensure that this cache entry doesn't get evicted next time + add_file_to_cache_tab is called. */ + m_use_count = ++highest_use_count; + m_missing_trailing_newline = true; + + + /* Check the input configuration to determine if we need to do any + transformations, such as charset conversion or BOM skipping. */ + if (const char *input_charset = in_context.ccb (file_path)) + { + /* Need a full-blown conversion of the input charset. */ + fclose (m_fp); + m_fp = NULL; + const cpp_converted_source cs + = cpp_get_converted_source (file_path, input_charset); + if (!cs.data) + return false; + if (m_data) + XDELETEVEC (m_data); + m_data = cs.data; + m_nb_read = m_size = cs.len; + m_alloc_offset = cs.data - cs.to_free; + } + else if (in_context.should_skip_bom) + { + if (read_data ()) + { + const int offset = cpp_check_utf8_bom (m_data, m_nb_read); + offset_buffer (offset); + m_nb_read -= offset; + } + } + + return true; +} + +void +file_cache_slot::set_content (const char *buf, size_t sz) +{ + m_data = (char *)xmalloc (sz); + memcpy (m_data, buf, sz); + m_nb_read = m_size = sz; + m_alloc_offset = 0; + + if (m_fp) + { + fclose (m_fp); + m_fp = nullptr; + } +} + +/* file_cache's ctor. */ + +file_cache::file_cache () +: m_num_file_slots (16), m_file_slots (new file_cache_slot[m_num_file_slots]) +{ + initialize_input_context (nullptr, false); +} + +/* file_cache's dtor. */ + +file_cache::~file_cache () +{ + delete[] m_file_slots; +} + +void +file_cache::dump (FILE *out, int indent) const +{ + for (size_t i = 0; i < m_num_file_slots; ++i) + { + fprintf (out, "%*sslot[%i]:\n", indent, "", (int)i); + m_file_slots[i].dump (out, indent + 2); + } +} + +void +file_cache::dump () const +{ + dump (stderr, 0); +} + +/* Lookup the cache used for the content of a given file accessed by + caret diagnostic. If no cached file was found, create a new cache + for this file, add it to the array of cached file and return + it. + + This can return nullptr on a cache miss if FILE_PATH can't be opened for + reading, or if the content can't be converted to the input_charset. */ + +file_cache_slot* +file_cache::lookup_or_add_file (const char *file_path) +{ + file_cache_slot *r = lookup_file (file_path); + if (r == NULL) + r = add_file (file_path); + return r; +} + +/* Default constructor for a cache of file used by caret + diagnostic. */ + +file_cache_slot::file_cache_slot () +: m_use_count (0), m_file_path (NULL), m_fp (NULL), m_error (false), m_data (0), + m_alloc_offset (0), m_size (0), m_nb_read (0), m_line_start_idx (0), + m_line_num (0), m_missing_trailing_newline (true), + m_line_recent_last (0), m_line_recent_first (0) +{ + m_line_record.create (0); + m_line_recent.create (1U << recent_cached_lines_shift); + for (int i = 0; i < 1 << recent_cached_lines_shift; i++) + m_line_recent.quick_push (file_cache_slot::line_info (0, 0, 0)); +} + +/* Destructor for a cache of file used by caret diagnostic. */ + +file_cache_slot::~file_cache_slot () +{ + free (m_file_path); + if (m_fp) + { + fclose (m_fp); + m_fp = NULL; + } + if (m_data) + { + offset_buffer (-m_alloc_offset); + XDELETEVEC (m_data); + m_data = 0; + } + m_line_record.release (); + m_line_recent.release (); +} + +void +file_cache_slot::dump (FILE *out, int indent) const +{ + if (!m_file_path) + { + fprintf (out, "%*s(unused)\n", indent, ""); + return; + } + fprintf (out, "%*sfile_path: %s\n", indent, "", m_file_path); + fprintf (out, "%*sfp: %p\n", indent, "", (void *)m_fp); + fprintf (out, "%*sneeds_read_p: %i\n", indent, "", (int)needs_read_p ()); + fprintf (out, "%*sneeds_grow_p: %i\n", indent, "", (int)needs_grow_p ()); + fprintf (out, "%*suse_count: %i\n", indent, "", m_use_count); + fprintf (out, "%*ssize: %zi\n", indent, "", m_size); + fprintf (out, "%*snb_read: %zi\n", indent, "", m_nb_read); + fprintf (out, "%*sstart_line_idx: %zi\n", indent, "", m_line_start_idx); + fprintf (out, "%*sline_num: %zi\n", indent, "", m_line_num); + fprintf (out, "%*smissing_trailing_newline: %i\n", + indent, "", (int)m_missing_trailing_newline); + fprintf (out, "%*sline records (%i):\n", + indent, "", m_line_record.length ()); + int idx = 0; + for (auto &line : m_line_record) + fprintf (out, "%*s[%i]: line %zi: byte offsets: %zi-%zi\n", + indent + 2, "", + idx++, line.line_num, line.start_pos, line.end_pos); +} + +/* Returns TRUE iff the cache would need to be filled with data coming + from the file. That is, either the cache is empty or full or the + current line is empty. Note that if the cache is full, it would + need to be extended and filled again. */ + +bool +file_cache_slot::needs_read_p () const +{ + return m_fp && (m_nb_read == 0 + || m_nb_read == m_size + || (m_line_start_idx >= m_nb_read - 1)); +} + +/* Return TRUE iff the cache is full and thus needs to be + extended. */ + +bool +file_cache_slot::needs_grow_p () const +{ + return m_nb_read == m_size; +} + +/* Grow the cache if it needs to be extended. */ + +void +file_cache_slot::maybe_grow () +{ + if (!needs_grow_p ()) + return; + + if (!m_data) + { + gcc_assert (m_size == 0 && m_alloc_offset == 0); + m_size = buffer_size; + m_data = XNEWVEC (char, m_size); + } + else + { + const int offset = m_alloc_offset; + offset_buffer (-offset); + m_size *= 2; + m_data = XRESIZEVEC (char, m_data, m_size); + offset_buffer (offset); + } +} + +/* Read more data into the cache. Extends the cache if need be. + Returns TRUE iff new data could be read. */ + +bool +file_cache_slot::read_data () +{ + if (feof (m_fp) || ferror (m_fp)) + return false; + + maybe_grow (); + + char * from = m_data + m_nb_read; + size_t to_read = m_size - m_nb_read; + size_t nb_read = fread (from, 1, to_read, m_fp); + + if (ferror (m_fp)) + { + m_error = true; + return false; + } + + m_nb_read += nb_read; + return !!nb_read; +} + +/* Read new data iff the cache needs to be filled with more data + coming from the file FP. Return TRUE iff the cache was filled with + mode data. */ + +bool +file_cache_slot::maybe_read_data () +{ + if (!needs_read_p ()) + return false; + return read_data (); +} + +/* Helper function for file_cache_slot::get_next_line (), to find the end of + the next line. Returns with the memchr convention, i.e. nullptr if a line + terminator was not found. We need to determine line endings in the same + manner that libcpp does: any of \n, \r\n, or \r is a line ending. */ + +static const char * +find_end_of_line (const char *s, size_t len) +{ + for (const auto end = s + len; s != end; ++s) + { + if (*s == '\n') + return s; + if (*s == '\r') + { + const auto next = s + 1; + if (next == end) + { + /* Don't find the line ending if \r is the very last character + in the buffer; we do not know if it's the end of the file or + just the end of what has been read so far, and we wouldn't + want to break in the middle of what's actually a \r\n + sequence. Instead, we will handle the case of a file ending + in a \r later. */ + break; + } + return (*next == '\n' ? next : s); + } + } + return nullptr; +} + +/* Read a new line from file FP, using C as a cache for the data + coming from the file. Upon successful completion, *LINE is set to + the beginning of the line found. *LINE points directly in the + line cache and is only valid until the next call of get_next_line. + *LINE_LEN is set to the length of the line. Note that the line + does not contain any terminal delimiter. This function returns + true if some data was read or process from the cache, false + otherwise. Note that subsequent calls to get_next_line might + make the content of *LINE invalid. */ + +bool +file_cache_slot::get_next_line (char **line, ssize_t *line_len) +{ + /* Fill the cache with data to process. */ + maybe_read_data (); + + size_t remaining_size = m_nb_read - m_line_start_idx; + if (remaining_size == 0) + /* There is no more data to process. */ + return false; + + const char *line_start = m_data + m_line_start_idx; + + const char *next_line_start = NULL; + size_t len = 0; + const char *line_end = find_end_of_line (line_start, remaining_size); + if (line_end == NULL) + { + /* We haven't found an end-of-line delimiter in the cache. + Fill the cache with more data from the file and look again. */ + while (maybe_read_data ()) + { + line_start = m_data + m_line_start_idx; + remaining_size = m_nb_read - m_line_start_idx; + line_end = find_end_of_line (line_start, remaining_size); + if (line_end != NULL) + { + next_line_start = line_end + 1; + break; + } + } + if (line_end == NULL) + { + /* We've loaded all the file into the cache and still no + terminator. Let's say the line ends up at one byte past the + end of the file. This is to stay consistent with the case + of when the line ends up with a terminator and line_end points to + that. That consistency is useful below in the len calculation. + + If the file ends in a \r, we didn't identify it as a line + terminator above, so do that now instead. */ + line_end = m_data + m_nb_read; + if (m_nb_read && line_end[-1] == '\r') + { + --line_end; + m_missing_trailing_newline = false; + } + else + m_missing_trailing_newline = true; + } + else + m_missing_trailing_newline = false; + } + else + { + next_line_start = line_end + 1; + m_missing_trailing_newline = false; + } + + if (m_error) + return false; + + /* At this point, we've found the end of the of line. It either points to + the line terminator or to one byte after the last byte of the file. */ + gcc_assert (line_end != NULL); + + len = line_end - line_start; + + if (m_line_start_idx < m_nb_read) + *line = const_cast<char *> (line_start); + + ++m_line_num; + + /* Now update our line record so that re-reading lines from the + before m_line_start_idx is faster. */ + size_t rlen = m_line_record.length (); + /* Only update when beyond the previously cached region. */ + if (rlen == 0 || m_line_record[rlen - 1].line_num < m_line_num) + { + size_t spacing + = (rlen >= 2 + ? (m_line_record[rlen - 1].line_num + - m_line_record[rlen - 2].line_num) : 1); + size_t delta + = rlen >= 1 ? m_line_num - m_line_record[rlen - 1].line_num : 1; + + size_t max_size = line_record_size; + /* One anchor per hundred input lines. */ + if (max_size == 0) + max_size = m_line_num / 100; + + /* If we're too far beyond drop half of the lines to rebalance. */ + if (rlen == max_size && delta >= spacing * 2) + { + size_t j = 0; + for (size_t i = 1; i < rlen; i += 2) + m_line_record[j++] = m_line_record[i]; + m_line_record.truncate (j); + rlen = j; + spacing *= 2; + } + + if (rlen < max_size && delta >= spacing) + { + file_cache_slot::line_info li (m_line_num, m_line_start_idx, + line_end - m_data); + m_line_record.safe_push (li); + } + } + + /* Cache recent tail lines separately for fast access. This assumes + most accesses do not skip backwards. */ + if (m_line_recent_last == m_line_recent_first + || m_line_recent[m_line_recent_last].line_num == m_line_num - 1) + { + size_t mask = ((size_t) 1 << recent_cached_lines_shift) - 1; + m_line_recent_last = (m_line_recent_last + 1) & mask; + if (m_line_recent_last == m_line_recent_first) + m_line_recent_first = (m_line_recent_first + 1) & mask; + m_line_recent[m_line_recent_last] + = file_cache_slot::line_info (m_line_num, m_line_start_idx, + line_end - m_data); + } + + /* Update m_line_start_idx so that it points to the next line to be + read. */ + if (next_line_start) + m_line_start_idx = next_line_start - m_data; + else + /* We didn't find any terminal '\n'. Let's consider that the end + of line is the end of the data in the cache. The next + invocation of get_next_line will either read more data from the + underlying file or return false early because we've reached the + end of the file. */ + m_line_start_idx = m_nb_read; + + *line_len = len; + + return true; +} + +/* Consume the next bytes coming from the cache (or from its + underlying file if there are remaining unread bytes in the file) + until we reach the next end-of-line (or end-of-file). There is no + copying from the cache involved. Return TRUE upon successful + completion. */ + +bool +file_cache_slot::goto_next_line () +{ + char *l; + ssize_t len; + + return get_next_line (&l, &len); +} + +/* Read an arbitrary line number LINE_NUM from the file cached in C. + If the line was read successfully, *LINE points to the beginning + of the line in the file cache and *LINE_LEN is the length of the + line. *LINE is not nul-terminated, but may contain zero bytes. + *LINE is only valid until the next call of read_line_num. + This function returns bool if a line was read. */ + +bool +file_cache_slot::read_line_num (size_t line_num, + char ** line, ssize_t *line_len) +{ + gcc_assert (line_num > 0); + + /* Is the line in the recent line cache? + This assumes the main file processing is only using + a single contiguous cursor with only temporary excursions. */ + if (m_line_recent_first != m_line_recent_last + && m_line_recent[m_line_recent_first].line_num <= line_num + && m_line_recent[m_line_recent_last].line_num >= line_num) + { + line_info &last = m_line_recent[m_line_recent_last]; + size_t mask = (1U << recent_cached_lines_shift) - 1; + size_t idx = (m_line_recent_last - (last.line_num - line_num)) & mask; + line_info &recent = m_line_recent[idx]; + gcc_assert (recent.line_num == line_num); + *line = m_data + recent.start_pos; + *line_len = recent.end_pos - recent.start_pos; + return true; + } + + if (line_num <= m_line_num) + { + line_info l (line_num, 0, 0); + int i = m_line_record.lower_bound (l, line_info::less_than); + if (i == 0) + { + m_line_start_idx = 0; + m_line_num = 0; + } + else if (m_line_record[i - 1].line_num == line_num) + { + /* We have the start/end of the line. */ + *line = m_data + m_line_record[i - 1].start_pos; + *line_len = m_line_record[i - 1].end_pos - m_line_record[i - 1].start_pos; + return true; + } + else + { + gcc_assert (m_line_record[i - 1].line_num < m_line_num); + m_line_start_idx = m_line_record[i - 1].start_pos; + m_line_num = m_line_record[i - 1].line_num - 1; + } + } + + /* Let's walk from line m_line_num up to line_num - 1, without + copying any line. */ + while (m_line_num < line_num - 1) + if (!goto_next_line ()) + return false; + + /* The line we want is the next one. Let's read it. */ + return get_next_line (line, line_len); +} + +/* Return the physical source line that corresponds to FILE_PATH/LINE. + The line is not nul-terminated. The returned pointer is only + valid until the next call of location_get_source_line. + Note that the line can contain several null characters, + so the returned value's length has the actual length of the line. + If the function fails, a NULL char_span is returned. */ + +char_span +file_cache::get_source_line (const char *file_path, int line) +{ + char *buffer = NULL; + ssize_t len; + + if (line == 0) + return char_span (NULL, 0); + + if (file_path == NULL) + return char_span (NULL, 0); + + file_cache_slot *c = lookup_or_add_file (file_path); + if (c == NULL) + return char_span (NULL, 0); + + bool read = c->read_line_num (line, &buffer, &len); + if (!read) + return char_span (NULL, 0); + + return char_span (buffer, len); +} + +char_span +file_cache::get_source_file_content (const char *file_path) +{ + file_cache_slot *c = lookup_or_add_file (file_path); + if (c == nullptr) + return char_span (nullptr, 0); + return c->get_full_file_content (); +} + +#if CHECKING_P + +namespace selftest { + + using temp_source_file = ::selftest::temp_source_file; + +/* Verify reading of a specific line LINENUM in TMP, FC. */ + +static void +check_line (temp_source_file &tmp, file_cache &fc, int linenum) +{ + char_span line = fc.get_source_line (tmp.get_filename (), linenum); + int n; + const char *b = line.get_buffer (); + size_t l = line.length (); + char buf[5]; + ASSERT_LT (l, 5); + memcpy (buf, b, l); + buf[l] = '\0'; + ASSERT_TRUE (sscanf (buf, "%d", &n) == 1); + ASSERT_EQ (n, linenum); +} + +/* Test file cache replacement. */ + +static void +test_replacement () +{ + const int maxline = 1000; + + char *vec = XNEWVEC (char, maxline * 5); + char *p = vec; + int i; + for (i = 1; i <= maxline; i++) + p += sprintf (p, "%d\n", i); + + temp_source_file tmp (SELFTEST_LOCATION, ".txt", vec); + free (vec); + file_cache fc; + + for (i = 2; i <= maxline; i++) + { + check_line (tmp, fc, i); + check_line (tmp, fc, i - 1); + if (i >= 10) + check_line (tmp, fc, i - 9); + if (i >= 350) /* Exceed the look behind cache. */ + check_line (tmp, fc, i - 300); + } + for (i = 5; i <= maxline; i += 100) + check_line (tmp, fc, i); + for (i = 1; i <= maxline; i++) + check_line (tmp, fc, i); +} + +/* Verify reading of input files (e.g. for caret-based diagnostics). */ + +static void +test_reading_source_line () +{ + /* Create a tempfile and write some text to it. */ + temp_source_file tmp (SELFTEST_LOCATION, ".txt", + "01234567890123456789\n" + "This is the test text\n" + "This is the 3rd line"); + file_cache fc; + + /* Read back a specific line from the tempfile. */ + char_span source_line = fc.get_source_line (tmp.get_filename (), 3); + ASSERT_TRUE (source_line); + ASSERT_TRUE (source_line.get_buffer () != NULL); + ASSERT_EQ (20, source_line.length ()); + ASSERT_TRUE (!strncmp ("This is the 3rd line", + source_line.get_buffer (), source_line.length ())); + + source_line = fc.get_source_line (tmp.get_filename (), 2); + ASSERT_TRUE (source_line); + ASSERT_TRUE (source_line.get_buffer () != NULL); + ASSERT_EQ (21, source_line.length ()); + ASSERT_TRUE (!strncmp ("This is the test text", + source_line.get_buffer (), source_line.length ())); + + source_line = fc.get_source_line (tmp.get_filename (), 4); + ASSERT_FALSE (source_line); + ASSERT_TRUE (source_line.get_buffer () == NULL); +} + +/* Verify reading from buffers (e.g. for sarif-replay). */ + +static void +test_reading_source_buffer () +{ + const char *text = ("01234567890123456789\n" + "This is the test text\n" + "This is the 3rd line"); + const char *filename = "foo.txt"; + file_cache fc; + fc.add_buffered_content (filename, text, strlen (text)); + + /* Read back a specific line from the tempfile. */ + char_span source_line = fc.get_source_line (filename, 3); + ASSERT_TRUE (source_line); + ASSERT_TRUE (source_line.get_buffer () != NULL); + ASSERT_EQ (20, source_line.length ()); + ASSERT_TRUE (!strncmp ("This is the 3rd line", + source_line.get_buffer (), source_line.length ())); + + source_line = fc.get_source_line (filename, 2); + ASSERT_TRUE (source_line); + ASSERT_TRUE (source_line.get_buffer () != NULL); + ASSERT_EQ (21, source_line.length ()); + ASSERT_TRUE (!strncmp ("This is the test text", + source_line.get_buffer (), source_line.length ())); + + source_line = fc.get_source_line (filename, 4); + ASSERT_FALSE (source_line); + ASSERT_TRUE (source_line.get_buffer () == NULL); +} + +/* Run all of the selftests within this file. */ + +void +file_cache_cc_tests () +{ + test_reading_source_line (); + test_reading_source_buffer (); + test_replacement (); +} + +} // namespace selftest + +#endif /* CHECKING_P */ + +} // namespace diagnostics diff --git a/gcc/diagnostics/file-cache.h b/gcc/diagnostics/file-cache.h new file mode 100644 index 0000000..832a960 --- /dev/null +++ b/gcc/diagnostics/file-cache.h @@ -0,0 +1,125 @@ +/* Caching input files for use by diagnostics. + Copyright (C) 2004-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_DIAGNOSTICS_FILE_CACHE_H +#define GCC_DIAGNOSTICS_FILE_CACHE_H + +namespace diagnostics { + +/* A class capturing the bounds of a buffer, to allow for run-time + bounds-checking in a checked build. */ + +class char_span +{ + public: + char_span (const char *ptr, size_t n_elts) : m_ptr (ptr), m_n_elts (n_elts) {} + + /* Test for a non-NULL pointer. */ + operator bool() const { return m_ptr; } + + /* Get length, not including any 0-terminator (which may not be, + in fact, present). */ + size_t length () const { return m_n_elts; } + + const char *get_buffer () const { return m_ptr; } + + char operator[] (int idx) const + { + gcc_assert (idx >= 0); + gcc_assert ((size_t)idx < m_n_elts); + return m_ptr[idx]; + } + + char_span subspan (int offset, int n_elts) const + { + gcc_assert (offset >= 0); + gcc_assert (offset < (int)m_n_elts); + gcc_assert (n_elts >= 0); + gcc_assert (offset + n_elts <= (int)m_n_elts); + return char_span (m_ptr + offset, n_elts); + } + + char *xstrdup () const + { + return ::xstrndup (m_ptr, m_n_elts); + } + + private: + const char *m_ptr; + size_t m_n_elts; +}; + +/* Forward decl of slot within file_cache, so that the definition doesn't + need to be in this header. */ +class file_cache_slot; + +/* A cache of source files for use when emitting diagnostics + (and in a few places in the C/C++ frontends). + + Results are only valid until the next call to the cache, as + slots can be evicted. + + Filenames are stored by pointer, and so must outlive the cache + instance. */ + +class file_cache +{ + public: + file_cache (); + ~file_cache (); + + void dump (FILE *out, int indent) const; + void DEBUG_FUNCTION dump () const; + + file_cache_slot *lookup_or_add_file (const char *file_path); + void forcibly_evict_file (const char *file_path); + + /* See comments in diagnostic.h about the input conversion context. */ + struct input_context + { + diagnostic_input_charset_callback ccb; + bool should_skip_bom; + }; + void initialize_input_context (diagnostic_input_charset_callback ccb, + bool should_skip_bom); + + char_span get_source_file_content (const char *file_path); + char_span get_source_line (const char *file_path, int line); + bool missing_trailing_newline_p (const char *file_path); + + void add_buffered_content (const char *file_path, + const char *buffer, + size_t sz); + + void tune (size_t num_file_slots, size_t lines); + + private: + file_cache_slot *evicted_cache_tab_entry (unsigned *highest_use_count); + file_cache_slot *add_file (const char *file_path); + file_cache_slot *lookup_file (const char *file_path); + + private: + size_t m_num_file_slots; + file_cache_slot *m_file_slots; + input_context m_input_context; +}; + +} // namespace diagnostics + +#endif // #ifndef GCC_DIAGNOSTICS_FILE_CACHE_H diff --git a/gcc/diagnostics/html-sink.cc b/gcc/diagnostics/html-sink.cc new file mode 100644 index 0000000..13d6309 --- /dev/null +++ b/gcc/diagnostics/html-sink.cc @@ -0,0 +1,1708 @@ +/* HTML output for diagnostics. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + +#include "config.h" +#define INCLUDE_MAP +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostics/metadata.h" +#include "diagnostics/sink.h" +#include "diagnostics/html-sink.h" +#include "diagnostics/text-sink.h" +#include "diagnostics/sarif-sink.h" +#include "diagnostics/output-file.h" +#include "diagnostics/buffering.h" +#include "diagnostics/paths.h" +#include "diagnostics/client-data-hooks.h" +#include "selftest.h" +#include "diagnostics/selftest-context.h" +#include "pretty-print-format-impl.h" +#include "pretty-print-urlifier.h" +#include "diagnostics/changes.h" +#include "intl.h" +#include "xml.h" +#include "xml-printer.h" +#include "diagnostics/digraphs.h" +#include "diagnostics/state-graphs.h" +#include "graphviz.h" +#include "json.h" +#include "selftest-xml.h" + +namespace diagnostics { + +// struct html_generation_options + +html_generation_options::html_generation_options () +: m_css (true), + m_javascript (true), + m_show_state_diagrams (false), + m_show_state_diagrams_sarif (false), + m_show_state_diagrams_dot_src (false) +{ +} + +class html_builder; + +/* Concrete buffering implementation subclass for HTML output. */ + +class html_sink_buffer : public per_sink_buffer +{ +public: + friend class html_builder; + friend class html_sink; + + html_sink_buffer (html_builder &builder) + : m_builder (builder) + {} + + void dump (FILE *out, int indent) const final override; + bool empty_p () const final override; + void move_to (per_sink_buffer &dest) final override; + void clear () final override; + void flush () final override; + + void add_result (std::unique_ptr<xml::element> result) + { + m_results.push_back (std::move (result)); + } + +private: + html_builder &m_builder; + std::vector<std::unique_ptr<xml::element>> m_results; +}; + +/* A class for managing HTML output of diagnostics. + + Implemented: + - message text + + Known limitations/missing functionality: + - title for page + - file/line/column + - error vs warning + - CWEs + - rules + - fix-it hints + - paths +*/ + +class html_builder +{ +public: + friend class html_sink_buffer; + + html_builder (context &dc, + pretty_printer &pp, + const line_maps *line_maps, + const html_generation_options &html_gen_opts); + + void + set_main_input_filename (const char *name); + + void on_report_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + html_sink_buffer *buffer); + void emit_diagram (const diagram &d); + void emit_global_graph (const lazily_created<digraphs::digraph> &); + + void end_group (); + + std::unique_ptr<xml::element> take_current_diagnostic () + { + return std::move (m_cur_diagnostic_element); + } + + void flush_to_file (FILE *outf); + + const xml::document &get_document () const { return *m_document; } + + void set_printer (pretty_printer &pp) + { + m_printer = &pp; + } + + std::unique_ptr<xml::element> + make_element_for_metadata (const metadata &); + + std::unique_ptr<xml::element> + make_element_for_patch (const diagnostic_info &diagnostic); + + void add_focus_id (std::string focus_id) + { + m_ui_focus_ids.append_string (focus_id.c_str ()); + } + + std::unique_ptr<xml::node> + maybe_make_state_diagram (const paths::event &event); + +private: + void + add_stylesheet (std::string url); + + std::unique_ptr<xml::element> + make_element_for_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + bool alert); + + std::unique_ptr<xml::element> + make_metadata_element (label_text label, + label_text url); + + void + add_at_nesting_level (size_t nesting_level, + std::unique_ptr<xml::element> child_diag_element); + + void + push_nesting_level (); + + void + pop_nesting_level (); + + void + add_graph (const digraphs::digraph &dg, + xml::element &parent_element); + + context &m_context; + pretty_printer *m_printer; + const line_maps *m_line_maps; + html_generation_options m_html_gen_opts; + const logical_locations::manager *m_logical_loc_mgr; + + std::unique_ptr<xml::document> m_document; + xml::element *m_head_element; + xml::element *m_title_element; + xml::element *m_body_element; + xml::element *m_diagnostics_element; + std::unique_ptr<xml::element> m_cur_diagnostic_element; + std::vector<xml::element *> m_cur_nesting_levels; + int m_next_diag_id; // for handing out unique IDs + json::array m_ui_focus_ids; + logical_locations::key m_last_logical_location; + location_t m_last_location; + expanded_location m_last_expanded_location; +}; + +static std::unique_ptr<xml::element> +make_div (std::string class_) +{ + auto div = std::make_unique<xml::element> ("div", false); + div->set_attr ("class", std::move (class_)); + return div; +} + +static std::unique_ptr<xml::element> +make_span (std::string class_) +{ + auto span = std::make_unique<xml::element> ("span", true); + span->set_attr ("class", std::move (class_)); + return span; +} + +/* class html_sink_buffer : public per_sink_buffer. */ + +void +html_sink_buffer::dump (FILE *out, int indent) const +{ + fprintf (out, "%*shtml_sink_buffer:\n", indent, ""); + int idx = 0; + for (auto &result : m_results) + { + fprintf (out, "%*sresult[%i]:\n", indent + 2, "", idx); + result->dump (out); + fprintf (out, "\n"); + ++idx; + } +} + +bool +html_sink_buffer::empty_p () const +{ + return m_results.empty (); +} + +void +html_sink_buffer::move_to (per_sink_buffer &base) +{ + html_sink_buffer &dest + = static_cast<html_sink_buffer &> (base); + for (auto &&result : m_results) + dest.m_results.push_back (std::move (result)); + m_results.clear (); +} + +void +html_sink_buffer::clear () +{ + m_results.clear (); +} + +void +html_sink_buffer::flush () +{ + for (auto &&result : m_results) + m_builder.m_diagnostics_element->add_child (std::move (result)); + m_results.clear (); +} + +/* class html_builder. */ + +/* Style information for writing out HTML paths. + Colors taken from https://pf3.patternfly.org/v3/styles/color-palette/ */ + +static const char * const HTML_STYLE + = (" <style>\n" + " .linenum { color: white;\n" + " background-color: #0088ce;\n" + " white-space: pre;\n" + " border-right: 1px solid black; }\n" + " .ruler { color: red;\n" + " white-space: pre; }\n" + " .source { color: blue;\n" + " background-color: white;\n" + " white-space: pre; }\n" + " .annotation { color: green;\n" + " background-color: white;\n" + " white-space: pre; }\n" + " .linenum-gap { text-align: center;\n" + " border-top: 1px solid black;\n" + " border-right: 1px solid black;\n" + " background-color: #ededed; }\n" + " .source-gap { border-bottom: 1px dashed black;\n" + " border-top: 1px dashed black;\n" + " background-color: #ededed; }\n" + " .no-locus-event { font-family: monospace;\n" + " color: green;\n" + " white-space: pre; }\n" + " .funcname { font-weight: bold; }\n" + " .events-hdr { color: white;\n" + " background-color: #030303; }\n" + " .event-range { border: 1px solid black;\n" + " padding: 0px; }\n" + " .event-range-with-margin { border-spacing: 0; }\n" + " .locus { font-family: monospace;\n" + " border-spacing: 0px; }\n" + " .selected { color: white;\n" + " background-color: #0088ce; }\n" + " .stack-frame-with-margin { border-spacing: 0; }\n" + " .stack-frame { padding: 5px;\n" + " box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.5); }\n" + " .frame-funcname { text-align: right;\n" + " font-style: italic; } \n" + " .highlight-a { color: #703fec;\n" // pf-purple-400 + " font-weight: bold; }\n" + " .highlight-b { color: #3f9c35;\n" // pf-green-400 + " font-weight: bold; }\n" + " .gcc-quoted-text { font-weight: bold;\n" + " font-family: mono; }\n" + " </style>\n"); + +/* A little JavaScript for ease of navigation. + Keys j/k move forward and backward cyclically through a list + of focus ids (written out in another <script> tag as the HTML + is flushed). */ + +const char * const HTML_SCRIPT + = (" var current_focus_idx = 0;\n" + "\n" + " function get_focus_span (focus_idx)\n" + " {\n" + " const element_id = focus_ids[focus_idx];\n" + " return document.getElementById(element_id);\n" + " }\n" + " function get_any_state_diagram (focus_idx)\n" + " {\n" + " const element_id = focus_ids[focus_idx];\n" + " return document.getElementById(element_id + \"-state-diagram\");\n" + " }\n" + " function unhighlight_current_focus_idx ()\n" + " {\n" + " get_focus_span (current_focus_idx).classList.remove ('selected');\n" + " state_diagram = get_any_state_diagram (current_focus_idx);\n" + " if (state_diagram) {\n" + " state_diagram.style.visibility = \"hidden\";\n" + " }\n" + " }\n" + " function highlight_current_focus_idx ()\n" + " {\n" + " const el = get_focus_span (current_focus_idx);\n" + " el.classList.add ('selected');\n" + " state_diagram = get_any_state_diagram (current_focus_idx);\n" + " if (state_diagram) {\n" + " state_diagram.style.visibility = \"visible\";\n" + " }\n" + " // Center the element on the screen\n" + " const top_y = el.getBoundingClientRect ().top + window.pageYOffset;\n" + " const middle = top_y - (window.innerHeight / 2);\n" + " window.scrollTo (0, middle);\n" + " }\n" + " function select_prev_focus_idx ()\n" + " {\n" + " unhighlight_current_focus_idx ();\n" + " if (current_focus_idx > 0)\n" + " current_focus_idx -= 1;\n" + " else\n" + " current_focus_idx = focus_ids.length - 1;\n" + " highlight_current_focus_idx ();\n" + " }\n" + " function select_next_focus_idx ()\n" + " {\n" + " unhighlight_current_focus_idx ();\n" + " if (current_focus_idx < focus_ids.length - 1)\n" + " current_focus_idx += 1;\n" + " else\n" + " current_focus_idx = 0;\n" + " highlight_current_focus_idx ();\n" + " }\n" + " document.addEventListener('keydown', function (ev) {\n" + " if (ev.key == 'j')\n" + " select_next_focus_idx ();\n" + " else if (ev.key == 'k')\n" + " select_prev_focus_idx ();\n" + " });\n" + " highlight_current_focus_idx ();\n"); + +struct html_doctypedecl : public xml::doctypedecl +{ + void write_as_xml (pretty_printer *pp, + int depth, bool indent) const final override + { + if (indent) + { + for (int i = 0; i < depth; ++i) + pp_string (pp, " "); + } + pp_string (pp, "<!DOCTYPE html\n" + " PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n" + " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"); + if (indent) + pp_newline (pp); + } +}; + +/* html_builder's ctor. */ + +html_builder::html_builder (context &dc, + pretty_printer &pp, + const line_maps *line_maps, + const html_generation_options &html_gen_opts) +: m_context (dc), + m_printer (&pp), + m_line_maps (line_maps), + m_html_gen_opts (html_gen_opts), + m_logical_loc_mgr (nullptr), + m_head_element (nullptr), + m_title_element (nullptr), + m_body_element (nullptr), + m_diagnostics_element (nullptr), + m_next_diag_id (0), + m_last_location (UNKNOWN_LOCATION), + m_last_expanded_location ({}) +{ + gcc_assert (m_line_maps); + + if (auto client_data_hooks = dc.get_client_data_hooks ()) + m_logical_loc_mgr = client_data_hooks->get_logical_location_manager (); + + m_document = std::make_unique<xml::document> (); + m_document->m_doctypedecl = std::make_unique<html_doctypedecl> (); + { + auto html_element = std::make_unique<xml::element> ("html", false); + html_element->set_attr ("xmlns", + "http://www.w3.org/1999/xhtml"); + xml::printer xp (*html_element.get ()); + m_document->add_child (std::move (html_element)); + + { + xml::auto_print_element head (xp, "head"); + m_head_element = xp.get_insertion_point (); + { + 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) + { + add_stylesheet ("https://cdnjs.cloudflare.com/ajax/libs/patternfly/3.24.0/css/patternfly.min.css"); + add_stylesheet ("https://cdnjs.cloudflare.com/ajax/libs/patternfly/3.24.0/css/patternfly-additions.min.css"); + xp.add_raw (HTML_STYLE); + } + if (m_html_gen_opts.m_javascript) + { + xp.push_tag ("script"); + /* Escaping rules are different for HTML <script> elements, + so add the script "raw" for now. */ + xp.add_raw (HTML_SCRIPT); + xp.pop_tag ("script"); + } + } + + { + xml::auto_print_element body (xp, "body"); + m_body_element = xp.get_insertion_point (); + { + auto diagnostics_element = make_div ("gcc-diagnostic-list"); + m_diagnostics_element = diagnostics_element.get (); + xp.append (std::move (diagnostics_element)); + } + } + } +} + +void +html_builder::set_main_input_filename (const char *name) +{ + gcc_assert (m_title_element); + if (name) + { + m_title_element->m_children.clear (); + m_title_element->add_text (name); + } +} + +void +html_builder::add_stylesheet (std::string url) +{ + gcc_assert (m_head_element); + + xml::printer xp (*m_head_element); + xp.push_tag ("link", false); + xp.set_attr ("rel", "stylesheet"); + xp.set_attr ("type", "text/css"); + xp.set_attr ("href", std::move (url)); +} + +/* Implementation of "on_report_diagnostic" for HTML output. */ + +void +html_builder::on_report_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + html_sink_buffer *buffer) +{ + if (diagnostic.m_kind == kind::ice || diagnostic.m_kind == kind::ice_nobt) + { + /* Print a header for the remaining output to stderr, and + return, attempting to print the usual ICE messages to + stderr. Hopefully this will be helpful to the user in + indicating what's gone wrong (also for DejaGnu, for pruning + those messages). */ + fnotice (stderr, "Internal compiler error:\n"); + } + + const int nesting_level = m_context.get_diagnostic_nesting_level (); + bool alert = true; + if (m_cur_diagnostic_element && nesting_level > 0) + alert = false; + if (!m_cur_diagnostic_element) + m_last_logical_location = logical_locations::key (); + auto diag_element + = make_element_for_diagnostic (diagnostic, orig_diag_kind, alert); + if (buffer) + { + gcc_assert (!m_cur_diagnostic_element); + buffer->m_results.push_back (std::move (diag_element)); + } + else + { + if (m_cur_diagnostic_element) + { + /* Nested diagnostic. */ + gcc_assert (nesting_level >= 0); + add_at_nesting_level (nesting_level, std::move (diag_element)); + } + else + /* Top-level diagnostic. */ + { + m_cur_diagnostic_element = std::move (diag_element); + m_cur_nesting_levels.clear (); + } + } +} + +// For ease of comparison with experimental-nesting-show-levels=yes + +static void +add_nesting_level_attr (xml::element &element, + int nesting_level) +{ + element.set_attr ("nesting-level", std::to_string (nesting_level)); +} + +void +html_builder:: +add_at_nesting_level (size_t nesting_level, + std::unique_ptr<xml::element> child_diag_element) +{ + gcc_assert (m_cur_diagnostic_element); + while (nesting_level > m_cur_nesting_levels.size ()) + push_nesting_level (); + while (nesting_level < m_cur_nesting_levels.size ()) + pop_nesting_level (); + + if (nesting_level > 0) + { + gcc_assert (!m_cur_nesting_levels.empty ()); + auto current_nesting_level = m_cur_nesting_levels.back (); + xml::printer xp (*current_nesting_level); + xp.push_tag ("li"); + add_nesting_level_attr (*xp.get_insertion_point (), + m_cur_nesting_levels.size ()); + xp.append (std::move (child_diag_element)); + xp.pop_tag ("li"); + } + else + m_cur_diagnostic_element->add_child (std::move (child_diag_element)); +} + +void +html_builder::push_nesting_level () +{ + gcc_assert (m_cur_diagnostic_element); + auto new_nesting_level = std::make_unique<xml::element> ("ul", false); + add_nesting_level_attr (*new_nesting_level, + m_cur_nesting_levels.size () + 1); + xml::element *current_nesting_level = nullptr; + if (!m_cur_nesting_levels.empty ()) + current_nesting_level = m_cur_nesting_levels.back (); + m_cur_nesting_levels.push_back (new_nesting_level.get ()); + if (current_nesting_level) + current_nesting_level->add_child (std::move (new_nesting_level)); + else + m_cur_diagnostic_element->add_child (std::move (new_nesting_level)); +} + +void +html_builder::pop_nesting_level () +{ + gcc_assert (m_cur_diagnostic_element); + m_cur_nesting_levels.pop_back (); +} + +static void +print_pre_source (xml::printer &xp, const char *text) +{ + xp.push_tag_with_class ("pre", "source", true); + xp.add_text (text); + xp.pop_tag ("pre"); +} + +std::unique_ptr<xml::node> +html_builder::maybe_make_state_diagram (const paths::event &event) +{ + if (!m_html_gen_opts.m_show_state_diagrams) + return nullptr; + + if (!m_logical_loc_mgr) + return nullptr; + + /* Get state graph; if we're going to print it later, also request + the debug version. */ + auto state_graph + = event.maybe_make_diagnostic_state_graph + (m_html_gen_opts.m_show_state_diagrams_sarif); + if (!state_graph) + return nullptr; + + // Convert it to .dot AST + auto dot_graph = state_graphs::make_dot_graph (*state_graph, + *m_logical_loc_mgr); + gcc_assert (dot_graph); + + auto wrapper = std::make_unique<xml::element> ("div", false); + xml::printer xp (*wrapper); + + if (m_html_gen_opts.m_show_state_diagrams_sarif) + { + // For debugging, show the SARIF src inline: + pretty_printer pp; + state_graph->make_json_sarif_graph ()->print (&pp, true); + print_pre_source (xp, pp_formatted_text (&pp)); + } + + if (m_html_gen_opts.m_show_state_diagrams_dot_src) + { + // For debugging, show the dot src inline: + pretty_printer pp; + dot::writer w (pp); + dot_graph->print (w); + print_pre_source (xp, pp_formatted_text (&pp)); + } + + // Turn the .dot into SVG and splice into place + auto svg = dot::make_svg_from_graph (*dot_graph); + if (svg) + xp.append (std::move (svg)); + + return wrapper; +} + +/* Custom subclass of html_label_writer. + Wrap labels within a <span> element, supplying them with event IDs. + Add the IDs to the list of focus IDs. */ + +class html_path_label_writer : public html_label_writer +{ +public: + html_path_label_writer (xml::printer &xp, + html_builder &builder, + const paths::path &path, + const std::string &event_id_prefix) + : m_xp (xp), + m_html_builder (builder), + m_path (path), + m_event_id_prefix (event_id_prefix), + m_next_event_idx (0), + m_curr_event_id () + { + } + + void begin_label () final override + { + m_curr_event_id = m_next_event_idx++; + m_xp.push_tag_with_class ("span", "event", true); + m_xp.set_attr ("id", get_element_id ()); + m_html_builder.add_focus_id (get_element_id ()); + } + + void end_label () final override + { + const paths::event &event + = m_path.get_event (m_curr_event_id.zero_based ()); + if (auto state_doc = m_html_builder.maybe_make_state_diagram (event)) + { + m_xp.push_tag_with_class ("div", "state-diagram", false); + m_xp.set_attr ("id", get_element_id () + "-state-diagram"); + m_xp.set_attr ("style", + ("position: absolute;" + " z-index: 1;" + " visibility: hidden;")); + m_xp.append (std::move (state_doc)); + m_xp.pop_tag ("div"); + } + + m_xp.pop_tag ("span"); // from begin_label + } + +private: + std::string + get_element_id () const + { + gcc_assert (m_curr_event_id.known_p ()); + return (m_event_id_prefix + + std::to_string (m_curr_event_id.zero_based ())); + } + + xml::printer &m_xp; + html_builder &m_html_builder; + const paths::path &m_path; + const std::string &m_event_id_prefix; + int m_next_event_idx; + paths::event_id_t m_curr_event_id; +}; + +/* See https://pf3.patternfly.org/v3/pattern-library/widgets/#alerts */ +static const char * +get_pf_class_for_alert_div (enum kind diag_kind) +{ + switch (diag_kind) + { + case kind::debug: + case kind::note: + return "alert alert-info"; + + case kind::anachronism: + case kind::warning: + return "alert alert-warning"; + + case kind::error: + case kind::sorry: + case kind::ice: + case kind::ice_nobt: + case kind::fatal: + return "alert alert-danger"; + + default: + gcc_unreachable (); + } +} + +static const char * +get_pf_class_for_alert_icon (enum kind diag_kind) +{ + switch (diag_kind) + { + case kind::debug: + case kind::note: + return "pficon pficon-info"; + + case kind::anachronism: + case kind::warning: + return "pficon pficon-warning-triangle-o"; + + case kind::error: + case kind::sorry: + case kind::ice: + case kind::ice_nobt: + case kind::fatal: + return "pficon pficon-error-circle-o"; + + default: + gcc_unreachable (); + } +} + +static const char * +get_label_for_logical_location_kind (enum logical_locations::kind kind) +{ + switch (kind) + { + default: + gcc_unreachable (); + case logical_locations::kind::unknown: + return nullptr; + + /* Kinds within executable code. */ + case logical_locations::kind::function: + return "Function"; + case logical_locations::kind::member: + return "Member"; + case logical_locations::kind::module_: + return "Module"; + case logical_locations::kind::namespace_: + return "Namespace"; + case logical_locations::kind::type: + return "Type"; + case logical_locations::kind::return_type: + return "Return type"; + case logical_locations::kind::parameter: + return "Parameter"; + case logical_locations::kind::variable: + return "Variable"; + + /* Kinds within XML or HTML documents. */ + case logical_locations::kind::element: + return "Element"; + case logical_locations::kind::attribute: + return "Attribute"; + case logical_locations::kind::text: + return "Text"; + case logical_locations::kind::comment: + return "Comment"; + case logical_locations::kind::processing_instruction: + return "Processing Instruction"; + case logical_locations::kind::dtd: + return "DTD"; + case logical_locations::kind::declaration: + return "Declaration"; + + /* Kinds within JSON documents. */ + case logical_locations::kind::object: + return "Object"; + case logical_locations::kind::array: + return "Array"; + case logical_locations::kind::property: + return "Property"; + case logical_locations::kind::value: + return "Value"; + } +} + +static void +add_labelled_value (xml::printer &xp, + std::string id, + std::string label, + std::string value, + bool quote_value) +{ + xp.push_tag ("div", true); + xp.set_attr ("id", id); + xp.push_tag ("span"); + xp.add_text (label); + xp.add_text (" "); + xp.pop_tag ("span"); + xp.push_tag ("span"); + if (quote_value) + xp.set_attr ("class", "gcc-quoted-text"); + xp.add_text (std::move (value)); + xp.pop_tag ("span"); + xp.pop_tag ("div"); +} + +class html_token_printer : public token_printer +{ +public: + html_token_printer (xml::element &parent_element) + /* Ideally pp_token_lists that reach a token_printer should be + "balanced", but for now they can have mismatching pp_tokens + e.g. a begin_color without an end_color (PR other/120610). + Give html_token_printer its own xml::printer as a firewall to + limit the scope of the mismatches in the HTML. */ + : m_xp (parent_element, + /* Similarly we don't check that the popped tags match. */ + false) + { + } + void print_tokens (pretty_printer */*pp*/, + const pp_token_list &tokens) final override + { + /* Implement print_tokens by adding child elements to + m_parent_element. */ + for (auto iter = tokens.m_first; iter; iter = iter->m_next) + switch (iter->m_kind) + { + default: + gcc_unreachable (); + + case pp_token::kind::text: + { + pp_token_text *sub = as_a <pp_token_text *> (iter); + /* The value might be in the obstack, so we may need to + copy it. */ + m_xp.add_text (sub->m_value.get ()); + } + break; + + case pp_token::kind::begin_color: + { + pp_token_begin_color *sub = as_a <pp_token_begin_color *> (iter); + gcc_assert (sub->m_value.get ()); + m_xp.push_tag_with_class ("span", sub->m_value.get ()); + } + break; + + case pp_token::kind::end_color: + m_xp.pop_tag ("span"); + break; + + case pp_token::kind::begin_quote: + { + m_xp.add_text (open_quote); + m_xp.push_tag_with_class ("span", "gcc-quoted-text"); + } + break; + case pp_token::kind::end_quote: + { + m_xp.pop_tag ("span"); + m_xp.add_text (close_quote); + } + break; + + case pp_token::kind::begin_url: + { + pp_token_begin_url *sub = as_a <pp_token_begin_url *> (iter); + m_xp.push_tag ("a", true); + m_xp.set_attr ("href", sub->m_value.get ()); + } + break; + case pp_token::kind::end_url: + m_xp.pop_tag ("a"); + break; + + case pp_token::kind::event_id: + { + pp_token_event_id *sub = as_a <pp_token_event_id *> (iter); + gcc_assert (sub->m_event_id.known_p ()); + m_xp.add_text ("("); + m_xp.add_text (std::to_string (sub->m_event_id.one_based ())); + m_xp.add_text (")"); + } + break; + } + } + +private: + xml::printer m_xp; +}; + +/* Make a <div class="gcc-diagnostic"> for DIAGNOSTIC. + + If ALERT is true, make it be a PatternFly alert (see + https://pf3.patternfly.org/v3/pattern-library/widgets/#alerts) and + show severity text (e.g. "error: "). + + If ALERT is false, don't show the severity text and don't show + the filename if it's the same as the previous diagnostic within the + diagnostic group. */ + +std::unique_ptr<xml::element> +html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + bool alert) +{ + const int diag_idx = m_next_diag_id++; + std::string diag_id; + { + pretty_printer pp; + pp_printf (&pp, "gcc-diag-%i", diag_idx); + diag_id = pp_formatted_text (&pp); + } + + // TODO: might be nice to emulate the text output format, but colorize it + + /* See https://pf3.patternfly.org/v3/pattern-library/widgets/#alerts + which has this example: +<div class="alert alert-danger"> + <span class="pficon pficon-error-circle-o"></span> + <strong>Hey there is a problem!</strong> Yeah this is really messed up and you should <a href="#" class="alert-link">know about it</a>. +</div> + */ + auto diag_element = make_div ("gcc-diagnostic"); + diag_element->set_attr ("id", diag_id); + if (alert) + diag_element->set_attr ("class", + get_pf_class_for_alert_div (diagnostic.m_kind)); + + xml::printer xp (*diag_element.get ()); + const size_t depth_within_alert_div = 1; + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + if (alert) + { + xp.push_tag_with_class ("span", + get_pf_class_for_alert_icon (diagnostic.m_kind), + true); + xp.add_text (" "); + xp.pop_tag ("span"); + } + + // The rest goes in the <div>... + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + xp.push_tag_with_class ("div", "gcc-message", true); + std::string message_alert_id (diag_id + "-message"); + xp.set_attr ("id", message_alert_id); + add_focus_id (message_alert_id); + + const size_t depth_within_message_div = depth_within_alert_div + 1; + gcc_assert (xp.get_num_open_tags () == depth_within_message_div); + + // Severity e.g. "warning: " + bool show_severity = true; + if (!alert) + show_severity = false; + if (show_severity) + { + xp.push_tag ("strong"); + xp.add_text (_(get_text_for_kind (diagnostic.m_kind))); + xp.pop_tag ("strong"); + xp.add_text (" "); + } + + // Add the message itself: + html_token_printer tok_printer (*xp.get_insertion_point ()); + m_printer->set_token_printer (&tok_printer); + pp_output_formatted_text (m_printer, m_context.get_urlifier ()); + m_printer->set_token_printer (nullptr); + pp_clear_output_area (m_printer); + + // Add any metadata as a suffix to the message + if (diagnostic.m_metadata) + { + xp.add_text (" "); + xp.append (make_element_for_metadata (*diagnostic.m_metadata)); + } + + // Add any option as a suffix to the message + + label_text option_text = label_text::take + (m_context.make_option_name (diagnostic.m_option_id, + orig_diag_kind, diagnostic.m_kind)); + if (option_text.get ()) + { + label_text option_url = label_text::take + (m_context.make_option_url (diagnostic.m_option_id)); + + xp.add_text (" "); + auto option_span = make_span ("gcc-option"); + option_span->add_text ("["); + { + if (option_url.get ()) + { + auto anchor = std::make_unique<xml::element> ("a", true); + anchor->set_attr ("href", option_url.get ()); + anchor->add_text (option_text.get ()); + option_span->add_child (std::move (anchor)); + } + else + option_span->add_text (option_text.get ()); + option_span->add_text ("]"); + } + xp.append (std::move (option_span)); + } + + gcc_assert (xp.get_num_open_tags () == depth_within_message_div); + + xp.pop_tag ("div"); + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + /* Show any logical location. */ + if (m_logical_loc_mgr) + if (auto client_data_hooks = m_context.get_client_data_hooks ()) + if (auto logical_loc = client_data_hooks->get_current_logical_location ()) + if (logical_loc != m_last_logical_location) + { + enum logical_locations::kind kind + = m_logical_loc_mgr->get_kind (logical_loc);; + if (const char *label = get_label_for_logical_location_kind (kind)) + if (const char *name_with_scope + = m_logical_loc_mgr->get_name_with_scope (logical_loc)) + add_labelled_value (xp, "logical-location", + label, name_with_scope, true); + m_last_logical_location = logical_loc; + } + + /* Show any physical location. */ + const expanded_location s + = diagnostic_expand_location (&diagnostic); + if (s != m_last_expanded_location + || alert) + { + if (s.file + && (s.file != m_last_expanded_location.file + || alert)) + add_labelled_value (xp, "file", "File", s.file, false); + if (s.line) + { + add_labelled_value (xp, "line", "Line", std::to_string (s.line), false); + column_policy col_policy (m_context); + int converted_column = col_policy.converted_column (s); + if (converted_column >= 0) + add_labelled_value (xp, "column", "Column", + std::to_string (converted_column), + false); + } + if (s.file) + m_last_expanded_location = s; + } + + /* Source (and fix-it hints). */ + { + // TODO: m_context.m_last_location should be moved into the sink + location_t saved = m_context.m_last_location; + m_context.m_last_location = m_last_location; + m_context.maybe_show_locus_as_html + (*diagnostic.m_richloc, + m_context.get_source_printing_options (), + diagnostic.m_kind, + xp, + nullptr, + nullptr); + m_context.m_last_location = saved; + m_last_location = m_context.m_last_location; + } + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + /* Execution path. */ + if (auto path = diagnostic.m_richloc->get_path ()) + { + xp.push_tag ("div"); + xp.set_attr ("id", "execution-path"); + + xp.push_tag ("label", true); + const int num_events = path->num_events (); + pretty_printer pp; + pp_printf_n (&pp, num_events, + "Execution path with %i event", + "Execution path with %i events", + num_events); + xp.add_text_from_pp (pp); + xp.pop_tag ("label"); + + std::string event_id_prefix (diag_id + "-event-"); + html_path_label_writer event_label_writer (xp, *this, *path, + event_id_prefix); + + source_print_policy dspp (m_context); + print_path_as_html (xp, *path, m_context, &event_label_writer, + dspp); + + xp.pop_tag ("div"); + } + + gcc_assert (xp.get_num_open_tags () == depth_within_alert_div); + + // Try to display any per-diagnostic graphs + if (diagnostic.m_metadata) + if (auto ldg = diagnostic.m_metadata->get_lazy_digraphs ()) + { + auto &digraphs = ldg->get_or_create (); + for (auto &dg : digraphs) + add_graph (*dg, *xp.get_insertion_point ()); + } + + if (auto patch_element = make_element_for_patch (diagnostic)) + { + xp.push_tag ("div"); + xp.set_attr ("id", "suggested-fix"); + xp.push_tag ("label", true); + xp.add_text ("Suggested fix"); + xp.pop_tag ("label"); + xp.append (std::move (patch_element)); + xp.pop_tag ("div"); + } + + return diag_element; +} + +std::unique_ptr<xml::element> +html_builder::make_element_for_patch (const diagnostic_info &diagnostic) +{ + changes::change_set edit (m_context.get_file_cache ()); + edit.add_fixits (diagnostic.m_richloc); + if (char *diff = edit.generate_diff (true)) + { + if (strlen (diff) > 0) + { + auto element = std::make_unique<xml::element> ("pre", true); + element->set_attr ("class", "gcc-generated-patch"); + element->add_text (diff); + free (diff); + return element; + } + else + free (diff); + } + return nullptr; +} + +std::unique_ptr<xml::element> +html_builder::make_metadata_element (label_text label, + label_text url) +{ + auto item = make_span ("gcc-metadata-item"); + xml::printer xp (*item.get ()); + xp.add_text ("["); + { + if (url.get ()) + { + xp.push_tag ("a", true); + xp.set_attr ("href", url.get ()); + } + xp.add_text (label.get ()); + if (url.get ()) + xp.pop_tag ("a"); + } + xp.add_text ("]"); + return item; +} + +std::unique_ptr<xml::element> +html_builder::make_element_for_metadata (const metadata &m) +{ + auto span_metadata = make_span ("gcc-metadata"); + + int cwe = m.get_cwe (); + if (cwe) + { + pretty_printer pp; + pp_printf (&pp, "CWE-%i", cwe); + label_text label = label_text::take (xstrdup (pp_formatted_text (&pp))); + label_text url = label_text::take (get_cwe_url (cwe)); + span_metadata->add_child + (make_metadata_element (std::move (label), std::move (url))); + } + + for (unsigned idx = 0; idx < m.get_num_rules (); ++idx) + { + auto &rule = m.get_rule (idx); + label_text label = label_text::take (rule.make_description ()); + label_text url = label_text::take (rule.make_url ()); + span_metadata->add_child + (make_metadata_element (std::move (label), std::move (url))); + } + + return span_metadata; +} + +/* Implementation of diagnostics::context::m_diagrams.m_emission_cb + for HTML output. */ + +void +html_builder::emit_diagram (const diagram &) +{ + /* We must be within the emission of a top-level diagnostic. */ + gcc_assert (m_cur_diagnostic_element); + + // TODO: currently a no-op +} + +void +html_builder::add_graph (const digraphs::digraph &dg, + xml::element &parent_element) +{ + if (auto dot_graph = dg.make_dot_graph ()) + if (auto svg_element = dot::make_svg_from_graph (*dot_graph)) + { + auto div = std::make_unique<xml::element> ("div", false); + div->set_attr ("class", "gcc-directed-graph"); + xml::printer xp (*div); + if (const char *description = dg.get_description ()) + { + xp.push_tag ("h2", true); + xp.add_text (description); + xp.pop_tag ("h2"); + } + xp.append (std::move (svg_element)); + parent_element.add_child (std::move (div)); + } +} + +void +html_builder::emit_global_graph (const lazily_created<digraphs::digraph> &ldg) +{ + auto &dg = ldg.get_or_create (); + gcc_assert (m_body_element); + add_graph (dg, *m_body_element); +} + +/* Implementation of "end_group_cb" for HTML output. */ + +void +html_builder::end_group () +{ + if (m_cur_diagnostic_element) + m_diagnostics_element->add_child (std::move (m_cur_diagnostic_element)); +} + +/* Create a top-level object, and add it to all the results + (and other entities) we've seen so far. + + Flush it all to OUTF. */ + +void +html_builder::flush_to_file (FILE *outf) +{ + if (m_html_gen_opts.m_javascript) + { + gcc_assert (m_head_element); + xml::printer xp (*m_head_element); + /* Add an initialization of the global js variable "focus_ids" + using the array of IDs we saved as we went. */ + xp.push_tag ("script"); + pretty_printer pp; + pp_string (&pp, "focus_ids = "); + m_ui_focus_ids.print (&pp, true); + pp_string (&pp, ";\n"); + xp.add_raw (pp_formatted_text (&pp)); + xp.pop_tag ("script"); + } + auto top = m_document.get (); + top->dump (outf); + fprintf (outf, "\n"); +} + +class html_sink : public sink +{ +public: + ~html_sink () + { + /* Any diagnostics should have been handled by now. + If not, then something's gone wrong with diagnostic + groupings. */ + std::unique_ptr<xml::element> pending_diag + = m_builder.take_current_diagnostic (); + gcc_assert (!pending_diag); + } + + void dump (FILE *out, int indent) const override + { + fprintf (out, "%*shtml_sink\n", indent, ""); + sink::dump (out, indent); + } + + void + set_main_input_filename (const char *name) final override + { + m_builder.set_main_input_filename (name); + } + + std::unique_ptr<per_sink_buffer> + make_per_sink_buffer () final override + { + return std::make_unique<html_sink_buffer> (m_builder); + } + void set_buffer (per_sink_buffer *base_buffer) final override + { + html_sink_buffer *buffer + = static_cast<html_sink_buffer *> (base_buffer); + m_buffer = buffer; + } + + void on_begin_group () final override + { + /* No-op, */ + } + void on_end_group () final override + { + m_builder.end_group (); + } + void + on_report_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind) final override + { + m_builder.on_report_diagnostic (diagnostic, orig_diag_kind, m_buffer); + } + void on_diagram (const diagram &d) final override + { + m_builder.emit_diagram (d); + } + void after_diagnostic (const diagnostic_info &) final override + { + /* No-op, but perhaps could show paths here. */ + } + bool follows_reference_printer_p () const final override + { + return false; + } + void update_printer () final override + { + m_printer = m_context.clone_printer (); + + /* Don't colorize the text. */ + pp_show_color (m_printer.get ()) = false; + + /* No textual URLs. */ + m_printer->set_url_format (URL_FORMAT_NONE); + + /* Update the builder to use the new printer. */ + m_builder.set_printer (*get_printer ()); + } + + void + report_global_digraph (const lazily_created<digraphs::digraph> &ldg) + final override + { + m_builder.emit_global_graph (ldg); + } + + const xml::document &get_document () const + { + return m_builder.get_document (); + } + + html_builder &get_builder () { return m_builder; } + +protected: + html_sink (context &dc, + const line_maps *line_maps, + const html_generation_options &html_gen_opts) + : sink (dc), + m_builder (dc, *get_printer (), line_maps, html_gen_opts), + m_buffer (nullptr) + {} + + html_builder m_builder; + html_sink_buffer *m_buffer; +}; + +class html_file_sink : public html_sink +{ +public: + html_file_sink (context &dc, + const line_maps *line_maps, + const html_generation_options &html_gen_opts, + output_file output_file_) + : html_sink (dc, line_maps, html_gen_opts), + m_output_file (std::move (output_file_)) + { + gcc_assert (m_output_file.get_open_file ()); + gcc_assert (m_output_file.get_filename ()); + } + ~html_file_sink () + { + m_builder.flush_to_file (m_output_file.get_open_file ()); + } + void dump (FILE *out, int indent) const override + { + fprintf (out, "%*shtml_file_sink: %s\n", + indent, "", + m_output_file.get_filename ()); + sink::dump (out, indent); + } + bool machine_readable_stderr_p () const final override + { + return false; + } + +private: + output_file m_output_file; +}; + +/* Attempt to open BASE_FILE_NAME.html for writing. + Return a non-null output_file, + or return a null output_file and complain to DC + using LINE_MAPS. */ + +output_file +open_html_output_file (context &dc, + line_maps *line_maps, + const char *base_file_name) +{ + if (!base_file_name) + { + rich_location richloc (line_maps, UNKNOWN_LOCATION); + dc.emit_diagnostic_with_group + (kind::error, richloc, nullptr, 0, + "unable to determine filename for HTML output"); + return output_file (); + } + + label_text filename = label_text::take (concat (base_file_name, + ".html", + nullptr)); + FILE *outf = fopen (filename.get (), "w"); + if (!outf) + { + rich_location richloc (line_maps, UNKNOWN_LOCATION); + dc.emit_diagnostic_with_group + (kind::error, richloc, nullptr, 0, + "unable to open %qs for HTML output: %m", + filename.get ()); + return output_file (); + } + return output_file (outf, true, std::move (filename)); +} + +std::unique_ptr<sink> +make_html_sink (context &dc, + const line_maps &line_maps, + const html_generation_options &html_gen_opts, + output_file output_file_) +{ + auto sink + = std::make_unique<html_file_sink> (dc, + &line_maps, + html_gen_opts, + std::move (output_file_)); + sink->update_printer (); + return sink; +} + +#if CHECKING_P + +namespace selftest { + +/* Helper for writing tests of html_token_printer. + Printing to m_pp will appear as HTML within m_top_element, a <div>. */ + +struct token_printer_test +{ + token_printer_test () + : m_top_element ("div", true), + m_tok_printer (m_top_element) + { + m_pp.set_token_printer (&m_tok_printer); + } + + xml::element m_top_element; + html_token_printer m_tok_printer; + pretty_printer m_pp; +}; + +static void +test_token_printer () +{ + { + token_printer_test t; + pp_printf (&t.m_pp, "hello world"); + ASSERT_XML_PRINT_EQ + (t.m_top_element, + "<div>hello world</div>\n"); + } + + { + token_printer_test t; + pp_printf (&t.m_pp, "%qs: %qs", "foo", "bar"); + ASSERT_XML_PRINT_EQ + (t.m_top_element, + "<div>" + "`" + "<span class=\"gcc-quoted-text\">" + "foo" + "</span>" + "': `" + "<span class=\"gcc-quoted-text\">" + "bar" + "</span>" + "'" + "</div>\n"); + } + + { + token_printer_test t; + paths::event_id_t event_id (0); + pp_printf (&t.m_pp, "foo %@ bar", &event_id); + ASSERT_XML_PRINT_EQ + (t.m_top_element, + "<div>foo (1) bar</div>\n"); + } +} + +/* A subclass of html_sink for writing selftests. + The XML output is cached internally, rather than written + out to a file. */ + +class test_html_context : public test_context +{ +public: + test_html_context () + { + html_generation_options html_gen_opts; + html_gen_opts.m_css = false; + html_gen_opts.m_javascript = false; + auto sink = std::make_unique<html_buffered_sink> (*this, + line_table, + html_gen_opts); + sink->update_printer (); + sink->set_main_input_filename ("(main input filename)"); + m_format = sink.get (); // borrowed + + set_sink (std::move (sink)); + } + + const xml::document &get_document () const + { + return m_format->get_document (); + } + + html_builder &get_builder () const + { + return m_format->get_builder (); + } + +private: + class html_buffered_sink : public html_sink + { + public: + html_buffered_sink (context &dc, + const line_maps *line_maps, + const html_generation_options &html_gen_opts) + : html_sink (dc, line_maps, html_gen_opts) + { + } + bool machine_readable_stderr_p () const final override + { + return true; + } + }; + + html_sink *m_format; // borrowed +}; + +/* Test of reporting a diagnostic at UNKNOWN_LOCATION to a + diagnostics::context and examining the generated XML document. + Verify various basic properties. */ + +static void +test_simple_log () +{ + test_html_context dc; + + rich_location richloc (line_table, UNKNOWN_LOCATION); + dc.report (kind::error, richloc, nullptr, 0, "this is a test: %qs", "foo"); + + const xml::document &doc = dc.get_document (); + + ASSERT_XML_PRINT_EQ + (doc, + ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE html\n" + " PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n" + " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n" + "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" + " <head>\n" + " <title>(main input filename)</title>\n" + " </head>\n" + " <body>\n" + " <div class=\"gcc-diagnostic-list\">\n" + " <div class=\"alert alert-danger\" id=\"gcc-diag-0\">\n" + " <span class=\"pficon pficon-error-circle-o\"> </span>\n" + " <div class=\"gcc-message\" id=\"gcc-diag-0-message\"><strong>error: </strong> this is a test: `<span class=\"gcc-quoted-text\">foo</span>'</div>\n" + " </div>\n" + " </div>\n" + " </body>\n" + "</html>\n")); +} + +static void +test_metadata () +{ + test_html_context dc; + html_builder &b = dc.get_builder (); + + { + metadata m; + m.add_cwe (415); + auto element = b.make_element_for_metadata (m); + ASSERT_XML_PRINT_EQ + (*element, + "<span class=\"gcc-metadata\">" + "<span class=\"gcc-metadata-item\">" + "[" + "<a href=\"https://cwe.mitre.org/data/definitions/415.html\">" + "CWE-415" + "</a>" + "]" + "</span>" + "</span>\n"); + } + + { + metadata m; + metadata::precanned_rule rule ("MISC-42", + "http://example.com"); + m.add_rule (rule); + auto element = b.make_element_for_metadata (m); + ASSERT_XML_PRINT_EQ + (*element, + "<span class=\"gcc-metadata\">" + "<span class=\"gcc-metadata-item\">" + "[" + "<a href=\"http://example.com\">" + "MISC-42" + "</a>" + "]" + "</span>" + "</span>\n"); + } +} + +/* Run all of the selftests within this file. */ + +void +html_sink_cc_tests () +{ + ::selftest::auto_fix_quotes fix_quotes; + test_token_printer (); + test_simple_log (); + test_metadata (); +} + +} // namespace selftest + +#endif /* CHECKING_P */ + +} // namespace diagnostics diff --git a/gcc/diagnostics/html-sink.h b/gcc/diagnostics/html-sink.h new file mode 100644 index 0000000..d86bde8 --- /dev/null +++ b/gcc/diagnostics/html-sink.h @@ -0,0 +1,68 @@ +/* HTML output for diagnostics. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_HTML_SINK_H +#define GCC_DIAGNOSTICS_HTML_SINK_H + +#include "diagnostics/sink.h" +#include "diagnostics/output-file.h" + +namespace diagnostics { + +struct html_generation_options +{ + html_generation_options (); + + bool m_css; + bool m_javascript; + + // Debugging options: + + // If true, attempt to show state diagrams at events + bool m_show_state_diagrams; + + // If true, show the SARIF form of the state with such diagrams + bool m_show_state_diagrams_sarif; + + // If true, show the .dot source used for the diagram + bool m_show_state_diagrams_dot_src; +}; + +extern diagnostics::output_file +open_html_output_file (context &dc, + line_maps *line_maps, + const char *base_file_name); + +extern std::unique_ptr<sink> +make_html_sink (context &dc, + const line_maps &line_maps, + const html_generation_options &html_gen_opts, + output_file output_file_); + +extern void +print_path_as_html (xml::printer &xp, + const paths::path &path, + context &dc, + html_label_writer *event_label_writer, + const source_print_policy &dspp); + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_HTML_SINK_H */ diff --git a/gcc/diagnostics/kinds.def b/gcc/diagnostics/kinds.def new file mode 100644 index 0000000..2a0a0a6 --- /dev/null +++ b/gcc/diagnostics/kinds.def @@ -0,0 +1,55 @@ +/* Copyright (C) 2001-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/>. */ + +/* kind::unspecified must be first so it has a value of zero. We never + assign this kind to an actual diagnostic, we only use this in + variables that can hold a kind, to mean they have yet to have a + kind specified. I.e. they're uninitialized. Within the diagnostic + machinery, this kind also means "don't change the existing kind", + meaning "no change is specified". */ +DEFINE_DIAGNOSTIC_KIND (unspecified, "", NULL) + +/* If a diagnostic is set to kind::ignored, it won't get reported at all. + This is used by the diagnostic machinery when it wants to disable a + diagnostic without disabling the option which causes it. */ +DEFINE_DIAGNOSTIC_KIND (ignored, "", NULL) + +/* The remainder are real diagnostic types. */ +DEFINE_DIAGNOSTIC_KIND (fatal, "fatal error: ", "error") +DEFINE_DIAGNOSTIC_KIND (ice, "internal compiler error: ", "error") +DEFINE_DIAGNOSTIC_KIND (error, "error: ", "error") +DEFINE_DIAGNOSTIC_KIND (sorry, "sorry, unimplemented: ", "error") +DEFINE_DIAGNOSTIC_KIND (warning, "warning: ", "warning") +DEFINE_DIAGNOSTIC_KIND (anachronism, "anachronism: ", "warning") +DEFINE_DIAGNOSTIC_KIND (note, "note: ", "note") +DEFINE_DIAGNOSTIC_KIND (debug, "debug: ", "note") + +/* For use when using the diagnostic_show_locus machinery to show + a range of events within a path. */ +DEFINE_DIAGNOSTIC_KIND (path, "path: ", "path") + +/* These two would be re-classified as kind::warning or kind::error, so the +prefix does not matter. */ +DEFINE_DIAGNOSTIC_KIND (pedwarn, "pedwarn: ", NULL) +DEFINE_DIAGNOSTIC_KIND (permerror, "permerror: ", NULL) +/* This one is just for counting kind::warning promoted to kind::error + due to -Werror and -Werror=warning. */ +DEFINE_DIAGNOSTIC_KIND (werror, "error: ", NULL) +/* This is like kind::ICE, but backtrace is not printed. Used in the driver + when reporting fatal signal in the compiler. */ +DEFINE_DIAGNOSTIC_KIND (ice_nobt, "internal compiler error: ", "error") diff --git a/gcc/diagnostics/kinds.h b/gcc/diagnostics/kinds.h new file mode 100644 index 0000000..7b4a168 --- /dev/null +++ b/gcc/diagnostics/kinds.h @@ -0,0 +1,45 @@ +/* An enum used to discriminate severities of diagnostics. + Copyright (C) 1998-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_DIAGNOSTICS_KINDS_H +#define GCC_DIAGNOSTICS_KINDS_H + +namespace diagnostics { + +/* Constants used to discriminate diagnostics. */ +enum class kind +{ +#define DEFINE_DIAGNOSTIC_KIND(K, msgid, C) K, +#include "diagnostics/kinds.def" +#undef DEFINE_DIAGNOSTIC_KIND + last_diagnostic_kind, + /* This is used for tagging pragma pops in the diagnostic + classification history chain. */ + pop, + /* This is used internally to note that a diagnostic is enabled + without mandating any specific type. */ + any +}; + +extern const char *get_text_for_kind (enum diagnostics::kind); +extern const char *get_color_for_kind (enum diagnostics::kind); + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_KINDS_H */ diff --git a/gcc/diagnostics/lazy-paths.cc b/gcc/diagnostics/lazy-paths.cc new file mode 100644 index 0000000..4934651 --- /dev/null +++ b/gcc/diagnostics/lazy-paths.cc @@ -0,0 +1,236 @@ +/* Helper class for deferring path creation until a diagnostic is emitted. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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/>. */ + + +#include "config.h" +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostics/lazy-paths.h" +#include "selftest.h" +#include "diagnostics/selftest-context.h" +#include "diagnostics/selftest-paths.h" +#include "diagnostics/text-sink.h" + +using namespace diagnostics::paths; + +/* class lazy_path : public path. */ + +/* Implementation of path vfuncs in terms of a lazily-generated + path. */ + +unsigned +lazy_path::num_events () const +{ + lazily_generate_path (); + return m_inner_path->num_events (); +} + +const event & +lazy_path::get_event (int idx) const +{ + lazily_generate_path (); + return m_inner_path->get_event (idx); +} + +unsigned +lazy_path::num_threads () const +{ + lazily_generate_path (); + return m_inner_path->num_threads (); +} + +const thread & +lazy_path::get_thread (thread_id_t idx) const +{ + lazily_generate_path (); + return m_inner_path->get_thread (idx); +} + +bool +lazy_path::same_function_p (int event_idx_a, + int event_idx_b) const +{ + lazily_generate_path (); + return m_inner_path->same_function_p (event_idx_a, event_idx_b); +} + +void +lazy_path::lazily_generate_path () const +{ + if (!m_inner_path) + m_inner_path = make_inner_path (); + gcc_assert (m_inner_path != nullptr); +} + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +using auto_fix_quotes = ::selftest::auto_fix_quotes; + +class test_lazy_path : public lazy_path +{ +public: + test_lazy_path (pretty_printer &pp) + : lazy_path (m_logical_loc_mgr), + m_pp (pp) + { + } + std::unique_ptr<path> make_inner_path () const final override + { + auto path + = std::make_unique<paths::selftest::test_path> (m_logical_loc_mgr, + &m_pp); + path->add_event (UNKNOWN_LOCATION, "foo", 0, "first %qs", "free"); + path->add_event (UNKNOWN_LOCATION, "foo", 0, "double %qs", "free"); + return path; + } +private: + mutable logical_locations::selftest::test_manager m_logical_loc_mgr; + pretty_printer &m_pp; +}; + +static void +test_intraprocedural_path (pretty_printer *event_pp) +{ + test_lazy_path path (*event_pp); + ASSERT_FALSE (path.generated_p ()); + ASSERT_EQ (path.num_events (), 2); + ASSERT_TRUE (path.generated_p ()); + ASSERT_EQ (path.num_threads (), 1); + ASSERT_FALSE (path.interprocedural_p ()); + ASSERT_STREQ (path.get_event (0).get_desc (*event_pp).get (), + "first `free'"); + ASSERT_STREQ (path.get_event (1).get_desc (*event_pp).get (), + "double `free'"); +} + +/* Implementation of diagnostics::option_manager for which all + options are disabled, for use in selftests. + Note that this is *not* called for option_id (0), which + means "always warn" */ + +class all_warnings_disabled : public diagnostics::option_manager +{ +public: + int option_enabled_p (diagnostics::option_id) const final override + { + /* Treat all options as disabled. */ + return 0; + } + char *make_option_name (diagnostics::option_id, + enum kind, + enum kind) const final override + { + return nullptr; + } + char *make_option_url (diagnostics::option_id) const final override + { + return nullptr; + } +}; + +static void +test_emission (pretty_printer *event_pp) +{ + struct test_rich_location : public rich_location + { + test_rich_location (pretty_printer &event_pp) + : rich_location (line_table, UNKNOWN_LOCATION), + m_path (event_pp) + { + set_path (&m_path); + } + test_lazy_path m_path; + }; + + /* Verify that we don't bother generating the inner path if the warning + is skipped. */ + { + test_context dc; + dc.set_option_manager (std::make_unique<all_warnings_disabled> (), 0); + + test_rich_location rich_loc (*event_pp); + ASSERT_FALSE (rich_loc.m_path.generated_p ()); + + diagnostics::option_id opt_id (42); // has to be non-zero + bool emitted + = dc.emit_diagnostic_with_group (kind::warning, rich_loc, nullptr, + opt_id, + "this warning should be skipped"); + ASSERT_FALSE (emitted); + ASSERT_FALSE (rich_loc.m_path.generated_p ()); + } + + /* Verify that we *do* generate the inner path for a diagnostic that + is emitted, such as an error. */ + { + test_context dc; + + test_rich_location rich_loc (*event_pp); + ASSERT_FALSE (rich_loc.m_path.generated_p ()); + + bool emitted + = dc.emit_diagnostic_with_group (kind::error, rich_loc, nullptr, 0, + "this is a test"); + ASSERT_TRUE (emitted); + ASSERT_TRUE (rich_loc.m_path.generated_p ()); + + /* Verify that the path works as expected. */ + dc.set_path_format (DPF_INLINE_EVENTS); + diagnostics::text_sink sink_ (dc); + pp_buffer (sink_.get_printer ())->m_flush_p = false; + sink_.print_path (rich_loc.m_path); + ASSERT_STREQ (pp_formatted_text (sink_.get_printer ()), + " `foo': event 1\n" + " (1): first `free'\n" + " `foo': event 2\n" + " (2): double `free'\n"); + } +} + +/* Run all of the selftests within this file. */ + +void +lazy_paths_cc_tests () +{ + /* In a few places we use the global dc's printer to determine + colorization so ensure this off during the tests. */ + pretty_printer *global_pp = global_dc->get_reference_printer (); + const bool saved_show_color = pp_show_color (global_pp); + pp_show_color (global_pp) = false; + + auto_fix_quotes fix_quotes; + std::unique_ptr<pretty_printer> event_pp + = std::unique_ptr<pretty_printer> (global_pp->clone ()); + + test_intraprocedural_path (event_pp.get ()); + test_emission (event_pp.get ()); + + pp_show_color (global_pp) = saved_show_color; +} + +} // namespace diagnostics::selftest +} // namespace diagnostics + +#endif /* #if CHECKING_P */ diff --git a/gcc/diagnostics/lazy-paths.h b/gcc/diagnostics/lazy-paths.h new file mode 100644 index 0000000..01b921b --- /dev/null +++ b/gcc/diagnostics/lazy-paths.h @@ -0,0 +1,70 @@ +/* Helper class for deferring path creation until a diagnostic is emitted. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_LAZY_PATHS_H +#define GCC_DIAGNOSTICS_LAZY_PATHS_H + +#include "diagnostics/paths.h" + +namespace diagnostics { +namespace paths { + +/* An implementation of diagnostics::paths::path which has a trivial ctor + and lazily creates another path the first time the path + is queried, deferring to this inner path for all queries. + + Use this to avoid expensive path creation logic when creating + rich_location instances, so that expense can be deferred until the path + is actually used by a diagnostic, and thus avoided for warnings that + are disabled. */ + +class lazy_path : public path +{ + public: + virtual ~lazy_path () {} + + unsigned num_events () const final override; + const event & get_event (int idx) const final override; + unsigned num_threads () const final override; + const thread & + get_thread (thread_id_t) const final override; + bool + same_function_p (int event_idx_a, + int event_idx_b) const final override; + + bool generated_p () const { return m_inner_path != nullptr; } + +protected: + lazy_path (const logical_locations::manager &logical_loc_mgr) + : path (logical_loc_mgr) + { + } + + private: + void lazily_generate_path () const; + virtual std::unique_ptr<path> make_inner_path () const = 0; + + mutable std::unique_ptr<path> m_inner_path; +}; + +} // namespace paths +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_LAZY_PATHS_H */ diff --git a/gcc/diagnostics/logical-locations.h b/gcc/diagnostics/logical-locations.h new file mode 100644 index 0000000..b52a9b4 --- /dev/null +++ b/gcc/diagnostics/logical-locations.h @@ -0,0 +1,180 @@ +/* Logical location support, without knowledge of "tree". + Copyright (C) 2022-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_LOGICAL_LOCATIONS_H +#define GCC_DIAGNOSTICS_LOGICAL_LOCATIONS_H + +#include "label-text.h" + +namespace diagnostics { +namespace logical_locations { + +/* An enum for discriminating between different kinds of logical location + for a diagnostic. + + Roughly corresponds to logicalLocation's "kind" property in SARIF v2.1.0 + (section 3.33.7). */ + +enum class kind +{ + unknown, + + /* Kinds within executable code. */ + function, + member, + module_, + namespace_, + type, + return_type, + parameter, + variable, + + /* Kinds within XML or HTML documents. */ + element, + attribute, + text, + comment, + processing_instruction, + dtd, + declaration, + + /* Kinds within JSON documents. */ + object, + array, + property, + value +}; + +/* We want to efficiently support passing around logical locations in the + diagnostics subsystem, such as: + - "within function 'foo'", or + - "within method 'bar'" + + However we want to do this *without* requiring knowledge of trees (or of + libgdiagnostics internals), and without requiring heap allocation of an + interface class when emitting a diagnostic. + + To do this, we split the implementation into logical_locations::key, which is + a wrapper around a (const void *), and logical_locations::manager which + is provided by the client and has vfunc hooks for interpreting + key instances. + + Every logical_locations::key is associated with a logical_locations::manager + and only has meaning in relation to that manager. + + A "nullptr" within a key means "no logical location". + + See tree-logical-location.h for concrete subclasses relating to trees, + where the pointer is a const_tree. + + See diagnostics/selftest-logical-locations.h for a concrete subclass for + selftests. */ + +/* Extrinsic state for identifying a specific logical location. + This will be our logical location type. + This only makes sense with respect to a specific manager. + e.g. for a tree-based one it's a wrapper around "tree". + + "nullptr" means "no logical location". + + Note that there is no integration with GCC's garbage collector and thus + keys can't be long-lived. */ + +class key +{ +public: + key () : m_ptr (nullptr) {} + + static key from_ptr (const void *ptr) + { + return key (ptr); + } + + operator bool () const + { + return m_ptr != nullptr; + } + + template <typename T> + T cast_to () const { return static_cast<T> (m_ptr); } + + bool + operator== (const key &other) const + { + return m_ptr == other.m_ptr; + } + + bool + operator!= (const key &other) const + { + return m_ptr != other.m_ptr; + } + + bool + operator< (const key &other) const + { + return m_ptr < other.m_ptr; + } + +private: + explicit key (const void *ptr) : m_ptr (ptr) {} + + const void *m_ptr; +}; + +/* Abstract base class for giving meaning to keys. + Typically there will just be one client-provided instance, of a + client-specific subclass. */ + +class manager +{ +public: + virtual ~manager () {} + + /* vfuncs for interpreting keys. */ + + /* Get a string (or NULL) for K suitable for use by the SARIF logicalLocation + "name" property (SARIF v2.1.0 section 3.33.4). */ + virtual const char *get_short_name (key k) const = 0; + + /* Get a string (or NULL) for K suitable for use by the SARIF logicalLocation + "fullyQualifiedName" property (SARIF v2.1.0 section 3.33.5). */ + virtual const char *get_name_with_scope (key k) const = 0; + + /* Get a string (or NULL) for K suitable for use by the SARIF logicalLocation + "decoratedName" property (SARIF v2.1.0 section 3.33.6). */ + virtual const char *get_internal_name (key k) const = 0; + + /* Get what kind of SARIF logicalLocation K is (if any). */ + virtual enum kind get_kind (key k) const = 0; + + /* Get a string for location K in a form suitable for path output. */ + virtual label_text get_name_for_path_output (key k) const = 0; + + /* Get the parent logical_logical of K, if any, or nullptr. */ + virtual key get_parent (key k) const = 0; + + bool function_p (key k) const; +}; + +} // namespace diagnostics::logical_locations +} // namespace diagnostics + +#endif /* GCC_DIAGNOSTICS_LOGICAL_LOCATIONS_H. */ diff --git a/gcc/diagnostics/macro-unwinding.cc b/gcc/diagnostics/macro-unwinding.cc new file mode 100644 index 0000000..66bad1c --- /dev/null +++ b/gcc/diagnostics/macro-unwinding.cc @@ -0,0 +1,227 @@ +/* Code for unwinding macro expansions in diagnostics. + Copyright (C) 1999-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/>. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "tree.h" +#include "diagnostic.h" +#include "diagnostics/macro-unwinding.h" +#include "diagnostics/text-sink.h" +#include "intl.h" + +namespace diagnostics { + +/* This is a pair made of a location and the line map it originated + from. It's used in the maybe_unwind_expanded_macro_loc function + below. */ +struct loc_map_pair +{ + const line_map_macro *map; + location_t where; +}; + + +/* Unwind the different macro expansions that lead to the token which + location is WHERE and emit diagnostics showing the resulting + unwound macro expansion trace. Let's look at an example to see how + the trace looks like. Suppose we have this piece of code, + artificially annotated with the line numbers to increase + legibility: + + $ cat -n test.c + 1 #define OPERATE(OPRD1, OPRT, OPRD2) \ + 2 OPRD1 OPRT OPRD2; + 3 + 4 #define SHIFTL(A,B) \ + 5 OPERATE (A,<<,B) + 6 + 7 #define MULT(A) \ + 8 SHIFTL (A,1) + 9 + 10 void + 11 g () + 12 { + 13 MULT (1.0);// 1.0 << 1; <-- so this is an error. + 14 } + + Here is the diagnostic that we want the compiler to generate: + + test.c: In function ‘g’: + test.c:5:14: error: invalid operands to binary << (have ‘double’ and ‘int’) + test.c:2:9: note: in definition of macro 'OPERATE' + test.c:8:3: note: in expansion of macro 'SHIFTL' + test.c:13:3: note: in expansion of macro 'MULT' + + The part that goes from the third to the fifth line of this + diagnostic (the lines containing the 'note:' string) is called the + unwound macro expansion trace. That's the part generated by this + function. */ + +void +maybe_unwind_expanded_macro_loc (text_sink &text_output, + location_t where) +{ + const struct line_map *map; + auto_vec<loc_map_pair> loc_vec; + unsigned ix; + loc_map_pair loc, *iter; + + const location_t original_loc = where; + + map = linemap_lookup (line_table, where); + if (!linemap_macro_expansion_map_p (map)) + return; + + /* Let's unwind the macros that got expanded and led to the token + which location is WHERE. We are going to store these macros into + LOC_VEC, so that we can later walk it at our convenience to + display a somewhat meaningful trace of the macro expansion + history to the user. Note that the first macro of the trace + (which is OPERATE in the example above) is going to be stored at + the beginning of LOC_VEC. */ + + do + { + loc.where = where; + loc.map = linemap_check_macro (map); + + loc_vec.safe_push (loc); + + /* WHERE is the location of a token inside the expansion of a + macro. MAP is the map holding the locations of that macro + expansion. Let's get the location of the token inside the + context that triggered the expansion of this macro. + This is basically how we go "down" in the trace of macro + expansions that led to WHERE. */ + where = linemap_unwind_toward_expansion (line_table, where, &map); + } while (linemap_macro_expansion_map_p (map)); + + /* Now map is set to the map of the location in the source that + first triggered the macro expansion. This must be an ordinary map. */ + const line_map_ordinary *ord_map = linemap_check_ordinary (map); + + /* Walk LOC_VEC and print the macro expansion trace, unless the + first macro which expansion triggered this trace was expanded + inside a system header. */ + int saved_location_line = + expand_location_to_spelling_point (original_loc).line; + + if (!LINEMAP_SYSP (ord_map)) + FOR_EACH_VEC_ELT (loc_vec, ix, iter) + { + /* Sometimes, in the unwound macro expansion trace, we want to + print a part of the context that shows where, in the + definition of the relevant macro, is the token (we are + looking at) used. That is the case in the introductory + comment of this function, where we print: + + test.c:2:9: note: in definition of macro 'OPERATE'. + + We print that "macro definition context" because the + diagnostic line (emitted by the call to + pp_ouput_formatted_text in diagnostic_report_diagnostic): + + test.c:5:14: error: invalid operands to binary << (have ‘double’ and ‘int’) + + does not point into the definition of the macro where the + token '<<' (that is an argument to the function-like macro + OPERATE) is used. So we must "display" the line of that + macro definition context to the user somehow. + + A contrario, when the first interesting diagnostic line + points into the definition of the macro, we don't need to + display any line for that macro definition in the trace + anymore, otherwise it'd be redundant. */ + + /* Okay, now here is what we want. For each token resulting + from macro expansion we want to show: 1/ where in the + definition of the macro the token comes from; 2/ where the + macro got expanded. */ + + /* Resolve the location iter->where into the locus 1/ of the + comment above. */ + location_t resolved_def_loc = + linemap_resolve_location (line_table, iter->where, + LRK_MACRO_DEFINITION_LOCATION, nullptr); + + /* Don't print trace for locations that are reserved or from + within a system header. */ + const line_map_ordinary *m = nullptr; + location_t l = + linemap_resolve_location (line_table, resolved_def_loc, + LRK_SPELLING_LOCATION, &m); + location_t l0 = l; + if (IS_ADHOC_LOC (l0)) + l0 = get_location_from_adhoc_loc (line_table, l0); + if (l0 < RESERVED_LOCATION_COUNT || LINEMAP_SYSP (m)) + continue; + + /* We need to print the context of the macro definition only + when the locus of the first displayed diagnostic (displayed + before this trace) was inside the definition of the + macro. */ + const int resolved_def_loc_line = SOURCE_LINE (m, l0); + if (ix == 0 && saved_location_line != resolved_def_loc_line) + { + text_output.append_note (resolved_def_loc, + "in definition of macro %qs", + linemap_map_get_macro_name (iter->map)); + /* At this step, as we've printed the context of the macro + definition, we don't want to print the context of its + expansion, otherwise, it'd be redundant. */ + continue; + } + + /* Resolve the location of the expansion point of the macro + which expansion gave the token represented by def_loc. + This is the locus 2/ of the earlier comment. */ + location_t resolved_exp_loc = + linemap_resolve_location (line_table, + iter->map->get_expansion_point_location (), + LRK_MACRO_DEFINITION_LOCATION, nullptr); + + text_output.append_note (resolved_exp_loc, + "in expansion of macro %qs", + linemap_map_get_macro_name (iter->map)); + } +} + +/* This is a diagnostic finalizer implementation that is aware of + virtual locations produced by libcpp. + + It has to be called by the diagnostic finalizer of front ends that + uses libcpp and wish to get diagnostics involving tokens resulting + from macro expansion. + + For a given location, if said location belongs to a token + resulting from a macro expansion, this starter prints the context + of the token. E.g, for multiply nested macro expansion, it + unwinds the nested macro expansions and prints them in a manner + that is similar to what is done for function call stacks, or + template instantiation contexts. */ +void +virt_loc_aware_text_finalizer (text_sink &text_output, + const diagnostic_info *diagnostic) +{ + maybe_unwind_expanded_macro_loc (text_output, + diagnostic_location (diagnostic)); +} + +} // namespace diagnostics diff --git a/gcc/diagnostics/macro-unwinding.h b/gcc/diagnostics/macro-unwinding.h new file mode 100644 index 0000000..1f28d58 --- /dev/null +++ b/gcc/diagnostics/macro-unwinding.h @@ -0,0 +1,33 @@ +/* Code for unwinding macro expansions in diagnostics. + Copyright (C) 2000-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_DIAGNOSTICS_MACRO_UNWINDING_H +#define GCC_DIAGNOSTICS_MACRO_UNWINDING_H + +namespace diagnostics { + +extern void virt_loc_aware_text_finalizer (text_sink &, + const diagnostic_info *); + +extern void maybe_unwind_expanded_macro_loc (text_sink &, + location_t where); + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_MACRO_UNWINDING_H */ diff --git a/gcc/diagnostics/metadata.h b/gcc/diagnostics/metadata.h new file mode 100644 index 0000000..c28f982 --- /dev/null +++ b/gcc/diagnostics/metadata.h @@ -0,0 +1,124 @@ +/* Additional metadata for a diagnostic. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_METADATA_H +#define GCC_DIAGNOSTICS_METADATA_H + +#include "lazily-created.h" + +namespace diagnostics { + + class sarif_object; + namespace digraphs { class digraph; } + +/* A bundle of additional metadata that can be associated with a + diagnostic. + + This supports an optional CWE identifier, and zero or more + "rules". + + Additionally, this provides a place to associate a diagnostic + with zero or more directed graphs. */ + +class metadata +{ + public: + using lazy_digraphs + = lazily_created<std::vector<std::unique_ptr<digraphs::digraph>>>; + + /* Abstract base class for referencing a rule that has been violated, + such as within a coding standard, or within a specification. */ + class rule + { + public: + virtual char *make_description () const = 0; + virtual char *make_url () const = 0; + }; + + /* Concrete subclass. */ + class precanned_rule : public rule + { + public: + precanned_rule (const char *desc, const char *url) + : m_desc (desc), m_url (url) + {} + + char *make_description () const final override + { + return m_desc ? xstrdup (m_desc) : NULL; + } + + char *make_url () const final override + { + return m_url ? xstrdup (m_url) : NULL; + } + + private: + const char *m_desc; + const char *m_url; + }; + + metadata () : m_cwe (0), m_lazy_digraphs (nullptr) {} + virtual ~metadata () {} + + /* Hook for SARIF output to allow for adding diagnostic-specific + properties to the result object's property bag. */ + virtual void + maybe_add_sarif_properties (sarif_object &/*result_obj*/) const + { + } + + void add_cwe (int cwe) { m_cwe = cwe; } + int get_cwe () const { return m_cwe; } + + /* Associate R with the diagnostic. R must outlive + the metadata. */ + void add_rule (const rule &r) + { + m_rules.safe_push (&r); + } + + unsigned get_num_rules () const { return m_rules.length (); } + const rule &get_rule (unsigned idx) const { return *(m_rules[idx]); } + + void + set_lazy_digraphs (const lazy_digraphs *lazy_digraphs_) + { + m_lazy_digraphs = lazy_digraphs_; + } + + const lazy_digraphs * + get_lazy_digraphs () const + { + return m_lazy_digraphs; + } + + private: + int m_cwe; + auto_vec<const rule *> m_rules; + + /* An optional way to create directed graphs associated with the + diagnostic, for the sinks that support this (e.g. SARIF). */ + const lazy_digraphs *m_lazy_digraphs; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_METADATA_H */ diff --git a/gcc/diagnostics/option-classifier.cc b/gcc/diagnostics/option-classifier.cc new file mode 100644 index 0000000..f98fd55 --- /dev/null +++ b/gcc/diagnostics/option-classifier.cc @@ -0,0 +1,222 @@ +/* Stacks of set of classifications of diagnostics. + Copyright (C) 1999-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/>. */ + + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "version.h" +#include "diagnostic.h" + +namespace diagnostics { + +void +option_classifier::init (int n_opts) +{ + m_n_opts = n_opts; + m_classify_diagnostic = XNEWVEC (enum kind, n_opts); + for (int i = 0; i < n_opts; i++) + m_classify_diagnostic[i] = kind::unspecified; + m_push_list = vNULL; + m_classification_history = vNULL; +} + +void +option_classifier::fini () +{ + XDELETEVEC (m_classify_diagnostic); + m_classify_diagnostic = nullptr; + m_classification_history.release (); + m_push_list.release (); +} + +/* Save the diagnostics::option_classifier state to F for PCH + output. Returns 0 on success, -1 on error. */ + +int +option_classifier::pch_save (FILE *f) +{ + unsigned int lengths[2] = { m_classification_history.length (), + m_push_list.length () }; + if (fwrite (lengths, sizeof (lengths), 1, f) != 1 + || (lengths[0] + && fwrite (m_classification_history.address (), + sizeof (classification_change_t), + lengths[0], f) != lengths[0]) + || (lengths[1] + && fwrite (m_push_list.address (), sizeof (int), + lengths[1], f) != lengths[1])) + return -1; + return 0; +} + +/* Read the diagnostics::option_classifier state from F for PCH + read. Returns 0 on success, -1 on error. */ + +int +option_classifier::pch_restore (FILE *f) +{ + unsigned int lengths[2]; + if (fread (lengths, sizeof (lengths), 1, f) != 1) + return -1; + gcc_checking_assert (m_classification_history.is_empty ()); + gcc_checking_assert (m_push_list.is_empty ()); + m_classification_history.safe_grow (lengths[0]); + m_push_list.safe_grow (lengths[1]); + if ((lengths[0] + && fread (m_classification_history.address (), + sizeof (classification_change_t), + lengths[0], f) != lengths[0]) + || (lengths[1] + && fread (m_push_list.address (), sizeof (int), + lengths[1], f) != lengths[1])) + return -1; + return 0; +} + +/* Save all diagnostic classifications in a stack. */ + +void +option_classifier::push () +{ + m_push_list.safe_push (m_classification_history.length ()); +} + +/* Restore the topmost classification set off the stack. If the stack + is empty, revert to the state based on command line parameters. */ + +void +option_classifier::pop (location_t where) +{ + int jump_to; + + if (!m_push_list.is_empty ()) + jump_to = m_push_list.pop (); + else + jump_to = 0; + + classification_change_t v = { where, jump_to, kind::pop }; + m_classification_history.safe_push (v); +} + +/* Interface to specify diagnostic kind overrides. Returns the + previous setting, or kind::unspecified if the parameters are out of + range. If OPTION_ID is zero, the new setting is for all the + diagnostics. */ + +enum kind +option_classifier::classify_diagnostic (const context *dc, + option_id opt_id, + enum kind new_kind, + location_t where) +{ + enum kind old_kind; + + if (opt_id.m_idx < 0 + || opt_id.m_idx >= m_n_opts + || new_kind >= kind::last_diagnostic_kind) + return kind::unspecified; + + old_kind = m_classify_diagnostic[opt_id.m_idx]; + + /* Handle pragmas separately, since we need to keep track of *where* + the pragmas were. */ + if (where != UNKNOWN_LOCATION) + { + unsigned i; + + /* Record the command-line status, so we can reset it back on kind::pop. */ + if (old_kind == kind::unspecified) + { + old_kind = (!dc->option_enabled_p (opt_id) + ? kind::ignored : kind::any); + m_classify_diagnostic[opt_id.m_idx] = old_kind; + } + + classification_change_t *p; + FOR_EACH_VEC_ELT_REVERSE (m_classification_history, i, p) + if (p->option == opt_id.m_idx) + { + old_kind = p->kind; + break; + } + + classification_change_t v + = { where, opt_id.m_idx, new_kind }; + m_classification_history.safe_push (v); + } + else + m_classify_diagnostic[opt_id.m_idx] = new_kind; + + return old_kind; +} + +/* Update the kind of DIAGNOSTIC based on its location(s), including + any of those in its inlining stack, relative to any + #pragma GCC diagnostic + directives recorded within this object. + + Return the new kind of DIAGNOSTIC if it was updated, or kind::unspecified + otherwise. */ + +enum kind +option_classifier:: +update_effective_level_from_pragmas (diagnostic_info *diagnostic) const +{ + if (m_classification_history.is_empty ()) + return kind::unspecified; + + /* Iterate over the locations, checking the diagnostic disposition + for the diagnostic at each. If it's explicitly set as opposed + to unspecified, update the disposition for this instance of + the diagnostic and return it. */ + for (location_t loc: diagnostic->m_iinfo.m_ilocs) + { + /* FIXME: Stupid search. Optimize later. */ + unsigned int i; + classification_change_t *p; + FOR_EACH_VEC_ELT_REVERSE (m_classification_history, i, p) + { + location_t pragloc = p->location; + if (!linemap_location_before_p (line_table, pragloc, loc)) + continue; + + if (p->kind == kind::pop) + { + /* Move on to the next region. */ + i = p->option; + continue; + } + + option_id opt_id = p->option; + /* The option 0 is for all the diagnostics. */ + if (opt_id == 0 || opt_id == diagnostic->m_option_id) + { + enum kind kind = p->kind; + if (kind != diagnostics::kind::unspecified) + diagnostic->m_kind = kind; + return kind; + } + } + } + + return kind::unspecified; +} + +} // namespace diagnostics diff --git a/gcc/diagnostics/option-classifier.h b/gcc/diagnostics/option-classifier.h new file mode 100644 index 0000000..3b16c74 --- /dev/null +++ b/gcc/diagnostics/option-classifier.h @@ -0,0 +1,110 @@ +/* Stacks of set of classifications of diagnostics. + Copyright (C) 2000-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_DIAGNOSTICS_OPTION_CLASSIFIER_H +#define GCC_DIAGNOSTICS_OPTION_CLASSIFIER_H + +namespace diagnostics { + +/* Forward declarations. */ +class context; + +/* A stack of sets of classifications: each entry in the stack is + a mapping from option index to diagnostic severity that can be changed + via pragmas. The stack can be pushed and popped. */ + +class option_classifier +{ +public: + void init (int n_opts); + void fini (); + + /* Save all diagnostic classifications in a stack. */ + void push (); + + /* Restore the topmost classification set off the stack. If the stack + is empty, revert to the state based on command line parameters. */ + void pop (location_t where); + + bool option_unspecified_p (option_id opt_id) const + { + return get_current_override (opt_id) == kind::unspecified; + } + + enum kind get_current_override (option_id opt_id) const + { + gcc_assert (opt_id.m_idx < m_n_opts); + return m_classify_diagnostic[opt_id.m_idx]; + } + + enum kind + classify_diagnostic (const context *context, + option_id opt_id, + enum kind new_kind, + location_t where); + + enum kind + update_effective_level_from_pragmas (diagnostic_info *diagnostic) const; + + int pch_save (FILE *); + int pch_restore (FILE *); + +private: + /* Each time a diagnostic's classification is changed with a pragma, + we record the change and the location of the change in an array of + these structs. */ + struct classification_change_t + { + location_t location; + + /* For kind::pop, this is the index of the corresponding push (as stored + in m_push_list). + Otherwise, this is an option index. */ + int option; + + enum kind kind; + }; + + int m_n_opts; + + /* For each option index that can be passed to warning() et al + (OPT_* from options.h when using this code with the core GCC + options), this array may contain a new kind that the diagnostic + should be changed to before reporting, or kind::unspecified to leave + it as the reported kind, or kind::ignored to not report it at + all. */ + enum kind *m_classify_diagnostic; + + /* History of all changes to the classifications above. This list + is stored in location-order, so we can search it, either + binary-wise or end-to-front, to find the most recent + classification for a given diagnostic, given the location of the + diagnostic. */ + vec<classification_change_t> m_classification_history; + + /* For context::get_classification_history, declared later. */ + friend class context; + + /* For pragma push/pop. */ + vec<int> m_push_list; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_OPTION_CLASSIFIER_H */ diff --git a/gcc/diagnostics/option-id.h b/gcc/diagnostics/option-id.h new file mode 100644 index 0000000..4132775 --- /dev/null +++ b/gcc/diagnostics/option-id.h @@ -0,0 +1,49 @@ +/* Declaration of struct diagnostics::option_id. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_OPTION_ID_H +#define GCC_DIAGNOSTICS_OPTION_ID_H + +namespace diagnostics { + +/* A class to use for the ID of an option that controls + a particular diagnostic. + This is just a wrapper around "int", but better documents + the intent of the code. */ + +struct option_id +{ + option_id () : m_idx (0) {} + + option_id (int idx) : m_idx (idx) {} + /* Ideally we'd take an enum opt_code here, but we don't + want to depend on its decl. */ + + bool operator== (option_id other) const + { + return m_idx == other.m_idx; + } + + int m_idx; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_OPTION_ID_H */ diff --git a/gcc/diagnostics/output-file.h b/gcc/diagnostics/output-file.h new file mode 100644 index 0000000..f936387 --- /dev/null +++ b/gcc/diagnostics/output-file.h @@ -0,0 +1,111 @@ +/* RAII class for managing FILE * for diagnostic formats. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_OUTPUT_FILE_H +#define GCC_DIAGNOSTICS_OUTPUT_FILE_H + +namespace diagnostics { + +/* RAII class for wrapping a FILE * that could be borrowed or owned, + along with the underlying filename. */ + +class output_file +{ +public: + output_file () + : m_outf (nullptr), + m_owned (false), + m_filename () + { + } + output_file (FILE *outf, bool owned, label_text filename) + : m_outf (outf), + m_owned (owned), + m_filename (std::move (filename)) + { + gcc_assert (m_filename.get ()); + if (m_owned) + gcc_assert (m_outf); + } + ~output_file () + { + if (m_owned) + { + gcc_assert (m_outf); + fclose (m_outf); + } + } + output_file (const output_file &other) = delete; + output_file (output_file &&other) + : m_outf (other.m_outf), + m_owned (other.m_owned), + m_filename (std::move (other.m_filename)) + { + other.m_outf = nullptr; + other.m_owned = false; + + gcc_assert (m_filename.get ()); + if (m_owned) + gcc_assert (m_outf); + } + output_file & + operator= (const output_file &other) = delete; + output_file & + operator= (output_file &&other) + { + if (m_owned) + { + gcc_assert (m_outf); + fclose (m_outf); + } + + m_outf = other.m_outf; + other.m_outf = nullptr; + + m_owned = other.m_owned; + other.m_owned = false; + + m_filename = std::move (other.m_filename); + + if (m_owned) + gcc_assert (m_outf); + return *this; + } + + operator bool () const { return m_outf != nullptr; } + FILE *get_open_file () const { return m_outf; } + const char *get_filename () const { return m_filename.get (); } + + static output_file + try_to_open (context &dc, + line_maps *line_maps, + const char *base_file_name, + const char *extension, + bool binary); + +private: + FILE *m_outf; + bool m_owned; + label_text m_filename; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_OUTPUT_FILE_H */ diff --git a/gcc/diagnostics/output-spec.cc b/gcc/diagnostics/output-spec.cc new file mode 100644 index 0000000..83f128c --- /dev/null +++ b/gcc/diagnostics/output-spec.cc @@ -0,0 +1,852 @@ +/* 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 "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=. */ + +namespace diagnostics { +namespace 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 sink + 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<sink> + make_sink (const context &ctxt, + diagnostics::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<sink> + make_sink (const context &ctxt, + diagnostics::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<sink> + make_sink (const context &ctxt, + diagnostics::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<sink> + make_sink (const context &ctxt, + diagnostics::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 state_graph); + + static std::unique_ptr<sarif_serialization_format> + 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<sink> + make_sink (const context &ctxt, + diagnostics::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); +} + +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, 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<sink> +context::parse_and_make_sink (const char *unparsed_arg, + diagnostics::context &dc) +{ + auto parsed_arg = parse (*this, unparsed_arg); + if (!parsed_arg) + return nullptr; + + 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<sink> +output_factory::make_sink (const context &ctxt, + diagnostics::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<sink> +text_scheme_handler::make_sink (const context &ctxt, + diagnostics::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<diagnostics::text_sink> (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<sink> +sarif_scheme_handler::make_sink (const context &ctxt, + diagnostics::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 state_graph = 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 == "state-graphs") + { + if (!parse_bool_value (ctxt, unparsed_arg, key, value, + state_graph)) + 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 ("state-graphs"); + known_keys.safe_push ("version"); + ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (), + known_keys); + return nullptr; + } + + 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_ + = open_sarif_output_file (dc, + ctxt.get_affected_location_mgr (), + basename, + serialization_kind); + } + if (!output_file_) + return nullptr; + + auto sarif_gen_opts = make_sarif_gen_opts (version, state_graph); + + 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 state_graph) +{ + sarif_generation_options sarif_gen_opts; + sarif_gen_opts.m_version = version; + sarif_gen_opts.m_state_graph = state_graph; + return sarif_gen_opts; +} + +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 output_factory::scheme_handler. */ + +std::unique_ptr<sink> +html_scheme_handler::make_sink (const context &ctxt, + diagnostics::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_diagrams_sarif = false; + bool show_state_diagrams_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-diagrams-dot-src") + { + if (!parse_bool_value (ctxt, unparsed_arg, key, value, + show_state_diagrams_dot_src)) + return nullptr; + continue; + } + if (key == "show-state-diagrams-sarif") + { + if (!parse_bool_value (ctxt, unparsed_arg, key, value, + show_state_diagrams_sarif)) + 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-sarif"); + ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (), + known_keys); + return nullptr; + } + + 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_ + = open_html_output_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_diagrams_sarif = show_state_diagrams_sarif; + html_gen_opts.m_show_state_diagrams_dot_src = show_state_diagrams_dot_src; + + auto sink = make_html_sink (dc, + *ctxt.get_affected_location_mgr (), + html_gen_opts, + std::move (output_file_)); + return sink; +} + +} // 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 (diagnostics::context &dc, + line_maps *location_mgr, + location_t loc, + const char *option_name) + : dc_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_sink (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: + diagnostics::selftest::test_context m_dc; + test_spec_context m_ctxt; + diagnostics::sink &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 +output_spec_cc_tests () +{ + test_output_arg_parsing (); +} + +} // namespace diagnostics::selftest + +#endif /* #if CHECKING_P */ + +} // namespace diagnostics diff --git a/gcc/diagnostics/output-spec.h b/gcc/diagnostics/output-spec.h new file mode 100644 index 0000000..c84d237 --- /dev/null +++ b/gcc/diagnostics/output-spec.h @@ -0,0 +1,118 @@ +/* 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_DIAGNOSTICS_OUTPUT_SPEC_H +#define GCC_DIAGNOSTICS_OUTPUT_SPEC_H + +#include "diagnostics/sink.h" +#include "diagnostics/output-file.h" + +namespace diagnostics { +namespace output_spec { + +/* An abstract base class for handling the DSL of -fdiagnostics-add-output= + and -fdiagnostics-set-output=. */ + +class context +{ + public: + std::unique_ptr<sink> + parse_and_make_sink (const char *, + diagnostics::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; + + 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 diagnostics::context. */ + +struct dc_spec_context : public output_spec::context +{ +public: + dc_spec_context (diagnostics::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, kind::error); + m_dc.end_group (); + } + + diagnostics::context &m_dc; + line_maps *m_control_location_mgr; + location_t m_loc; +}; + +} // namespace output_spec +} // namespace diagnostics + +#endif // #ifndef GCC_DIAGNOSTICS_OUTPUT_SPEC_H diff --git a/gcc/diagnostics/paths-output.cc b/gcc/diagnostics/paths-output.cc new file mode 100644 index 0000000..a3ac9a0 --- /dev/null +++ b/gcc/diagnostics/paths-output.cc @@ -0,0 +1,2742 @@ +/* Printing paths through the code associated with a diagnostic. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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/>. */ + +#include "config.h" +#define INCLUDE_ALGORITHM +#define INCLUDE_MAP +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostics/macro-unwinding.h" +#include "intl.h" +#include "diagnostics/paths.h" +#include "gcc-rich-location.h" +#include "diagnostics/color.h" +#include "diagnostics/event-id.h" +#include "diagnostics/source-printing-effects.h" +#include "pretty-print-markup.h" +#include "selftest.h" +#include "diagnostics/selftest-context.h" +#include "diagnostics/selftest-paths.h" +#include "text-art/theme.h" +#include "diagnostics/text-sink.h" +#include "diagnostics/html-sink.h" +#include "xml.h" +#include "xml-printer.h" + +/* Disable warnings about missing quoting in GCC diagnostics for the print + calls below. */ +#if __GNUC__ >= 10 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-diag" +#endif + +/* Anonymous namespace for path-printing code. */ + +namespace { + +using namespace diagnostics; +using namespace diagnostics::paths; + +/* A bundle of state for printing a path. */ + +class path_print_policy +{ +public: + path_print_policy (const diagnostics::text_sink &text_output) + : m_source_policy (text_output.get_context ()) + { + } + + path_print_policy (const diagnostics::context &dc) + : m_source_policy (dc) + { + } + + text_art::theme * + get_diagram_theme () const + { + return m_source_policy.get_diagram_theme (); + } + + const diagnostics::source_print_policy & + get_source_policy () const { return m_source_policy; } + +private: + diagnostics::source_print_policy m_source_policy; +}; + +/* Subclass of range_label for showing a particular event + when showing a consecutive run of events within a diagnostic path as + labelled ranges within one gcc_rich_location. */ + +class path_label : public range_label +{ + public: + path_label (const path &path_, + const pretty_printer &ref_pp, + unsigned start_idx, + bool colorize, + bool allow_emojis) + : m_path (path_), + m_ref_pp (ref_pp), + m_start_idx (start_idx), m_effects (*this), + m_colorize (colorize), m_allow_emojis (allow_emojis) + {} + + label_text get_text (unsigned range_idx) const final override + { + unsigned event_idx = m_start_idx + range_idx; + const event &ev = m_path.get_event (event_idx); + + const event::meaning meaning (ev.get_meaning ()); + + auto pp = m_ref_pp.clone (); + pp_show_color (pp.get ()) = m_colorize; + event_id_t event_id (event_idx); + pp_printf (pp.get (), "%@", &event_id); + pp_space (pp.get ()); + + if (meaning.m_verb == event::verb::danger + && m_allow_emojis) + { + pp_unicode_character (pp.get (), 0x26A0); /* U+26A0 WARNING SIGN. */ + /* Append U+FE0F VARIATION SELECTOR-16 to select the emoji + variation of the char. */ + pp_unicode_character (pp.get (), 0xFE0F); + /* U+26A0 WARNING SIGN has East_Asian_Width == Neutral, but in its + emoji variant is printed (by vte at least) with a 2nd half + overlapping the next char. Hence we add two spaces here: a space + to be covered by this overlap, plus another space of padding. */ + pp_string (pp.get (), " "); + } + + ev.print_desc (*pp.get ()); + + label_text result + = label_text::take (xstrdup (pp_formatted_text (pp.get ()))); + return result; + } + + const diagnostics::label_effects * + get_effects (unsigned /*range_idx*/) const final override + { + return &m_effects; + } + + private: + class path_label_effects : public label_effects + { + public: + path_label_effects (const path_label &path_label) + : m_path_label (path_label) + { + } + bool has_in_edge (unsigned range_idx) const final override + { + if (const event *prev_event + = m_path_label.get_prev_event (range_idx)) + return prev_event->connect_to_next_event_p (); + return false; + } + bool has_out_edge (unsigned range_idx) const final override + { + const event &ev = m_path_label.get_event (range_idx); + return ev.connect_to_next_event_p (); + } + + private: + const path_label &m_path_label; + }; + + const event &get_event (unsigned range_idx) const + { + unsigned event_idx = m_start_idx + range_idx; + return m_path.get_event (event_idx); + } + + const event *get_prev_event (unsigned range_idx) const + { + if (m_start_idx + range_idx == 0) + return nullptr; + unsigned event_idx = m_start_idx + range_idx - 1; + return &m_path.get_event (event_idx); + } + + const path &m_path; + const pretty_printer &m_ref_pp; + unsigned m_start_idx; + path_label_effects m_effects; + const bool m_colorize; + const bool m_allow_emojis; +}; + +/* Return true if E1 and E2 can be consolidated into the same run of events + when printing a diagnostic path. */ + +static bool +can_consolidate_events (const path &p, + const event &e1, + unsigned ev1_idx, + const event &e2, + unsigned ev2_idx, + bool check_locations) +{ + if (e1.get_thread_id () != e2.get_thread_id ()) + return false; + + if (!p.same_function_p (ev1_idx, ev2_idx)) + return false; + + if (e1.get_stack_depth () != e2.get_stack_depth ()) + return false; + + if (check_locations) + { + location_t loc1 = e1.get_location (); + location_t loc2 = e2.get_location (); + + if (loc1 < RESERVED_LOCATION_COUNT + || loc2 < RESERVED_LOCATION_COUNT) + return false; + + /* Neither can be macro-based. */ + if (linemap_location_from_macro_expansion_p (line_table, loc1)) + return false; + if (linemap_location_from_macro_expansion_p (line_table, loc2)) + return false; + } + + /* Passed all the tests. */ + return true; +} + +struct event_range; +struct path_summary; +class thread_event_printer; + +/* A bundle of information about all of the events in a diagnostic path + relating to a specific path, for use by path_summary. */ + +class per_thread_summary +{ +public: + per_thread_summary (const path &path_, + const logical_locations::manager &logical_loc_mgr, + label_text name, unsigned swimlane_idx) + : m_path (path_), + m_logical_loc_mgr (logical_loc_mgr), + m_name (std::move (name)), + m_swimlane_idx (swimlane_idx), + m_last_event (nullptr), + m_min_depth (INT_MAX), + m_max_depth (INT_MIN) + {} + + void update_depth_limits (int stack_depth) + { + if (stack_depth < m_min_depth) + m_min_depth = stack_depth; + if (stack_depth > m_max_depth) + m_max_depth = stack_depth; + } + + const char *get_name () const { return m_name.get (); } + unsigned get_swimlane_index () const { return m_swimlane_idx; } + + bool interprocedural_p () const; + +private: + friend struct path_summary; + friend class thread_event_printer; + friend struct event_range; + + const path &m_path; + const logical_locations::manager &m_logical_loc_mgr; + + const label_text m_name; + + /* The "swimlane index" is the order in which this per_thread_summary + was created, for use when printing the events. */ + const unsigned m_swimlane_idx; + + // The event ranges specific to this thread: + auto_vec<event_range *> m_event_ranges; + + const event *m_last_event; + + int m_min_depth; + int m_max_depth; +}; + +/* A stack frame for use in HTML output, holding child stack frames, + and event ranges. */ + +struct stack_frame +{ + stack_frame (std::unique_ptr<stack_frame> parent, + logical_locations::key logical_loc, + int stack_depth) + : m_parent (std::move (parent)), + m_logical_loc (logical_loc), + m_stack_depth (stack_depth) + {} + + std::unique_ptr<stack_frame> m_parent; + logical_locations::key m_logical_loc; + const int m_stack_depth; +}; + +/* Begin emitting content relating to a new stack frame within PARENT. + Allocated a new stack_frame and return it. */ + +static std::unique_ptr<stack_frame> +begin_html_stack_frame (xml::printer &xp, + std::unique_ptr<stack_frame> parent, + logical_locations::key logical_loc, + int stack_depth, + const logical_locations::manager *logical_loc_mgr) +{ + if (logical_loc) + { + gcc_assert (logical_loc_mgr); + xp.push_tag_with_class ("table", "stack-frame-with-margin", false); + xp.push_tag ("tr", false); + { + xp.push_tag_with_class ("td", "interprocmargin", false); + xp.set_attr ("style", "padding-left: 100px"); + xp.pop_tag ("td"); + } + xp.push_tag_with_class ("td", "stack-frame", false); + label_text funcname + = logical_loc_mgr->get_name_for_path_output (logical_loc); + if (funcname.get ()) + { + xp.push_tag_with_class ("div", "frame-funcname", false); + xp.push_tag ("span", true); + xp.add_text (funcname.get ()); + xp.pop_tag ("span"); + xp.pop_tag ("div"); + } + } + return std::make_unique<stack_frame> (std::move (parent), + logical_loc, + stack_depth); +} + +/* Finish emitting content for FRAME and delete it. + Return parent. */ + +static std::unique_ptr<stack_frame> +end_html_stack_frame (xml::printer &xp, + std::unique_ptr<stack_frame> frame) +{ + auto parent = std::move (frame->m_parent); + if (frame->m_logical_loc) + { + xp.pop_tag ("td"); + xp.pop_tag ("tr"); + xp.pop_tag ("table"); + } + return parent; +} + +/* Append an HTML <div> element to XP containing an SVG arrow representing + a change in stack depth from OLD_DEPTH to NEW_DEPTH. */ + +static void +emit_svg_arrow (xml::printer &xp, int old_depth, int new_depth) +{ + const int pixels_per_depth = 100; + const int min_depth = MIN (old_depth, new_depth); + const int base_x = 20; + const int excess = 30; + const int last_x + = base_x + (old_depth - min_depth) * pixels_per_depth; + const int this_x + = base_x + (new_depth - min_depth) * pixels_per_depth; + pretty_printer tmp_pp; + pretty_printer *pp = &tmp_pp; + pp_printf (pp, "<div class=\"%s\">\n", + old_depth < new_depth + ? "between-ranges-call" : "between-ranges-return"); + pp_printf (pp, " <svg height=\"30\" width=\"%i\">\n", + MAX (last_x, this_x) + excess); + pp_string + (pp, + " <defs>\n" + " <marker id=\"arrowhead\" markerWidth=\"10\" markerHeight=\"7\"\n" + " refX=\"0\" refY=\"3.5\" orient=\"auto\" stroke=\"#0088ce\" fill=\"#0088ce\">\n" + " <polygon points=\"0 0, 10 3.5, 0 7\"/>\n" + " </marker>\n" + " </defs>\n"); + pp_printf (pp, + " <polyline points=\"%i,0 %i,10 %i,10 %i,20\"\n", + last_x, last_x, this_x, this_x); + pp_string + (pp, + " style=\"fill:none;stroke: #0088ce\"\n" + " marker-end=\"url(#arrowhead)\"/>\n" + " </svg>\n" + "</div>\n\n"); + xp.add_raw (pp_formatted_text (pp)); +} + +/* A range of consecutive events within a diagnostic path, all within the + same thread, and with the same fndecl and stack_depth, and which are suitable + to print with a single call to diagnostic_show_locus. */ +struct event_range +{ + /* A struct for tracking the mergability of labels on a particular + source line. In particular, track information about links between + labels to ensure that we only consolidate events involving links + that the source-printing code is able to handle (splitting them + otherwise). */ + struct per_source_line_info + { + void init (int line) + { + m_line = line; + m_has_in_edge = false; + m_has_out_edge = false; + m_min_label_source_column = INT_MAX; + m_max_label_source_column = INT_MIN; + } + + /* Return true if our source-printing/labelling/linking code can handle + the events already on this source line, *and* a new event at COLUMN. */ + bool + can_add_label_for_event_p (bool has_in_edge, + const event *prev_event, + bool has_out_edge, + int column) const + { + /* Any existing in-edge has to be the left-most label on its + source line. */ + if (m_has_in_edge && column < m_min_label_source_column) + return false; + /* Any existing out-edge has to be the right-most label on its + source line. */ + if (m_has_out_edge && column > m_max_label_source_column) + return false; + /* Can't have more than one in-edge. */ + if (m_has_in_edge && has_in_edge) + return false; + /* Can't have more than one out-edge. */ + if (m_has_out_edge && has_out_edge) + return false; + + if (has_in_edge) + { + /* Any new in-edge needs to be the left-most label on its + source line. */ + if (column > m_min_label_source_column) + return false; + + gcc_assert (prev_event); + const location_t prev_loc = prev_event->get_location (); + expanded_location prev_exploc + = linemap_client_expand_location_to_spelling_point + (line_table, prev_loc, LOCATION_ASPECT_CARET); + /* The destination in-edge's line number has to be <= the + source out-edge's line number (if any). */ + if (prev_exploc.line >= m_line) + return false; + } + + /* Any new out-edge needs to be the right-most label on its + source line. */ + if (has_out_edge) + if (column < m_max_label_source_column) + return false; + + /* All checks passed; we can add the new event at COLUMN. */ + return true; + } + + void + add_label_for_event (bool has_in_edge, bool has_out_edge, int column) + { + if (has_in_edge) + m_has_in_edge = true; + if (has_out_edge) + m_has_out_edge = true; + m_min_label_source_column = std::min (m_min_label_source_column, column); + m_max_label_source_column = std::max (m_max_label_source_column, column); + } + + int m_line; + bool m_has_in_edge; + bool m_has_out_edge; + int m_min_label_source_column; + int m_max_label_source_column; + }; + + event_range (const path &path_, + const pretty_printer &ref_pp, + unsigned start_idx, + const event &initial_event, + per_thread_summary &t, + bool show_event_links, + bool colorize_labels, + bool allow_emojis) + : m_path (path_), + m_initial_event (initial_event), + m_logical_loc (initial_event.get_logical_location ()), + m_stack_depth (initial_event.get_stack_depth ()), + m_start_idx (start_idx), m_end_idx (start_idx), + m_path_label (path_, ref_pp, + start_idx, colorize_labels, allow_emojis), + m_richloc (initial_event.get_location (), &m_path_label, nullptr), + m_thread_id (initial_event.get_thread_id ()), + m_per_thread_summary (t), + m_show_event_links (show_event_links) + { + if (m_show_event_links) + { + expanded_location exploc + = linemap_client_expand_location_to_spelling_point + (line_table, initial_event.get_location (), LOCATION_ASPECT_CARET); + per_source_line_info &source_line_info + = get_per_source_line_info (exploc.line); + + const event *prev_thread_event = t.m_last_event; + const bool has_in_edge + = (prev_thread_event + ? prev_thread_event->connect_to_next_event_p () + : false); + const bool has_out_edge = initial_event.connect_to_next_event_p (); + + source_line_info.add_label_for_event + (has_in_edge, has_out_edge, exploc.column); + } + } + + per_source_line_info & + get_per_source_line_info (int source_line) + { + bool existed = false; + per_source_line_info &result + = m_source_line_info_map.get_or_insert (source_line, &existed); + if (!existed) + result.init (source_line); + return result; + } + + bool maybe_add_event (const path_print_policy &policy, + const event &new_ev, + unsigned new_ev_idx, + bool check_rich_locations) + { + if (!can_consolidate_events (m_path, + m_initial_event, m_start_idx, + new_ev, new_ev_idx, + check_rich_locations)) + return false; + + /* Verify compatibility of the new label and existing labels + with respect to the link-printing code. */ + expanded_location exploc + = linemap_client_expand_location_to_spelling_point + (line_table, new_ev.get_location (), LOCATION_ASPECT_CARET); + per_source_line_info &source_line_info + = get_per_source_line_info (exploc.line); + const event *prev_event = nullptr; + if (new_ev_idx > 0) + prev_event = &m_path.get_event (new_ev_idx - 1); + const bool has_in_edge = (prev_event + ? prev_event->connect_to_next_event_p () + : false); + const bool has_out_edge = new_ev.connect_to_next_event_p (); + if (m_show_event_links) + if (!source_line_info.can_add_label_for_event_p + (has_in_edge, prev_event, + has_out_edge, exploc.column)) + return false; + + /* Potentially verify that the locations are sufficiently close. */ + if (check_rich_locations) + if (!m_richloc.add_location_if_nearby (policy.get_source_policy (), + new_ev.get_location (), + false, &m_path_label)) + return false; + + m_end_idx = new_ev_idx; + m_per_thread_summary.m_last_event = &new_ev; + + if (m_show_event_links) + source_line_info.add_label_for_event + (has_in_edge, has_out_edge, exploc.column); + + return true; + } + + /* Print the events in this range to PP, typically as a single + call to diagnostic_show_locus. */ + + void print_as_text (pretty_printer &pp, + diagnostics::text_sink &text_output, + diagnostics::source_effect_info *effect_info) + { + location_t initial_loc = m_initial_event.get_location (); + + diagnostics::context &dc = text_output.get_context (); + + /* Emit a span indicating the filename (and line/column) if the + line has changed relative to the last call to + diagnostic_show_locus. */ + if (dc.get_source_printing_options ().enabled) + { + expanded_location exploc + = linemap_client_expand_location_to_spelling_point + (line_table, initial_loc, LOCATION_ASPECT_CARET); + if (exploc.file != LOCATION_FILE (dc.m_last_location)) + { + diagnostics::location_print_policy loc_policy (text_output); + loc_policy.print_text_span_start (dc, pp, exploc); + } + } + + /* If we have an UNKNOWN_LOCATION (or BUILTINS_LOCATION) as the + primary location for an event, diagnostic_show_locus won't print + anything. + + In particular the label for the event won't get printed. + Fail more gracefully in this case by showing the event + index and text, at no particular location. */ + if (get_pure_location (initial_loc) <= BUILTINS_LOCATION) + { + for (unsigned i = m_start_idx; i <= m_end_idx; i++) + { + const event &iter_event = m_path.get_event (i); + diagnostic_event_id_t event_id (i); + pp_printf (&pp, " %@: ", &event_id); + iter_event.print_desc (pp); + pp_newline (&pp); + } + return; + } + + /* Call diagnostic_show_locus to show the events using labels. */ + diagnostic_show_locus (&dc, text_output.get_source_printing_options (), + &m_richloc, diagnostics::kind::path, &pp, + effect_info); + + /* If we have a macro expansion, show the expansion to the user. */ + if (linemap_location_from_macro_expansion_p (line_table, initial_loc)) + { + gcc_assert (m_start_idx == m_end_idx); + maybe_unwind_expanded_macro_loc (text_output, initial_loc); + } + } + + /* Print the events in this range to XP, typically as a single + call to diagnostic_show_locus_as_html. */ + + void print_as_html (xml::printer &xp, + diagnostics::context &dc, + diagnostics::source_effect_info *effect_info, + html_label_writer *event_label_writer) + { + location_t initial_loc = m_initial_event.get_location (); + + /* Emit a span indicating the filename (and line/column) if the + line has changed relative to the last call to + diagnostic_show_locus. */ + if (dc.get_source_printing_options ().enabled) + { + expanded_location exploc + = linemap_client_expand_location_to_spelling_point + (line_table, initial_loc, LOCATION_ASPECT_CARET); + if (exploc.file != LOCATION_FILE (dc.m_last_location)) + { + diagnostics::location_print_policy loc_policy (dc); + loc_policy.print_html_span_start (dc, xp, exploc); + } + } + + /* If we have an UNKNOWN_LOCATION (or BUILTINS_LOCATION) as the + primary location for an event, diagnostic_show_locus_as_html won't print + anything. + + In particular the label for the event won't get printed. + Fail more gracefully in this case by showing the event + index and text, at no particular location. */ + if (get_pure_location (initial_loc) <= BUILTINS_LOCATION) + { + for (unsigned i = m_start_idx; i <= m_end_idx; i++) + { + const event &iter_event = m_path.get_event (i); + diagnostic_event_id_t event_id (i); + pretty_printer pp; + pp_printf (&pp, " %@: ", &event_id); + iter_event.print_desc (pp); + if (event_label_writer) + event_label_writer->begin_label (); + xp.add_text_from_pp (pp); + if (event_label_writer) + event_label_writer->end_label (); + } + return; + } + + /* Call diagnostic_show_locus_as_html to show the source, + showing events using labels. */ + diagnostic_show_locus_as_html (&dc, dc.get_source_printing_options (), + &m_richloc, diagnostics::kind::path, xp, + effect_info, event_label_writer); + + // TODO: show macro expansions + } + + const path &m_path; + const event &m_initial_event; + logical_locations::key m_logical_loc; + int m_stack_depth; + unsigned m_start_idx; + unsigned m_end_idx; + path_label m_path_label; + gcc_rich_location m_richloc; + thread_id_t m_thread_id; + per_thread_summary &m_per_thread_summary; + hash_map<int_hash<int, -1, -2>, + per_source_line_info> m_source_line_info_map; + bool m_show_event_links; +}; + +/* A struct for grouping together the events in a path into + ranges of events, partitioned by thread and by stack frame (i.e. by fndecl + and stack depth). */ + +struct path_summary +{ + path_summary (const path_print_policy &policy, + const pretty_printer &ref_pp, + const path &path_, + bool check_rich_locations, + bool colorize = false, + bool show_event_links = true); + + const logical_locations::manager &get_logical_location_manager () const + { + return m_logical_loc_mgr; + } + unsigned get_num_ranges () const { return m_ranges.length (); } + bool multithreaded_p () const { return m_per_thread_summary.length () > 1; } + + const per_thread_summary &get_events_for_thread_id (thread_id_t tid) + { + per_thread_summary **slot = m_thread_id_to_events.get (tid); + gcc_assert (slot); + gcc_assert (*slot); + return **slot; + } + + const logical_locations::manager &m_logical_loc_mgr; + auto_delete_vec <event_range> m_ranges; + auto_delete_vec <per_thread_summary> m_per_thread_summary; + hash_map<int_hash<thread_id_t, -1, -2>, + per_thread_summary *> m_thread_id_to_events; + +private: + per_thread_summary & + get_or_create_events_for_thread_id (const path &path_, + thread_id_t tid) + { + if (per_thread_summary **slot = m_thread_id_to_events.get (tid)) + return **slot; + + const thread &thread = path_.get_thread (tid); + per_thread_summary *pts + = new per_thread_summary (path_, + m_logical_loc_mgr, + thread.get_name (false), + m_per_thread_summary.length ()); + m_thread_id_to_events.put (tid, pts); + m_per_thread_summary.safe_push (pts); + return *pts; + } +}; + +/* Return true iff there is more than one stack frame used by the events + of this thread. */ + +bool +per_thread_summary::interprocedural_p () const +{ + if (m_event_ranges.is_empty ()) + return false; + int first_stack_depth = m_event_ranges[0]->m_stack_depth; + for (auto range : m_event_ranges) + { + if (!m_path.same_function_p (m_event_ranges[0]->m_start_idx, + range->m_start_idx)) + return true; + if (range->m_stack_depth != first_stack_depth) + return true; + } + return false; +} + +/* path_summary's ctor. */ + +path_summary::path_summary (const path_print_policy &policy, + const pretty_printer &ref_pp, + const path &path_, + bool check_rich_locations, + bool colorize, + bool show_event_links) +: m_logical_loc_mgr (path_.get_logical_location_manager ()) +{ + const unsigned num_events = path_.num_events (); + + event_range *cur_event_range = nullptr; + for (unsigned idx = 0; idx < num_events; idx++) + { + const event &ev = path_.get_event (idx); + const thread_id_t thread_id = ev.get_thread_id (); + per_thread_summary &pts + = get_or_create_events_for_thread_id (path_, thread_id); + + pts.update_depth_limits (ev.get_stack_depth ()); + + if (cur_event_range) + if (cur_event_range->maybe_add_event (policy, + ev, + idx, check_rich_locations)) + continue; + + auto theme = policy.get_diagram_theme (); + const bool allow_emojis = theme ? theme->emojis_p () : false; + cur_event_range = new event_range (path_, ref_pp, + idx, ev, pts, + show_event_links, + colorize, + allow_emojis); + m_ranges.safe_push (cur_event_range); + pts.m_event_ranges.safe_push (cur_event_range); + pts.m_last_event = &ev; + } +} + +/* Write SPACES to PP. */ + +static void +write_indent (pretty_printer *pp, int spaces) +{ + for (int i = 0; i < spaces; i++) + pp_space (pp); +} + +static const int base_indent = 2; +static const int per_frame_indent = 2; + +/* A bundle of state for printing event_range instances for a particular + thread. */ + +class thread_event_printer +{ +public: + thread_event_printer (const per_thread_summary &t, bool show_depths) + : m_per_thread_summary (t), + m_show_depths (show_depths), + m_cur_indent (base_indent), + m_vbar_column_for_depth (), + m_num_printed (0) + { + } + + /* Get the previous event_range within this thread, if any. */ + const event_range *get_any_prev_range () const + { + if (m_num_printed > 0) + return m_per_thread_summary.m_event_ranges[m_num_printed - 1]; + else + return nullptr; + } + + /* Get the next event_range within this thread, if any. */ + const event_range *get_any_next_range () const + { + if (m_num_printed < m_per_thread_summary.m_event_ranges.length () - 1) + return m_per_thread_summary.m_event_ranges[m_num_printed + 1]; + else + return nullptr; + } + + void + print_swimlane_for_event_range_as_text (diagnostics::text_sink &text_output, + pretty_printer *pp, + const logical_locations::manager &logical_loc_mgr, + event_range *range, + diagnostics::source_effect_info *effect_info) + { + gcc_assert (pp); + const char *const line_color = "path"; + const char *start_line_color + = colorize_start (pp_show_color (pp), line_color); + const char *end_line_color = colorize_stop (pp_show_color (pp)); + + text_art::ascii_theme fallback_theme; + text_art::theme *theme = text_output.get_diagram_theme (); + if (!theme) + theme = &fallback_theme; + + cppchar_t depth_marker_char = theme->get_cppchar + (text_art::theme::cell_kind::INTERPROCEDURAL_DEPTH_MARKER); + /* e.g. "|". */ + + const bool interprocedural_p = m_per_thread_summary.interprocedural_p (); + + write_indent (pp, m_cur_indent); + if (const event_range *prev_range = get_any_prev_range ()) + { + if (range->m_stack_depth > prev_range->m_stack_depth) + { + gcc_assert (interprocedural_p); + /* Show pushed stack frame(s). */ + cppchar_t left = theme->get_cppchar + (text_art::theme::cell_kind::INTERPROCEDURAL_PUSH_FRAME_LEFT); + cppchar_t middle = theme->get_cppchar + (text_art::theme::cell_kind::INTERPROCEDURAL_PUSH_FRAME_MIDDLE); + cppchar_t right = theme->get_cppchar + (text_art::theme::cell_kind::INTERPROCEDURAL_PUSH_FRAME_RIGHT); + /* e.g. "+--> ". */ + pp_string (pp, start_line_color); + pp_unicode_character (pp, left); + pp_unicode_character (pp, middle); + pp_unicode_character (pp, middle); + pp_unicode_character (pp, right); + pp_space (pp); + pp_string (pp, end_line_color); + m_cur_indent += 5; + } + } + if (range->m_logical_loc) + { + label_text name + (logical_loc_mgr.get_name_for_path_output (range->m_logical_loc)); + if (name.get ()) + pp_printf (pp, "%qs: ", name.get ()); + } + if (range->m_start_idx == range->m_end_idx) + pp_printf (pp, "event %i", + range->m_start_idx + 1); + else + pp_printf (pp, "events %i-%i", + range->m_start_idx + 1, range->m_end_idx + 1); + if (m_show_depths) + pp_printf (pp, " (depth %i)", range->m_stack_depth); + pp_newline (pp); + + /* Print a run of events. */ + if (interprocedural_p) + { + write_indent (pp, m_cur_indent + per_frame_indent); + pp_string (pp, start_line_color); + pp_unicode_character (pp, depth_marker_char); + pp_string (pp, end_line_color); + pp_newline (pp); + + char *saved_prefix = pp_take_prefix (pp); + char *prefix; + { + pretty_printer tmp_pp; + write_indent (&tmp_pp, m_cur_indent + per_frame_indent); + pp_string (&tmp_pp, start_line_color); + pp_unicode_character (&tmp_pp, depth_marker_char); + pp_string (&tmp_pp, end_line_color); + prefix = xstrdup (pp_formatted_text (&tmp_pp)); + } + pp_set_prefix (pp, prefix); + pp_prefixing_rule (pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE; + range->print_as_text (*pp, text_output, effect_info); + pp_set_prefix (pp, saved_prefix); + + write_indent (pp, m_cur_indent + per_frame_indent); + pp_string (pp, start_line_color); + pp_unicode_character (pp, depth_marker_char); + pp_string (pp, end_line_color); + pp_newline (pp); + } + else + range->print_as_text (*pp, text_output, effect_info); + + if (const event_range *next_range = get_any_next_range ()) + { + if (range->m_stack_depth > next_range->m_stack_depth) + { + if (m_vbar_column_for_depth.get (next_range->m_stack_depth)) + { + /* Show returning from stack frame(s), by printing + something like: + " |\n" + " <-------------+\n" + " |\n". */ + gcc_assert (interprocedural_p); + cppchar_t left = theme->get_cppchar + (text_art::theme::cell_kind::INTERPROCEDURAL_POP_FRAMES_LEFT); + cppchar_t middle = theme->get_cppchar + (text_art::theme::cell_kind::INTERPROCEDURAL_POP_FRAMES_MIDDLE); + cppchar_t right = theme->get_cppchar + (text_art::theme::cell_kind::INTERPROCEDURAL_POP_FRAMES_RIGHT); + int vbar_for_next_frame + = *m_vbar_column_for_depth.get (next_range->m_stack_depth); + + int indent_for_next_frame + = vbar_for_next_frame - per_frame_indent; + write_indent (pp, vbar_for_next_frame); + pp_string (pp, start_line_color); + pp_unicode_character (pp, left); + for (int i = indent_for_next_frame + per_frame_indent; + i < m_cur_indent + per_frame_indent - 1; i++) + pp_unicode_character (pp, middle); + pp_unicode_character (pp, right); + pp_string (pp, end_line_color); + pp_newline (pp); + m_cur_indent = indent_for_next_frame; + + write_indent (pp, vbar_for_next_frame); + pp_string (pp, start_line_color); + pp_unicode_character (pp, depth_marker_char); + pp_string (pp, end_line_color); + pp_newline (pp); + } + else + { + /* Handle disjoint paths (e.g. a callback at some later + time). */ + m_cur_indent = base_indent; + } + } + else if (range->m_stack_depth < next_range->m_stack_depth) + { + /* Prepare to show pushed stack frame. */ + gcc_assert (interprocedural_p); + gcc_assert (range->m_stack_depth != EMPTY); + gcc_assert (range->m_stack_depth != DELETED); + m_vbar_column_for_depth.put (range->m_stack_depth, + m_cur_indent + per_frame_indent); + m_cur_indent += per_frame_indent; + } + } + + m_num_printed++; + } + + void + print_swimlane_for_event_range_as_html (diagnostics::context &dc, + xml::printer &xp, + html_label_writer *event_label_writer, + event_range *range, + diagnostics::source_effect_info *effect_info) + { + range->print_as_html (xp, dc, effect_info, event_label_writer); + m_num_printed++; + } + + int get_cur_indent () const { return m_cur_indent; } + +private: + const per_thread_summary &m_per_thread_summary; + bool m_show_depths; + + /* Print the ranges. */ + int m_cur_indent; + + /* Keep track of column numbers of existing '|' characters for + stack depths we've already printed. */ + static const int EMPTY = -1; + static const int DELETED = -2; + typedef int_hash <int, EMPTY, DELETED> vbar_hash; + hash_map <vbar_hash, int> m_vbar_column_for_depth; + + /* How many event ranges within this swimlane have we printed. + This is the index of the next event_range to print. */ + unsigned m_num_printed; +}; + +/* Print path_summary PS to TEXT_OUTPUT, giving an overview of the + interprocedural calls and returns. + + Print the event descriptions in a nested form, printing the event + descriptions within calls to diagnostic_show_locus, using labels to + show the events: + + 'foo' (events 1-2) + | NN | + | | + +--> 'bar' (events 3-4) + | NN | + | | + +--> 'baz' (events 5-6) + | NN | + | | + <------------ + + | + 'foo' (events 7-8) + | NN | + | | + +--> 'bar' (events 9-10) + | NN | + | | + +--> 'baz' (events 11-12) + | NN | + | | + + If SHOW_DEPTHS is true, append " (depth N)" to the header of each run + of events. + + For events with UNKNOWN_LOCATION, print a summary of each the event. */ + +static void +print_path_summary_as_text (const path_summary &ps, + diagnostics::text_sink &text_output, + bool show_depths) +{ + pretty_printer *const pp = text_output.get_printer (); + + std::vector<thread_event_printer> thread_event_printers; + for (auto t : ps.m_per_thread_summary) + thread_event_printers.push_back (thread_event_printer (*t, show_depths)); + + unsigned i; + event_range *range; + int last_out_edge_column = -1; + FOR_EACH_VEC_ELT (ps.m_ranges, i, range) + { + const int swimlane_idx + = range->m_per_thread_summary.get_swimlane_index (); + if (ps.multithreaded_p ()) + if (i == 0 || ps.m_ranges[i - 1]->m_thread_id != range->m_thread_id) + { + if (i > 0) + pp_newline (pp); + pp_printf (pp, "Thread: %qs", + range->m_per_thread_summary.get_name ()); + pp_newline (pp); + } + thread_event_printer &tep = thread_event_printers[swimlane_idx]; + /* Wire up any trailing out-edge from previous range to leading in-edge + of this range. */ + diagnostics::source_effect_info effect_info; + effect_info.m_leading_in_edge_column = last_out_edge_column; + tep.print_swimlane_for_event_range_as_text + (text_output, pp, + ps.get_logical_location_manager (), + range, &effect_info); + last_out_edge_column = effect_info.m_trailing_out_edge_column; + } +} + +/* Print PS as HTML to XP, using DC and, if non-null EVENT_LABEL_WRITER. */ + +static void +print_path_summary_as_html (const path_summary &ps, + diagnostics::context &dc, + xml::printer &xp, + html_label_writer *event_label_writer, + bool show_depths) +{ + std::vector<thread_event_printer> thread_event_printers; + for (auto t : ps.m_per_thread_summary) + thread_event_printers.push_back (thread_event_printer (*t, show_depths)); + + const logical_locations::manager *logical_loc_mgr + = dc.get_logical_location_manager (); + + xp.push_tag_with_class ("div", "event-ranges", false); + + /* Group the ranges into stack frames. */ + std::unique_ptr<stack_frame> curr_frame; + unsigned i; + event_range *range; + int last_out_edge_column = -1; + FOR_EACH_VEC_ELT (ps.m_ranges, i, range) + { + const int swimlane_idx + = range->m_per_thread_summary.get_swimlane_index (); + + const logical_locations::key this_logical_loc = range->m_logical_loc; + const int this_depth = range->m_stack_depth; + if (curr_frame) + { + int old_stack_depth = curr_frame->m_stack_depth; + if (this_depth > curr_frame->m_stack_depth) + { + emit_svg_arrow (xp, old_stack_depth, this_depth); + curr_frame + = begin_html_stack_frame (xp, + std::move (curr_frame), + range->m_logical_loc, + range->m_stack_depth, + logical_loc_mgr); + } + else + { + while (this_depth < curr_frame->m_stack_depth + || this_logical_loc != curr_frame->m_logical_loc) + { + curr_frame = end_html_stack_frame (xp, std::move (curr_frame)); + if (curr_frame == nullptr) + { + curr_frame + = begin_html_stack_frame (xp, + nullptr, + range->m_logical_loc, + range->m_stack_depth, + logical_loc_mgr); + break; + } + } + emit_svg_arrow (xp, old_stack_depth, this_depth); + } + } + else + { + curr_frame = begin_html_stack_frame (xp, + nullptr, + range->m_logical_loc, + range->m_stack_depth, + logical_loc_mgr); + } + + xp.push_tag_with_class ("table", "event-range-with-margin", false); + xp.push_tag ("tr", false); + xp.push_tag_with_class ("td", "event-range", false); + xp.push_tag_with_class ("div", "events-hdr", true); + if (range->m_logical_loc) + { + gcc_assert (logical_loc_mgr); + label_text funcname + = logical_loc_mgr->get_name_for_path_output (range->m_logical_loc); + if (funcname.get ()) + { + xp.push_tag_with_class ("span", "funcname", true); + xp.add_text (funcname.get ()); + xp.pop_tag ("span"); + xp.add_text (": "); + } + } + { + xp.push_tag_with_class ("span", "event-ids", true); + pretty_printer pp; + if (range->m_start_idx == range->m_end_idx) + pp_printf (&pp, "event %i", + range->m_start_idx + 1); + else + pp_printf (&pp, "events %i-%i", + range->m_start_idx + 1, range->m_end_idx + 1); + xp.add_text_from_pp (pp); + xp.pop_tag ("span"); + } + if (show_depths) + { + xp.add_text (" "); + xp.push_tag_with_class ("span", "depth", true); + pretty_printer pp; + pp_printf (&pp, "(depth %i)", range->m_stack_depth); + xp.add_text_from_pp (pp); + xp.pop_tag ("span"); + } + xp.pop_tag ("div"); + + /* Print a run of events. */ + thread_event_printer &tep = thread_event_printers[swimlane_idx]; + /* Wire up any trailing out-edge from previous range to leading in-edge + of this range. */ + diagnostics::source_effect_info effect_info; + effect_info.m_leading_in_edge_column = last_out_edge_column; + tep.print_swimlane_for_event_range_as_html (dc, xp, event_label_writer, + range, &effect_info); + last_out_edge_column = effect_info.m_trailing_out_edge_column; + + xp.pop_tag ("td"); + xp.pop_tag ("tr"); + xp.pop_tag ("table"); + } + + /* Close outstanding frames. */ + while (curr_frame) + curr_frame = end_html_stack_frame (xp, std::move (curr_frame)); + + xp.pop_tag ("div"); +} + +} /* end of anonymous namespace for path-printing code. */ + +class element_event_desc : public pp_element +{ +public: + element_event_desc (const event &event_) + : m_event (event_) + { + } + + void add_to_phase_2 (pp_markup::context &ctxt) final override + { + auto pp = ctxt.m_pp.clone (); + m_event.print_desc (*pp.get ()); + pp_string (&ctxt.m_pp, pp_formatted_text (pp.get ())); + } + +private: + const event &m_event; +}; + +/* Print PATH according to the context's path_format. */ + +void +diagnostics::text_sink::print_path (const path &path_) +{ + const unsigned num_events = path_.num_events (); + + switch (get_context ().get_path_format ()) + { + case DPF_NONE: + /* Do nothing. */ + return; + + case DPF_SEPARATE_EVENTS: + { + /* A note per event. */ + auto &logical_loc_mgr = path_.get_logical_location_manager (); + for (unsigned i = 0; i < num_events; i++) + { + const event &ev = path_.get_event (i); + element_event_desc e_event_desc (ev); + diagnostic_event_id_t event_id (i); + if (get_context ().show_path_depths_p ()) + { + int stack_depth = ev.get_stack_depth (); + /* -fdiagnostics-path-format=separate-events doesn't print + fndecl information, so with -fdiagnostics-show-path-depths + print the fndecls too, if any. */ + if (logical_locations::key logical_loc + = ev.get_logical_location ()) + { + label_text name + (logical_loc_mgr.get_name_for_path_output (logical_loc)); + inform (ev.get_location (), + "%@ %e (fndecl %qs, depth %i)", + &event_id, &e_event_desc, + name.get (), stack_depth); + } + else + inform (ev.get_location (), + "%@ %e (depth %i)", + &event_id, &e_event_desc, + stack_depth); + } + else + inform (ev.get_location (), + "%@ %e", &event_id, &e_event_desc); + } + } + break; + + case DPF_INLINE_EVENTS: + { + /* Consolidate related events. */ + path_print_policy policy (*this); + pretty_printer *const pp = get_printer (); + const bool check_rich_locations = true; + const bool colorize = pp_show_color (pp); + const bool show_event_links = m_source_printing.show_event_links_p; + path_summary summary (policy, + *pp, + path_, + check_rich_locations, + colorize, + show_event_links); + char *saved_prefix = pp_take_prefix (pp); + pp_set_prefix (pp, nullptr); + print_path_summary_as_text (summary, *this, + get_context ().show_path_depths_p ()); + pp_flush (pp); + pp_set_prefix (pp, saved_prefix); + } + break; + } +} + +/* Print PATH_ as HTML to XP, using DC and DSPP for settings. + If non-null, use EVENT_LABEL_WRITER when writing events. */ + +void +diagnostics::print_path_as_html (xml::printer &xp, + const path &path_, + context &dc, + html_label_writer *event_label_writer, + const source_print_policy &dspp) +{ + path_print_policy policy (dc); + const bool check_rich_locations = true; + const bool colorize = false; + const source_printing_options &source_printing_opts + = dspp.get_options (); + const bool show_event_links = source_printing_opts.show_event_links_p; + path_summary summary (policy, + *dc.get_reference_printer (), + path_, + check_rich_locations, + colorize, + show_event_links); + print_path_summary_as_html (summary, dc, xp, event_label_writer, + dc.show_path_depths_p ()); +} + +#if CHECKING_P + +namespace diagnostics { +namespace paths { +namespace selftest { + +using location = ::selftest::location; +using line_table_case = ::selftest::line_table_case; +using line_table_test = ::selftest::line_table_test; +using temp_source_file = ::selftest::temp_source_file; + +using test_context = diagnostics::selftest::test_context; + +/* Return true iff all events in PATH_ have locations for which column data + is available, so that selftests that require precise string output can + bail out for awkward line_table cases. */ + +static bool +path_events_have_column_data_p (const path &path_) +{ + for (unsigned idx = 0; idx < path_.num_events (); idx++) + { + location_t event_loc = path_.get_event (idx).get_location (); + if (line_table->get_pure_location (event_loc) + > LINE_MAP_MAX_LOCATION_WITH_COLS) + return false; + if (line_table->get_start (event_loc) > LINE_MAP_MAX_LOCATION_WITH_COLS) + return false; + if (line_table->get_finish (event_loc) > LINE_MAP_MAX_LOCATION_WITH_COLS) + return false; + } + return true; +} + +/* Verify that empty paths are handled gracefully. */ + +static void +test_empty_path (pretty_printer *event_pp) +{ + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + ASSERT_FALSE (path.interprocedural_p ()); + + test_context dc; + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false); + ASSERT_EQ (summary.get_num_ranges (), 0); + + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ ("", + pp_formatted_text (text_output.get_printer ())); +} + +/* Verify that print_path_summary works on a purely intraprocedural path. */ + +static void +test_intraprocedural_path (pretty_printer *event_pp) +{ + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + const char *const funcname = "foo"; + path.add_event (UNKNOWN_LOCATION, funcname, 0, "first %qs", "free"); + path.add_event (UNKNOWN_LOCATION, funcname, 0, "double %qs", "free"); + + ASSERT_FALSE (path.interprocedural_p ()); + + selftest::test_context dc; + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false, false, false); + ASSERT_EQ (summary.get_num_ranges (), 1); + + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ (" `foo': events 1-2 (depth 0)\n" + " (1): first `free'\n" + " (2): double `free'\n", + pp_formatted_text (text_output.get_printer ())); +} + +/* Verify that print_path_summary works on an interprocedural path. */ + +static void +test_interprocedural_path_1 (pretty_printer *event_pp) +{ + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + path.add_entry ("test", 0); + path.add_call ("test", 0, "make_boxed_int"); + path.add_call ("make_boxed_int", 1, "wrapped_malloc"); + path.add_event (UNKNOWN_LOCATION, + "wrapped_malloc", 2, "calling malloc"); + path.add_return ("test", 0); + path.add_call ("test", 0, "free_boxed_int"); + path.add_call ("free_boxed_int", 1, "wrapped_free"); + path.add_event (UNKNOWN_LOCATION, "wrapped_free", 2, "calling free"); + path.add_return ("test", 0); + path.add_call ("test", 0, "free_boxed_int"); + path.add_call ("free_boxed_int", 1, "wrapped_free"); + path.add_event (UNKNOWN_LOCATION, "wrapped_free", 2, "calling free"); + ASSERT_EQ (path.num_events (), 18); + + ASSERT_TRUE (path.interprocedural_p ()); + + { + selftest::test_context dc; + text_sink text_output (dc, nullptr, false); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false); + ASSERT_EQ (summary.get_num_ranges (), 9); + + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ + (" `test': events 1-2 (depth 0)\n" + " |\n" + " | (1): entering `test'\n" + " | (2): calling `make_boxed_int'\n" + " |\n" + " +--> `make_boxed_int': events 3-4 (depth 1)\n" + " |\n" + " | (3): entering `make_boxed_int'\n" + " | (4): calling `wrapped_malloc'\n" + " |\n" + " +--> `wrapped_malloc': events 5-6 (depth 2)\n" + " |\n" + " | (5): entering `wrapped_malloc'\n" + " | (6): calling malloc\n" + " |\n" + " <-------------+\n" + " |\n" + " `test': events 7-8 (depth 0)\n" + " |\n" + " | (7): returning to `test'\n" + " | (8): calling `free_boxed_int'\n" + " |\n" + " +--> `free_boxed_int': events 9-10 (depth 1)\n" + " |\n" + " | (9): entering `free_boxed_int'\n" + " | (10): calling `wrapped_free'\n" + " |\n" + " +--> `wrapped_free': events 11-12 (depth 2)\n" + " |\n" + " | (11): entering `wrapped_free'\n" + " | (12): calling free\n" + " |\n" + " <-------------+\n" + " |\n" + " `test': events 13-14 (depth 0)\n" + " |\n" + " | (13): returning to `test'\n" + " | (14): calling `free_boxed_int'\n" + " |\n" + " +--> `free_boxed_int': events 15-16 (depth 1)\n" + " |\n" + " | (15): entering `free_boxed_int'\n" + " | (16): calling `wrapped_free'\n" + " |\n" + " +--> `wrapped_free': events 17-18 (depth 2)\n" + " |\n" + " | (17): entering `wrapped_free'\n" + " | (18): calling free\n" + " |\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false); + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ + (" `test': events 1-2 (depth 0)\n" + " │\n" + " │ (1): entering `test'\n" + " │ (2): calling `make_boxed_int'\n" + " │\n" + " └──> `make_boxed_int': events 3-4 (depth 1)\n" + " │\n" + " │ (3): entering `make_boxed_int'\n" + " │ (4): calling `wrapped_malloc'\n" + " │\n" + " └──> `wrapped_malloc': events 5-6 (depth 2)\n" + " │\n" + " │ (5): entering `wrapped_malloc'\n" + " │ (6): calling malloc\n" + " │\n" + " <─────────────┘\n" + " │\n" + " `test': events 7-8 (depth 0)\n" + " │\n" + " │ (7): returning to `test'\n" + " │ (8): calling `free_boxed_int'\n" + " │\n" + " └──> `free_boxed_int': events 9-10 (depth 1)\n" + " │\n" + " │ (9): entering `free_boxed_int'\n" + " │ (10): calling `wrapped_free'\n" + " │\n" + " └──> `wrapped_free': events 11-12 (depth 2)\n" + " │\n" + " │ (11): entering `wrapped_free'\n" + " │ (12): calling free\n" + " │\n" + " <─────────────┘\n" + " │\n" + " `test': events 13-14 (depth 0)\n" + " │\n" + " │ (13): returning to `test'\n" + " │ (14): calling `free_boxed_int'\n" + " │\n" + " └──> `free_boxed_int': events 15-16 (depth 1)\n" + " │\n" + " │ (15): entering `free_boxed_int'\n" + " │ (16): calling `wrapped_free'\n" + " │\n" + " └──> `wrapped_free': events 17-18 (depth 2)\n" + " │\n" + " │ (17): entering `wrapped_free'\n" + " │ (18): calling free\n" + " │\n", + pp_formatted_text (text_output.get_printer ())); + } + +} + +/* Example where we pop the stack to an intermediate frame, rather than the + initial one. */ + +static void +test_interprocedural_path_2 (pretty_printer *event_pp) +{ + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + path.add_entry ("foo", 0); + path.add_call ("foo", 0, "bar"); + path.add_call ("bar", 1, "baz"); + path.add_return ("bar", 1); + path.add_call ("bar", 1, "baz"); + ASSERT_EQ (path.num_events (), 8); + + ASSERT_TRUE (path.interprocedural_p ()); + + { + selftest::test_context dc; + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false); + ASSERT_EQ (summary.get_num_ranges (), 5); + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ + (" `foo': events 1-2 (depth 0)\n" + " |\n" + " | (1): entering `foo'\n" + " | (2): calling `bar'\n" + " |\n" + " +--> `bar': events 3-4 (depth 1)\n" + " |\n" + " | (3): entering `bar'\n" + " | (4): calling `baz'\n" + " |\n" + " +--> `baz': event 5 (depth 2)\n" + " |\n" + " | (5): entering `baz'\n" + " |\n" + " <------+\n" + " |\n" + " `bar': events 6-7 (depth 1)\n" + " |\n" + " | (6): returning to `bar'\n" + " | (7): calling `baz'\n" + " |\n" + " +--> `baz': event 8 (depth 2)\n" + " |\n" + " | (8): entering `baz'\n" + " |\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false); + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ + (" `foo': events 1-2 (depth 0)\n" + " │\n" + " │ (1): entering `foo'\n" + " │ (2): calling `bar'\n" + " │\n" + " └──> `bar': events 3-4 (depth 1)\n" + " │\n" + " │ (3): entering `bar'\n" + " │ (4): calling `baz'\n" + " │\n" + " └──> `baz': event 5 (depth 2)\n" + " │\n" + " │ (5): entering `baz'\n" + " │\n" + " <──────┘\n" + " │\n" + " `bar': events 6-7 (depth 1)\n" + " │\n" + " │ (6): returning to `bar'\n" + " │ (7): calling `baz'\n" + " │\n" + " └──> `baz': event 8 (depth 2)\n" + " │\n" + " │ (8): entering `baz'\n" + " │\n", + pp_formatted_text (text_output.get_printer ())); + } +} + +/* Verify that print_path_summary is sane in the face of a recursive + diagnostic path. */ + +static void +test_recursion (pretty_printer *event_pp) +{ + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + path.add_entry ("factorial", 0); + for (int depth = 0; depth < 3; depth++) + path.add_call ("factorial", depth, "factorial"); + ASSERT_EQ (path.num_events (), 7); + + ASSERT_TRUE (path.interprocedural_p ()); + + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false); + ASSERT_EQ (summary.get_num_ranges (), 4); + + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ + (" `factorial': events 1-2 (depth 0)\n" + " |\n" + " | (1): entering `factorial'\n" + " | (2): calling `factorial'\n" + " |\n" + " +--> `factorial': events 3-4 (depth 1)\n" + " |\n" + " | (3): entering `factorial'\n" + " | (4): calling `factorial'\n" + " |\n" + " +--> `factorial': events 5-6 (depth 2)\n" + " |\n" + " | (5): entering `factorial'\n" + " | (6): calling `factorial'\n" + " |\n" + " +--> `factorial': event 7 (depth 3)\n" + " |\n" + " | (7): entering `factorial'\n" + " |\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE); + + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, false); + print_path_summary_as_text (summary, text_output, true); + ASSERT_STREQ + (" `factorial': events 1-2 (depth 0)\n" + " │\n" + " │ (1): entering `factorial'\n" + " │ (2): calling `factorial'\n" + " │\n" + " └──> `factorial': events 3-4 (depth 1)\n" + " │\n" + " │ (3): entering `factorial'\n" + " │ (4): calling `factorial'\n" + " │\n" + " └──> `factorial': events 5-6 (depth 2)\n" + " │\n" + " │ (5): entering `factorial'\n" + " │ (6): calling `factorial'\n" + " │\n" + " └──> `factorial': event 7 (depth 3)\n" + " │\n" + " │ (7): entering `factorial'\n" + " │\n", + pp_formatted_text (text_output.get_printer ())); + } +} + +/* Helper class for writing tests of control flow visualization. */ + +class control_flow_test +{ +public: + control_flow_test (const selftest::location &loc, + const line_table_case &case_, + const char *content) + : m_tmp_file (loc, ".c", content, + /* gcc_rich_location::add_location_if_nearby implicitly + uses global_dc's file_cache, so we need to evict + tmp when we're done. */ + &global_dc->get_file_cache ()), + m_ltt (case_) + { + m_ord_map + = linemap_check_ordinary (linemap_add (line_table, LC_ENTER, false, + m_tmp_file.get_filename (), 0)); + linemap_line_start (line_table, 1, 100); + } + + location_t get_line_and_column (int line, int column) + { + return linemap_position_for_line_and_column (line_table, m_ord_map, + line, column); + } + + location_t get_line_and_columns (int line, int first_column, int last_column) + { + return get_line_and_columns (line, + first_column, first_column, last_column); + } + + location_t get_line_and_columns (int line, + int first_column, + int caret_column, + int last_column) + { + return make_location (get_line_and_column (line, caret_column), + get_line_and_column (line, first_column), + get_line_and_column (line, last_column)); + } + +private: + temp_source_file m_tmp_file; + line_table_test m_ltt; + const line_map_ordinary *m_ord_map; +}; + +/* Example of event edges where all events can go in the same layout, + testing the 6 combinations of: + - ASCII vs Unicode vs event links off + - line numbering on and off. */ + +static void +test_control_flow_1 (const line_table_case &case_, + pretty_printer *event_pp) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333. + ...123456789012345678901234567890123456789. */ + const char *content + = ("int test (int *p)\n" /* line 1. */ + "{\n" /* line 2. */ + " if (p)\n" /* line 3. */ + " return 0;\n" /* line 4. */ + " return *p;\n" /* line 5. */ + "}\n"); /* line 6. */ + + control_flow_test t (SELFTEST_LOCATION, case_, content); + + const location_t conditional = t.get_line_and_column (3, 7); + const location_t cfg_dest = t.get_line_and_column (5, 10); + + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + path.add_event (conditional, nullptr, 0, + "following %qs branch (when %qs is NULL)...", + "false", "p"); + path.connect_to_next_event (); + + path.add_event (cfg_dest, nullptr, 0, + "...to here"); + path.add_event (cfg_dest, nullptr, 0, + "dereference of NULL %qs", + "p"); + + if (!path_events_have_column_data_p (path)) + return; + + + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_event_links (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:3:7:\n" + " if (p)\n" + " ^\n" + " |\n" + " (1) following `false' branch (when `p' is NULL)... ->-+\n" + " |\n" + "FILENAME:5:10:\n" + " |\n" + "+------------------------------------------------------------+\n" + "| return *p;\n" + "| ~\n" + "| |\n" + "+-------->(2) ...to here\n" + " (3) dereference of NULL `p'\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_event_links (false); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:3:7:\n" + " if (p)\n" + " ^\n" + " |\n" + " (1) following `false' branch (when `p' is NULL)...\n" + "FILENAME:5:10:\n" + " return *p;\n" + " ~\n" + " |\n" + " (2) ...to here\n" + " (3) dereference of NULL `p'\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_line_numbers (true); + dc.show_event_links (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:3:7:\n" + " 3 | if (p)\n" + " | ^\n" + " | |\n" + " | (1) following `false' branch (when `p' is NULL)... ->-+\n" + " | |\n" + " | |\n" + " |+------------------------------------------------------------+\n" + " 4 || return 0;\n" + " 5 || return *p;\n" + " || ~\n" + " || |\n" + " |+-------->(2) ...to here\n" + " | (3) dereference of NULL `p'\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_line_numbers (true); + dc.show_event_links (false); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:3:7:\n" + " 3 | if (p)\n" + " | ^\n" + " | |\n" + " | (1) following `false' branch (when `p' is NULL)...\n" + " 4 | return 0;\n" + " 5 | return *p;\n" + " | ~\n" + " | |\n" + " | (2) ...to here\n" + " | (3) dereference of NULL `p'\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE); + dc.show_event_links (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:3:7:\n" + " if (p)\n" + " ^\n" + " |\n" + " (1) following `false' branch (when `p' is NULL)... ─>─┐\n" + " │\n" + "FILENAME:5:10:\n" + " │\n" + "┌────────────────────────────────────────────────────────────┘\n" + "│ return *p;\n" + "│ ~\n" + "│ |\n" + "└────────>(2) ...to here\n" + " (3) dereference of NULL `p'\n", + pp_formatted_text (text_output.get_printer ())); + } + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE); + dc.show_event_links (true); + dc.show_line_numbers (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:3:7:\n" + " 3 | if (p)\n" + " | ^\n" + " | |\n" + " | (1) following `false' branch (when `p' is NULL)... ─>─┐\n" + " | │\n" + " | │\n" + " |┌────────────────────────────────────────────────────────────┘\n" + " 4 |│ return 0;\n" + " 5 |│ return *p;\n" + " |│ ~\n" + " |│ |\n" + " |└────────>(2) ...to here\n" + " | (3) dereference of NULL `p'\n", + pp_formatted_text (text_output.get_printer ())); + } +} + +/* Complex example involving a backedge. */ + +static void +test_control_flow_2 (const line_table_case &case_, + pretty_printer *event_pp) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333. + ...123456789012345678901234567890123456789. */ + const char *content + = ("int for_loop_noop_next (struct node *n)\n" /* <--------- line 1. */ + "{\n" /* <----------------------------------------------- line 2. */ + " int sum = 0;\n" /* <---------------------------------- line 3. */ + " for (struct node *iter = n; iter; iter->next)\n" /* <- line 4. */ + " sum += n->val;\n" /* <------------------------------ line 5. */ + " return sum;\n" /* <----------------------------------- line 6. */ + "}\n"); /* <-------------------------------------------- line 7. */ + /* Adapted from infinite-loop-linked-list.c where + "iter->next" should be "iter = iter->next". */ + + control_flow_test t (SELFTEST_LOCATION, case_, content); + + const location_t iter_test = t.get_line_and_columns (4, 31, 34); + const location_t loop_body_start = t.get_line_and_columns (5, 12, 17); + const location_t loop_body_end = t.get_line_and_columns (5, 5, 9, 17); + + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + path.add_event (iter_test, nullptr, 0, "infinite loop here"); + + path.add_event (iter_test, nullptr, 0, "looping from here..."); + path.connect_to_next_event (); + + path.add_event (loop_body_start, nullptr, 0, "...to here"); + + path.add_event (loop_body_end, nullptr, 0, "looping back..."); + path.connect_to_next_event (); + + path.add_event (iter_test, nullptr, 0, "...to here"); + + if (!path_events_have_column_data_p (path)) + return; + + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_event_links (true); + dc.show_line_numbers (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:4:31:\n" + " 4 | for (struct node *iter = n; iter; iter->next)\n" + " | ^~~~\n" + " | |\n" + " | (1) infinite loop here\n" + " | (2) looping from here... ->-+\n" + " | |\n" + " | |\n" + " |+----------------------------------------------------------+\n" + " 5 || sum += n->val;\n" + " || ~~~~~~ \n" + " || |\n" + " |+---------->(3) ...to here\n" + /* We need to start an new event_range here as event (4) is to the + left of event (3), and thus (4) would mess up the in-edge to (3). */ + " event 4\n" + " 5 | sum += n->val;\n" + " | ~~~~^~~~~~~~~\n" + " | |\n" + " | (4) looping back... ->-+\n" + " | |\n" + /* We need to start an new event_range here as event (4) with an + out-edge is on a later line (line 5) than its destination event (5), + on line 4. */ + " event 5\n" + " | |\n" + " |+-------------------------------+\n" + " 4 || for (struct node *iter = n; iter; iter->next)\n" + " || ^~~~\n" + " || |\n" + " |+----------------------------->(5) ...to here\n", + pp_formatted_text (text_output.get_printer ())); + } +} + +/* Complex example involving a backedge and both an in-edge and out-edge + on the same line. */ + +static void +test_control_flow_3 (const line_table_case &case_, + pretty_printer *event_pp) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333. + ...123456789012345678901234567890123456789. */ + const char *content + = ("void test_missing_comparison_in_for_condition_1 (int n)\n" + "{\n" /* <------------------------- line 2. */ + " for (int i = 0; n; i++)\n" /* <- line 3. */ + " {\n" /* <--------------------- line 4. */ + " }\n" /* <--------------------- line 5. */ + "}\n"); /* <----------------------- line 6. */ + /* Adapted from infinite-loop-1.c where the condition should have been + "i < n", rather than just "n". */ + + control_flow_test t (SELFTEST_LOCATION, case_, content); + + const location_t iter_test = t.get_line_and_column (3, 19); + const location_t iter_next = t.get_line_and_columns (3, 22, 24); + + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + path.add_event (iter_test, nullptr, 0, "infinite loop here"); + + path.add_event (iter_test, nullptr, 0, "looping from here..."); + path.connect_to_next_event (); + + path.add_event (iter_next, nullptr, 0, "...to here"); + + path.add_event (iter_next, nullptr, 0, "looping back..."); + path.connect_to_next_event (); + + path.add_event (iter_test, nullptr, 0, "...to here"); + + if (!path_events_have_column_data_p (path)) + return; + + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_event_links (true); + dc.show_line_numbers (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-2\n" + "FILENAME:3:19:\n" + " 3 | for (int i = 0; n; i++)\n" + " | ^\n" + " | |\n" + " | (1) infinite loop here\n" + " | (2) looping from here... ->-+\n" + " | |\n" + " events 3-4\n" + " | |\n" + " |+----------------------------------------------+\n" + " 3 || for (int i = 0; n; i++)\n" + " || ^~~\n" + " || |\n" + " |+-------------------->(3) ...to here\n" + " | (4) looping back... ->-+\n" + " | |\n" + /* We need to start an new event_range here as event (4) with an + out-edge is on the same line as its destination event (5), but + to the right, which we can't handle as a single event_range. */ + " event 5\n" + " | |\n" + " |+--------------------------------------------+\n" + " 3 || for (int i = 0; n; i++)\n" + " || ^\n" + " || |\n" + " |+----------------->(5) ...to here\n", + pp_formatted_text (text_output.get_printer ())); + } +} + +/* Implementation of ASSERT_CFG_EDGE_PATH_STREQ. */ + +static void +assert_cfg_edge_path_streq (const location &loc, + pretty_printer *event_pp, + const location_t src_loc, + const location_t dst_loc, + const char *expected_str) +{ + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + path.add_event (src_loc, nullptr, 0, "from here..."); + path.connect_to_next_event (); + + path.add_event (dst_loc, nullptr, 0, "...to here"); + + if (!path_events_have_column_data_p (path)) + return; + + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_event_links (true); + dc.show_line_numbers (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ_AT (loc, expected_str, + pp_formatted_text (text_output.get_printer ())); +} + +/* Assert that if we make a path with an event with "from here..." at SRC_LOC + leading to an event "...to here" at DST_LOC that we print the path + as EXPECTED_STR. */ + +#define ASSERT_CFG_EDGE_PATH_STREQ(SRC_LOC, DST_LOC, EXPECTED_STR) \ + assert_cfg_edge_path_streq ((SELFTEST_LOCATION), (event_pp), \ + (SRC_LOC), (DST_LOC), (EXPECTED_STR)) + +/* Various examples of edge, trying to cover all combinations of: + - relative x positive of src label and dst label + - relative y position of labels: + - on same line + - on next line + - on line after next + - big gap, where src is before dst + - big gap, where src is after dst + and other awkward cases. */ + +static void +test_control_flow_4 (const line_table_case &case_, + pretty_printer *event_pp) +{ + std::string many_lines; + for (int i = 1; i <= 100; i++) + /* ............000000000111 + ............123456789012. */ + many_lines += "LHS RHS\n"; + control_flow_test t (SELFTEST_LOCATION, case_, many_lines.c_str ()); + + /* Same line. */ + { + /* LHS -> RHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 1, 3), + t.get_line_and_columns (3, 10, 12), + (" event 1\n" + "FILENAME:3:1:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " event 2\n" + " | |\n" + " |+--------------------+\n" + " 3 ||LHS RHS\n" + " || ^~~\n" + " || |\n" + " |+-------->(2) ...to here\n")); + + /* RHS -> LHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 10, 12), + t.get_line_and_columns (3, 1, 3), + (" event 1\n" + "FILENAME:3:10:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " event 2\n" + " | |\n" + " |+-----------------------------+\n" + " 3 ||LHS RHS\n" + " ||^~~\n" + " |||\n" + " |+(2) ...to here\n")); + } + + /* Next line. */ + { + /* LHS -> RHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 1, 3), + t.get_line_and_columns (4, 5, 7), + (" events 1-2\n" + "FILENAME:3:1:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " | |\n" + " |+--------------------+\n" + " 4 ||LHS RHS\n" + " || ~~~\n" + " || |\n" + " |+--->(2) ...to here\n")); + + /* RHS -> LHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 10, 12), + t.get_line_and_columns (4, 1, 3), + (" events 1-2\n" + "FILENAME:3:10:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " | |\n" + " |+-----------------------------+\n" + " 4 ||LHS RHS\n" + " ||~~~ \n" + " |||\n" + " |+(2) ...to here\n")); + } + + /* Line after next. */ + { + /* LHS -> RHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 1, 3), + t.get_line_and_columns (5, 10, 12), + (" events 1-2\n" + "FILENAME:3:1:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " | |\n" + " |+--------------------+\n" + " 4 ||LHS RHS\n" + " 5 ||LHS RHS\n" + " || ~~~\n" + " || |\n" + " |+-------->(2) ...to here\n")); + + /* RHS -> LHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 10, 12), + t.get_line_and_columns (5, 1, 3), + (" events 1-2\n" + "FILENAME:3:10:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " | |\n" + " |+-----------------------------+\n" + " 4 ||LHS RHS\n" + " 5 ||LHS RHS\n" + " ||~~~ \n" + " |||\n" + " |+(2) ...to here\n")); + } + + /* Big gap, increasing line number. */ + { + /* LHS -> RHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 1, 3), + t.get_line_and_columns (97, 10, 12), + (" events 1-2\n" + "FILENAME:3:1:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + "......\n" + " | |\n" + " |+--------------------+\n" + " 97 ||LHS RHS\n" + " || ~~~\n" + " || |\n" + " |+-------->(2) ...to here\n")); + + /* RHS -> LHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 10, 12), + t.get_line_and_columns (97, 1, 3), + (" events 1-2\n" + "FILENAME:3:10:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + "......\n" + " | |\n" + " |+-----------------------------+\n" + " 97 ||LHS RHS\n" + " ||~~~ \n" + " |||\n" + " |+(2) ...to here\n")); + } + + /* Big gap, decreasing line number. */ + { + /* LHS -> RHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (97, 1, 3), + t.get_line_and_columns (3, 10, 12), + (" event 1\n" + "FILENAME:97:1:\n" + " 97 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " event 2\n" + " | |\n" + " |+--------------------+\n" + " 3 ||LHS RHS\n" + " || ^~~\n" + " || |\n" + " |+-------->(2) ...to here\n")); + + /* RHS -> LHS. */ + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (97, 10, 12), + t.get_line_and_columns (3, 1, 3), + (" event 1\n" + "FILENAME:97:10:\n" + " 97 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " event 2\n" + " | |\n" + " |+-----------------------------+\n" + " 3 ||LHS RHS\n" + " ||^~~\n" + " |||\n" + " |+(2) ...to here\n")); + } + + /* Unknown src. */ + { + ASSERT_CFG_EDGE_PATH_STREQ + (UNKNOWN_LOCATION, + t.get_line_and_columns (3, 10, 12), + (" event 1\n" + " (1): from here...\n" + " event 2\n" + "FILENAME:3:10:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " |+-------->(2) ...to here\n")); + } + + /* Unknown dst. */ + { + ASSERT_CFG_EDGE_PATH_STREQ + (t.get_line_and_columns (3, 1, 3), + UNKNOWN_LOCATION, + (" event 1\n" + "FILENAME:3:1:\n" + " 3 | LHS RHS\n" + " | ^~~\n" + " | |\n" + " | (1) from here... ->-+\n" + " | |\n" + " event 2\n" + "FILENAME:\n" + " (2): ...to here\n")); + } +} + +/* Another complex example, adapted from data-model-20.c. */ + +static void +test_control_flow_5 (const line_table_case &case_, + pretty_printer *event_pp) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333444444444455555555556666666666. + ...123456789012345678901234567890123456789012345678901234567890123456789. */ + const char *content + = (" if ((arr = (struct foo **)malloc(n * sizeof(struct foo *))) == NULL)\n" + " return NULL;\n" /* <------------------------- line 2. */ + "\n" /* <----------------------------------------- line 3. */ + " for (i = 0; i < n; i++) {\n" /* <-------------- line 4. */ + " if ((arr[i] = (struct foo *)malloc(sizeof(struct foo))) == NULL) {\n"); + + control_flow_test t (SELFTEST_LOCATION, case_, content); + + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + /* (1) */ + path.add_event (t.get_line_and_column (1, 6), nullptr, 0, + "following %qs branch (when %qs is non-NULL)...", + "false", "arr"); + path.connect_to_next_event (); + + /* (2) */ + path.add_event (t.get_line_and_columns (4, 8, 10, 12), nullptr, 0, + "...to here"); + + /* (3) */ + path.add_event (t.get_line_and_columns (4, 15, 17, 19), nullptr, 0, + "following %qs branch (when %qs)...", + "true", "i < n"); + path.connect_to_next_event (); + + /* (4) */ + path.add_event (t.get_line_and_column (5, 13), nullptr, 0, + "...to here"); + + /* (5) */ + path.add_event (t.get_line_and_columns (5, 33, 58), nullptr, 0, + "allocated here"); + + if (!path_events_have_column_data_p (path)) + return; + + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_event_links (true); + dc.show_line_numbers (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-5\n" + "FILENAME:1:6:\n" + " 1 | if ((arr = (struct foo **)malloc(n * sizeof(struct foo *))) == NULL)\n" + " | ^\n" + " | |\n" + " | (1) following `false' branch (when `arr' is non-NULL)... ->-+\n" + " | |\n" + "......\n" + " | |\n" + " |+-----------------------------------------------------------------+\n" + " 4 || for (i = 0; i < n; i++) {\n" + " || ~~~~~ ~~~~~\n" + " || | |\n" + " || | (3) following `true' branch (when `i < n')... ->-+\n" + " |+-------->(2) ...to here |\n" + " | |\n" + " | |\n" + " |+-----------------------------------------------------------------+\n" + " 5 || if ((arr[i] = (struct foo *)malloc(sizeof(struct foo))) == NULL) {\n" + " || ~ ~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + " || | |\n" + " |+----------->(4) ...to here (5) allocated here\n", + pp_formatted_text (text_output.get_printer ())); + } +} + +/* Another complex example, adapted from loop-3.c. */ + +static void +test_control_flow_6 (const line_table_case &case_, + pretty_printer *event_pp) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333. + ...123456789012345678901234567890123456. */ + const char *content + = ("#include <stdlib.h>\n" /* <------------------ line 1. */ + "\n" /* <------------------------------------- line 2. */ + "void test(int c)\n" /* <--------------------- line 3. */ + "{\n" /* <------------------------------------ line 4. */ + " int i;\n" /* <----------------------------- line 5. */ + " char *buffer = (char*)malloc(256);\n" /* <- line 6. */ + "\n" /* <------------------------------------- line 7. */ + " for (i=0; i<255; i++) {\n" /* <------------ line 8. */ + " buffer[i] = c;\n" /* <------------------- line 9. */ + "\n" /* <------------------------------------- line 10. */ + " free(buffer);\n" /* <-------------------- line 11. */ + " }\n"); /* <-------------------------------- line 12. */ + + control_flow_test t (SELFTEST_LOCATION, case_, content); + + logical_locations::selftest::test_manager logical_loc_mgr; + test_path path (logical_loc_mgr, event_pp); + /* (1) */ + path.add_event (t.get_line_and_columns (6, 25, 35), nullptr, 0, + "allocated here"); + + /* (2) */ + path.add_event (t.get_line_and_columns (8, 13, 14, 17), nullptr, 0, + "following %qs branch (when %qs)...", + "true", "i <= 254"); + path.connect_to_next_event (); + + /* (3) */ + path.add_event (t.get_line_and_columns (9, 5, 15, 17), nullptr, 0, + "...to here"); + + /* (4) */ + path.add_event (t.get_line_and_columns (8, 13, 14, 17), nullptr, 0, + "following %qs branch (when %qs)...", + "true", "i <= 254"); + path.connect_to_next_event (); + + /* (5) */ + path.add_event (t.get_line_and_columns (9, 5, 15, 17), nullptr, 0, + "...to here"); + + if (!path_events_have_column_data_p (path)) + return; + + { + selftest::test_context dc; + dc.set_text_art_charset (DIAGNOSTICS_TEXT_ART_CHARSET_ASCII); + dc.show_event_links (true); + dc.show_line_numbers (true); + text_sink text_output (dc); + path_print_policy policy (text_output); + path_summary summary (policy, *event_pp, path, true); + print_path_summary_as_text (summary, text_output, false); + ASSERT_STREQ + (" events 1-3\n" + "FILENAME:6:25:\n" + " 6 | char *buffer = (char*)malloc(256);\n" + " | ^~~~~~~~~~~\n" + " | |\n" + " | (1) allocated here\n" + " 7 | \n" + " 8 | for (i=0; i<255; i++) {\n" + " | ~~~~~ \n" + " | |\n" + " | (2) following `true' branch (when `i <= 254')... ->-+\n" + " | |\n" + " | |\n" + " |+-----------------------------------------------------------------+\n" + " 9 || buffer[i] = c;\n" + " || ~~~~~~~~~~~~~ \n" + " || |\n" + " |+------------->(3) ...to here\n" + " events 4-5\n" + " 8 | for (i=0; i<255; i++) {\n" + " | ~^~~~\n" + " | |\n" + " | (4) following `true' branch (when `i <= 254')... ->-+\n" + " | |\n" + " | |\n" + " |+-----------------------------------------------------------------+\n" + " 9 || buffer[i] = c;\n" + " || ~~~~~~~~~~~~~\n" + " || |\n" + " |+------------->(5) ...to here\n", + pp_formatted_text (text_output.get_printer ())); + } +} + +static void +control_flow_tests (const line_table_case &case_) +{ + pretty_printer pp; + pp_show_color (&pp) = false; + + test_control_flow_1 (case_, &pp); + test_control_flow_2 (case_, &pp); + test_control_flow_3 (case_, &pp); + test_control_flow_4 (case_, &pp); + test_control_flow_5 (case_, &pp); + test_control_flow_6 (case_, &pp); +} + +} // namespace diagnostics::paths::selftest +} // namespace diagnostics::paths + +namespace selftest { // diagnostics::selftest + +/* Run all of the selftests within this file. */ + +void +paths_output_cc_tests () +{ + pretty_printer pp; + pp_show_color (&pp) = false; + + ::selftest::auto_fix_quotes fix_quotes; + diagnostics::paths::selftest::test_empty_path (&pp); + diagnostics::paths::selftest::test_intraprocedural_path (&pp); + diagnostics::paths::selftest::test_interprocedural_path_1 (&pp); + diagnostics::paths::selftest::test_interprocedural_path_2 (&pp); + diagnostics::paths::selftest::test_recursion (&pp); + for_each_line_table_case (diagnostics::paths::selftest::control_flow_tests); +} + +} // namespace diagnostics::selftest +} // namespace diagnostics + +#if __GNUC__ >= 10 +# pragma GCC diagnostic pop +#endif + +#endif /* #if CHECKING_P */ diff --git a/gcc/diagnostics/paths.cc b/gcc/diagnostics/paths.cc new file mode 100644 index 0000000..bc769c4 --- /dev/null +++ b/gcc/diagnostics/paths.cc @@ -0,0 +1,233 @@ +/* Paths through the code associated with a diagnostic. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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/>. */ + +#include "config.h" +#define INCLUDE_ALGORITHM +#define INCLUDE_MAP +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostics/paths.h" +#include "diagnostics/state-graphs.h" + +/* Disable warnings about missing quoting in GCC diagnostics for the print + calls below. */ +#if __GNUC__ >= 10 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-diag" +#endif + +using namespace diagnostics; +using namespace diagnostics::paths; + +/* class diagnostics::paths::event. */ + +/* struct event::meaning. */ + +void +event::meaning::dump_to_pp (pretty_printer *pp) const +{ + bool need_comma = false; + pp_character (pp, '{'); + if (const char *verb_str = maybe_get_verb_str (m_verb)) + { + pp_printf (pp, "verb: %qs", verb_str); + need_comma = true; + } + if (const char *noun_str = maybe_get_noun_str (m_noun)) + { + if (need_comma) + pp_string (pp, ", "); + pp_printf (pp, "noun: %qs", noun_str); + need_comma = true; + } + if (const char *property_str = maybe_get_property_str (m_property)) + { + if (need_comma) + pp_string (pp, ", "); + pp_printf (pp, "property: %qs", property_str); + need_comma = true; + } + pp_character (pp, '}'); +} + +/* Get a string (or nullptr) for V suitable for use within a SARIF + threadFlowLocation "kinds" property (SARIF v2.1.0 section 3.38.8). */ + +const char * +event::meaning::maybe_get_verb_str (enum verb v) +{ + switch (v) + { + default: + gcc_unreachable (); + case verb::unknown: + return nullptr; + case verb::acquire: + return "acquire"; + case verb::release: + return "release"; + case verb::enter: + return "enter"; + case verb::exit: + return "exit"; + case verb::call: + return "call"; + case verb::return_: + return "return"; + case verb::branch: + return "branch"; + case verb::danger: + return "danger"; + } +} + +/* Get a string (or nullptr) for N suitable for use within a SARIF + threadFlowLocation "kinds" property (SARIF v2.1.0 section 3.38.8). */ + +const char * +event::meaning::maybe_get_noun_str (enum noun n) +{ + switch (n) + { + default: + gcc_unreachable (); + case noun::unknown: + return nullptr; + case noun::taint: + return "taint"; + case noun::sensitive: + return "sensitive"; + case noun::function: + return "function"; + case noun::lock: + return "lock"; + case noun::memory: + return "memory"; + case noun::resource: + return "resource"; + } +} + +/* Get a string (or nullptr) for P suitable for use within a SARIF + threadFlowLocation "kinds" property (SARIF v2.1.0 section 3.38.8). */ + +const char * +event::meaning::maybe_get_property_str (enum property p) +{ + switch (p) + { + default: + gcc_unreachable (); + case property::unknown: + return nullptr; + case property::true_: + return "true"; + case property::false_: + return "false"; + } +} + +/* Generate a label_text containing the description of this event + (for debugging/logging purposes). */ + +label_text +event::get_desc (pretty_printer &ref_pp) const +{ + auto pp = ref_pp.clone (); + pp_show_color (pp.get ()) = false; + print_desc (*pp.get ()); + return label_text::take (xstrdup (pp_formatted_text (pp.get ()))); +} + +// Base implementation of event::maybe_make_diagnostic_state_graph + +std::unique_ptr<digraphs::digraph> +event::maybe_make_diagnostic_state_graph (bool) const +{ + // Don't attempt to make a state graph: + return nullptr; +} + +/* class diagnostics::paths::path. */ + +/* Subroutine of path::interprocedural_p. + Look for the first event in this path that is within a function + i.e. has a non-null logical location for which function_p is true. + If found, write its index to *OUT_IDX and return true. + Otherwise return false. */ + +bool +path::get_first_event_in_a_function (unsigned *out_idx) const +{ + const unsigned num = num_events (); + for (unsigned i = 0; i < num; i++) + { + const event &event = get_event (i); + if (logical_locations::key logical_loc = event.get_logical_location ()) + if (m_logical_loc_mgr.function_p (logical_loc)) + { + *out_idx = i; + return true; + } + } + return false; +} + +/* Return true if the events in this path involve more than one + function, or false if it is purely intraprocedural. */ + +bool +path::interprocedural_p () const +{ + /* Ignore leading events that are outside of any function. */ + unsigned first_fn_event_idx; + if (!get_first_event_in_a_function (&first_fn_event_idx)) + return false; + + const event &first_fn_event = get_event (first_fn_event_idx); + int first_fn_stack_depth = first_fn_event.get_stack_depth (); + + const unsigned num = num_events (); + for (unsigned i = first_fn_event_idx + 1; i < num; i++) + { + if (!same_function_p (first_fn_event_idx, i)) + return true; + if (get_event (i).get_stack_depth () != first_fn_stack_depth) + return true; + } + return false; +} + +/* Print PATH by emitting a dummy "note" associated with it. */ + +DEBUG_FUNCTION +void debug (path *p) +{ + rich_location richloc (line_table, UNKNOWN_LOCATION); + richloc.set_path (p); + inform (&richloc, "debug path"); +} + +#if __GNUC__ >= 10 +# pragma GCC diagnostic pop +#endif diff --git a/gcc/diagnostics/paths.h b/gcc/diagnostics/paths.h new file mode 100644 index 0000000..d30c420 --- /dev/null +++ b/gcc/diagnostics/paths.h @@ -0,0 +1,250 @@ +/* Paths through the code associated with a diagnostic. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_PATHS_H +#define GCC_DIAGNOSTICS_PATHS_H + +#include "diagnostic.h" /* for ATTRIBUTE_GCC_DIAG. */ +#include "diagnostics/event-id.h" +#include "diagnostics/logical-locations.h" + +namespace diagnostics { + namespace digraphs { + class digraph; + } // namespace digraphs + namespace logical_locations { + class manager; + } // logical_locations + class sarif_builder; + class sarif_object; +} //namespace diagnostics + +namespace diagnostics { +namespace paths { + +/* A diagnostics::paths::path is an optional additional piece of metadata associated + with a diagnostic (via its rich_location). + + It describes a sequence of events predicted by the compiler that + lead to the problem occurring, with their locations in the user's source, + and text descriptions. + + For example, the following error has a 3-event path: + + test.c: In function 'demo': + test.c:29:5: error: passing NULL as argument 1 to 'PyList_Append' which + requires a non-NULL parameter + 29 | PyList_Append(list, item); + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 'demo': events 1-3 + 25 | list = PyList_New(0); + | ^~~~~~~~~~~~~ + | | + | (1) when 'PyList_New' fails, returning NULL + 26 | + 27 | for (i = 0; i < count; i++) { + | ~~~ + | | + | (2) when 'i < count' + 28 | item = PyLong_FromLong(random()); + 29 | PyList_Append(list, item); + | ~~~~~~~~~~~~~~~~~~~~~~~~~ + | | + | (3) when calling 'PyList_Append', passing NULL from (1) as argument 1 + + The diagnostic-printing code has consolidated the path into a single + run of events, since all the events are near each other and within the same + function; more complicated examples (such as interprocedural paths) + might be printed as multiple runs of events. */ + +/* Abstract base classes, describing events within a path, and the paths + themselves. */ + +/* One event within a path. */ + +class event +{ + public: + /* Enums for giving a sense of what this event means. + Roughly corresponds to SARIF v2.1.0 section 3.38.8. */ + enum class verb + { + unknown, + + acquire, + release, + enter, + exit, + call, + return_, + branch, + + danger + }; + enum class noun + { + unknown, + + taint, + sensitive, // this one isn't in SARIF v2.1.0; filed as https://github.com/oasis-tcs/sarif-spec/issues/530 + function, + lock, + memory, + resource + }; + enum class property + { + unknown, + + true_, + false_ + }; + /* A bundle of such enums, allowing for descriptions of the meaning of + an event, such as + - "acquire memory": meaning (verb::acquire, noun::memory) + - "take true branch"": meaning (verb::branch, property::true) + - "return from function": meaning (verb::return, noun::function) + etc, as per SARIF's threadFlowLocation "kinds" property + (SARIF v2.1.0 section 3.38.8). */ + struct meaning + { + meaning () + : m_verb (verb::unknown), + m_noun (noun::unknown), + m_property (property::unknown) + { + } + meaning (enum verb verb, enum noun noun) + : m_verb (verb), m_noun (noun), m_property (property::unknown) + { + } + meaning (enum verb verb, enum property property) + : m_verb (verb), m_noun (noun::unknown), m_property (property) + { + } + + void dump_to_pp (pretty_printer *pp) const; + + static const char *maybe_get_verb_str (enum verb); + static const char *maybe_get_noun_str (enum noun); + static const char *maybe_get_property_str (enum property); + + enum verb m_verb; + enum noun m_noun; + enum property m_property; + }; + + virtual ~event () {} + + virtual location_t get_location () const = 0; + + /* Stack depth, so that consumers can visualize the interprocedural + calls, returns, and frame nesting. */ + virtual int get_stack_depth () const = 0; + + /* Print a localized (and possibly colorized) description of this event. */ + virtual void print_desc (pretty_printer &pp) const = 0; + + /* Get a logical location for this event, or null if there is none. */ + virtual logical_locations::key get_logical_location () const = 0; + + virtual meaning get_meaning () const = 0; + + /* True iff we should draw a line connecting this event to the + next event (e.g. to highlight control flow). */ + virtual bool connect_to_next_event_p () const = 0; + + virtual thread_id_t get_thread_id () const = 0; + + /* Hook for SARIF output to allow for adding diagnostic-specific + properties to the threadFlowLocation object's property bag. */ + virtual void + maybe_add_sarif_properties (sarif_builder &, + sarif_object &/*thread_flow_loc_obj*/) const + { + } + + /* Hook for capturing state at this event, potentially for visualizing + in HTML output, or for adding to SARIF. */ + virtual std::unique_ptr<digraphs::digraph> + maybe_make_diagnostic_state_graph (bool debug) const; + + label_text get_desc (pretty_printer &ref_pp) const; +}; + +/* Abstract base class representing a thread of execution within + a diagnostics::paths::path. + Each event is associated with one thread. + Typically there is just one thread per diagnostics::paths::path. */ + +class thread +{ +public: + virtual ~thread () {} + virtual label_text get_name (bool can_colorize) const = 0; +}; + +/* Abstract base class for getting at a sequence of events. */ + +class path +{ + public: + virtual ~path () {} + virtual unsigned num_events () const = 0; + virtual const event & get_event (int idx) const = 0; + virtual unsigned num_threads () const = 0; + virtual const thread & + get_thread (thread_id_t) const = 0; + + /* Return true iff the two events are both within the same function, + or both outside of any function. */ + virtual bool + same_function_p (int event_idx_a, + int event_idx_b) const = 0; + + bool interprocedural_p () const; + bool multithreaded_p () const; + + const logical_locations::manager &get_logical_location_manager () const + { + return m_logical_loc_mgr; + } + +protected: + path (const logical_locations::manager &logical_loc_mgr) + : m_logical_loc_mgr (logical_loc_mgr) + { + } + +private: + bool get_first_event_in_a_function (unsigned *out_idx) const; + + const logical_locations::manager &m_logical_loc_mgr; +}; + +} // namespace paths +} // namespace diagnostics + +/* Concrete subclasses of the above can be found in + simple-diagnostic-path.h. */ + +extern void debug (diagnostics::paths::path *path); + +#endif /* ! GCC_DIAGNOSTICS_PATHS_H */ diff --git a/gcc/diagnostics/sarif-sink.cc b/gcc/diagnostics/sarif-sink.cc new file mode 100644 index 0000000..4738ae9 --- /dev/null +++ b/gcc/diagnostics/sarif-sink.cc @@ -0,0 +1,5078 @@ +/* SARIF output for diagnostics + Copyright (C) 2018-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + + +#include "config.h" +#define INCLUDE_LIST +#define INCLUDE_MAP +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostics/metadata.h" +#include "diagnostics/digraphs.h" +#include "diagnostics/state-graphs.h" +#include "diagnostics/paths.h" +#include "diagnostics/sink.h" +#include "diagnostics/buffering.h" +#include "json.h" +#include "cpplib.h" +#include "diagnostics/logical-locations.h" +#include "diagnostics/client-data-hooks.h" +#include "diagnostics/diagram.h" +#include "text-art/canvas.h" +#include "diagnostics/sarif-sink.h" +#include "diagnostics/text-sink.h" +#include "ordered-hash-map.h" +#include "sbitmap.h" +#include "selftest.h" +#include "diagnostics/selftest-context.h" +#include "diagnostics/selftest-source-printing.h" +#include "selftest-json.h" +#include "text-range-label.h" +#include "pretty-print-format-impl.h" +#include "pretty-print-urlifier.h" +#include "demangle.h" +#include "backtrace.h" +#include "xml.h" + +namespace diagnostics { + +/* A json::array where the values are "unique" as per + SARIF v2.1.0 section 3.7.3 ("Array properties with unique values"). */ + +template <typename JsonElementType> +class sarif_array_of_unique : public json::array +{ + public: + size_t append_uniquely (std::unique_ptr<JsonElementType> val) + { + /* This should be O(log(n)) due to the std::map. */ + auto search = m_index_by_value.find (val.get ()); + if (search != m_index_by_value.end()) + return (*search).second; + + const size_t insertion_idx = size (); + m_index_by_value.insert ({val.get (), insertion_idx}); + append (std::move (val)); + return insertion_idx; + } + + /* For ease of reading output, add "index": idx to all + objects in the array. + We don't do this until we've added everything, since + the "index" property would otherwise confuse the + comparison against new elements. */ + void add_explicit_index_values () + { + for (size_t idx = 0; idx < length (); ++idx) + if (json::object *obj = get (idx)->dyn_cast_object ()) + obj->set_integer ("index", idx); + } + +private: + struct comparator_t { + bool operator () (const json::value *a, const json::value *b) const + { + gcc_assert (a); + gcc_assert (b); + return json::value::compare (*a, *b) < 0; + } + }; + + // json::value * here is borrowed from m_elements + std::map<json::value *, int, comparator_t> m_index_by_value; +}; + +/* Forward decls. */ +class sarif_builder; +class content_renderer; + class escape_nonascii_renderer; + +/* Subclasses of sarif_object. + Keep these in order of their descriptions in the specification. */ +class sarif_artifact_content; // 3.3 +class sarif_artifact_location; // 3.4 +class sarif_message; // 3.11 +class sarif_multiformat_message_string; // 3.12 +class sarif_log; // 3.13 +class sarif_run; // 3.14 +class sarif_tool; // 3.18 +class sarif_tool_component; // 3.19 +class sarif_invocation; // 3.20 +class sarif_artifact; // 3.24 +class sarif_location_manager; // not in the spec +class sarif_result; // 3.27 +class sarif_location; // 3.28 +class sarif_physical_location; // 3.29 +class sarif_region; // 3.30 +class sarif_logical_location; // 3.33 +class sarif_location_relationship; // 3.34 +class sarif_code_flow; // 3.36 +class sarif_thread_flow; // 3.37 +class sarif_thread_flow_location; // 3.38 +class sarif_reporting_descriptor; // 3.49 +class sarif_reporting_descriptor_reference; // 3.53 +class sarif_tool_component_reference; // 3.54 +class sarif_fix; // 3.55 +class sarif_artifact_change; // 3.56 +class sarif_replacement; // 3.57 +class sarif_ice_notification; // 3.58 + +// Valid values for locationRelationship's "kinds" property (3.34.3) + +enum class location_relationship_kind +{ + includes, + is_included_by, + relevant, + + NUM_KINDS +}; + +/* Declarations of subclasses of sarif_object. + Keep these in order of their descriptions in the specification. */ + +/* Subclass of sarif_object for SARIF "artifactContent" objects + (SARIF v2.1.0 section 3.3). */ + +class sarif_artifact_content : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "artifactLocation" objects + (SARIF v2.1.0 section 3.4). */ + +class sarif_artifact_location : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "message" objects + (SARIF v2.1.0 section 3.11). */ + +class sarif_message : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "multiformatMessageString" objects + (SARIF v2.1.0 section 3.12). */ + +class sarif_multiformat_message_string : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "log" objects + (SARIF v2.1.0 section 3.13). */ + +class sarif_log : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "run" objects + (SARIF v2.1.0 section 3.14). */ + +class sarif_run : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "tool" objects + (SARIF v2.1.0 section 3.18). */ + +class sarif_tool : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "toolComponent" objects + (SARIF v2.1.0 section 3.19). */ + +class sarif_tool_component : public sarif_object {}; + +/* Make a JSON string for the current date and time. + See SARIF v2.1.0 section 3.9 "Date/time properties". + Given that we don't run at the very beginning/end of the + process, it doesn't make sense to be more accurate than + the current second. */ + +static std::unique_ptr<json::string> +make_date_time_string_for_current_time () +{ + time_t t = time (nullptr); + struct tm *tm = gmtime (&t); + char buf[256]; + snprintf (buf, sizeof (buf) - 1, + ("%04i-%02i-%02iT" + "%02i:%02i:%02iZ"), + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + return std::make_unique<json::string> (buf); +} + +/* Subclass of sarif_object for SARIF "invocation" objects + (SARIF v2.1.0 section 3.20). */ + +class sarif_invocation : public sarif_object +{ +public: + sarif_invocation (sarif_builder &builder, + const char * const *original_argv); + + void add_notification_for_ice (const diagnostic_info &diagnostic, + sarif_builder &builder, + std::unique_ptr<json::object> backtrace); + void prepare_to_flush (sarif_builder &builder); + +private: + std::unique_ptr<json::array> m_notifications_arr; + bool m_success; +}; + +/* Corresponds to values for the SARIF artifact objects "roles" property. + (SARIF v2.1.0 section 3.24.6). */ + +enum class diagnostic_artifact_role +{ + analysis_target, /* "analysisTarget". */ + debug_output_file, /* "debugOutputFile". */ + result_file, /* "resultFile". */ + + /* "scannedFile" added in 2.2; + see https://github.com/oasis-tcs/sarif-spec/issues/459 */ + scanned_file, + + traced_file, /* "tracedFile". */ + + NUM_ROLES +}; + +/* Subclass of sarif_object for SARIF artifact objects + (SARIF v2.1.0 section 3.24). */ + +class sarif_artifact : public sarif_object +{ +public: + sarif_artifact (const char *filename) + : m_filename (filename), + m_roles ((unsigned)diagnostic_artifact_role::NUM_ROLES), + m_embed_contents (false) + { + bitmap_clear (m_roles); + } + + void add_role (enum diagnostic_artifact_role role, + bool embed_contents); + + bool embed_contents_p () const { return m_embed_contents; } + void populate_contents (sarif_builder &builder); + void populate_roles (); + +private: + const char *m_filename; + auto_sbitmap m_roles; + + /* Flag to track whether this artifact should have a "contents" property + (SARIF v2.1.0 section 3.24.8). + We only add the contents for those artifacts that have a location + referencing them (so that a consumer might want to quote the source). */ + bool m_embed_contents; +}; + +/* A class for sarif_objects that own a "namespace" of numeric IDs for + managing location objects within them. Currently (SARIF v2.1.0) + this is just for sarif_result (section 3.28.2), but it will likely + eventually also be for notification objects; see + https://github.com/oasis-tcs/sarif-spec/issues/540 + + Consider locations with chains of include information e.g. + + > include-chain-1.c: + > #include "include-chain-1.h" + + include-chain-1.h: + | // First set of decls, which will be referenced in notes + | #include "include-chain-1-1.h" + | + | // Second set of decls, which will trigger the errors + | #include "include-chain-1-2.h" + + include-chain-1-1.h: + | int p; + | int q; + + include-chain-1-1.h: + | char p; + | char q; + + GCC's textual output emits: + | In file included from PATH/include-chain-1.h:5, + | from PATH/include-chain-1.c:30: + | PATH/include-chain-1-2.h:1:6: error: conflicting types for 'p'; have 'char' + | 1 | char p; + | | ^ + | In file included from PATH/include-chain-1.h:2: + | PATH/include-chain-1-1.h:1:5: note: previous declaration of 'p' with type 'int' + | 1 | int p; + | | ^ + | PATH/include-chain-1-2.h:2:6: error: conflicting types for 'q'; have 'char' + | 2 | char q; + | | ^ + | PATH/include-chain-1-1.h:2:5: note: previous declaration of 'q' with type 'int' + | 2 | int q; + | | ^ + + Whenever a SARIF location is added for a location_t that + was #included from somewhere, we queue up the creation of a SARIF + location for the location of the #include. The worklist of queued + locations is flushed when the result is finished, which lazily creates + any additional related locations for the include chain, and the + relationships between the locations. Doing so can lead to further + include locations being processed. The worklist approach allows us + to lazily explore the relevant part of the directed graph of location_t + values implicit in our line_maps structure, replicating it as a directed + graph of SARIF locations within the SARIF result object, like this: + + [0]: error in include-chain-1-2.h ("conflicting types for 'p'; have 'char'") + [1]: #include "include-chain-1-2.h" in include-chain-1.h + [2]: note in include-chain-1-2.h ("previous declaration of 'p' with type 'int'") + [3]: #include "include-chain-1-1.h" in include-chain-1.h + [4]: #include "include-chain-1.h" in include-chain-1.c + + where we want to capture this "includes" graph in SARIF form: + . +-----------------------------------+ +----------------------------------+ + . |"id": 0 | |"id": 2 | + . | error: "conflicting types for 'p';| | note: previous declaration of 'p'| + . | have 'char'"| | | with type 'int'") | + . | in include-chain-1-2.h | | in include-chain-1-1.h | + . +-----------------------------------+ +----------------------------------+ + . ^ | ^ | + . includes | | included-by includes | | included-by + . | V | V + . +--------------------------------+ +--------------------------------+ + . |"id": 1 | |"id": 3 | + . | #include "include-chain-1-2.h" | | #include "include-chain-1-1.h" | + . | in include-chain-1.h | | in include-chain-1.h | + . +--------------------------------+ +--------------------------------+ + . ^ | ^ | + . includes | | included-by includes | | included-by + . | V | V + . +------------------------------------+ + . |"id": 4 | + . | The #include "include-chain-1.h" | + . | in include-chain-1.c | + . +------------------------------------+ + */ + +class sarif_location_manager : public sarif_object +{ +public: + /* A worklist of pending actions needed to fully process this object. + + This lets us lazily walk our data structures to build the + directed graph of locations, whilst keeping "notes" at the top + of the "relatedLocations" array, and avoiding the need for + recursion. */ + struct worklist_item + { + enum class kind + { + /* Process a #include relationship where m_location_obj + was #included-d at m_where. */ + included_from, + + /* Process a location_t that was added as a secondary location + to a rich_location without a label. */ + unlabelled_secondary_location + }; + + worklist_item (sarif_location &location_obj, + enum kind kind, + location_t where) + : m_location_obj (location_obj), + m_kind (kind), + m_where (where) + { + } + + sarif_location &m_location_obj; + enum kind m_kind; + location_t m_where; + }; + + sarif_location_manager () + : m_related_locations_arr (nullptr), + m_next_location_id (0) + { + } + + unsigned allocate_location_id () + { + return m_next_location_id++; + } + + virtual void + add_related_location (std::unique_ptr<sarif_location> location_obj, + sarif_builder &builder); + + void + add_relationship_to_worklist (sarif_location &location_obj, + enum worklist_item::kind kind, + location_t where); + + void + process_worklist (sarif_builder &builder); + + void + process_worklist_item (sarif_builder &builder, + const worklist_item &item); +private: + json::array *m_related_locations_arr; // borrowed + unsigned m_next_location_id; + + std::list<worklist_item> m_worklist; + std::map<location_t, sarif_location *> m_included_from_locations; + std::map<location_t, sarif_location *> m_unlabelled_secondary_locations; +}; + +/* Subclass of sarif_object for SARIF "result" objects + (SARIF v2.1.0 section 3.27). + Each SARIF result object has its own "namespace" of numeric IDs for + managing location objects (SARIF v2.1.0 section 3.28.2). */ + +class sarif_result : public sarif_location_manager +{ +public: + sarif_result (unsigned idx_within_parent) + : m_idx_within_parent (idx_within_parent) + {} + + unsigned get_index_within_parent () const { return m_idx_within_parent; } + + void + on_nested_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + sarif_builder &builder); + void on_diagram (const diagram &d, + sarif_builder &builder); + +private: + const unsigned m_idx_within_parent; +}; + +/* Subclass of sarif_object for SARIF "location" objects + (SARIF v2.1.0 section 3.28). + A location object can have an "id" which must be unique within + the enclosing result, if any (see SARIF v2.1.0 section 3.28.2). */ + +class sarif_location : public sarif_object +{ +public: + long lazily_add_id (sarif_location_manager &loc_mgr); + long get_id () const; + + void lazily_add_relationship (sarif_location &target, + enum location_relationship_kind kind, + sarif_location_manager &loc_mgr); + +private: + sarif_location_relationship & + lazily_add_relationship_object (sarif_location &target, + sarif_location_manager &loc_mgr); + + json::array &lazily_add_relationships_array (); + + std::map<sarif_location *, + sarif_location_relationship *> m_relationships_map; +}; + +/* Subclass of sarif_object for SARIF "physicalLocation" objects + (SARIF v2.1.0 section 3.29). */ + +class sarif_physical_location : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "region" objects + (SARIF v2.1.0 section 3.30). */ + +class sarif_region : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "logicalLocation" objects + (SARIF v2.1.0 section 3.33). */ + +class sarif_logical_location : public sarif_object +{ +}; + +/* Subclass of sarif_object for SARIF "locationRelationship" objects + (SARIF v2.1.0 section 3.34). */ + +class sarif_location_relationship : public sarif_object +{ +public: + sarif_location_relationship (sarif_location &target, + sarif_location_manager &loc_mgr); + + long get_target_id () const; + + void lazily_add_kind (enum location_relationship_kind kind); + +private: + auto_sbitmap m_kinds; +}; + +/* Subclass of sarif_object for SARIF "codeFlow" objects + (SARIF v2.1.0 section 3.36). */ + +class sarif_code_flow : public sarif_object +{ +public: + sarif_code_flow (sarif_result &parent, + unsigned idx_within_parent); + + sarif_result &get_parent () const { return m_parent; } + unsigned get_index_within_parent () const { return m_idx_within_parent; } + + sarif_thread_flow & + get_or_append_thread_flow (const paths::thread &thread, + paths::thread_id_t thread_id); + + sarif_thread_flow & + get_thread_flow (paths::thread_id_t thread_id); + + void add_location (sarif_thread_flow_location &); + + sarif_thread_flow_location & + get_thread_flow_loc_obj (paths::event_id_t event_id) const; + +private: + sarif_result &m_parent; + const unsigned m_idx_within_parent; + + hash_map<int_hash<paths::thread_id_t, -1, -2>, + sarif_thread_flow *> m_thread_id_map; // borrowed ptr + json::array *m_thread_flows_arr; // borrowed + + /* Vec of borrowed ptr, allowing for going easily from + an event_id to the corresponding threadFlowLocation object. */ + std::vector<sarif_thread_flow_location *> m_all_tfl_objs; +}; + +/* Subclass of sarif_object for SARIF "threadFlow" objects + (SARIF v2.1.0 section 3.37). */ + +class sarif_thread_flow : public sarif_object +{ +public: + sarif_thread_flow (sarif_code_flow &parent, + const paths::thread &thread, + unsigned idx_within_parent); + + sarif_code_flow &get_parent () const { return m_parent; } + unsigned get_index_within_parent () const { return m_idx_within_parent; } + + sarif_thread_flow_location &add_location (); + +private: + sarif_code_flow &m_parent; + json::array *m_locations_arr; // borrowed + const unsigned m_idx_within_parent; +}; + +/* Subclass of sarif_object for SARIF "threadFlowLocation" objects + (SARIF v2.1.0 section 3.38). */ + +class sarif_thread_flow_location : public sarif_object +{ +public: + sarif_thread_flow_location (sarif_thread_flow &parent, + unsigned idx_within_parent) + : m_parent (parent), + m_idx_within_parent (idx_within_parent) + { + } + + sarif_thread_flow &get_parent () const { return m_parent; } + unsigned get_index_within_parent () const { return m_idx_within_parent; } + +private: + sarif_thread_flow &m_parent; + const unsigned m_idx_within_parent; +}; + +/* Subclass of sarif_object for SARIF "reportingDescriptor" objects + (SARIF v2.1.0 section 3.49). */ + +class sarif_reporting_descriptor : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "reportingDescriptorReference" objects + (SARIF v2.1.0 section 3.53). */ + +class sarif_reporting_descriptor_reference : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "toolComponentReference" objects + (SARIF v2.1.0 section 3.54). */ + +class sarif_tool_component_reference : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "fix" objects + (SARIF v2.1.0 section 3.55). */ + +class sarif_fix : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "artifactChange" objects + (SARIF v2.1.0 section 3.56). */ + +class sarif_artifact_change : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "replacement" objects + (SARIF v2.1.0 section 3.57). */ + +class sarif_replacement : public sarif_object {}; + +/* Subclass of sarif_object for SARIF "notification" objects + (SARIF v2.1.0 section 3.58). + + This subclass is specifically for notifying when an + internal compiler error occurs. */ + +class sarif_ice_notification : public sarif_location_manager +{ +public: + sarif_ice_notification (const diagnostic_info &diagnostic, + sarif_builder &builder, + std::unique_ptr<json::object> backtrace); + + void + add_related_location (std::unique_ptr<sarif_location> location_obj, + sarif_builder &builder) final override; +}; + +/* Abstract base class for use when making an "artifactContent" + object (SARIF v2.1.0 section 3.3): generate a value for the + 3.3.4 "rendered" property. + Can return nullptr, for "no property". */ + +class content_renderer +{ +public: + virtual ~content_renderer () {} + + virtual std::unique_ptr<sarif_multiformat_message_string> + render (const sarif_builder &builder) const = 0; +}; + +/* Concrete buffering implementation subclass for SARIF output. */ + +class sarif_sink_buffer : public per_sink_buffer +{ +public: + friend class sarif_sink; + + sarif_sink_buffer (sarif_builder &builder) + : m_builder (builder) + {} + + void dump (FILE *out, int indent) const final override; + bool empty_p () const final override; + void move_to (per_sink_buffer &dest) final override; + void clear () final override; + void flush () final override; + + void add_result (std::unique_ptr<sarif_result> result) + { + m_results.push_back (std::move (result)); + } + + size_t num_results () const { return m_results.size (); } + sarif_result &get_result (size_t idx) { return *m_results[idx]; } + +private: + sarif_builder &m_builder; + std::vector<std::unique_ptr<sarif_result>> m_results; +}; + +/* Classes for abstracting away JSON vs other serialization formats. */ + +// class sarif_serialization_format_json : public sarif_serialization_format + +void +sarif_serialization_format_json::write_to_file (FILE *outf, + const json::value &top) +{ + top.dump (outf, m_formatted); + fprintf (outf, "\n"); +} + +/* A class for managing SARIF output (for -fdiagnostics-format=sarif-stderr + and -fdiagnostics-format=sarif-file). + + As diagnostics occur, we build "result" JSON objects, and + accumulate state: + - which source files are referenced + - which warnings are emitted + - which CWEs are used + + At the end of the compile, we use the above to build the full SARIF + object tree, adding the result objects to the correct place, and + creating objects for the various source files, warnings and CWEs + referenced. + + Implemented: + - fix-it hints + - CWE metadata + - diagnostic groups (see limitations below) + - logical locations (e.g. cfun) + - labelled ranges (as annotations) + - secondary ranges without labels (as related locations) + + Known limitations: + - GCC supports nesting of diagnostics (one-deep nesting via + auto_diagnostic_group, and arbitrary nesting via + auto_diagnostic_nesting_level). These are captured in the SARIF + as related locations, and so we only capture location and message + information from such nested diagnostics (e.g. we ignore fix-it + hints on them). Diagnostics within an auto_diagnostic_nesting_level + have their nesting level captured as a property. + - although we capture command-line arguments (section 3.20.2), we don't + yet capture response files. + - doesn't capture "artifact.encoding" property + (SARIF v2.1.0 section 3.24.9). + - doesn't capture hashes of the source files + ("artifact.hashes" property (SARIF v2.1.0 section 3.24.11). + - doesn't capture the "analysisTarget" property + (SARIF v2.1.0 section 3.27.13). + - doesn't capture -Werror cleanly + - doesn't capture inlining information (can SARIF handle this?) + - doesn't capture macro expansion information (can SARIF handle this?). + - doesn't capture any diagnostics::metadata::rules associated with + a diagnostic. */ + +class sarif_builder +{ +public: + friend class sarif_sink_buffer; + + sarif_builder (diagnostics::context &dc, + pretty_printer &printer, + const line_maps *line_maps, + std::unique_ptr<sarif_serialization_format> serialization_format, + const sarif_generation_options &sarif_gen_opts); + ~sarif_builder (); + + void set_printer (pretty_printer &printer) + { + m_printer = &printer; + } + + const logical_locations::manager * + get_logical_location_manager () const + { + return m_logical_loc_mgr; + } + + void + set_main_input_filename (const char *name); + + void on_report_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + sarif_sink_buffer *buffer); + void emit_diagram (const diagram &d); + void end_group (); + + void + report_global_digraph (const lazily_created<digraphs::digraph> &); + + std::unique_ptr<sarif_result> take_current_result () + { + return std::move (m_cur_group_result); + } + + std::unique_ptr<sarif_log> flush_to_object (); + void flush_to_file (FILE *outf); + + std::unique_ptr<json::array> + make_locations_arr (sarif_location_manager &loc_mgr, + const diagnostic_info &diagnostic, + enum diagnostic_artifact_role role); + std::unique_ptr<sarif_location> + make_location_object (sarif_location_manager *loc_mgr, + const rich_location &rich_loc, + logical_locations::key logical_loc, + enum diagnostic_artifact_role role); + std::unique_ptr<sarif_location> + make_location_object (sarif_location_manager &loc_mgr, + location_t where, + enum diagnostic_artifact_role role); + std::unique_ptr<sarif_message> + make_message_object (const char *msg) const; + std::unique_ptr<sarif_message> + make_message_object_for_diagram (const diagram &d); + std::unique_ptr<sarif_artifact_content> + maybe_make_artifact_content_object (const char *filename) const; + + std::unique_ptr<sarif_artifact_location> + make_artifact_location_object (const char *filename); + + const sarif_code_flow * + get_code_flow_for_event_ids () const + { + return m_current_code_flow; + } + + diagnostics::context &get_context () const { return m_context; } + pretty_printer *get_printer () const { return m_printer; } + token_printer &get_token_printer () { return m_token_printer; } + enum sarif_version get_version () const { return m_sarif_gen_opts.m_version; } + + size_t num_results () const { return m_results_array->size (); } + sarif_result &get_result (size_t idx) + { + auto element = (*m_results_array)[idx]; + gcc_assert (element); + return *static_cast<sarif_result *> (element); + } + + const sarif_generation_options &get_opts () const { return m_sarif_gen_opts; } + + std::unique_ptr<sarif_logical_location> + make_minimal_sarif_logical_location (logical_locations::key); + +private: + class sarif_token_printer : public token_printer + { + public: + sarif_token_printer (sarif_builder &builder) + : m_builder (builder) + { + } + void print_tokens (pretty_printer *pp, + const pp_token_list &tokens) final override; + private: + sarif_builder &m_builder; + }; + + std::unique_ptr<sarif_result> + make_result_object (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + unsigned idx_within_parent); + void + add_any_include_chain (sarif_location_manager &loc_mgr, + sarif_location &location_obj, + location_t where); + void + set_any_logical_locs_arr (sarif_location &location_obj, + logical_locations::key logical_loc); + std::unique_ptr<sarif_location> + make_location_object (sarif_location_manager &loc_mgr, + const paths::event &event, + enum diagnostic_artifact_role role); + std::unique_ptr<sarif_code_flow> + make_code_flow_object (sarif_result &result, + unsigned idx_within_parent, + const paths::path &path); + void + populate_thread_flow_location_object (sarif_result &result, + sarif_thread_flow_location &thread_flow_loc_obj, + const paths::event &event, + int event_execution_idx); + std::unique_ptr<json::array> + maybe_make_kinds_array (paths::event::meaning m) const; + std::unique_ptr<sarif_physical_location> + maybe_make_physical_location_object (location_t loc, + enum diagnostic_artifact_role role, + int column_override, + const content_renderer *snippet_renderer); + std::unique_ptr<sarif_artifact_location> + make_artifact_location_object (location_t loc); + std::unique_ptr<sarif_artifact_location> + make_artifact_location_object_for_pwd () const; + std::unique_ptr<sarif_region> + maybe_make_region_object (location_t loc, + int column_override) const; + std::unique_ptr<sarif_region> + maybe_make_region_object_for_context (location_t loc, + const content_renderer *snippet_renderer) const; + std::unique_ptr<sarif_region> + make_region_object_for_hint (const fixit_hint &hint) const; + + int + ensure_sarif_logical_location_for (logical_locations::key k); + + std::unique_ptr<sarif_multiformat_message_string> + make_multiformat_message_string (const char *msg) const; + std::unique_ptr<sarif_log> + make_top_level_object (std::unique_ptr<sarif_invocation> invocation_obj, + std::unique_ptr<json::array> results); + std::unique_ptr<sarif_run> + make_run_object (std::unique_ptr<sarif_invocation> invocation_obj, + std::unique_ptr<json::array> results); + std::unique_ptr<sarif_tool> + make_tool_object (); + std::unique_ptr<sarif_tool_component> + make_driver_tool_component_object (); + std::unique_ptr<json::array> maybe_make_taxonomies_array () const; + std::unique_ptr<sarif_tool_component> + maybe_make_cwe_taxonomy_object () const; + std::unique_ptr<sarif_tool_component_reference> + make_tool_component_reference_object_for_cwe () const; + std::unique_ptr<sarif_reporting_descriptor> + make_reporting_descriptor_object_for_warning (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + const char *option_text); + std::unique_ptr<sarif_reporting_descriptor> + make_reporting_descriptor_object_for_cwe_id (int cwe_id) const; + std::unique_ptr<sarif_reporting_descriptor_reference> + make_reporting_descriptor_reference_object_for_cwe_id (int cwe_id); + sarif_artifact & + get_or_create_artifact (const char *filename, + enum diagnostic_artifact_role role, + bool embed_contents); + char * + get_source_lines (const char *filename, + int start_line, + int end_line) const; + std::unique_ptr<sarif_artifact_content> + maybe_make_artifact_content_object (const char *filename, + int start_line, + int end_line, + const content_renderer *r) const; + std::unique_ptr<sarif_fix> + make_fix_object (const rich_location &rich_loc); + std::unique_ptr<sarif_artifact_change> + make_artifact_change_object (const rich_location &richloc); + std::unique_ptr<sarif_replacement> + make_replacement_object (const fixit_hint &hint) const; + std::unique_ptr<sarif_artifact_content> + make_artifact_content_object (const char *text) const; + int get_sarif_column (expanded_location exploc) const; + + std::unique_ptr<json::object> + make_stack_from_backtrace (); + + diagnostics::context &m_context; + pretty_printer *m_printer; + const line_maps *m_line_maps; + sarif_token_printer m_token_printer; + + const logical_locations::manager *m_logical_loc_mgr; + + /* The JSON object for the invocation object. */ + std::unique_ptr<sarif_invocation> m_invocation_obj; + + /* The JSON array of pending diagnostics. */ + std::unique_ptr<json::array> m_results_array; + + /* The JSON object for the result object (if any) in the current + diagnostic group. */ + std::unique_ptr<sarif_result> m_cur_group_result; + + /* Ideally we'd use std::unique_ptr<sarif_artifact> here, but I had + trouble getting this to work when building with GCC 4.8. */ + ordered_hash_map <nofree_string_hash, + sarif_artifact *> m_filename_to_artifact_map; + + bool m_seen_any_relative_paths; + hash_set <free_string_hash> m_rule_id_set; + std::unique_ptr<json::array> m_rules_arr; + + /* The set of all CWE IDs we've seen, if any. */ + hash_set <int_hash <int, 0, 1> > m_cwe_id_set; + + std::unique_ptr<sarif_array_of_unique<sarif_logical_location>> m_cached_logical_locs; + + std::unique_ptr<sarif_array_of_unique<sarif_graph>> m_run_graphs; + + int m_tabstop; + + std::unique_ptr<sarif_serialization_format> m_serialization_format; + const sarif_generation_options m_sarif_gen_opts; + + unsigned m_next_result_idx; + sarif_code_flow *m_current_code_flow; +}; + +/* class sarif_object : public json::object. */ + +sarif_property_bag & +sarif_object::get_or_create_properties () +{ + json::value *properties_val = get ("properties"); + if (properties_val) + { + if (properties_val->get_kind () == json::JSON_OBJECT) + return *static_cast <sarif_property_bag *> (properties_val); + } + + sarif_property_bag *bag = new sarif_property_bag (); + set ("properties", bag); + return *bag; +} + +/* class sarif_invocation : public sarif_object. */ + +sarif_invocation::sarif_invocation (sarif_builder &builder, + const char * const *original_argv) +: m_notifications_arr (std::make_unique<json::array> ()), + m_success (true) +{ + // "arguments" property (SARIF v2.1.0 section 3.20.2) + if (original_argv) + { + auto arguments_arr = std::make_unique<json::array> (); + for (size_t i = 0; original_argv[i]; ++i) + arguments_arr->append_string (original_argv[i]); + set<json::array> ("arguments", std::move (arguments_arr)); + } + + // "workingDirectory" property (SARIF v2.1.0 section 3.20.19) + if (const char *pwd = getpwd ()) + set<sarif_artifact_location> ("workingDirectory", + builder.make_artifact_location_object (pwd)); + + // "startTimeUtc" property (SARIF v2.1.0 section 3.20.7) + set<json::string> ("startTimeUtc", + make_date_time_string_for_current_time ()); +} + +/* Handle an internal compiler error DIAGNOSTIC. + Add an object representing the ICE to the notifications array. */ + +void +sarif_invocation::add_notification_for_ice (const diagnostic_info &diagnostic, + sarif_builder &builder, + std::unique_ptr<json::object> backtrace) +{ + m_success = false; + + auto notification + = std::make_unique<sarif_ice_notification> (diagnostic, + builder, + std::move (backtrace)); + + /* Support for related locations within a notification was added + in SARIF 2.2; see https://github.com/oasis-tcs/sarif-spec/issues/540 */ + if (builder.get_version () >= sarif_version::v2_2_prerelease_2024_08_08) + notification->process_worklist (builder); + + m_notifications_arr->append<sarif_ice_notification> + (std::move (notification)); +} + +void +sarif_invocation::prepare_to_flush (sarif_builder &builder) +{ + const context &dc = builder.get_context (); + + /* "executionSuccessful" property (SARIF v2.1.0 section 3.20.14). */ + if (dc.execution_failed_p ()) + m_success = false; + set_bool ("executionSuccessful", m_success); + + /* "toolExecutionNotifications" property (SARIF v2.1.0 section 3.20.21). */ + set ("toolExecutionNotifications", std::move (m_notifications_arr)); + + /* Call client hook, allowing it to create a custom property bag for + this object (SARIF v2.1.0 section 3.8) e.g. for recording time vars. */ + if (auto client_data_hooks = dc.get_client_data_hooks ()) + client_data_hooks->add_sarif_invocation_properties (*this); + + // "endTimeUtc" property (SARIF v2.1.0 section 3.20.8); + set<json::string> ("endTimeUtc", + make_date_time_string_for_current_time ()); +} + +/* class sarif_artifact : public sarif_object. */ + +/* Add ROLE to this artifact's roles. + If EMBED_CONTENTS is true, then flag that we will attempt to embed the + contents of this artifact when writing it out. */ + +void +sarif_artifact::add_role (enum diagnostic_artifact_role role, + bool embed_contents) +{ + /* TODO(SARIF 2.2): "scannedFile" is to be added as a role in SARIF 2.2; + see https://github.com/oasis-tcs/sarif-spec/issues/459 + + For now, skip them. + Ultimately, we probably shouldn't bother embedding the contents + of such artifacts, just the snippets. */ + if (role == diagnostic_artifact_role::scanned_file) + return; + + if (embed_contents) + m_embed_contents = true; + + /* In SARIF v2.1.0 section 3.24.6 "roles" property: + "resultFile" is for an artifact + "which the analysis tool was not explicitly instructed to scan", + whereas "analysisTarget" is for one where the + "analysis tool was instructed to scan this artifact". + Hence the latter excludes the former. */ + if (role == diagnostic_artifact_role::result_file) + if (bitmap_bit_p (m_roles, (int)diagnostic_artifact_role::analysis_target)) + return; + + bitmap_set_bit (m_roles, (int)role); +} + +/* Populate the "contents" property (SARIF v2.1.0 section 3.24.8). + We do this after initialization to + (a) ensure that any charset options have been set + (b) only populate it for artifacts that are referenced by a location. */ + +void +sarif_artifact::populate_contents (sarif_builder &builder) +{ + if (auto artifact_content_obj + = builder.maybe_make_artifact_content_object (m_filename)) + set<sarif_artifact_content> ("contents", std::move (artifact_content_obj)); +} + +/* Get a string for ROLE corresponding to the + SARIF v2.1.0 section 3.24.6 "roles" property. */ + +static const char * +get_artifact_role_string (enum diagnostic_artifact_role role) +{ + switch (role) + { + default: + gcc_unreachable (); + case diagnostic_artifact_role::analysis_target: + return "analysisTarget"; + case diagnostic_artifact_role::debug_output_file: + return "debugOutputFile"; + case diagnostic_artifact_role::result_file: + return "resultFile"; + case diagnostic_artifact_role::scanned_file: + return "scannedFile"; + case diagnostic_artifact_role::traced_file: + return "tracedFile"; + } +} + +/* Populate the "roles" property of this sarif_artifact with a new + json::array for the artifact.roles property (SARIF v2.1.0 section 3.24.6) + containing strings such as "analysisTarget", "resultFile" + and/or "tracedFile". */ + +void +sarif_artifact::populate_roles () +{ + if (bitmap_empty_p (m_roles)) + return; + auto roles_arr (std::make_unique<json::array> ()); + for (int i = 0; i < (int)diagnostic_artifact_role::NUM_ROLES; i++) + if (bitmap_bit_p (m_roles, i)) + { + enum diagnostic_artifact_role role = (enum diagnostic_artifact_role)i; + roles_arr->append_string (get_artifact_role_string (role)); + } + set<json::array> ("roles", std::move (roles_arr)); +} + +/* class sarif_location_manager : public sarif_object. */ + +/* Base implementation of sarif_location_manager::add_related_location vfunc. + + Add LOCATION_OBJ to this object's "relatedLocations" array, + creating it if it doesn't yet exist. */ + +void +sarif_location_manager:: +add_related_location (std::unique_ptr<sarif_location> location_obj, + sarif_builder &) +{ + if (!m_related_locations_arr) + { + m_related_locations_arr = new json::array (); + /* Give ownership of m_related_locations_arr to json::object; + keep a borrowed ptr. */ + set ("relatedLocations", m_related_locations_arr); + } + m_related_locations_arr->append (std::move (location_obj)); +} + +void +sarif_location_manager:: +add_relationship_to_worklist (sarif_location &location_obj, + enum worklist_item::kind kind, + location_t where) +{ + m_worklist.push_back (worklist_item (location_obj, + kind, + where)); +} + +/* Process all items in this result's worklist. + Doing so may temporarily add new items to the end + of the worklist. + Handling any item should be "lazy", and thus we should + eventually drain the queue and terminate. */ + +void +sarif_location_manager::process_worklist (sarif_builder &builder) +{ + while (!m_worklist.empty ()) + { + const worklist_item &item = m_worklist.front (); + process_worklist_item (builder, item); + m_worklist.pop_front (); + } +} + +/* Process one item in this result's worklist, potentially + adding new items to the end of the worklist. */ + +void +sarif_location_manager::process_worklist_item (sarif_builder &builder, + const worklist_item &item) +{ + switch (item.m_kind) + { + default: + gcc_unreachable (); + case worklist_item::kind::included_from: + { + sarif_location &included_loc_obj = item.m_location_obj; + sarif_location *includer_loc_obj = nullptr; + auto iter = m_included_from_locations.find (item.m_where); + if (iter != m_included_from_locations.end ()) + includer_loc_obj = iter->second; + else + { + std::unique_ptr<sarif_location> new_loc_obj + = builder.make_location_object + (*this, + item.m_where, + diagnostic_artifact_role::scanned_file); + includer_loc_obj = new_loc_obj.get (); + add_related_location (std::move (new_loc_obj), builder); + auto kv + = std::pair<location_t, sarif_location *> (item.m_where, + includer_loc_obj); + m_included_from_locations.insert (kv); + } + + includer_loc_obj->lazily_add_relationship + (included_loc_obj, + location_relationship_kind::includes, + *this); + included_loc_obj.lazily_add_relationship + (*includer_loc_obj, + location_relationship_kind::is_included_by, + *this); + } + break; + case worklist_item::kind::unlabelled_secondary_location: + { + sarif_location &primary_loc_obj = item.m_location_obj; + sarif_location *secondary_loc_obj = nullptr; + auto iter = m_unlabelled_secondary_locations.find (item.m_where); + if (iter != m_unlabelled_secondary_locations.end ()) + secondary_loc_obj = iter->second; + else + { + std::unique_ptr<sarif_location> new_loc_obj + = builder.make_location_object + (*this, + item.m_where, + diagnostic_artifact_role::scanned_file); + secondary_loc_obj = new_loc_obj.get (); + add_related_location (std::move (new_loc_obj), builder); + auto kv + = std::pair<location_t, sarif_location *> (item.m_where, + secondary_loc_obj); + m_unlabelled_secondary_locations.insert (kv); + } + gcc_assert (secondary_loc_obj); + primary_loc_obj.lazily_add_relationship + (*secondary_loc_obj, + location_relationship_kind::relevant, + *this); + } + break; + } +} + +/* class sarif_result : public sarif_location_manager. */ + +/* Handle secondary diagnostics that occur within a diagnostic group. + The closest SARIF seems to have to nested diagnostics is the + "relatedLocations" property of result objects (SARIF v2.1.0 section 3.27.22), + so we lazily set this property and populate the array if and when + secondary diagnostics occur (such as notes to a warning). */ + +void +sarif_result::on_nested_diagnostic (const diagnostic_info &diagnostic, + enum kind /*orig_diag_kind*/, + sarif_builder &builder) +{ + /* We don't yet generate meaningful logical locations for notes; + sometimes these will related to current_function_decl, but + often they won't. */ + auto location_obj + = builder.make_location_object (this, *diagnostic.m_richloc, + logical_locations::key (), + diagnostic_artifact_role::result_file); + auto message_obj + = builder.make_message_object (pp_formatted_text (builder.get_printer ())); + pp_clear_output_area (builder.get_printer ()); + location_obj->set<sarif_message> ("message", std::move (message_obj)); + + /* Add nesting level, as per "P3358R0 SARIF for Structured Diagnostics" + https://wg21.link/P3358R0 */ + sarif_property_bag &bag = location_obj->get_or_create_properties (); + bag.set_integer ("nestingLevel", + builder.get_context ().get_diagnostic_nesting_level ()); + + add_related_location (std::move (location_obj), builder); +} + +/* Handle diagrams that occur within a diagnostic group. + The closest thing in SARIF seems to be to add a location to the + "releatedLocations" property (SARIF v2.1.0 section 3.27.22), + and to put the diagram into the "message" property of that location + (SARIF v2.1.0 section 3.28.5). */ + +void +sarif_result::on_diagram (const diagram &d, + sarif_builder &builder) +{ + auto location_obj = std::make_unique<sarif_location> (); + auto message_obj = builder.make_message_object_for_diagram (d); + location_obj->set<sarif_message> ("message", std::move (message_obj)); + + add_related_location (std::move (location_obj), builder); +} + +/* class sarif_location : public sarif_object. */ + +/* Ensure this location has an "id" and return it. + Use LOC_MGR if an id needs to be allocated. + + See the "id" property (3.28.2). + + We use this to only assign ids to locations that are + referenced by another sarif object; others have no "id". */ + +long +sarif_location::lazily_add_id (sarif_location_manager &loc_mgr) +{ + long id = get_id (); + if (id != -1) + return id; + id = loc_mgr.allocate_location_id (); + set_integer ("id", id); + gcc_assert (id != -1); + return id; +} + +/* Get the id of this location, or -1 if it doesn't have one. */ + +long +sarif_location::get_id () const +{ + json::value *id = get ("id"); + if (!id) + return -1; + gcc_assert (id->get_kind () == json::JSON_INTEGER); + return static_cast <json::integer_number *> (id)->get (); +} + +// 3.34.3 kinds property +static const char * +get_string_for_location_relationship_kind (enum location_relationship_kind kind) +{ + switch (kind) + { + default: + gcc_unreachable (); + case location_relationship_kind::includes: + return "includes"; + case location_relationship_kind::is_included_by: + return "isIncludedBy"; + case location_relationship_kind::relevant: + return "relevant"; + } +} + +/* Lazily populate this location's "relationships" property (3.28.7) + with the relationship of KIND to TARGET, creating objects + as necessary. + Use LOC_MGR for any locations that need "id" values. */ + +void +sarif_location::lazily_add_relationship (sarif_location &target, + enum location_relationship_kind kind, + sarif_location_manager &loc_mgr) +{ + sarif_location_relationship &relationship_obj + = lazily_add_relationship_object (target, loc_mgr); + + relationship_obj.lazily_add_kind (kind); +} + +/* Lazily populate this location's "relationships" property (3.28.7) + with a location_relationship to TARGET, creating objects + as necessary. + Use LOC_MGR for any locations that need "id" values. */ + +sarif_location_relationship & +sarif_location::lazily_add_relationship_object (sarif_location &target, + sarif_location_manager &loc_mgr) +{ + /* See if THIS already has a locationRelationship referencing TARGET. */ + auto iter = m_relationships_map.find (&target); + if (iter != m_relationships_map.end ()) + { + /* We already have a locationRelationship from THIS to TARGET. */ + sarif_location_relationship *relationship = iter->second; + gcc_assert (relationship->get_target_id() == target.get_id ()); + return *relationship; + } + + // Ensure that THIS has a "relationships" property (3.28.7). + json::array &relationships_arr = lazily_add_relationships_array (); + + /* No existing locationRelationship from THIS to TARGET; make one, + record it, and add it to the "relationships" array. */ + auto relationship_obj + = std::make_unique<sarif_location_relationship> (target, loc_mgr); + sarif_location_relationship *relationship = relationship_obj.get (); + auto kv + = std::pair<sarif_location *, + sarif_location_relationship *> (&target, relationship); + m_relationships_map.insert (kv); + + relationships_arr.append (std::move (relationship_obj)); + + return *relationship; +} + +/* Ensure this location has a "relationships" array (3.28.7). */ + +json::array & +sarif_location::lazily_add_relationships_array () +{ + const char *const property_name = "relationships"; + if (json::value *relationships = get (property_name)) + { + gcc_assert (relationships->get_kind () == json::JSON_ARRAY); + return *static_cast <json::array *> (relationships); + } + json::array *relationships_arr = new json::array (); + set (property_name, relationships_arr); + return *relationships_arr; +} + +/* class sarif_ice_notification : public sarif_location_manager. */ + +/* sarif_ice_notification's ctor. + DIAGNOSTIC is an internal compiler error. */ + +sarif_ice_notification:: +sarif_ice_notification (const diagnostic_info &diagnostic, + sarif_builder &builder, + std::unique_ptr<json::object> backtrace) +{ + /* "locations" property (SARIF v2.1.0 section 3.58.4). */ + auto locations_arr + = builder.make_locations_arr (*this, + diagnostic, + diagnostic_artifact_role::result_file); + set<json::array> ("locations", std::move (locations_arr)); + + /* "message" property (SARIF v2.1.0 section 3.85.5). */ + auto message_obj + = builder.make_message_object (pp_formatted_text (builder.get_printer ())); + pp_clear_output_area (builder.get_printer ()); + set<sarif_message> ("message", std::move (message_obj)); + + /* "level" property (SARIF v2.1.0 section 3.58.6). */ + set_string ("level", "error"); + + /* If we have backtrace information, add it as part of a property bag. */ + if (backtrace) + { + sarif_property_bag &bag = get_or_create_properties (); + bag.set ("gcc/backtrace", std::move (backtrace)); + } +} + +/* Implementation of sarif_location_manager::add_related_location vfunc + for notifications. */ + +void +sarif_ice_notification:: +add_related_location (std::unique_ptr<sarif_location> location_obj, + sarif_builder &builder) +{ + /* Support for related locations within a notification was added + in SARIF 2.2; see https://github.com/oasis-tcs/sarif-spec/issues/540 */ + if (builder.get_version () >= sarif_version::v2_2_prerelease_2024_08_08) + sarif_location_manager::add_related_location (std::move (location_obj), + builder); + /* Otherwise implicitly discard LOCATION_OBJ. */ +} + +/* class sarif_location_relationship : public sarif_object. */ + +sarif_location_relationship:: +sarif_location_relationship (sarif_location &target, + sarif_location_manager &loc_mgr) +: m_kinds ((unsigned)location_relationship_kind::NUM_KINDS) +{ + bitmap_clear (m_kinds); + set_integer ("target", target.lazily_add_id (loc_mgr)); +} + +long +sarif_location_relationship::get_target_id () const +{ + json::value *id = get ("id"); + gcc_assert (id); + return static_cast <json::integer_number *> (id)->get (); +} + +void +sarif_location_relationship:: +lazily_add_kind (enum location_relationship_kind kind) +{ + if (bitmap_bit_p (m_kinds, (int)kind)) + return; // already have this kind + bitmap_set_bit (m_kinds, (int)kind); + + // 3.34.3 kinds property + json::array *kinds_arr = nullptr; + if (json::value *kinds_val = get ("kinds")) + { + gcc_assert (kinds_val->get_kind () == json::JSON_ARRAY); + } + else + { + kinds_arr = new json::array (); + set ("kinds", kinds_arr); + } + const char *kind_str = get_string_for_location_relationship_kind (kind); + kinds_arr->append_string (kind_str); +} + +/* class sarif_code_flow : public sarif_object. */ + +sarif_code_flow::sarif_code_flow (sarif_result &parent, + unsigned idx_within_parent) +: m_parent (parent), + m_idx_within_parent (idx_within_parent) +{ + /* "threadFlows" property (SARIF v2.1.0 section 3.36.3). */ + auto thread_flows_arr = std::make_unique<json::array> (); + m_thread_flows_arr = thread_flows_arr.get (); // borrowed + set<json::array> ("threadFlows", std::move (thread_flows_arr)); +} + +sarif_thread_flow & +sarif_code_flow::get_or_append_thread_flow (const paths::thread &thread, + paths::thread_id_t thread_id) +{ + sarif_thread_flow **slot = m_thread_id_map.get (thread_id); + if (slot) + return **slot; + + unsigned next_thread_flow_idx = m_thread_flows_arr->size (); + auto thread_flow_obj + = std::make_unique<sarif_thread_flow> (*this, thread, next_thread_flow_idx); + m_thread_id_map.put (thread_id, thread_flow_obj.get ()); // borrowed + sarif_thread_flow *result = thread_flow_obj.get (); + m_thread_flows_arr->append<sarif_thread_flow> (std::move (thread_flow_obj)); + return *result; +} + +sarif_thread_flow & +sarif_code_flow::get_thread_flow (paths::thread_id_t thread_id) +{ + sarif_thread_flow **slot = m_thread_id_map.get (thread_id); + gcc_assert (slot); // it must already have one + return **slot; +} + +void +sarif_code_flow::add_location (sarif_thread_flow_location &tfl_obj) +{ + m_all_tfl_objs.push_back (&tfl_obj); +} + +sarif_thread_flow_location & +sarif_code_flow::get_thread_flow_loc_obj (paths::event_id_t event_id) const +{ + gcc_assert (event_id.known_p ()); + gcc_assert ((size_t)event_id.zero_based () < m_all_tfl_objs.size ()); + sarif_thread_flow_location *tfl_obj = m_all_tfl_objs[event_id.zero_based ()]; + gcc_assert (tfl_obj); + return *tfl_obj; +} + +/* class sarif_thread_flow : public sarif_object. */ + +sarif_thread_flow::sarif_thread_flow (sarif_code_flow &parent, + const paths::thread &thread, + unsigned idx_within_parent) +: m_parent (parent), + m_idx_within_parent (idx_within_parent) +{ + /* "id" property (SARIF v2.1.0 section 3.37.2). */ + label_text name (thread.get_name (false)); + set_string ("id", name.get ()); + + /* "locations" property (SARIF v2.1.0 section 3.37.6). */ + m_locations_arr = new json::array (); + + /* Give ownership of m_locations_arr to json::object; + keep a borrowed ptr. */ + set ("locations", m_locations_arr); +} + +/* Add a sarif_thread_flow_location to this threadFlow object, but + don't populate it yet. */ + +sarif_thread_flow_location & +sarif_thread_flow::add_location () +{ + const unsigned thread_flow_location_idx = m_locations_arr->size (); + sarif_thread_flow_location *thread_flow_loc_obj + = new sarif_thread_flow_location (*this, thread_flow_location_idx); + m_locations_arr->append (thread_flow_loc_obj); + m_parent.add_location (*thread_flow_loc_obj); + return *thread_flow_loc_obj; +} + +/* class sarif_builder. */ + +/* sarif_builder's ctor. */ + +sarif_builder::sarif_builder (diagnostics::context &dc, + pretty_printer &printer, + const line_maps *line_maps, + std::unique_ptr<sarif_serialization_format> serialization_format, + const sarif_generation_options &sarif_gen_opts) +: m_context (dc), + m_printer (&printer), + m_line_maps (line_maps), + m_token_printer (*this), + m_logical_loc_mgr (nullptr), + m_invocation_obj + (std::make_unique<sarif_invocation> (*this, + dc.get_original_argv ())), + m_results_array (new json::array ()), + m_cur_group_result (nullptr), + m_seen_any_relative_paths (false), + m_rule_id_set (), + m_rules_arr (new json::array ()), + m_cached_logical_locs + (std::make_unique<sarif_array_of_unique<sarif_logical_location>> ()), + m_run_graphs + (std::make_unique<sarif_array_of_unique<sarif_graph>> ()), + m_tabstop (dc.m_tabstop), + m_serialization_format (std::move (serialization_format)), + m_sarif_gen_opts (sarif_gen_opts), + m_next_result_idx (0), + m_current_code_flow (nullptr) +{ + gcc_assert (m_line_maps); + gcc_assert (m_serialization_format); + + if (auto client_data_hooks = dc.get_client_data_hooks ()) + m_logical_loc_mgr = client_data_hooks->get_logical_location_manager (); +} + +sarif_builder::~sarif_builder () +{ + /* Normally m_filename_to_artifact_map will have been emptied as part + of make_run_object, but this isn't run by all the selftests. + Ensure the artifact objects are cleaned up for such cases. */ + for (auto iter : m_filename_to_artifact_map) + { + sarif_artifact *artifact_obj = iter.second; + delete artifact_obj; + } +} + +/* Functions at which to stop the backtrace print. It's not + particularly helpful to print the callers of these functions. */ + +static const char * const bt_stop[] = +{ + "main", + "toplev::main", + "execute_one_pass", + "compile_file", +}; + +struct bt_closure +{ + bt_closure (sarif_builder &builder, + json::array *frames_arr) + : m_builder (builder), + m_frames_arr (frames_arr) + { + } + + sarif_builder &m_builder; + json::array *m_frames_arr; +}; + +/* A callback function passed to the backtrace_full function. */ + +static int +bt_callback (void *data, uintptr_t pc, const char *filename, int lineno, + const char *function) +{ + bt_closure *closure = (bt_closure *)data; + + /* If we don't have any useful information, don't print + anything. */ + if (filename == nullptr && function == nullptr) + return 0; + + /* Skip functions in context.cc or diagnostic-global-context.cc. */ + if (closure->m_frames_arr->size () == 0 + && filename != nullptr + && (strcmp (lbasename (filename), "context.cc") == 0 + || strcmp (lbasename (filename), + "diagnostic-global-context.cc") == 0)) + return 0; + + /* Print up to 20 functions. We could make this a --param, but + since this is only for debugging just use a constant for now. */ + if (closure->m_frames_arr->size () >= 20) + { + /* Returning a non-zero value stops the backtrace. */ + return 1; + } + + char *alc = nullptr; + if (function != nullptr) + { + char *str = cplus_demangle_v3 (function, + (DMGL_VERBOSE | DMGL_ANSI + | DMGL_GNU_V3 | DMGL_PARAMS)); + if (str != nullptr) + { + alc = str; + function = str; + } + + for (size_t i = 0; i < ARRAY_SIZE (bt_stop); ++i) + { + size_t len = strlen (bt_stop[i]); + if (strncmp (function, bt_stop[i], len) == 0 + && (function[len] == '\0' || function[len] == '(')) + { + if (alc != nullptr) + free (alc); + /* Returning a non-zero value stops the backtrace. */ + return 1; + } + } + } + + auto frame_obj = std::make_unique<json::object> (); + + /* I tried using sarifStack and sarifStackFrame for this + but it's not a good fit e.g. PC information. */ + char buf[128]; + snprintf (buf, sizeof (buf) - 1, "0x%lx", (unsigned long)pc); + frame_obj->set_string ("pc", buf); + if (function) + frame_obj->set_string ("function", function); + if (filename) + frame_obj->set_string ("filename", filename); + frame_obj->set_integer ("lineno", lineno); + closure->m_frames_arr->append (std::move (frame_obj)); + + if (alc != nullptr) + free (alc); + + return 0; +} + +/* Attempt to generate a JSON object representing a backtrace, + for adding to ICE notifications. */ + +std::unique_ptr<json::object> +sarif_builder::make_stack_from_backtrace () +{ + auto frames_arr = std::make_unique<json::array> (); + + backtrace_state *state = nullptr; + state = backtrace_create_state (nullptr, 0, nullptr, nullptr); + bt_closure closure (*this, frames_arr.get ()); + const int frames_to_skip = 5; + if (state != nullptr) + backtrace_full (state, frames_to_skip, bt_callback, nullptr, + (void *) &closure); + + if (frames_arr->size () == 0) + return nullptr; + + auto stack = std::make_unique<json::object> (); + stack->set ("frames", std::move (frames_arr)); + return stack; +} + +void +sarif_builder::set_main_input_filename (const char *name) +{ + /* Mark NAME as the artifact that the tool was instructed to scan. + Only quote the contents if it gets referenced by physical locations, + since otherwise the "no diagnostics" case would quote the main input + file, and doing so noticeably bloated the output seen in analyzer + integration testing (build directory went from 20G -> 21G). */ + if (name) + get_or_create_artifact (name, + diagnostic_artifact_role::analysis_target, + false); +} + +/* Implementation of "on_report_diagnostic" for SARIF output. */ + +void +sarif_builder::on_report_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + sarif_sink_buffer *buffer) +{ + pp_output_formatted_text (m_printer, m_context.get_urlifier ()); + + if (diagnostic.m_kind == kind::ice || diagnostic.m_kind == kind::ice_nobt) + { + std::unique_ptr<json::object> stack = make_stack_from_backtrace (); + m_invocation_obj->add_notification_for_ice (diagnostic, *this, + std::move (stack)); + + /* Print a header for the remaining output to stderr, and + return, attempting to print the usual ICE messages to + stderr. Hopefully this will be helpful to the user in + indicating what's gone wrong (also for DejaGnu, for pruning + those messages). */ + fnotice (stderr, "Internal compiler error:\n"); + + return; + } + + if (buffer) + { + /* When buffering, we can only handle top-level results. */ + gcc_assert (!m_cur_group_result); + buffer->add_result (make_result_object (diagnostic, orig_diag_kind, + m_next_result_idx++)); + return; + } + + if (m_cur_group_result) + /* Nested diagnostic. */ + m_cur_group_result->on_nested_diagnostic (diagnostic, + orig_diag_kind, + *this); + else + { + /* Top-level diagnostic. */ + m_cur_group_result = make_result_object (diagnostic, orig_diag_kind, + m_next_result_idx++); + } +} + +/* Implementation of diagnostics::context::m_diagrams.m_emission_cb + for SARIF output. */ + +void +sarif_builder::emit_diagram (const diagram &d) +{ + /* We must be within the emission of a top-level diagnostic. */ + gcc_assert (m_cur_group_result); + m_cur_group_result->on_diagram (d, *this); +} + +/* Implementation of "end_group_cb" for SARIF output. */ + +void +sarif_builder::end_group () +{ + if (m_cur_group_result) + { + m_cur_group_result->process_worklist (*this); + m_results_array->append<sarif_result> (std::move (m_cur_group_result)); + } +} + +void +sarif_builder:: +report_global_digraph (const lazily_created<digraphs::digraph> &ldg) +{ + auto &dg = ldg.get_or_create (); + + /* Presumably the location manager must be nullptr; see + https://github.com/oasis-tcs/sarif-spec/issues/712 */ + m_run_graphs->append (make_sarif_graph (dg, this, nullptr)); +} + +/* Create a top-level object, and add it to all the results + (and other entities) we've seen so far, moving ownership + to the object. */ + +std::unique_ptr<sarif_log> +sarif_builder::flush_to_object () +{ + m_invocation_obj->prepare_to_flush (*this); + std::unique_ptr<sarif_log> top + = make_top_level_object (std::move (m_invocation_obj), + std::move (m_results_array)); + return top; +} + +/* Create a top-level object, and add it to all the results + (and other entities) we've seen so far. + + Flush it all to OUTF. */ + +void +sarif_builder::flush_to_file (FILE *outf) +{ + std::unique_ptr<sarif_log> top = flush_to_object (); + m_serialization_format->write_to_file (outf, *top); +} + +/* Attempt to convert DIAG_KIND to a suitable value for the "level" + property (SARIF v2.1.0 section 3.27.10). + + Return nullptr if there isn't one. */ + +static const char * +maybe_get_sarif_level (enum kind diag_kind) +{ + switch (diag_kind) + { + case kind::warning: + return "warning"; + case kind::error: + return "error"; + case kind::note: + case kind::anachronism: + return "note"; + default: + return nullptr; + } +} + +/* Make a string for DIAG_KIND suitable for use a ruleId + (SARIF v2.1.0 section 3.27.5) as a fallback for when we don't + have anything better to use. */ + +static char * +make_rule_id_for_diagnostic_kind (enum kind diag_kind) +{ + /* Lose the trailing ": ". */ + const char *kind_text = get_text_for_kind (diag_kind); + size_t len = strlen (kind_text); + gcc_assert (len > 2); + gcc_assert (kind_text[len - 2] == ':'); + gcc_assert (kind_text[len - 1] == ' '); + char *rstrip = xstrdup (kind_text); + rstrip[len - 2] = '\0'; + return rstrip; +} + +/* Make a "result" object (SARIF v2.1.0 section 3.27) for DIAGNOSTIC. */ + +std::unique_ptr<sarif_result> +sarif_builder::make_result_object (const diagnostic_info &diagnostic, + enum kind orig_diag_kind, + unsigned idx_within_parent) +{ + auto result_obj = std::make_unique<sarif_result> (idx_within_parent); + + /* "ruleId" property (SARIF v2.1.0 section 3.27.5). */ + /* Ideally we'd have an option_name for these. */ + if (char *option_text + = m_context.make_option_name (diagnostic.m_option_id, + orig_diag_kind, diagnostic.m_kind)) + { + /* Lazily create reportingDescriptor objects for and add to m_rules_arr. + Set ruleId referencing them. */ + result_obj->set_string ("ruleId", option_text); + if (m_rule_id_set.contains (option_text)) + free (option_text); + else + { + /* This is the first time we've seen this ruleId. */ + /* Add to set, taking ownership. */ + m_rule_id_set.add (option_text); + + m_rules_arr->append<sarif_reporting_descriptor> + (make_reporting_descriptor_object_for_warning (diagnostic, + orig_diag_kind, + option_text)); + } + } + else + { + /* Otherwise, we have an "error" or a stray "note"; use the + diagnostic kind as the ruleId, so that the result object at least + has a ruleId. + We don't bother creating reportingDescriptor objects for these. */ + char *rule_id = make_rule_id_for_diagnostic_kind (orig_diag_kind); + result_obj->set_string ("ruleId", rule_id); + free (rule_id); + } + + if (diagnostic.m_metadata) + { + /* "taxa" property (SARIF v2.1.0 section 3.27.8). */ + if (int cwe_id = diagnostic.m_metadata->get_cwe ()) + { + auto taxa_arr = std::make_unique<json::array> (); + taxa_arr->append<sarif_reporting_descriptor_reference> + (make_reporting_descriptor_reference_object_for_cwe_id (cwe_id)); + result_obj->set<json::array> ("taxa", std::move (taxa_arr)); + } + + diagnostic.m_metadata->maybe_add_sarif_properties (*result_obj); + + /* We don't yet support diagnostics::metadata::rule. */ + } + + /* "level" property (SARIF v2.1.0 section 3.27.10). */ + if (const char *sarif_level = maybe_get_sarif_level (diagnostic.m_kind)) + result_obj->set_string ("level", sarif_level); + + /* "message" property (SARIF v2.1.0 section 3.27.11). */ + auto message_obj + = make_message_object (pp_formatted_text (m_printer)); + pp_clear_output_area (m_printer); + result_obj->set<sarif_message> ("message", std::move (message_obj)); + + /* "locations" property (SARIF v2.1.0 section 3.27.12). */ + result_obj->set<json::array> + ("locations", + make_locations_arr (*result_obj.get (), + diagnostic, + diagnostic_artifact_role::result_file)); + + /* "codeFlows" property (SARIF v2.1.0 section 3.27.18). */ + if (const paths::path *path = diagnostic.m_richloc->get_path ()) + { + auto code_flows_arr = std::make_unique<json::array> (); + const unsigned code_flow_index = 0; + code_flows_arr->append<sarif_code_flow> + (make_code_flow_object (*result_obj.get (), + code_flow_index, + *path)); + result_obj->set<json::array> ("codeFlows", std::move (code_flows_arr)); + } + + // "graphs" property (SARIF v2.1.0 section 3.27.19). */ + if (diagnostic.m_metadata) + if (auto ldg = diagnostic.m_metadata->get_lazy_digraphs ()) + { + auto &digraphs = ldg->get_or_create (); + auto graphs_arr = std::make_unique<json::array> (); + for (auto &iter : digraphs) + graphs_arr->append (make_sarif_graph (*iter, this, + result_obj.get ())); + if (graphs_arr->size () > 0) + result_obj->set<json::array> ("graphs", std::move (graphs_arr)); + } + + /* The "relatedLocations" property (SARIF v2.1.0 section 3.27.22) is + set up later, if any nested diagnostics occur within this diagnostic + group. */ + + /* "fixes" property (SARIF v2.1.0 section 3.27.30). */ + const rich_location *richloc = diagnostic.m_richloc; + if (richloc->get_num_fixit_hints ()) + { + auto fix_arr = std::make_unique<json::array> (); + fix_arr->append<sarif_fix> (make_fix_object (*richloc)); + result_obj->set<json::array> ("fixes", std::move (fix_arr)); + } + + return result_obj; +} + +/* Make a "reportingDescriptor" object (SARIF v2.1.0 section 3.49) + for a GCC warning. */ + +std::unique_ptr<sarif_reporting_descriptor> +sarif_builder:: +make_reporting_descriptor_object_for_warning (const diagnostic_info &diagnostic, + enum kind /*orig_diag_kind*/, + const char *option_text) +{ + auto reporting_desc = std::make_unique<sarif_reporting_descriptor> (); + + /* "id" property (SARIF v2.1.0 section 3.49.3). */ + reporting_desc->set_string ("id", option_text); + + /* We don't implement "name" property (SARIF v2.1.0 section 3.49.7), since + it seems redundant compared to "id". */ + + /* "helpUri" property (SARIF v2.1.0 section 3.49.12). */ + if (char *option_url = m_context.make_option_url (diagnostic.m_option_id)) + { + reporting_desc->set_string ("helpUri", option_url); + free (option_url); + } + + return reporting_desc; +} + +/* Make a "reportingDescriptor" object (SARIF v2.1.0 section 3.49) + for CWE_ID, for use within the CWE taxa array. */ + +std::unique_ptr<sarif_reporting_descriptor> +sarif_builder::make_reporting_descriptor_object_for_cwe_id (int cwe_id) const +{ + auto reporting_desc = std::make_unique<sarif_reporting_descriptor> (); + + /* "id" property (SARIF v2.1.0 section 3.49.3). */ + { + pretty_printer pp; + pp_printf (&pp, "%i", cwe_id); + reporting_desc->set_string ("id", pp_formatted_text (&pp)); + } + + /* "helpUri" property (SARIF v2.1.0 section 3.49.12). */ + { + char *url = get_cwe_url (cwe_id); + reporting_desc->set_string ("helpUri", url); + free (url); + } + + return reporting_desc; +} + +/* Make a "reportingDescriptorReference" object (SARIF v2.1.0 section 3.52) + referencing CWE_ID, for use within a result object. + Also, add CWE_ID to m_cwe_id_set. */ + +std::unique_ptr<sarif_reporting_descriptor_reference> +sarif_builder:: +make_reporting_descriptor_reference_object_for_cwe_id (int cwe_id) +{ + auto desc_ref_obj = std::make_unique<sarif_reporting_descriptor_reference> (); + + /* "id" property (SARIF v2.1.0 section 3.52.4). */ + { + pretty_printer pp; + pp_printf (&pp, "%i", cwe_id); + desc_ref_obj->set_string ("id", pp_formatted_text (&pp)); + } + + /* "toolComponent" property (SARIF v2.1.0 section 3.52.7). */ + desc_ref_obj->set<sarif_tool_component_reference> + ("toolComponent", make_tool_component_reference_object_for_cwe ()); + + /* Add CWE_ID to our set. */ + gcc_assert (cwe_id > 0); + m_cwe_id_set.add (cwe_id); + + return desc_ref_obj; +} + +/* Make a "toolComponentReference" object (SARIF v2.1.0 section 3.54) that + references the CWE taxonomy. */ + +std::unique_ptr<sarif_tool_component_reference> +sarif_builder:: +make_tool_component_reference_object_for_cwe () const +{ + auto comp_ref_obj = std::make_unique<sarif_tool_component_reference> (); + + /* "name" property (SARIF v2.1.0 section 3.54.3). */ + comp_ref_obj->set_string ("name", "cwe"); + + return comp_ref_obj; +} + +/* Make an array suitable for use as the "locations" property of: + - a "result" object (SARIF v2.1.0 section 3.27.12), or + - a "notification" object (SARIF v2.1.0 section 3.58.4). + Use LOC_MGR for any locations that need "id" values. */ + +std::unique_ptr<json::array> +sarif_builder::make_locations_arr (sarif_location_manager &loc_mgr, + const diagnostic_info &diagnostic, + enum diagnostic_artifact_role role) +{ + auto locations_arr = std::make_unique<json::array> (); + logical_locations::key logical_loc; + if (auto client_data_hooks = m_context.get_client_data_hooks ()) + logical_loc = client_data_hooks->get_current_logical_location (); + + auto location_obj + = make_location_object (&loc_mgr, *diagnostic.m_richloc, logical_loc, role); + /* Don't add entirely empty location objects to the array. */ + if (!location_obj->is_empty ()) + locations_arr->append<sarif_location> (std::move (location_obj)); + + return locations_arr; +} + +/* If LOGICAL_LOC is non-null, use it to create a "logicalLocations" property + within LOCATION_OBJ (SARIF v2.1.0 section 3.28.4) with a minimal logical + location object referencing theRuns.logicalLocations (3.33.3). */ + +void +sarif_builder:: +set_any_logical_locs_arr (sarif_location &location_obj, + logical_locations::key logical_loc) +{ + if (!logical_loc) + return; + gcc_assert (m_logical_loc_mgr); + auto location_locs_arr = std::make_unique<json::array> (); + + auto logical_loc_obj = make_minimal_sarif_logical_location (logical_loc); + + location_locs_arr->append<sarif_logical_location> + (std::move (logical_loc_obj)); + + location_obj.set<json::array> ("logicalLocations", + std::move (location_locs_arr)); +} + +/* Make a "location" object (SARIF v2.1.0 section 3.28) for RICH_LOC + and LOGICAL_LOC. + Use LOC_MGR for any locations that need "id" values, and for + any worklist items. + Note that we might not always have a LOC_MGR; see + https://github.com/oasis-tcs/sarif-spec/issues/712 */ + +std::unique_ptr<sarif_location> +sarif_builder::make_location_object (sarif_location_manager *loc_mgr, + const rich_location &rich_loc, + logical_locations::key logical_loc, + enum diagnostic_artifact_role role) +{ + class escape_nonascii_renderer : public content_renderer + { + public: + escape_nonascii_renderer (const rich_location &richloc, + enum diagnostics_escape_format escape_format) + : m_richloc (richloc), + m_escape_format (escape_format) + {} + + std::unique_ptr<sarif_multiformat_message_string> + render (const sarif_builder &builder) const final override + { + diagnostics::context dc; + diagnostic_initialize (&dc, 0); + auto &source_printing_opts = dc.get_source_printing_options (); + source_printing_opts.enabled = true; + source_printing_opts.colorize_source_p = false; + source_printing_opts.show_labels_p = true; + source_printing_opts.show_line_numbers_p = true; + + rich_location my_rich_loc (m_richloc); + my_rich_loc.set_escape_on_output (true); + + source_print_policy source_policy (dc); + dc.set_escape_format (m_escape_format); + text_sink text_output (dc); + source_policy.print (*text_output.get_printer (), + my_rich_loc, kind::error, nullptr); + + const char *buf = pp_formatted_text (text_output.get_printer ()); + std::unique_ptr<sarif_multiformat_message_string> result + = builder.make_multiformat_message_string (buf); + + diagnostic_finish (&dc); + + return result; + } + private: + const rich_location &m_richloc; + enum diagnostics_escape_format m_escape_format; + } the_renderer (rich_loc, + m_context.get_escape_format ()); + + auto location_obj = std::make_unique<sarif_location> (); + + /* Get primary loc from RICH_LOC. */ + location_t loc = rich_loc.get_loc (); + + /* "physicalLocation" property (SARIF v2.1.0 section 3.28.3). */ + const content_renderer *snippet_renderer + = rich_loc.escape_on_output_p () ? &the_renderer : nullptr; + if (auto phs_loc_obj + = maybe_make_physical_location_object (loc, role, + rich_loc.get_column_override (), + snippet_renderer)) + location_obj->set<sarif_physical_location> ("physicalLocation", + std::move (phs_loc_obj)); + + /* "logicalLocations" property (SARIF v2.1.0 section 3.28.4). */ + set_any_logical_locs_arr (*location_obj, logical_loc); + + /* Handle labelled ranges and/or secondary locations. */ + { + std::unique_ptr<json::array> annotations_arr = nullptr; + for (unsigned int i = 0; i < rich_loc.get_num_locations (); i++) + { + const location_range *range = rich_loc.get_range (i); + bool handled = false; + if (const range_label *label = range->m_label) + { + label_text text = label->get_text (i); + if (text.get ()) + { + /* Create annotations for any labelled ranges. */ + location_t range_loc = rich_loc.get_loc (i); + auto region + = maybe_make_region_object (range_loc, + rich_loc.get_column_override ()); + if (region) + { + if (!annotations_arr) + annotations_arr = std::make_unique<json::array> (); + region->set<sarif_message> + ("message", make_message_object (text.get ())); + annotations_arr->append<sarif_region> (std::move (region)); + handled = true; + } + } + } + + /* Add related locations for any secondary locations in RICH_LOC + that don't have labels (and thus aren't added to "annotations"). */ + if (loc_mgr && i > 0 && !handled) + loc_mgr->add_relationship_to_worklist + (*location_obj.get (), + sarif_location_manager::worklist_item::kind::unlabelled_secondary_location, + range->m_loc); + } + if (annotations_arr) + /* "annotations" property (SARIF v2.1.0 section 3.28.6). */ + location_obj->set<json::array> ("annotations", + std::move (annotations_arr)); + } + + if (loc_mgr) + add_any_include_chain (*loc_mgr, *location_obj.get (), loc); + + /* A flag for hinting that the diagnostic involves issues at the + level of character encodings (such as homoglyphs, or misleading + bidirectional control codes), and thus that it will be helpful + to the user if we show some representation of + how the characters in the pertinent source lines are encoded. */ + if (rich_loc.escape_on_output_p ()) + { + sarif_property_bag &bag = location_obj->get_or_create_properties (); + bag.set_bool ("gcc/escapeNonAscii", rich_loc.escape_on_output_p ()); + } + + return location_obj; +} + +/* If WHERE was #included from somewhere, add a worklist item + to LOC_MGR to lazily add a location for the #include location, + and relationships between it and the LOCATION_OBJ. + Compare with diagnostics::context::report_current_module, but rather + than iterating the current chain, we add the next edge and iterate + in the worklist, so that edges are only added once. */ + +void +sarif_builder::add_any_include_chain (sarif_location_manager &loc_mgr, + sarif_location &location_obj, + location_t where) +{ + if (where <= BUILTINS_LOCATION) + return; + + const line_map_ordinary *map = nullptr; + linemap_resolve_location (m_line_maps, where, + LRK_MACRO_DEFINITION_LOCATION, + &map); + + if (!map) + return; + + location_t include_loc = linemap_included_from (map); + map = linemap_included_from_linemap (m_line_maps, map); + if (!map) + return; + loc_mgr.add_relationship_to_worklist + (location_obj, + sarif_result::worklist_item::kind::included_from, + include_loc); +} + +/* Make a "location" object (SARIF v2.1.0 section 3.28) for WHERE + within an include chain. */ + +std::unique_ptr<sarif_location> +sarif_builder::make_location_object (sarif_location_manager &loc_mgr, + location_t loc, + enum diagnostic_artifact_role role) +{ + auto location_obj = std::make_unique<sarif_location> (); + + /* "physicalLocation" property (SARIF v2.1.0 section 3.28.3). */ + if (auto phs_loc_obj + = maybe_make_physical_location_object (loc, role, 0, nullptr)) + location_obj->set<sarif_physical_location> ("physicalLocation", + std::move (phs_loc_obj)); + + add_any_include_chain (loc_mgr, *location_obj.get (), loc); + + return location_obj; +} + +/* Make a "location" object (SARIF v2.1.0 section 3.28) for EVENT + within a paths::path. */ + +std::unique_ptr<sarif_location> +sarif_builder::make_location_object (sarif_location_manager &loc_mgr, + const paths::event &event, + enum diagnostic_artifact_role role) +{ + auto location_obj = std::make_unique<sarif_location> (); + + /* "physicalLocation" property (SARIF v2.1.0 section 3.28.3). */ + location_t loc = event.get_location (); + if (auto phs_loc_obj + = maybe_make_physical_location_object (loc, role, 0, nullptr)) + location_obj->set<sarif_physical_location> ("physicalLocation", + std::move (phs_loc_obj)); + + /* "logicalLocations" property (SARIF v2.1.0 section 3.28.4). */ + logical_locations::key logical_loc = event.get_logical_location (); + set_any_logical_locs_arr (*location_obj, logical_loc); + + /* "message" property (SARIF v2.1.0 section 3.28.5). */ + std::unique_ptr<pretty_printer> pp = get_printer ()->clone (); + event.print_desc (*pp); + location_obj->set<sarif_message> + ("message", + make_message_object (pp_formatted_text (pp.get ()))); + + add_any_include_chain (loc_mgr, *location_obj.get (), loc); + + return location_obj; +} + +/* Make a "physicalLocation" object (SARIF v2.1.0 section 3.29) for LOC. + + If COLUMN_OVERRIDE is non-zero, then use it as the column number + if LOC has no column information. + + Ensure that we have an artifact object for the file, adding ROLE to it, + and flagging that we will attempt to embed the contents of the artifact + when writing it out. */ + +std::unique_ptr<sarif_physical_location> +sarif_builder:: +maybe_make_physical_location_object (location_t loc, + enum diagnostic_artifact_role role, + int column_override, + const content_renderer *snippet_renderer) +{ + if (loc <= BUILTINS_LOCATION || LOCATION_FILE (loc) == nullptr) + return nullptr; + + auto phys_loc_obj = std::make_unique<sarif_physical_location> (); + + /* "artifactLocation" property (SARIF v2.1.0 section 3.29.3). */ + phys_loc_obj->set<sarif_artifact_location> + ("artifactLocation", make_artifact_location_object (loc)); + get_or_create_artifact (LOCATION_FILE (loc), role, true); + + /* "region" property (SARIF v2.1.0 section 3.29.4). */ + if (auto region_obj = maybe_make_region_object (loc, column_override)) + phys_loc_obj->set<sarif_region> ("region", std::move (region_obj)); + + /* "contextRegion" property (SARIF v2.1.0 section 3.29.5). */ + if (auto context_region_obj + = maybe_make_region_object_for_context (loc, snippet_renderer)) + phys_loc_obj->set<sarif_region> ("contextRegion", + std::move (context_region_obj)); + + /* Instead, we add artifacts to the run as a whole, + with artifact.contents. + Could do both, though. */ + + return phys_loc_obj; +} + +/* Make an "artifactLocation" object (SARIF v2.1.0 section 3.4) for LOC, + or return nullptr. */ + +std::unique_ptr<sarif_artifact_location> +sarif_builder::make_artifact_location_object (location_t loc) +{ + return make_artifact_location_object (LOCATION_FILE (loc)); +} + +/* The ID value for use in "uriBaseId" properties (SARIF v2.1.0 section 3.4.4) + for when we need to express paths relative to PWD. */ + +#define PWD_PROPERTY_NAME ("PWD") + +/* Make an "artifactLocation" object (SARIF v2.1.0 section 3.4) for FILENAME, + or return nullptr. */ + +std::unique_ptr<sarif_artifact_location> +sarif_builder::make_artifact_location_object (const char *filename) +{ + auto artifact_loc_obj = std::make_unique<sarif_artifact_location> (); + + /* "uri" property (SARIF v2.1.0 section 3.4.3). */ + artifact_loc_obj->set_string ("uri", filename); + + if (filename[0] != '/') + { + /* If we have a relative path, set the "uriBaseId" property + (SARIF v2.1.0 section 3.4.4). */ + artifact_loc_obj->set_string ("uriBaseId", PWD_PROPERTY_NAME); + m_seen_any_relative_paths = true; + } + + return artifact_loc_obj; +} + +/* Get the PWD, or nullptr, as an absolute file-based URI, + adding a trailing forward slash (as required by SARIF v2.1.0 + section 3.14.14). */ + +static char * +make_pwd_uri_str () +{ + /* The prefix of a file-based URI, up to, but not including the path. */ +#define FILE_PREFIX ("file://") + + const char *pwd = getpwd (); + if (!pwd) + return nullptr; + size_t len = strlen (pwd); + if (len == 0 || pwd[len - 1] != '/') + return concat (FILE_PREFIX, pwd, "/", nullptr); + else + { + gcc_assert (pwd[len - 1] == '/'); + return concat (FILE_PREFIX, pwd, nullptr); + } +} + +/* Make an "artifactLocation" object (SARIF v2.1.0 section 3.4) for the pwd, + for use in the "run.originalUriBaseIds" property (SARIF v2.1.0 + section 3.14.14) when we have any relative paths. */ + +std::unique_ptr<sarif_artifact_location> +sarif_builder::make_artifact_location_object_for_pwd () const +{ + auto artifact_loc_obj = std::make_unique<sarif_artifact_location> (); + + /* "uri" property (SARIF v2.1.0 section 3.4.3). */ + if (char *pwd = make_pwd_uri_str ()) + { + gcc_assert (strlen (pwd) > 0); + gcc_assert (pwd[strlen (pwd) - 1] == '/'); + artifact_loc_obj->set_string ("uri", pwd); + free (pwd); + } + + return artifact_loc_obj; +} + +/* Get the column number within EXPLOC. */ + +int +sarif_builder::get_sarif_column (expanded_location exploc) const +{ + cpp_char_column_policy policy (m_tabstop, cpp_wcwidth); + return location_compute_display_column (m_context.get_file_cache (), + exploc, policy); +} + +/* Make a "region" object (SARIF v2.1.0 section 3.30) for LOC, + or return nullptr. + + If COLUMN_OVERRIDE is non-zero, then use it as the column number + if LOC has no column information. + + We only support text properties of regions ("text regions"), + not binary properties ("binary regions"); see 3.30.1. */ + +std::unique_ptr<sarif_region> +sarif_builder::maybe_make_region_object (location_t loc, + int column_override) const +{ + location_t caret_loc = get_pure_location (loc); + + if (caret_loc <= BUILTINS_LOCATION) + return nullptr; + + location_t start_loc = get_start (loc); + location_t finish_loc = get_finish (loc); + + expanded_location exploc_caret = expand_location (caret_loc); + expanded_location exploc_start = expand_location (start_loc); + expanded_location exploc_finish = expand_location (finish_loc); + + if (exploc_start.file !=exploc_caret.file) + return nullptr; + if (exploc_finish.file !=exploc_caret.file) + return nullptr; + + /* We can have line == 0 in the presence of "#" lines. + SARIF requires lines > 0, so if we hit this case we don't have a + way of validly representing the region as SARIF; bail out. */ + if (exploc_start.line <= 0) + return nullptr; + + auto region_obj = std::make_unique<sarif_region> (); + + /* "startLine" property (SARIF v2.1.0 section 3.30.5) */ + region_obj->set_integer ("startLine", exploc_start.line); + + /* "startColumn" property (SARIF v2.1.0 section 3.30.6). + + We use column == 0 to mean the whole line, so omit the column + information for this case, unless COLUMN_OVERRIDE is non-zero, + (for handling certain awkward lexer diagnostics) */ + + if (exploc_start.column == 0 && column_override) + /* Use the provided column number. */ + exploc_start.column = column_override; + + if (exploc_start.column > 0) + { + int start_column = get_sarif_column (exploc_start); + region_obj->set_integer ("startColumn", start_column); + } + + /* "endLine" property (SARIF v2.1.0 section 3.30.7) */ + if (exploc_finish.line != exploc_start.line + && exploc_finish.line > 0) + region_obj->set_integer ("endLine", exploc_finish.line); + + /* "endColumn" property (SARIF v2.1.0 section 3.30.8). + This expresses the column immediately beyond the range. + + We use column == 0 to mean the whole line, so omit the column + information for this case. */ + if (exploc_finish.column > 0) + { + int next_column = get_sarif_column (exploc_finish) + 1; + region_obj->set_integer ("endColumn", next_column); + } + + return region_obj; +} + +/* Make a "region" object (SARIF v2.1.0 section 3.30) for the "contextRegion" + property (SARIF v2.1.0 section 3.29.5) of a "physicalLocation". + + This is similar to maybe_make_region_object, but ignores column numbers, + covering the line(s) as a whole, and including a "snippet" property + embedding those source lines, making it easier for consumers to show + the pertinent source. */ + +std::unique_ptr<sarif_region> +sarif_builder:: +maybe_make_region_object_for_context (location_t loc, + const content_renderer *snippet_renderer) + const +{ + location_t caret_loc = get_pure_location (loc); + + if (caret_loc <= BUILTINS_LOCATION) + return nullptr; + + location_t start_loc = get_start (loc); + location_t finish_loc = get_finish (loc); + + expanded_location exploc_caret = expand_location (caret_loc); + expanded_location exploc_start = expand_location (start_loc); + expanded_location exploc_finish = expand_location (finish_loc); + + if (exploc_start.file !=exploc_caret.file) + return nullptr; + if (exploc_finish.file !=exploc_caret.file) + return nullptr; + + /* We can have line == 0 in the presence of "#" lines. + SARIF requires lines > 0, so if we hit this case we don't have a + way of validly representing the region as SARIF; bail out. */ + if (exploc_start.line <= 0) + return nullptr; + + auto region_obj = std::make_unique<sarif_region> (); + + /* "startLine" property (SARIF v2.1.0 section 3.30.5) */ + region_obj->set_integer ("startLine", exploc_start.line); + + /* "endLine" property (SARIF v2.1.0 section 3.30.7) */ + if (exploc_finish.line != exploc_start.line + && exploc_finish.line > 0) + region_obj->set_integer ("endLine", exploc_finish.line); + + /* "snippet" property (SARIF v2.1.0 section 3.30.13). */ + if (auto artifact_content_obj + = maybe_make_artifact_content_object (exploc_start.file, + exploc_start.line, + exploc_finish.line, + snippet_renderer)) + region_obj->set<sarif_artifact_content> ("snippet", + std::move (artifact_content_obj)); + + return region_obj; +} + +/* Make a "region" object (SARIF v2.1.0 section 3.30) for the deletion region + of HINT (as per SARIF v2.1.0 section 3.57.3). */ + +std::unique_ptr<sarif_region> +sarif_builder::make_region_object_for_hint (const fixit_hint &hint) const +{ + location_t start_loc = hint.get_start_loc (); + location_t next_loc = hint.get_next_loc (); + + expanded_location exploc_start = expand_location (start_loc); + expanded_location exploc_next = expand_location (next_loc); + + auto region_obj = std::make_unique<sarif_region> (); + + /* "startLine" property (SARIF v2.1.0 section 3.30.5) */ + region_obj->set_integer ("startLine", exploc_start.line); + + /* "startColumn" property (SARIF v2.1.0 section 3.30.6) */ + int start_col = get_sarif_column (exploc_start); + region_obj->set_integer ("startColumn", start_col); + + /* "endLine" property (SARIF v2.1.0 section 3.30.7) */ + if (exploc_next.line != exploc_start.line) + region_obj->set_integer ("endLine", exploc_next.line); + + /* "endColumn" property (SARIF v2.1.0 section 3.30.8). + This expresses the column immediately beyond the range. */ + int next_col = get_sarif_column (exploc_next); + region_obj->set_integer ("endColumn", next_col); + + return region_obj; +} + +/* Attempt to get a string for a logicalLocation's "kind" property + (SARIF v2.1.0 section 3.33.7). + Return nullptr if unknown. */ + +static const char * +maybe_get_sarif_kind (enum logical_locations::kind kind) +{ + using namespace logical_locations; + + switch (kind) + { + default: + gcc_unreachable (); + case logical_locations::kind::unknown: + return nullptr; + + /* Kinds within executable code. */ + case logical_locations::kind::function: + return "function"; + case logical_locations::kind::member: + return "member"; + case logical_locations::kind::module_: + return "module"; + case logical_locations::kind::namespace_: + return "namespace"; + case logical_locations::kind::type: + return "type"; + case logical_locations::kind::return_type: + return "returnType"; + case logical_locations::kind::parameter: + return "parameter"; + case logical_locations::kind::variable: + return "variable"; + + /* Kinds within XML or HTML documents. */ + case logical_locations::kind::element: + return "element"; + case logical_locations::kind::attribute: + return "attribute"; + case logical_locations::kind::text: + return "text"; + case logical_locations::kind::comment: + return "comment"; + case logical_locations::kind::processing_instruction: + return "processingInstruction"; + case logical_locations::kind::dtd: + return "dtd"; + case logical_locations::kind::declaration: + return "declaration"; + + /* Kinds within JSON documents. */ + case logical_locations::kind::object: + return "object"; + case logical_locations::kind::array: + return "array"; + case logical_locations::kind::property: + return "property"; + case logical_locations::kind::value: + return "value"; + } +} + +/* Set PROPERTY_NAME within this bag to a "logicalLocation" object (SARIF v2.1.0 + section 3.33) for LOGICAL_LOC. The object has an "index" property to refer to + theRuns.logicalLocations (3.33.3). */ + +void +sarif_property_bag::set_logical_location (const char *property_name, + sarif_builder &builder, + logical_locations::key logical_loc) +{ + set<sarif_logical_location> + (property_name, + builder.make_minimal_sarif_logical_location (logical_loc)); +} + +static void +copy_any_property_bag (const digraphs::object &input_obj, + sarif_object &output_obj) +{ + if (input_obj.get_property_bag ()) + { + const json::object &old_bag = *input_obj.get_property_bag (); + sarif_property_bag &new_bag = output_obj.get_or_create_properties (); + for (size_t i = 0; i < old_bag.get_num_keys (); ++i) + { + const char *key = old_bag.get_key (i); + json::value *val = old_bag.get (key); + new_bag.set (key, val->clone ()); + } + } +} + +std::unique_ptr<sarif_graph> +make_sarif_graph (const digraphs::digraph &g, + sarif_builder *builder, + sarif_location_manager *sarif_location_mgr) +{ + auto result = std::make_unique<sarif_graph> (); + + // 3.39.2 description property + if (const char *desc = g.get_description ()) + if (builder) + result->set<sarif_message> ("description", + builder->make_message_object (desc)); + + copy_any_property_bag (g, *result); + + // 3.39.3 nodes property + auto nodes_arr = std::make_unique<json::array> (); + const int num_nodes = g.get_num_nodes (); + for (int i = 0; i < num_nodes; ++i) + nodes_arr->append (make_sarif_node (g.get_node (i), + builder, + sarif_location_mgr)); + result->set ("nodes", std::move (nodes_arr)); + + // 3.39.4 edges property + auto edges_arr = std::make_unique<json::array> (); + const int num_edges = g.get_num_edges (); + for (int i = 0; i < num_edges; ++i) + edges_arr->append (make_sarif_edge (g.get_edge (i), builder)); + result->set ("edges", std::move (edges_arr)); + + return result; +} + +std::unique_ptr<sarif_node> +make_sarif_node (const digraphs::node &n, + sarif_builder *builder, + sarif_location_manager *sarif_location_mgr) +{ + auto result = std::make_unique<sarif_node> (); + + // 3.40.2 id property + result->set_string ("id", n.get_id ().c_str ()); + + copy_any_property_bag (n, *result); + + // 3.40.3 label property + if (const char *label = n.get_label ()) + if (builder) + result->set<sarif_message> ("label", + builder->make_message_object (label)); + + // 3.40.4 location property + if (n.get_logical_loc () + || n.get_physical_loc () != UNKNOWN_LOCATION) + if (builder) + { + rich_location rich_loc + (line_table, n.get_physical_loc ()); + auto loc_obj + = builder->make_location_object + (sarif_location_mgr, + rich_loc, + n.get_logical_loc (), + diagnostic_artifact_role::scanned_file); + result->set<sarif_location> ("location", + std::move (loc_obj)); + } + + // 3.40.5 children property + if (const int num_children = n.get_num_children ()) + { + auto children_arr = std::make_unique<json::array> (); + for (int i = 0; i < num_children; ++i) + children_arr->append (make_sarif_node (n.get_child (i), + builder, + sarif_location_mgr)); + result->set ("children", std::move (children_arr)); + } + + return result; +} + +std::unique_ptr<sarif_edge> +make_sarif_edge (const digraphs::edge &e, + sarif_builder *builder) +{ + auto result = std::make_unique<sarif_edge> (); + + // 3.41.2 id property + result->set_string ("id", e.get_id ().c_str ()); + + copy_any_property_bag (e, *result); + + // 3.41.3 label property + if (const char *label = e.get_label ()) + if (builder) + result->set<sarif_message> ("label", + builder->make_message_object (label)); + + // 3.41.4 sourceNodeId property + result->set_string ("sourceNodeId", e.get_src_node ().get_id ().c_str ()); + + // 3.41.5 targetNodeId property + result->set_string ("targetNodeId", e.get_dst_node ().get_id ().c_str ()); + + return result; +} + +void +sarif_property_bag::set_graph (const char *property_name, + sarif_builder &builder, + sarif_location_manager *sarif_location_mgr, + const digraphs::digraph &g) +{ + set<sarif_graph> (property_name, + make_sarif_graph (g, &builder, sarif_location_mgr)); +} + +/* Ensure that m_cached_logical_locs has a "logicalLocation" object + (SARIF v2.1.0 section 3.33) for K, and return its index within the + array. */ + +int +sarif_builder:: +ensure_sarif_logical_location_for (logical_locations::key k) +{ + gcc_assert (m_logical_loc_mgr); + + auto sarif_logical_loc = std::make_unique<sarif_logical_location> (); + + if (const char *short_name = m_logical_loc_mgr->get_short_name (k)) + sarif_logical_loc->set_string ("name", short_name); + + /* "fullyQualifiedName" property (SARIF v2.1.0 section 3.33.5). */ + if (const char *name_with_scope = m_logical_loc_mgr->get_name_with_scope (k)) + sarif_logical_loc->set_string ("fullyQualifiedName", name_with_scope); + + /* "decoratedName" property (SARIF v2.1.0 section 3.33.6). */ + if (const char *internal_name = m_logical_loc_mgr->get_internal_name (k)) + sarif_logical_loc->set_string ("decoratedName", internal_name); + + /* "kind" property (SARIF v2.1.0 section 3.33.7). */ + enum logical_locations::kind kind = m_logical_loc_mgr->get_kind (k); + if (const char *sarif_kind_str = maybe_get_sarif_kind (kind)) + sarif_logical_loc->set_string ("kind", sarif_kind_str); + + /* "parentIndex" property (SARIF v2.1.0 section 3.33.8). */ + if (auto parent_key = m_logical_loc_mgr->get_parent (k)) + { + /* Recurse upwards. */ + int parent_index = ensure_sarif_logical_location_for (parent_key); + sarif_logical_loc->set_integer ("parentIndex", parent_index); + } + + /* Consolidate if this logical location already exists. */ + int index + = m_cached_logical_locs->append_uniquely (std::move (sarif_logical_loc)); + + return index; +} + +/* Ensure that theRuns.logicalLocations (3.14.17) has a "logicalLocation" object + (SARIF v2.1.0 section 3.33) for LOGICAL_LOC. + Create and return a minimal logicalLocation object referring to the + full object by index. */ + +std::unique_ptr<sarif_logical_location> +sarif_builder:: +make_minimal_sarif_logical_location (logical_locations::key logical_loc) +{ + gcc_assert (m_logical_loc_mgr); + + /* Ensure that m_cached_logical_locs has a "logicalLocation" object + (SARIF v2.1.0 section 3.33) for LOGICAL_LOC, and return its index within + the array. */ + + auto sarif_logical_loc = std::make_unique <sarif_logical_location> (); + + int index = ensure_sarif_logical_location_for (logical_loc); + + // 3.33.3 index property + sarif_logical_loc->set_integer ("index", index); + + /* "fullyQualifiedName" property (SARIF v2.1.0 section 3.33.5). */ + if (const char *name_with_scope + = m_logical_loc_mgr->get_name_with_scope (logical_loc)) + sarif_logical_loc->set_string ("fullyQualifiedName", name_with_scope); + + return sarif_logical_loc; +} + +label_text +make_sarif_url_for_event (const sarif_code_flow *code_flow, + paths::event_id_t event_id) +{ + gcc_assert (event_id.known_p ()); + + if (!code_flow) + return label_text (); + + const sarif_thread_flow_location &tfl_obj + = code_flow->get_thread_flow_loc_obj (event_id); + const int location_idx = tfl_obj.get_index_within_parent (); + + const sarif_thread_flow &thread_flow_obj = tfl_obj.get_parent (); + const int thread_flow_idx = thread_flow_obj.get_index_within_parent (); + + const sarif_code_flow &code_flow_obj = thread_flow_obj.get_parent (); + const int code_flow_idx = code_flow_obj.get_index_within_parent (); + + const sarif_result &result_obj = code_flow_obj.get_parent (); + const int result_idx = result_obj.get_index_within_parent (); + + /* We only support a single run object in the log. */ + const int run_idx = 0; + + char *buf = xasprintf + ("sarif:/runs/%i/results/%i/codeFlows/%i/threadFlows/%i/locations/%i", + run_idx, result_idx, code_flow_idx, thread_flow_idx, location_idx); + return label_text::take (buf); +} + +/* Make a "codeFlow" object (SARIF v2.1.0 section 3.36) for PATH. */ + +std::unique_ptr<sarif_code_flow> +sarif_builder::make_code_flow_object (sarif_result &result, + unsigned idx_within_parent, + const paths::path &path) +{ + auto code_flow_obj + = std::make_unique <sarif_code_flow> (result, idx_within_parent); + + /* First pass: + Create threadFlows and threadFlowLocation objects within them, + effectively recording a mapping from event_id to threadFlowLocation + so that we can later go from an event_id to a URI within the + SARIF file. */ + for (unsigned i = 0; i < path.num_events (); i++) + { + const paths::event &event = path.get_event (i); + const paths::thread_id_t thread_id = event.get_thread_id (); + + sarif_thread_flow &thread_flow_obj + = code_flow_obj->get_or_append_thread_flow (path.get_thread (thread_id), + thread_id); + thread_flow_obj.add_location (); + } + + /* Second pass: walk the events, populating the tfl objs. */ + m_current_code_flow = code_flow_obj.get (); + for (unsigned i = 0; i < path.num_events (); i++) + { + const paths::event &event = path.get_event (i); + sarif_thread_flow_location &thread_flow_loc_obj + = code_flow_obj->get_thread_flow_loc_obj (i); + populate_thread_flow_location_object (result, + thread_flow_loc_obj, + event, + i); + } + m_current_code_flow = nullptr; + + return code_flow_obj; +} + +/* Populate TFL_OBJ, a "threadFlowLocation" object (SARIF v2.1.0 section 3.38) + based on EVENT. */ + +void +sarif_builder:: +populate_thread_flow_location_object (sarif_result &result, + sarif_thread_flow_location &tfl_obj, + const paths::event &ev, + int event_execution_idx) +{ + /* Give paths::event subclasses a chance to add custom properties + via a property bag. */ + ev.maybe_add_sarif_properties (*this, tfl_obj); + + if (get_opts ().m_state_graph) + if (auto state_graph = ev.maybe_make_diagnostic_state_graph (true)) + { + sarif_property_bag &props = tfl_obj.get_or_create_properties (); + +#define PROPERTY_PREFIX "gcc/diagnostics/paths/event/" + props.set_graph (PROPERTY_PREFIX "state_graph", + *this, + /* Use RESULT for any related locations in the graph's + nodes. + It's not clear if this is correct; see: + https://github.com/oasis-tcs/sarif-spec/issues/712 + */ + &result, + *state_graph); +#undef PROPERTY_PREFIX + } + + /* "location" property (SARIF v2.1.0 section 3.38.3). */ + tfl_obj.set<sarif_location> + ("location", + make_location_object (result, ev, diagnostic_artifact_role::traced_file)); + + /* "kinds" property (SARIF v2.1.0 section 3.38.8). */ + paths::event::meaning m = ev.get_meaning (); + if (auto kinds_arr = maybe_make_kinds_array (m)) + tfl_obj.set<json::array> ("kinds", std::move (kinds_arr)); + + /* "nestingLevel" property (SARIF v2.1.0 section 3.38.10). */ + tfl_obj.set_integer ("nestingLevel", ev.get_stack_depth ()); + + /* "executionOrder" property (SARIF v2.1.0 3.38.11). + Offset by 1 to match the human-readable values emitted by %@. */ + tfl_obj.set_integer ("executionOrder", event_execution_idx + 1); + + /* It might be nice to eventually implement the following for -fanalyzer: + - the "stack" property (SARIF v2.1.0 section 3.38.5) + - the "state" property (SARIF v2.1.0 section 3.38.9) + - the "importance" property (SARIF v2.1.0 section 3.38.13). */ +} + +/* If M has any known meaning, make a json array suitable for the "kinds" + property of a "threadFlowLocation" object (SARIF v2.1.0 section 3.38.8). + + Otherwise, return nullptr. */ + +std::unique_ptr<json::array> +sarif_builder:: +maybe_make_kinds_array (paths::event::meaning m) const +{ + using namespace paths; + + if (m.m_verb == event::verb::unknown + && m.m_noun == event::noun::unknown + && m.m_property == event::property::unknown) + return nullptr; + + auto kinds_arr = std::make_unique<json::array> (); + if (const char *verb_str + = event::meaning::maybe_get_verb_str (m.m_verb)) + kinds_arr->append_string (verb_str); + if (const char *noun_str + = event::meaning::maybe_get_noun_str (m.m_noun)) + kinds_arr->append_string (noun_str); + if (const char *property_str + = event::meaning::maybe_get_property_str (m.m_property)) + kinds_arr->append_string (property_str); + return kinds_arr; +} + +/* In "3.11.5 Messages with placeholders": + "Within both plain text and formatted message strings, the characters + "{" and "}" SHALL be represented by the character sequences + "{{" and "}}" respectively." */ + +static std::string +escape_braces (const char *text) +{ + std::string result; + while (char ch = *text++) + switch (ch) + { + case '{': + case '}': + result += ch; + /* Fall through. */ + default: + result += ch; + break; + } + return result; +} + +static void +set_string_property_escaping_braces (json::object &obj, + const char *property_name, + const char *value) +{ + std::string escaped (escape_braces (value)); + obj.set_string (property_name, escaped.c_str ()); +} + +/* Make a "message" object (SARIF v2.1.0 section 3.11) for MSG. */ + +std::unique_ptr<sarif_message> +sarif_builder::make_message_object (const char *msg) const +{ + auto message_obj = std::make_unique<sarif_message> (); + + /* "text" property (SARIF v2.1.0 section 3.11.8). */ + set_string_property_escaping_braces (*message_obj, + "text", msg); + + return message_obj; +} + +/* Make a "message" object (SARIF v2.1.0 section 3.11) for D. + We emit the diagram as a code block within the Markdown part + of the message. */ + +std::unique_ptr<sarif_message> +sarif_builder::make_message_object_for_diagram (const diagram &d) +{ + auto message_obj = std::make_unique<sarif_message> (); + + /* "text" property (SARIF v2.1.0 section 3.11.8). */ + set_string_property_escaping_braces (*message_obj, + "text", d.get_alt_text ()); + + pretty_printer *const pp = m_printer; + char *saved_prefix = pp_take_prefix (pp); + pp_set_prefix (pp, nullptr); + + /* "To produce a code block in Markdown, simply indent every line of + the block by at least 4 spaces or 1 tab." + Here we use 4 spaces. */ + d.get_canvas ().print_to_pp (pp, " "); + pp_set_prefix (pp, saved_prefix); + + /* "markdown" property (SARIF v2.1.0 section 3.11.9). */ + set_string_property_escaping_braces (*message_obj, + "markdown", pp_formatted_text (pp)); + + pp_clear_output_area (pp); + + return message_obj; +} + +/* Make a "multiformatMessageString object" (SARIF v2.1.0 section 3.12) + for MSG. */ + +std::unique_ptr<sarif_multiformat_message_string> +sarif_builder::make_multiformat_message_string (const char *msg) const +{ + auto message_obj = std::make_unique<sarif_multiformat_message_string> (); + + /* "text" property (SARIF v2.1.0 section 3.12.3). */ + set_string_property_escaping_braces (*message_obj, + "text", msg); + + return message_obj; +} + +/* Convert VERSION to a value for the "$schema" property + of a "sarifLog" object (SARIF v2.1.0 section 3.13.3). */ + +static const char * +sarif_version_to_url (enum sarif_version version) +{ + switch (version) + { + default: + gcc_unreachable (); + case sarif_version::v2_1_0: + return "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json"; + case sarif_version::v2_2_prerelease_2024_08_08: + return "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/refs/tags/2.2-prerelease-2024-08-08/sarif-2.2/schema/sarif-2-2.schema.json"; + } +} + +/* Convert VERSION to a value for the "version" property + of a "sarifLog" object (SARIF v2.1.0 section 3.13.2). */ + +static const char * +sarif_version_to_property (enum sarif_version version) +{ + switch (version) + { + default: + gcc_unreachable (); + case sarif_version::v2_1_0: + return "2.1.0"; + case sarif_version::v2_2_prerelease_2024_08_08: + /* I would have used "2.2-prerelease-2024-08-08", + but the schema only accepts "2.2". */ + return "2.2"; + } +} + +/* Make a top-level "sarifLog" object (SARIF v2.1.0 section 3.13). */ + +std::unique_ptr<sarif_log> +sarif_builder:: +make_top_level_object (std::unique_ptr<sarif_invocation> invocation_obj, + std::unique_ptr<json::array> results) +{ + auto log_obj = std::make_unique<sarif_log> (); + + /* "$schema" property (SARIF v2.1.0 section 3.13.3) . */ + log_obj->set_string ("$schema", sarif_version_to_url (get_version ())); + + /* "version" property (SARIF v2.1.0 section 3.13.2). */ + log_obj->set_string ("version", sarif_version_to_property (get_version ())); + + /* "runs" property (SARIF v2.1.0 section 3.13.4). */ + auto run_arr = std::make_unique<json::array> (); + auto run_obj = make_run_object (std::move (invocation_obj), + std::move (results)); + run_arr->append<sarif_run> (std::move (run_obj)); + log_obj->set<json::array> ("runs", std::move (run_arr)); + + return log_obj; +} + +/* Make a "run" object (SARIF v2.1.0 section 3.14). */ + +std::unique_ptr<sarif_run> +sarif_builder:: +make_run_object (std::unique_ptr<sarif_invocation> invocation_obj, + std::unique_ptr<json::array> results) +{ + auto run_obj = std::make_unique<sarif_run> (); + + /* "tool" property (SARIF v2.1.0 section 3.14.6). */ + run_obj->set<sarif_tool> ("tool", make_tool_object ()); + + /* "taxonomies" property (SARIF v2.1.0 section 3.14.8). */ + if (auto taxonomies_arr = maybe_make_taxonomies_array ()) + run_obj->set<json::array> ("taxonomies", std::move (taxonomies_arr)); + + /* "invocations" property (SARIF v2.1.0 section 3.14.11). */ + { + auto invocations_arr = std::make_unique<json::array> (); + invocations_arr->append (std::move (invocation_obj)); + run_obj->set<json::array> ("invocations", std::move (invocations_arr)); + } + + /* "originalUriBaseIds (SARIF v2.1.0 section 3.14.14). */ + if (m_seen_any_relative_paths) + { + auto orig_uri_base_ids = std::make_unique<json::object> (); + orig_uri_base_ids->set<sarif_artifact_location> + (PWD_PROPERTY_NAME, make_artifact_location_object_for_pwd ()); + run_obj->set<json::object> ("originalUriBaseIds", + std::move (orig_uri_base_ids)); + } + + /* "artifacts" property (SARIF v2.1.0 section 3.14.15). */ + auto artifacts_arr = std::make_unique<json::array> (); + for (auto iter : m_filename_to_artifact_map) + { + sarif_artifact *artifact_obj = iter.second; + if (artifact_obj->embed_contents_p ()) + artifact_obj->populate_contents (*this); + artifact_obj->populate_roles (); + artifacts_arr->append (artifact_obj); + } + run_obj->set<json::array> ("artifacts", std::move (artifacts_arr)); + m_filename_to_artifact_map.empty (); + + /* "results" property (SARIF v2.1.0 section 3.14.23). */ + run_obj->set<json::array> ("results", std::move (results)); + + /* "logicalLocations" property (SARIF v2.1.0 3.14.17). */ + if (m_cached_logical_locs->size () > 0) + { + m_cached_logical_locs->add_explicit_index_values (); + run_obj->set<json::array> ("logicalLocations", + std::move (m_cached_logical_locs)); + } + + // "graphs" property (SARIF v2.1.0 3.14.20) + if (m_run_graphs->size () > 0) + run_obj->set<json::array> ("graphs", + std::move (m_run_graphs)); + + return run_obj; +} + +/* Make a "tool" object (SARIF v2.1.0 section 3.18). */ + +std::unique_ptr<sarif_tool> +sarif_builder::make_tool_object () +{ + auto tool_obj = std::make_unique<sarif_tool> (); + + /* "driver" property (SARIF v2.1.0 section 3.18.2). */ + tool_obj->set<sarif_tool_component> ("driver", + make_driver_tool_component_object ()); + + /* Report plugins via the "extensions" property + (SARIF v2.1.0 section 3.18.3). */ + if (auto client_data_hooks = m_context.get_client_data_hooks ()) + if (const client_version_info *vinfo + = client_data_hooks->get_any_version_info ()) + { + class my_plugin_visitor : public client_version_info :: plugin_visitor + { + public: + void + on_plugin (const client_plugin_info &p) final override + { + /* Create a "toolComponent" object (SARIF v2.1.0 section 3.19) + for the plugin. */ + auto plugin_obj = std::make_unique<sarif_tool_component> (); + + /* "name" property (SARIF v2.1.0 section 3.19.8). */ + if (const char *short_name = p.get_short_name ()) + plugin_obj->set_string ("name", short_name); + + /* "fullName" property (SARIF v2.1.0 section 3.19.9). */ + if (const char *full_name = p.get_full_name ()) + plugin_obj->set_string ("fullName", full_name); + + /* "version" property (SARIF v2.1.0 section 3.19.13). */ + if (const char *version = p.get_version ()) + plugin_obj->set_string ("version", version); + + m_plugin_objs.push_back (std::move (plugin_obj)); + } + std::vector<std::unique_ptr<sarif_tool_component>> m_plugin_objs; + }; + my_plugin_visitor v; + vinfo->for_each_plugin (v); + if (v.m_plugin_objs.size () > 0) + { + auto extensions_arr = std::make_unique<json::array> (); + for (auto &iter : v.m_plugin_objs) + extensions_arr->append<sarif_tool_component> (std::move (iter)); + tool_obj->set<json::array> ("extensions", + std::move (extensions_arr)); + } + } + + /* Perhaps we could also show GMP, MPFR, MPC, isl versions as other + "extensions" (see toplev.cc: print_version). */ + + return tool_obj; +} + +/* Make a "toolComponent" object (SARIF v2.1.0 section 3.19) for what SARIF + calls the "driver" (see SARIF v2.1.0 section 3.18.1). */ + +std::unique_ptr<sarif_tool_component> +sarif_builder::make_driver_tool_component_object () +{ + auto driver_obj = std::make_unique<sarif_tool_component> (); + + if (auto client_data_hooks = m_context.get_client_data_hooks ()) + if (const client_version_info *vinfo + = client_data_hooks->get_any_version_info ()) + { + /* "name" property (SARIF v2.1.0 section 3.19.8). */ + if (const char *name = vinfo->get_tool_name ()) + driver_obj->set_string ("name", name); + + /* "fullName" property (SARIF v2.1.0 section 3.19.9). */ + if (char *full_name = vinfo->maybe_make_full_name ()) + { + driver_obj->set_string ("fullName", full_name); + free (full_name); + } + + /* "version" property (SARIF v2.1.0 section 3.19.13). */ + if (const char *version = vinfo->get_version_string ()) + driver_obj->set_string ("version", version); + + /* "informationUri" property (SARIF v2.1.0 section 3.19.17). */ + if (char *version_url = vinfo->maybe_make_version_url ()) + { + driver_obj->set_string ("informationUri", version_url); + free (version_url); + } + } + + /* "rules" property (SARIF v2.1.0 section 3.19.23). */ + driver_obj->set<json::array> ("rules", std::move (m_rules_arr)); + + return driver_obj; +} + +/* If we've seen any CWE IDs, make an array for the "taxonomies" property + (SARIF v2.1.0 section 3.14.8) of a run object, containing a single + "toolComponent" (3.19) as per 3.19.3, representing the CWE. + + Otherwise return nullptr. */ + +std::unique_ptr<json::array> +sarif_builder::maybe_make_taxonomies_array () const +{ + auto cwe_obj = maybe_make_cwe_taxonomy_object (); + if (!cwe_obj) + return nullptr; + + /* "taxonomies" property (SARIF v2.1.0 section 3.14.8). */ + auto taxonomies_arr = std::make_unique<json::array> (); + taxonomies_arr->append<sarif_tool_component> (std::move (cwe_obj)); + return taxonomies_arr; +} + +/* If we've seen any CWE IDs, make a "toolComponent" object + (SARIF v2.1.0 section 3.19) representing the CWE taxonomy, as per 3.19.3. + Populate the "taxa" property with all of the CWE IDs in m_cwe_id_set. + + Otherwise return nullptr. */ + +std::unique_ptr<sarif_tool_component> +sarif_builder::maybe_make_cwe_taxonomy_object () const +{ + if (m_cwe_id_set.is_empty ()) + return nullptr; + + auto taxonomy_obj = std::make_unique<sarif_tool_component> (); + + /* "name" property (SARIF v2.1.0 section 3.19.8). */ + taxonomy_obj->set_string ("name", "CWE"); + + /* "version" property (SARIF v2.1.0 section 3.19.13). */ + taxonomy_obj->set_string ("version", "4.7"); + + /* "organization" property (SARIF v2.1.0 section 3.19.18). */ + taxonomy_obj->set_string ("organization", "MITRE"); + + /* "shortDescription" property (SARIF v2.1.0 section 3.19.19). */ + taxonomy_obj->set<sarif_multiformat_message_string> + ("shortDescription", + make_multiformat_message_string ("The MITRE" + " Common Weakness Enumeration")); + + /* "taxa" property (SARIF v2.1.0 3.section 3.19.25). */ + auto taxa_arr = std::make_unique<json::array> (); + for (auto cwe_id : m_cwe_id_set) + taxa_arr->append<sarif_reporting_descriptor> + (make_reporting_descriptor_object_for_cwe_id (cwe_id)); + taxonomy_obj->set<json::array> ("taxa", std::move (taxa_arr)); + + return taxonomy_obj; +} + +/* Ensure that we have an "artifact" object (SARIF v2.1.0 section 3.24) + for FILENAME, adding it to m_filename_to_artifact_map if not already + found, and adding ROLE to it. + If EMBED_CONTENTS is true, then flag that we will attempt to embed the + contents of this artifact when writing it out. */ + +sarif_artifact & +sarif_builder::get_or_create_artifact (const char *filename, + enum diagnostic_artifact_role role, + bool embed_contents) +{ + if (auto *slot = m_filename_to_artifact_map.get (filename)) + { + (*slot)->add_role (role, embed_contents); + return **slot; + } + + sarif_artifact *artifact_obj = new sarif_artifact (filename); + artifact_obj->add_role (role, embed_contents); + m_filename_to_artifact_map.put (filename, artifact_obj); + + /* "location" property (SARIF v2.1.0 section 3.24.2). */ + artifact_obj->set<sarif_artifact_location> + ("location", make_artifact_location_object (filename)); + + /* "sourceLanguage" property (SARIF v2.1.0 section 3.24.10). */ + switch (role) + { + default: + gcc_unreachable (); + case diagnostic_artifact_role::analysis_target: + case diagnostic_artifact_role::result_file: + case diagnostic_artifact_role::scanned_file: + case diagnostic_artifact_role::traced_file: + /* Assume that these are in the source language. */ + if (auto client_data_hooks = m_context.get_client_data_hooks ()) + if (const char *source_lang + = client_data_hooks->maybe_get_sarif_source_language (filename)) + artifact_obj->set_string ("sourceLanguage", source_lang); + break; + + case diagnostic_artifact_role::debug_output_file: + /* Assume that these are not in the source language. */ + break; + } + + return *artifact_obj; +} + +/* Make an "artifactContent" object (SARIF v2.1.0 section 3.3) for the + full contents of FILENAME. */ + +std::unique_ptr<sarif_artifact_content> +sarif_builder::maybe_make_artifact_content_object (const char *filename) const +{ + /* Let input.cc handle any charset conversion. */ + char_span utf8_content + = m_context.get_file_cache ().get_source_file_content (filename); + if (!utf8_content) + return nullptr; + + /* Don't add it if it's not valid UTF-8. */ + if (!cpp_valid_utf8_p(utf8_content.get_buffer (), utf8_content.length ())) + return nullptr; + + auto artifact_content_obj = std::make_unique<sarif_artifact_content> (); + artifact_content_obj->set<json::string> + ("text", + std::make_unique <json::string> (utf8_content.get_buffer (), + utf8_content.length ())); + return artifact_content_obj; +} + +/* Attempt to read the given range of lines from FILENAME; return + a freshly-allocated 0-terminated buffer containing them, or nullptr. */ + +char * +sarif_builder::get_source_lines (const char *filename, + int start_line, + int end_line) const +{ + auto_vec<char> result; + + for (int line = start_line; line <= end_line; line++) + { + char_span line_content + = m_context.get_file_cache ().get_source_line (filename, line); + if (!line_content.get_buffer ()) + return nullptr; + result.reserve (line_content.length () + 1); + for (size_t i = 0; i < line_content.length (); i++) + result.quick_push (line_content[i]); + result.quick_push ('\n'); + } + result.safe_push ('\0'); + + return xstrdup (result.address ()); +} + +/* Make an "artifactContent" object (SARIF v2.1.0 section 3.3) for the given + run of lines within FILENAME (including the endpoints). + If R is non-NULL, use it to potentially set the "rendered" + property (3.3.4). */ + +std::unique_ptr<sarif_artifact_content> +sarif_builder:: +maybe_make_artifact_content_object (const char *filename, + int start_line, + int end_line, + const content_renderer *r) const +{ + char *text_utf8 = get_source_lines (filename, start_line, end_line); + + if (!text_utf8) + return nullptr; + + /* Don't add it if it's not valid UTF-8. */ + if (!cpp_valid_utf8_p(text_utf8, strlen(text_utf8))) + { + free (text_utf8); + return nullptr; + } + + auto artifact_content_obj = std::make_unique<sarif_artifact_content> (); + artifact_content_obj->set_string ("text", text_utf8); + free (text_utf8); + + /* 3.3.4 "rendered" property. */ + if (r) + if (std::unique_ptr<sarif_multiformat_message_string> rendered + = r->render (*this)) + artifact_content_obj->set ("rendered", std::move (rendered)); + + return artifact_content_obj; +} + +/* Make a "fix" object (SARIF v2.1.0 section 3.55) for RICHLOC. */ + +std::unique_ptr<sarif_fix> +sarif_builder::make_fix_object (const rich_location &richloc) +{ + auto fix_obj = std::make_unique<sarif_fix> (); + + /* "artifactChanges" property (SARIF v2.1.0 section 3.55.3). */ + /* We assume that all fix-it hints in RICHLOC affect the same file. */ + auto artifact_change_arr = std::make_unique<json::array> (); + artifact_change_arr->append<sarif_artifact_change> + (make_artifact_change_object (richloc)); + fix_obj->set<json::array> ("artifactChanges", + std::move (artifact_change_arr)); + + return fix_obj; +} + +/* Make an "artifactChange" object (SARIF v2.1.0 section 3.56) for RICHLOC. */ + +std::unique_ptr<sarif_artifact_change> +sarif_builder::make_artifact_change_object (const rich_location &richloc) +{ + auto artifact_change_obj = std::make_unique<sarif_artifact_change> (); + + /* "artifactLocation" property (SARIF v2.1.0 section 3.56.2). */ + artifact_change_obj->set<sarif_artifact_location> + ("artifactLocation", + make_artifact_location_object (richloc.get_loc ())); + + /* "replacements" property (SARIF v2.1.0 section 3.56.3). */ + auto replacement_arr = std::make_unique<json::array> (); + for (unsigned int i = 0; i < richloc.get_num_fixit_hints (); i++) + { + const fixit_hint *hint = richloc.get_fixit_hint (i); + replacement_arr->append<sarif_replacement> + (make_replacement_object (*hint)); + } + artifact_change_obj->set<json::array> ("replacements", + std::move (replacement_arr)); + + return artifact_change_obj; +} + +/* Make a "replacement" object (SARIF v2.1.0 section 3.57) for HINT. */ + +std::unique_ptr<sarif_replacement> +sarif_builder::make_replacement_object (const fixit_hint &hint) const +{ + auto replacement_obj = std::make_unique<sarif_replacement> (); + + /* "deletedRegion" property (SARIF v2.1.0 section 3.57.3). */ + replacement_obj->set<sarif_region> ("deletedRegion", + make_region_object_for_hint (hint)); + + /* "insertedContent" property (SARIF v2.1.0 section 3.57.4). */ + replacement_obj->set<sarif_artifact_content> + ("insertedContent", + make_artifact_content_object (hint.get_string ())); + + return replacement_obj; +} + +/* Make an "artifactContent" object (SARIF v2.1.0 section 3.3) for TEXT. */ + +std::unique_ptr<sarif_artifact_content> +sarif_builder::make_artifact_content_object (const char *text) const +{ + auto content_obj = std::make_unique<sarif_artifact_content> (); + + /* "text" property (SARIF v2.1.0 section 3.3.2). */ + content_obj->set_string ("text", text); + + return content_obj; +} + +/* class sarif_sink_buffer : public per_sink_buffer. */ + +void +sarif_sink_buffer::dump (FILE *out, int indent) const +{ + fprintf (out, "%*ssarif_sink_buffer:\n", indent, ""); + int idx = 0; + for (auto &result : m_results) + { + fprintf (out, "%*sresult[%i]:\n", indent + 2, "", idx); + result->dump (out, true); + fprintf (out, "\n"); + ++idx; + } +} + +bool +sarif_sink_buffer::empty_p () const +{ + return m_results.empty (); +} + +void +sarif_sink_buffer::move_to (per_sink_buffer &base) +{ + sarif_sink_buffer &dest + = static_cast<sarif_sink_buffer &> (base); + for (auto &&result : m_results) + dest.m_results.push_back (std::move (result)); + m_results.clear (); +} + +void +sarif_sink_buffer::clear () +{ + m_results.clear (); +} + +void +sarif_sink_buffer::flush () +{ + for (auto &&result : m_results) + { + result->process_worklist (m_builder); + m_builder.m_results_array->append<sarif_result> (std::move (result)); + } + m_results.clear (); +} + +class sarif_sink : public sink +{ +public: + ~sarif_sink () + { + /* Any sarifResult objects should have been handled by now. + If not, then something's gone wrong with diagnostic + groupings. */ + std::unique_ptr<sarif_result> pending_result + = m_builder.take_current_result (); + gcc_assert (!pending_result); + } + + void dump (FILE *out, int indent) const override + { + fprintf (out, "%*ssarif_sink\n", indent, ""); + sink::dump (out, indent); + } + + void + set_main_input_filename (const char *name) final override + { + m_builder.set_main_input_filename (name); + } + + std::unique_ptr<per_sink_buffer> + make_per_sink_buffer () final override + { + return std::make_unique<sarif_sink_buffer> (m_builder); + } + void set_buffer (per_sink_buffer *base_buffer) final override + { + sarif_sink_buffer *buffer + = static_cast<sarif_sink_buffer *> (base_buffer); + m_buffer = buffer; + } + + bool follows_reference_printer_p () const final override + { + return false; + } + + void update_printer () final override + { + m_printer = m_context.clone_printer (); + + /* Don't colorize the text. */ + pp_show_color (m_printer.get ()) = false; + + /* No textual URLs. */ + m_printer->set_url_format (URL_FORMAT_NONE); + + /* Use builder's token printer. */ + get_printer ()->set_token_printer (&m_builder.get_token_printer ()); + + /* Update the builder to use the new printer. */ + m_builder.set_printer (*get_printer ()); + } + + void on_begin_group () final override + { + /* No-op, */ + } + void on_end_group () final override + { + m_builder.end_group (); + } + void + on_report_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind) final override + { + m_builder.on_report_diagnostic (diagnostic, orig_diag_kind, m_buffer); + } + void on_diagram (const diagram &d) final override + { + m_builder.emit_diagram (d); + } + void after_diagnostic (const diagnostic_info &) final override + { + /* No-op. */ + } + + void + report_global_digraph (const lazily_created<digraphs::digraph> &ldg) + final override + { + m_builder.report_global_digraph (ldg); + } + + sarif_builder &get_builder () { return m_builder; } + + size_t num_results () const { return m_builder.num_results (); } + sarif_result &get_result (size_t idx) { return m_builder.get_result (idx); } + +protected: + sarif_sink (context &dc, + const line_maps *line_maps, + std::unique_ptr<sarif_serialization_format> serialization_format, + const sarif_generation_options &sarif_gen_opts) + : sink (dc), + m_builder (dc, *get_printer (), line_maps, + std::move (serialization_format), sarif_gen_opts), + m_buffer (nullptr) + {} + + sarif_builder m_builder; + sarif_sink_buffer *m_buffer; +}; + +class sarif_stream_sink : public sarif_sink +{ +public: + sarif_stream_sink (context &dc, + const line_maps *line_maps, + std::unique_ptr<sarif_serialization_format> serialization_format, + const sarif_generation_options &sarif_gen_opts, + FILE *stream) + : sarif_sink (dc, line_maps, + std::move (serialization_format), sarif_gen_opts), + m_stream (stream) + { + } + ~sarif_stream_sink () + { + m_builder.flush_to_file (m_stream); + } + bool machine_readable_stderr_p () const final override + { + return m_stream == stderr; + } +private: + FILE *m_stream; +}; + +class sarif_file_sink : public sarif_sink +{ +public: + sarif_file_sink (context &dc, + const line_maps *line_maps, + std::unique_ptr<sarif_serialization_format> serialization_format, + const sarif_generation_options &sarif_gen_opts, + output_file output_file_) + : sarif_sink (dc, line_maps, + std::move (serialization_format), + sarif_gen_opts), + m_output_file (std::move (output_file_)) + { + gcc_assert (m_output_file.get_open_file ()); + gcc_assert (m_output_file.get_filename ()); + } + ~sarif_file_sink () + { + m_builder.flush_to_file (m_output_file.get_open_file ()); + } + void dump (FILE *out, int indent) const override + { + fprintf (out, "%*ssarif_file_sink: %s\n", + indent, "", + m_output_file.get_filename ()); + sink::dump (out, indent); + } + bool machine_readable_stderr_p () const final override + { + return false; + } + +private: + output_file m_output_file; +}; + +/* Print the start of an embedded link to PP, as per 3.11.6. */ + +static void +sarif_begin_embedded_link (pretty_printer *pp) +{ + pp_character (pp, '['); +} + +/* Print the end of an embedded link to PP, as per 3.11.6. */ + +static void +sarif_end_embedded_link (pretty_printer *pp, + const char *url) +{ + pp_string (pp, "]("); + /* TODO: does the URI need escaping? + See https://github.com/oasis-tcs/sarif-spec/issues/657 */ + pp_string (pp, url); + pp_character (pp, ')'); +} + +/* class sarif_token_printer : public token_printer. */ + +/* Implementation of pretty_printer::token_printer for SARIF output. + Emit URLs as per 3.11.6 ("Messages with embedded links"). */ + +void +sarif_builder::sarif_token_printer::print_tokens (pretty_printer *pp, + const pp_token_list &tokens) +{ + /* Convert to text, possibly with colorization, URLs, etc. */ + label_text current_url; + for (auto iter = tokens.m_first; iter; iter = iter->m_next) + switch (iter->m_kind) + { + default: + gcc_unreachable (); + + case pp_token::kind::text: + { + const pp_token_text *sub = as_a <const pp_token_text *> (iter); + const char * const str = sub->m_value.get (); + if (current_url.get ()) + { + /* Write iter->m_value, but escaping any + escaped link characters as per 3.11.6. */ + for (const char *ptr = str; *ptr; ptr++) + { + const char ch = *ptr; + switch (ch) + { + default: + pp_character (pp, ch); + break; + case '\\': + case '[': + case ']': + pp_character (pp, '\\'); + pp_character (pp, ch); + break; + } + } + } + else + /* TODO: is other escaping needed? (e.g. of '[') + See https://github.com/oasis-tcs/sarif-spec/issues/658 */ + pp_string (pp, str); + } + break; + + case pp_token::kind::begin_color: + case pp_token::kind::end_color: + /* These are no-ops. */ + break; + + case pp_token::kind::begin_quote: + pp_begin_quote (pp, pp_show_color (pp)); + break; + case pp_token::kind::end_quote: + pp_end_quote (pp, pp_show_color (pp)); + break; + + /* Emit URLs as per 3.11.6 ("Messages with embedded links"). */ + case pp_token::kind::begin_url: + { + pp_token_begin_url *sub = as_a <pp_token_begin_url *> (iter); + sarif_begin_embedded_link (pp); + current_url = std::move (sub->m_value); + } + break; + case pp_token::kind::end_url: + gcc_assert (current_url.get ()); + sarif_end_embedded_link (pp, current_url.get ()); + current_url = label_text::borrow (nullptr); + break; + + case pp_token::kind::event_id: + { + pp_token_event_id *sub = as_a <pp_token_event_id *> (iter); + gcc_assert (sub->m_event_id.known_p ()); + const sarif_code_flow *code_flow + = m_builder.get_code_flow_for_event_ids (); + label_text url = make_sarif_url_for_event (code_flow, + sub->m_event_id); + if (url.get ()) + sarif_begin_embedded_link (pp); + pp_character (pp, '('); + pp_decimal_int (pp, sub->m_event_id.one_based ()); + pp_character (pp, ')'); + if (url.get ()) + sarif_end_embedded_link (pp, url.get ()); + } + break; + } +} + +/* Populate CONTEXT in preparation for SARIF output (either to stderr, or + to a file). + Return a reference to *FMT. */ + +static sink & +init_sarif_sink (context &dc, + std::unique_ptr<sarif_sink> fmt) +{ + gcc_assert (fmt); + sink &out = *fmt; + + fmt->update_printer (); + + dc.set_sink (std::move (fmt)); + + return out; +} + +/* Populate DC in preparation for SARIF output to stderr. + Return a reference to the new sink. */ + +sink & +init_sarif_stderr (context &dc, + const line_maps *line_maps, + bool formatted) +{ + gcc_assert (line_maps); + const sarif_generation_options sarif_gen_opts; + auto serialization + = std::make_unique<sarif_serialization_format_json> (formatted); + return init_sarif_sink + (dc, + std::make_unique<sarif_stream_sink> (dc, + line_maps, + std::move (serialization), + sarif_gen_opts, + stderr)); +} + +/* Attempt to open "BASE_FILE_NAME""EXTENSION" for writing. + Return a non-null output_file, + or return a null output_file and complain to DC + using LINE_MAPS. */ + +output_file +output_file::try_to_open (context &dc, + line_maps *line_maps, + const char *base_file_name, + const char *extension, + bool is_binary) +{ + gcc_assert (extension); + gcc_assert (extension[0] == '.'); + + if (!base_file_name) + { + rich_location richloc (line_maps, UNKNOWN_LOCATION); + dc.emit_diagnostic_with_group + (kind::error, richloc, nullptr, 0, + "unable to determine filename for SARIF output"); + return output_file (); + } + + label_text filename = label_text::take (concat (base_file_name, + extension, + nullptr)); + FILE *outf = fopen (filename.get (), is_binary ? "wb" : "w"); + if (!outf) + { + rich_location richloc (line_maps, UNKNOWN_LOCATION); + dc.emit_diagnostic_with_group + (kind::error, richloc, nullptr, 0, + "unable to open %qs for diagnostic output: %m", + filename.get ()); + return output_file (); + } + return output_file (outf, true, std::move (filename)); +} + +/* Attempt to open BASE_FILE_NAME.sarif for writing JSON. + Return a non-null output_file, + or return a null output_file and complain to DC + using LINE_MAPS. */ + +output_file +open_sarif_output_file (context &dc, + line_maps *line_maps, + const char *base_file_name, + enum sarif_serialization_kind serialization_kind) +{ + const char *suffix; + bool is_binary; + switch (serialization_kind) + { + default: + gcc_unreachable (); + case sarif_serialization_kind::json: + suffix = ".sarif"; + is_binary = false; + break; + } + + return output_file::try_to_open (dc, + line_maps, + base_file_name, + suffix, + is_binary); +} + +/* Populate DC in preparation for SARIF output to a file named + BASE_FILE_NAME.sarif. + Return a reference to the new sink. */ + +sink & +init_sarif_file (context &dc, + line_maps *line_maps, + bool formatted, + const char *base_file_name) +{ + gcc_assert (line_maps); + + output_file output_file_ + = open_sarif_output_file (dc, + line_maps, + base_file_name, + sarif_serialization_kind::json); + auto serialization + = std::make_unique<sarif_serialization_format_json> (formatted); + + const sarif_generation_options sarif_gen_opts; + return init_sarif_sink + (dc, + std::make_unique<sarif_file_sink> (dc, + line_maps, + std::move (serialization), + sarif_gen_opts, + std::move (output_file_))); +} + +/* Populate DC in preparation for SARIF output to STREAM. + Return a reference to the new sink. */ + +sink & +init_sarif_stream (context &dc, + const line_maps *line_maps, + bool formatted, + FILE *stream) +{ + gcc_assert (line_maps); + const sarif_generation_options sarif_gen_opts; + auto serialization + = std::make_unique<sarif_serialization_format_json> (formatted); + return init_sarif_sink + (dc, + std::make_unique<sarif_stream_sink> (dc, + line_maps, + std::move (serialization), + sarif_gen_opts, + stream)); +} + +std::unique_ptr<sink> +make_sarif_sink (context &dc, + const line_maps &line_maps, + std::unique_ptr<sarif_serialization_format> serialization, + const sarif_generation_options &sarif_gen_opts, + output_file output_file_) +{ + auto sink + = std::make_unique<sarif_file_sink> (dc, + &line_maps, + std::move (serialization), + sarif_gen_opts, + std::move (output_file_)); + sink->update_printer (); + return sink; +} + +// struct sarif_generation_options + +sarif_generation_options::sarif_generation_options () +: m_version (sarif_version::v2_1_0), + m_state_graph (false) +{ +} + +#if CHECKING_P + +namespace selftest { + +using auto_fix_quotes = ::selftest::auto_fix_quotes; +using line_table_case = ::selftest::line_table_case; + +static void +test_sarif_array_of_unique_1 () +{ + sarif_array_of_unique<json::string> arr; + + ASSERT_EQ (arr.length (), 0); + + { + size_t idx = arr.append_uniquely (std::make_unique<json::string> ("foo")); + ASSERT_EQ (idx, 0); + ASSERT_EQ (arr.length (), 1); + } + { + size_t idx = arr.append_uniquely (std::make_unique<json::string> ("bar")); + ASSERT_EQ (idx, 1); + ASSERT_EQ (arr.length (), 2); + } + + /* Try adding them again, should be idempotent. */ + { + size_t idx = arr.append_uniquely (std::make_unique<json::string> ("foo")); + ASSERT_EQ (idx, 0); + ASSERT_EQ (arr.length (), 2); + } + { + size_t idx = arr.append_uniquely (std::make_unique<json::string> ("bar")); + ASSERT_EQ (idx, 1); + ASSERT_EQ (arr.length (), 2); + } +} + +static void +test_sarif_array_of_unique_2 () +{ + sarif_array_of_unique<json::object> arr; + + ASSERT_EQ (arr.length (), 0); + + { + auto obj0 = std::make_unique<json::object> (); + size_t idx = arr.append_uniquely (std::move (obj0)); + ASSERT_EQ (idx, 0); + ASSERT_EQ (arr.length (), 1); + + // Attempting to add another empty objects should be idempotent. + idx = arr.append_uniquely (std::make_unique<json::object> ()); + ASSERT_EQ (idx, 0); + ASSERT_EQ (arr.length (), 1); + } + { + auto obj1 = std::make_unique<json::object> (); + obj1->set_string ("foo", "bar"); + size_t idx = arr.append_uniquely (std::move (obj1)); + ASSERT_EQ (idx, 1); + ASSERT_EQ (arr.length (), 2); + + // Attempting to add an equivalent object should be idempotent. + auto other = std::make_unique<json::object> (); + other->set_string ("foo", "bar"); + idx = arr.append_uniquely (std::move (other)); + ASSERT_EQ (idx, 1); + ASSERT_EQ (arr.length (), 2); + } + + // Verify behavior of add_explicit_index_values. + arr.add_explicit_index_values (); + ASSERT_JSON_INT_PROPERTY_EQ (arr[0], "index", 0); + ASSERT_JSON_INT_PROPERTY_EQ (arr[1], "index", 1); +} + +/* A subclass of sarif_sink for writing selftests. + The JSON output is cached internally, rather than written + out to a file. */ + +class test_sarif_diagnostic_context : public test_context +{ +public: + test_sarif_diagnostic_context (const char *main_input_filename, + const sarif_generation_options &sarif_gen_opts) + { + auto sink_ = std::make_unique<buffered_sink> (*this, + line_table, + true, + sarif_gen_opts); + m_sink = sink_.get (); // borrowed + init_sarif_sink (*this, std::move (sink_)); + m_sink->set_main_input_filename (main_input_filename); + } + + std::unique_ptr<sarif_log> flush_to_object () + { + return m_sink->flush_to_object (); + } + + size_t num_results () const { return m_sink->num_results (); } + sarif_result &get_result (size_t idx) { return m_sink->get_result (idx); } + +private: + class buffered_sink : public sarif_sink + { + public: + buffered_sink (context &dc, + const line_maps *line_maps, + bool formatted, + const sarif_generation_options &sarif_gen_opts) + : sarif_sink (dc, line_maps, + std::make_unique<sarif_serialization_format_json> (formatted), + sarif_gen_opts) + { + } + bool machine_readable_stderr_p () const final override + { + return false; + } + std::unique_ptr<sarif_log> flush_to_object () + { + return m_builder.flush_to_object (); + } + }; + + buffered_sink *m_sink; // borrowed +}; + +/* Test making a sarif_location for a complex rich_location + with labels and escape-on-output. */ + +static void +test_make_location_object (const sarif_generation_options &sarif_gen_opts, + const ::selftest::line_table_case &case_) +{ + source_printing_fixture_one_liner_utf8 f (case_); + location_t line_end = linemap_position_for_column (line_table, 31); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + test_context dc; + pretty_printer pp; + sarif_builder builder + (dc, pp, line_table, + std::make_unique<sarif_serialization_format_json> (true), + sarif_gen_opts); + + /* These "columns" are byte offsets, whereas later on the columns + in the generated SARIF use sarif_builder::get_sarif_column and + thus respect tabs, encoding. */ + const location_t foo + = make_location (linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 8)); + const location_t bar + = make_location (linemap_position_for_column (line_table, 12), + linemap_position_for_column (line_table, 12), + linemap_position_for_column (line_table, 17)); + const location_t field + = make_location (linemap_position_for_column (line_table, 19), + linemap_position_for_column (line_table, 19), + linemap_position_for_column (line_table, 30)); + + text_range_label label0 ("label0"); + text_range_label label1 ("label1"); + text_range_label label2 ("label2"); + + rich_location richloc (line_table, foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + richloc.set_escape_on_output (true); + + sarif_result result (0); + + std::unique_ptr<sarif_location> location_obj + = builder.make_location_object + (&result, richloc, logical_locations::key (), + diagnostic_artifact_role::analysis_target); + ASSERT_NE (location_obj, nullptr); + + auto physical_location + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (location_obj.get (), + "physicalLocation"); + { + auto region + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (physical_location, "region"); + ASSERT_JSON_INT_PROPERTY_EQ (region, "startLine", 1); + ASSERT_JSON_INT_PROPERTY_EQ (region, "startColumn", 1); + ASSERT_JSON_INT_PROPERTY_EQ (region, "endColumn", 7); + } + { + auto context_region + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (physical_location, + "contextRegion"); + ASSERT_JSON_INT_PROPERTY_EQ (context_region, "startLine", 1); + + { + auto snippet + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (context_region, "snippet"); + + /* We expect the snippet's "text" to be a copy of the content. */ + ASSERT_JSON_STRING_PROPERTY_EQ (snippet, "text", f.m_content); + + /* We expect the snippet to have a "rendered" whose "text" has a + pure ASCII escaped copy of the line (with labels, etc). */ + { + auto rendered + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (snippet, "rendered"); + ASSERT_JSON_STRING_PROPERTY_EQ + (rendered, "text", + "1 | <U+1F602>_foo = <U+03C0>_bar.<U+1F602>_field<U+03C0>;\n" + " | ^~~~~~~~~~~~~ ~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~\n" + " | | | |\n" + " | label0 label1 label2\n"); + } + } + } + auto annotations + = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (location_obj.get (), + "annotations"); + ASSERT_EQ (annotations->size (), 3); + { + { + auto a0 = (*annotations)[0]; + ASSERT_JSON_INT_PROPERTY_EQ (a0, "startLine", 1); + ASSERT_JSON_INT_PROPERTY_EQ (a0, "startColumn", 1); + ASSERT_JSON_INT_PROPERTY_EQ (a0, "endColumn", 7); + auto message + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (a0, "message"); + ASSERT_JSON_STRING_PROPERTY_EQ (message, "text", "label0"); + } + { + auto a1 = (*annotations)[1]; + ASSERT_JSON_INT_PROPERTY_EQ (a1, "startLine", 1); + ASSERT_JSON_INT_PROPERTY_EQ (a1, "startColumn", 10); + ASSERT_JSON_INT_PROPERTY_EQ (a1, "endColumn", 15); + auto message + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (a1, "message"); + ASSERT_JSON_STRING_PROPERTY_EQ (message, "text", "label1"); + } + { + auto a2 = (*annotations)[2]; + ASSERT_JSON_INT_PROPERTY_EQ (a2, "startLine", 1); + ASSERT_JSON_INT_PROPERTY_EQ (a2, "startColumn", 16); + ASSERT_JSON_INT_PROPERTY_EQ (a2, "endColumn", 25); + auto message + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (a2, "message"); + ASSERT_JSON_STRING_PROPERTY_EQ (message, "text", "label2"); + } + } +} + +/* Test of reporting a diagnostic at UNKNOWN_LOCATION to a + diagnostics::context and examining the generated sarif_log. + Verify various basic properties. */ + +static void +test_simple_log (const sarif_generation_options &sarif_gen_opts) +{ + test_sarif_diagnostic_context dc ("MAIN_INPUT_FILENAME", sarif_gen_opts); + + rich_location richloc (line_table, UNKNOWN_LOCATION); + dc.report (kind::error, richloc, nullptr, 0, "this is a test: %i", 42); + + auto log_ptr = dc.flush_to_object (); + + // 3.13 sarifLog: + auto log = log_ptr.get (); + const enum sarif_version version = sarif_gen_opts.m_version; + ASSERT_JSON_STRING_PROPERTY_EQ (log, "$schema", + sarif_version_to_url (version)); + ASSERT_JSON_STRING_PROPERTY_EQ (log, "version", + sarif_version_to_property (version)); + + auto runs = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (log, "runs"); // 3.13.4 + ASSERT_EQ (runs->size (), 1); + + // 3.14 "run" object: + auto run = (*runs)[0]; + + { + // 3.14.6: + auto tool = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (run, "tool"); + + EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (tool, "driver"); // 3.18.2 + } + + { + // 3.14.11 + auto invocations + = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (run, "invocations"); + ASSERT_EQ (invocations->size (), 1); + + { + // 3.20 "invocation" object: + auto invocation = (*invocations)[0]; + + // 3.20.3 arguments property + + // 3.20.7 startTimeUtc property + EXPECT_JSON_OBJECT_WITH_STRING_PROPERTY (invocation, "startTimeUtc"); + + // 3.20.8 endTimeUtc property + EXPECT_JSON_OBJECT_WITH_STRING_PROPERTY (invocation, "endTimeUtc"); + + // 3.20.19 workingDirectory property + { + auto wd_obj + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (invocation, + "workingDirectory"); + EXPECT_JSON_OBJECT_WITH_STRING_PROPERTY (wd_obj, "uri"); + } + + // 3.20.21 toolExecutionNotifications property + auto notifications + = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY + (invocation, "toolExecutionNotifications"); + ASSERT_EQ (notifications->size (), 0); + } + } + + { + // 3.14.15: + auto artifacts = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (run, "artifacts"); + ASSERT_EQ (artifacts->size (), 1); + + { + // 3.24 "artifact" object: + auto artifact = (*artifacts)[0]; + + // 3.24.2: + auto location + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (artifact, "location"); + ASSERT_JSON_STRING_PROPERTY_EQ (location, "uri", "MAIN_INPUT_FILENAME"); + + // 3.24.6: + auto roles = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (artifact, "roles"); + ASSERT_EQ (roles->size (), 1); + { + auto role = (*roles)[0]; + ASSERT_JSON_STRING_EQ (role, "analysisTarget"); + } + } + } + + { + // 3.14.23: + auto results = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (run, "results"); + ASSERT_EQ (results->size (), 1); + + { + // 3.27 "result" object: + auto result = (*results)[0]; + ASSERT_JSON_STRING_PROPERTY_EQ (result, "ruleId", "error"); + ASSERT_JSON_STRING_PROPERTY_EQ (result, "level", "error"); // 3.27.10 + + { + // 3.27.11: + auto message + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (result, "message"); + ASSERT_JSON_STRING_PROPERTY_EQ (message, "text", + "this is a test: 42"); + } + + // 3.27.12: + auto locations + = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (result, "locations"); + ASSERT_EQ (locations->size (), 0); + } + } +} + +/* As above, but with a "real" location_t. */ + +static void +test_simple_log_2 (const sarif_generation_options &sarif_gen_opts, + const line_table_case &case_) +{ + auto_fix_quotes fix_quotes; + + const char *const content + /* 000000000111111 + 123456789012345. */ + = "unsinged int i;\n"; + source_printing_fixture f (case_, content); + location_t line_end = linemap_position_for_column (line_table, 31); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + test_sarif_diagnostic_context dc (f.get_filename (), sarif_gen_opts); + + const location_t typo_loc + = make_location (linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 8)); + + rich_location richloc (line_table, typo_loc); + dc.report (kind::error, richloc, nullptr, 0, + "did you misspell %qs again?", + "unsigned"); + + auto log_ptr = dc.flush_to_object (); + + // 3.13 sarifLog: + auto log = log_ptr.get (); + + auto runs = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (log, "runs"); // 3.13.4 + ASSERT_EQ (runs->size (), 1); + + // 3.14 "run" object: + auto run = (*runs)[0]; + + { + // 3.14.23: + auto results = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (run, "results"); + ASSERT_EQ (results->size (), 1); + + { + // 3.27 "result" object: + auto result = (*results)[0]; + ASSERT_JSON_STRING_PROPERTY_EQ (result, "ruleId", "error"); + ASSERT_JSON_STRING_PROPERTY_EQ (result, "level", "error"); // 3.27.10 + + { + // 3.27.11: + auto message + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (result, "message"); + ASSERT_JSON_STRING_PROPERTY_EQ (message, "text", + "did you misspell `unsigned' again?"); + } + + // 3.27.12: + auto locations + = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (result, "locations"); + ASSERT_EQ (locations->size (), 1); + + { + // 3.28 "location" object: + auto location = (*locations)[0]; + + auto physical_location + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (location, + "physicalLocation"); + { + auto region + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (physical_location, + "region"); + ASSERT_JSON_INT_PROPERTY_EQ (region, "startLine", 1); + ASSERT_JSON_INT_PROPERTY_EQ (region, "startColumn", 1); + ASSERT_JSON_INT_PROPERTY_EQ (region, "endColumn", 9); + } + { + auto context_region + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (physical_location, + "contextRegion"); + ASSERT_JSON_INT_PROPERTY_EQ (context_region, "startLine", 1); + + { + auto snippet + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (context_region, + "snippet"); + + /* We expect the snippet's "text" to be a copy of the content. */ + ASSERT_JSON_STRING_PROPERTY_EQ (snippet, "text", f.m_content); + } + } + } + } + } +} + +/* Assuming that a single diagnostic has been emitted within + LOG, get a json::object for the result object. */ + +static const json::object * +get_result_from_log (const sarif_log *log) +{ + auto runs = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (log, "runs"); // 3.13.4 + ASSERT_EQ (runs->size (), 1); + + // 3.14 "run" object: + auto run = (*runs)[0]; + + // 3.14.23: + auto results = EXPECT_JSON_OBJECT_WITH_ARRAY_PROPERTY (run, "results"); + ASSERT_EQ (results->size (), 1); + + // 3.27 "result" object: + auto result = (*results)[0]; + return expect_json_object (SELFTEST_LOCATION, result); +} + +static const json::object * +get_message_from_result (const sarif_result &result) +{ + // 3.27.11: + auto message_obj + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (&result, "message"); + return message_obj; +} + +/* Assuming that a single diagnostic has been emitted to + DC, get a json::object for the messsage object within + the result. */ + +static const json::object * +get_message_from_log (const sarif_log *log) +{ + auto result_obj = get_result_from_log (log); + + // 3.27.11: + auto message_obj + = EXPECT_JSON_OBJECT_WITH_OBJECT_PROPERTY (result_obj, "message"); + return message_obj; +} + +/* Tests of messages with embedded links; see SARIF v2.1.0 3.11.6. */ + +static void +test_message_with_embedded_link (const sarif_generation_options &sarif_gen_opts) +{ + auto_fix_quotes fix_quotes; + { + test_sarif_diagnostic_context dc ("test.c", sarif_gen_opts); + rich_location richloc (line_table, UNKNOWN_LOCATION); + dc.report (kind::error, richloc, nullptr, 0, + "before %{text%} after", + "http://example.com"); + std::unique_ptr<sarif_log> log = dc.flush_to_object (); + + auto message_obj = get_message_from_log (log.get ()); + ASSERT_JSON_STRING_PROPERTY_EQ + (message_obj, "text", + "before [text](http://example.com) after"); + } + + /* Escaping in message text. + This is "EXAMPLE 1" from 3.11.6. */ + { + test_sarif_diagnostic_context dc ("test.c", sarif_gen_opts); + rich_location richloc (line_table, UNKNOWN_LOCATION); + + /* Disable "unquoted sequence of 2 consecutive punctuation + characters `]\' in format" warning. */ +#if __GNUC__ >= 10 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-diag" +#endif + dc.report (kind::error, richloc, nullptr, 0, + "Prohibited term used in %{para[0]\\spans[2]%}.", + "1"); +#if __GNUC__ >= 10 +# pragma GCC diagnostic pop +#endif + + std::unique_ptr<sarif_log> log = dc.flush_to_object (); + + auto message_obj = get_message_from_log (log.get ()); + ASSERT_JSON_STRING_PROPERTY_EQ + (message_obj, "text", + "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1)."); + /* This isn't exactly what EXAMPLE 1 of the spec has; reported as + https://github.com/oasis-tcs/sarif-spec/issues/656 */ + } + + /* Urlifier. */ + { + class test_urlifier : public urlifier + { + public: + char * + get_url_for_quoted_text (const char *p, size_t sz) const final override + { + if (!strncmp (p, "-foption", sz)) + return xstrdup ("http://example.com"); + return nullptr; + } + }; + + test_sarif_diagnostic_context dc ("test.c", sarif_gen_opts); + dc.push_owned_urlifier (std::make_unique<test_urlifier> ()); + rich_location richloc (line_table, UNKNOWN_LOCATION); + dc.report (kind::error, richloc, nullptr, 0, + "foo %<-foption%> %<unrecognized%> bar"); + std::unique_ptr<sarif_log> log = dc.flush_to_object (); + + auto message_obj = get_message_from_log (log.get ()); + ASSERT_JSON_STRING_PROPERTY_EQ + (message_obj, "text", + "foo `[-foption](http://example.com)' `unrecognized' bar"); + } +} + +/* Verify that braces in messages get escaped, as per + 3.11.5 ("Messages with placeholders"). */ + +static void +test_message_with_braces (const sarif_generation_options &sarif_gen_opts) +{ + auto_fix_quotes fix_quotes; + { + test_sarif_diagnostic_context dc ("test.c", sarif_gen_opts); + rich_location richloc (line_table, UNKNOWN_LOCATION); + dc.report (kind::error, richloc, nullptr, 0, + "open brace: %qs close brace: %qs", + "{", "}"); + std::unique_ptr<sarif_log> log = dc.flush_to_object (); + + auto message_obj = get_message_from_log (log.get ()); + ASSERT_JSON_STRING_PROPERTY_EQ + (message_obj, "text", + "open brace: `{{' close brace: `}}'"); + } +} + +static void +test_buffering (const sarif_generation_options &sarif_gen_opts) +{ + test_sarif_diagnostic_context dc ("test.c", sarif_gen_opts); + + diagnostics::buffer buf_a (dc); + diagnostics::buffer buf_b (dc); + + rich_location rich_loc (line_table, UNKNOWN_LOCATION); + + ASSERT_EQ (dc.diagnostic_count (kind::error), 0); + ASSERT_EQ (buf_a.diagnostic_count (kind::error), 0); + ASSERT_EQ (buf_b.diagnostic_count (kind::error), 0); + ASSERT_EQ (dc.num_results (), 0); + ASSERT_TRUE (buf_a.empty_p ()); + ASSERT_TRUE (buf_b.empty_p ()); + + /* Unbuffered diagnostic. */ + { + dc.report (kind::error, rich_loc, nullptr, 0, + "message 1"); + + ASSERT_EQ (dc.diagnostic_count (kind::error), 1); + ASSERT_EQ (buf_a.diagnostic_count (kind::error), 0); + ASSERT_EQ (buf_b.diagnostic_count (kind::error), 0); + ASSERT_EQ (dc.num_results (), 1); + sarif_result &result_obj = dc.get_result (0); + auto message_obj = get_message_from_result (result_obj); + ASSERT_JSON_STRING_PROPERTY_EQ (message_obj, "text", + "message 1"); + ASSERT_TRUE (buf_a.empty_p ()); + ASSERT_TRUE (buf_b.empty_p ()); + } + + /* Buffer diagnostic into buffer A. */ + { + dc.set_diagnostic_buffer (&buf_a); + dc.report (kind::error, rich_loc, nullptr, 0, + "message in buffer a"); + ASSERT_EQ (dc.diagnostic_count (kind::error), 1); + ASSERT_EQ (buf_a.diagnostic_count (kind::error), 1); + ASSERT_EQ (buf_b.diagnostic_count (kind::error), 0); + ASSERT_EQ (dc.num_results (), 1); + ASSERT_FALSE (buf_a.empty_p ()); + ASSERT_TRUE (buf_b.empty_p ()); + } + + /* Buffer diagnostic into buffer B. */ + { + dc.set_diagnostic_buffer (&buf_b); + dc.report (kind::error, rich_loc, nullptr, 0, + "message in buffer b"); + ASSERT_EQ (dc.diagnostic_count (kind::error), 1); + ASSERT_EQ (buf_a.diagnostic_count (kind::error), 1); + ASSERT_EQ (buf_b.diagnostic_count (kind::error), 1); + ASSERT_EQ (dc.num_results (), 1); + ASSERT_FALSE (buf_a.empty_p ()); + ASSERT_FALSE (buf_b.empty_p ()); + } + + /* Flush buffer B to dc. */ + { + dc.flush_diagnostic_buffer (buf_b); + ASSERT_EQ (dc.diagnostic_count (kind::error), 2); + ASSERT_EQ (buf_a.diagnostic_count (kind::error), 1); + ASSERT_EQ (buf_b.diagnostic_count (kind::error), 0); + ASSERT_EQ (dc.num_results (), 2); + sarif_result &result_1_obj = dc.get_result (1); + auto message_1_obj = get_message_from_result (result_1_obj); + ASSERT_JSON_STRING_PROPERTY_EQ (message_1_obj, "text", + "message in buffer b"); + ASSERT_FALSE (buf_a.empty_p ()); + ASSERT_TRUE (buf_b.empty_p ()); + } + + /* Clear buffer A. */ + { + dc.clear_diagnostic_buffer (buf_a); + ASSERT_EQ (dc.diagnostic_count (kind::error), 2); + ASSERT_EQ (buf_a.diagnostic_count (kind::error), 0); + ASSERT_EQ (buf_b.diagnostic_count (kind::error), 0); + ASSERT_EQ (dc.num_results (), 2); + ASSERT_TRUE (buf_a.empty_p ()); + ASSERT_TRUE (buf_b.empty_p ()); + } +} + +template <class ...ArgTypes> +static void +for_each_sarif_gen_option (void (*callback) (const sarif_generation_options &, + ArgTypes ...), + ArgTypes ...args) +{ + sarif_generation_options sarif_gen_opts; + for (int version_idx = 0; + version_idx < (int)sarif_version::num_versions; + ++version_idx) + { + sarif_gen_opts.m_version = static_cast<enum sarif_version> (version_idx); + + callback (sarif_gen_opts, args...); + } +} + +static void +run_line_table_case_tests_per_version (const line_table_case &case_) +{ + for_each_sarif_gen_option<const line_table_case &> + (test_make_location_object, case_); + + for_each_sarif_gen_option<const line_table_case &> + (test_simple_log_2, case_); +} + +/* Run all of the selftests within this file. */ + +void +sarif_sink_cc_tests () +{ + test_sarif_array_of_unique_1 (); + test_sarif_array_of_unique_2 (); + + for_each_sarif_gen_option (test_simple_log); + for_each_sarif_gen_option (test_message_with_embedded_link); + for_each_sarif_gen_option (test_message_with_braces); + for_each_sarif_gen_option (test_buffering); + + /* Run tests per (SARIF gen-option, line-table-case) pair. */ + for_each_line_table_case (run_line_table_case_tests_per_version); +} + +} // namespace diagnostics::selftest + +#endif /* CHECKING_P */ + +} // namespace diagnostics diff --git a/gcc/diagnostics/sarif-sink.h b/gcc/diagnostics/sarif-sink.h new file mode 100644 index 0000000..9f8a73f --- /dev/null +++ b/gcc/diagnostics/sarif-sink.h @@ -0,0 +1,189 @@ +/* SARIF output for diagnostics. + Copyright (C) 2023-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_SARIF_SINK_H +#define GCC_DIAGNOSTICS_SARIF_SINK_H + +#include "json.h" +#include "diagnostics/sink.h" +#include "diagnostics/output-file.h" +#include "diagnostics/logical-locations.h" + +namespace diagnostics { + +namespace digraphs { + class digraph; + class node; + class edge; +} + +/* Enum for choosing what format to serializing the generated SARIF into. */ + +enum class sarif_serialization_kind +{ + json, + + num_values +}; + +extern output_file +open_sarif_output_file (context &dc, + line_maps *line_maps, + const char *base_file_name, + enum sarif_serialization_kind serialization_kind); + +extern sink & +init_sarif_stderr (context &dc, + const line_maps *line_maps, + bool formatted); +extern sink & +init_sarif_file (context &dc, + line_maps *line_maps, + bool formatted, + const char *base_file_name); +extern sink & +init_sarif_stream (context &dc, + const line_maps *line_maps, + bool formatted, + FILE *stream); + +/* Abstract base class for handling JSON output vs other kinds of + serialization of the json tree. */ + +class sarif_serialization_format +{ +public: + virtual ~sarif_serialization_format () {} + virtual void write_to_file (FILE *outf, + const json::value &top) = 0; +}; + +/* Concrete subclass for serializing SARIF as JSON. */ + +class sarif_serialization_format_json : public sarif_serialization_format +{ +public: + sarif_serialization_format_json (bool formatted) + : m_formatted (formatted) + { + } + void write_to_file (FILE *outf, const json::value &top) final override; + +private: + bool m_formatted; +}; + +/* Control of SARIF generation. */ + +enum class sarif_version +{ + v2_1_0, + v2_2_prerelease_2024_08_08, + + num_versions +}; + +/* A bundle of state for controlling what to put in SARIF output, + such as which version of SARIF to generate + (as opposed to SARIF *serialization* options, such as formatting). */ + +struct sarif_generation_options +{ + sarif_generation_options (); + + enum sarif_version m_version; + bool m_state_graph; +}; + +extern std::unique_ptr<sink> +make_sarif_sink (context &dc, + const line_maps &line_maps, + std::unique_ptr<sarif_serialization_format> serialization_format, + const sarif_generation_options &sarif_gen_opts, + output_file output_file_); + +class sarif_builder; +class sarif_location_manager; + +/* Concrete subclass of json::object for SARIF property bags + (SARIF v2.1.0 section 3.8). */ + +class sarif_property_bag : public json::object +{ +public: + void set_logical_location (const char *property_name, + sarif_builder &, + logical_locations::key logical_loc); + void set_graph (const char *property_name, + sarif_builder &, + sarif_location_manager *sarif_location_mgr, + const digraphs::digraph &g); +}; + +/* Concrete subclass of json::object for SARIF objects that can + contain property bags (as per SARIF v2.1.0 section 3.8.1, which has: + "In addition to those properties that are explicitly documented, every + object defined in this document MAY contain a property named properties + whose value is a property bag.") */ + +class sarif_object : public json::object +{ +public: + sarif_property_bag &get_or_create_properties (); +}; + +/* Subclass of sarif_object for SARIF "graph" objects + (SARIF v2.1.0 section 3.39). */ + +class sarif_graph : public sarif_object +{ +}; + +/* Subclass of sarif_object for SARIF "node" objects + (SARIF v2.1.0 section 3.40). */ + +class sarif_node : public sarif_object +{ +}; + +/* Subclass of sarif_object for SARIF "edge" objects + (SARIF v2.1.0 section 3.41). */ + +class sarif_edge : public sarif_object +{ +}; + +extern std::unique_ptr<sarif_graph> +make_sarif_graph (const digraphs::digraph &g, + sarif_builder *builder, + sarif_location_manager *sarif_location_mgr); + +extern std::unique_ptr<sarif_node> +make_sarif_node (const digraphs::node &n, + sarif_builder *builder, + sarif_location_manager *sarif_location_mgr); + +extern std::unique_ptr<sarif_edge> +make_sarif_edge (const digraphs::edge &e, + sarif_builder *builder); + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_SARIF_SINK_H */ diff --git a/gcc/diagnostics/selftest-context.cc b/gcc/diagnostics/selftest-context.cc new file mode 100644 index 0000000..2b6dd0b --- /dev/null +++ b/gcc/diagnostics/selftest-context.cc @@ -0,0 +1,104 @@ +/* Selftest support for diagnostics. + Copyright (C) 2016-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/>. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "diagnostic.h" +#include "diagnostics/sink.h" +#include "selftest.h" +#include "diagnostics/selftest-context.h" + +/* The selftest code should entirely disappear in a production + configuration, hence we guard all of it with #if CHECKING_P. */ + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +/* Implementation of class diagnostics::selftest::test_context. */ + +test_context::test_context () +{ + diagnostic_initialize (this, 0); + pp_show_color (get_reference_printer ()) = false; + + auto &source_printing_opts = get_source_printing_options (); + source_printing_opts.enabled = true; + source_printing_opts.show_labels_p = true; + m_show_column = true; + start_span (this) = start_span_cb; + source_printing_opts.min_margin_width = 6; + source_printing_opts.max_width = 80; + pp_buffer (get_sink (0).get_printer ())->m_flush_p = false; +} + +test_context::~test_context () +{ + diagnostic_finish (this); +} + +/* Implementation of diagnostics::start_span_fn, hiding the + real filename (to avoid printing the names of tempfiles). */ + +void +test_context:: +start_span_cb (const location_print_policy &loc_policy, + to_text &html_or_text, + expanded_location exploc) +{ + exploc.file = "FILENAME"; + default_start_span_fn<to_text> + (loc_policy, html_or_text, exploc); +} + +bool +test_context::report (enum kind kind, + rich_location &richloc, + const metadata *metadata_, + option_id opt_id, + const char * fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + begin_group (); + bool result = diagnostic_impl (&richloc, metadata_, opt_id, fmt, &ap, kind); + end_group (); + va_end (ap); + return result; +} + +/* Print RICHLOC's source and annotations to this context's m_printer. + Return the text buffer from the printer. */ + +const char * +test_context::test_show_locus (rich_location &richloc) +{ + pretty_printer *pp = get_reference_printer (); + gcc_assert (pp); + source_print_policy source_policy (*this); + source_policy.print (*pp, richloc, kind::error, nullptr); + return pp_formatted_text (pp); +} + +} // namespace selftest +} // namespace diagnostics + +#endif /* #if CHECKING_P */ diff --git a/gcc/diagnostics/selftest-context.h b/gcc/diagnostics/selftest-context.h new file mode 100644 index 0000000..cfba997 --- /dev/null +++ b/gcc/diagnostics/selftest-context.h @@ -0,0 +1,93 @@ +/* Selftest support for diagnostics. + Copyright (C) 2016-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_DIAGNOSTICS_SELFTEST_CONTEXT_H +#define GCC_DIAGNOSTICS_SELFTEST_CONTEXT_H + +/* The selftest code should entirely disappear in a production + configuration, hence we guard all of it with #if CHECKING_P. */ + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +/* Convenience subclass of diagnostics::context for testing + the diagnostic subsystem. */ + +class test_context : public context +{ + public: + test_context (); + ~test_context (); + + /* Implementation of diagnostics::start_span_fn, hiding the + real filename (to avoid printing the names of tempfiles). */ + static void + start_span_cb (const location_print_policy &, + to_text &sink, + expanded_location exploc); + + /* Report a diagnostic to this context. For a selftest, this + should only be called on a context that uses a non-standard formatter + that e.g. gathers the results in memory, rather than emits to stderr. */ + bool + report (enum kind kind, + rich_location &richloc, + const metadata *, + option_id opt_id, + const char * fmt, ...) ATTRIBUTE_GCC_DIAG(6,7); + + const char *test_show_locus (rich_location &richloc); + + /* Setters for the context's source_printing_options + for use in selftests. */ + void colorize_source (bool val) + { + get_source_printing_options ().colorize_source_p = val; + } + void show_labels (bool val) + { + get_source_printing_options ().show_labels_p = val; + } + void show_line_numbers (bool val) + { + get_source_printing_options ().show_line_numbers_p = val; + } + void show_ruler (bool val) + { + get_source_printing_options ().show_ruler_p = val; + } + void show_event_links (bool val) + { + get_source_printing_options ().show_event_links_p = val; + } + void set_caret_char (unsigned idx, char ch) + { + gcc_assert (idx < rich_location::STATICALLY_ALLOCATED_RANGES); + get_source_printing_options ().caret_chars[idx] = ch; + } +}; + +} // namespace selftest +} // namespace diagnostics + +#endif /* #if CHECKING_P */ + +#endif /* GCC_DIAGNOSTICS_SELFTEST_CONTEXT_H */ diff --git a/gcc/diagnostics/selftest-logical-locations.cc b/gcc/diagnostics/selftest-logical-locations.cc new file mode 100644 index 0000000..8ba4233 --- /dev/null +++ b/gcc/diagnostics/selftest-logical-locations.cc @@ -0,0 +1,122 @@ +/* Concrete subclass of logical_locations::manager for use in selftests. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "selftest.h" +#include "diagnostics/selftest-logical-locations.h" + +#if CHECKING_P + +namespace diagnostics { +namespace logical_locations { +namespace selftest { + +/* class test_manager : public manager. */ + +test_manager::~test_manager () +{ + for (auto iter : m_name_to_item_map) + delete iter.second; +} + +const char * +test_manager::get_short_name (key k) const +{ + auto item = item_from_key (k); + if (!item) + return nullptr; + return item->m_name; +} + +const char * +test_manager::get_name_with_scope (key k) const +{ + auto item = item_from_key (k); + return item->m_name; +} + +const char * +test_manager::get_internal_name (key k) const +{ + auto item = item_from_key (k); + return item->m_name; +} + +enum diagnostics::logical_locations::kind +test_manager::get_kind (key k) const +{ + auto item = item_from_key (k); + return item->m_kind; +} + +label_text +test_manager::get_name_for_path_output (key k) const +{ + auto item = item_from_key (k); + return label_text::borrow (item->m_name); +} + +diagnostics::logical_locations::key +test_manager:: +logical_location_from_funcname (const char *funcname) +{ + const item *i = item_from_funcname (funcname); + return key::from_ptr (i); +} + +const test_manager::item * +test_manager::item_from_funcname (const char *funcname) +{ + if (!funcname) + return nullptr; + + if (item **slot = m_name_to_item_map.get (funcname)) + return *slot; + + item *i = new item (kind::function, funcname); + m_name_to_item_map.put (funcname, i); + return i; +} + +/* Run all of the selftests within this file. */ + +void +selftest_logical_locations_cc_tests () +{ + test_manager mgr; + + ASSERT_FALSE (mgr.logical_location_from_funcname (nullptr)); + + key loc_foo = mgr.logical_location_from_funcname ("foo"); + key loc_bar = mgr.logical_location_from_funcname ("bar"); + + ASSERT_NE (loc_foo, loc_bar); + + ASSERT_STREQ (mgr.get_short_name (loc_foo), "foo"); + ASSERT_STREQ (mgr.get_short_name (loc_bar), "bar"); +} + +} // namespace diagnostics::logical_locations::selftest +} // namespace diagnostics::logical_locations +} // namespace diagnostics + +#endif /* #if CHECKING_P */ diff --git a/gcc/diagnostics/selftest-logical-locations.h b/gcc/diagnostics/selftest-logical-locations.h new file mode 100644 index 0000000..c14a282 --- /dev/null +++ b/gcc/diagnostics/selftest-logical-locations.h @@ -0,0 +1,86 @@ +/* Concrete subclass of logical_locations::manager for use in selftests. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_SELFTEST_LOGICAL_LOCATIONS_H +#define GCC_DIAGNOSTICS_SELFTEST_LOGICAL_LOCATIONS_H + +#include "diagnostics/logical-locations.h" + +/* The selftest code should entirely disappear in a production + configuration, hence we guard all of it with #if CHECKING_P. */ + +#if CHECKING_P + +namespace diagnostics { +namespace logical_locations { +namespace selftest { + +/* Concrete subclass of logical_locations::manager for use in selftests. */ + +class test_manager : public manager +{ +public: + ~test_manager (); + + const char *get_short_name (key) const final override; + const char *get_name_with_scope (key) const final override; + const char *get_internal_name (key) const final override; + kind get_kind (key) const final override; + label_text get_name_for_path_output (key) const final override; + key get_parent (key) const final override + { + return key (); + } + + key + logical_location_from_funcname (const char *funcname); + +private: + struct item + { + item (kind kind_, + const char *name) + : m_kind (kind_), + m_name (name) + { + } + + kind m_kind; + const char *m_name; + }; + + const item * + item_from_funcname (const char *funcname); + + static const item *item_from_key (key k) + { + return k.cast_to<const item *> (); + } + + hash_map<nofree_string_hash, item *> m_name_to_item_map; +}; + +} // namespace diagnostics::logical_locations::selftest +} // namespace diagnostics::logical_locations:: +} // namespace diagnostics + +#endif /* #if CHECKING_P */ + +#endif /* GCC_DIAGNOSTICS_SELFTEST_LOGICAL_LOCATIONS_H. */ diff --git a/gcc/diagnostics/selftest-paths.cc b/gcc/diagnostics/selftest-paths.cc new file mode 100644 index 0000000..56ce7ff --- /dev/null +++ b/gcc/diagnostics/selftest-paths.cc @@ -0,0 +1,247 @@ +/* Concrete classes for selftests involving diagnostic paths. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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/>. */ + + +#include "config.h" +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "version.h" +#include "diagnostic.h" +#include "diagnostics/selftest-paths.h" + +#if CHECKING_P + +namespace diagnostics { +namespace paths { +namespace selftest { + +/* class test_path : public diagnostics::paths::path. */ + +test_path:: +test_path (logical_locations::selftest::test_manager &logical_loc_mgr, + pretty_printer *event_pp) +: path (logical_loc_mgr), + m_test_logical_loc_mgr (logical_loc_mgr), + m_event_pp (event_pp) +{ + add_thread ("main"); +} + +/* Implementation of path::num_events vfunc for + test_path: simply get the number of events in the vec. */ + +unsigned +test_path::num_events () const +{ + return m_events.length (); +} + +/* Implementation of path::get_event vfunc for + test_path: simply return the event in the vec. */ + +const event & +test_path::get_event (int idx) const +{ + return *m_events[idx]; +} + +unsigned +test_path::num_threads () const +{ + return m_threads.length (); +} + +const thread & +test_path::get_thread (thread_id_t idx) const +{ + return *m_threads[idx]; +} + +bool +test_path::same_function_p (int event_idx_a, + int event_idx_b) const +{ + return (m_events[event_idx_a]->get_logical_location () + == m_events[event_idx_b]->get_logical_location ()); +} + +thread_id_t +test_path::add_thread (const char *name) +{ + m_threads.safe_push (new test_thread (name)); + return m_threads.length () - 1; +} + +/* Add an event to this path at LOC within function FNDECL at + stack depth DEPTH. + + Use m_context's printer to format FMT, as the text of the new + event. + + Return the id of the new event. */ + +event_id_t +test_path::add_event (location_t loc, + const char *funcname, + int depth, + const char *fmt, ...) +{ + pretty_printer *pp = m_event_pp; + pp_clear_output_area (pp); + + rich_location rich_loc (line_table, UNKNOWN_LOCATION); + + va_list ap; + + va_start (ap, fmt); + + /* No localization is done on FMT. */ + text_info ti (fmt, &ap, 0, nullptr, &rich_loc); + pp_format (pp, &ti); + pp_output_formatted_text (pp); + + va_end (ap); + + test_event *new_event + = new test_event (loc, + logical_location_from_funcname (funcname), + depth, + pp_formatted_text (pp)); + m_events.safe_push (new_event); + + pp_clear_output_area (pp); + + return event_id_t (m_events.length () - 1); +} + +event_id_t +test_path::add_thread_event (thread_id_t thread_id, + location_t loc, + const char *funcname, + int depth, + const char *fmt, ...) +{ + pretty_printer *pp = m_event_pp; + pp_clear_output_area (pp); + + rich_location rich_loc (line_table, UNKNOWN_LOCATION); + + va_list ap; + + va_start (ap, fmt); + + /* No localization is done on FMT. */ + text_info ti (fmt, &ap, 0, nullptr, &rich_loc); + + pp_format (pp, &ti); + pp_output_formatted_text (pp); + + va_end (ap); + + test_event *new_event + = new test_event (loc, + logical_location_from_funcname (funcname), + depth, + pp_formatted_text (pp), + thread_id); + m_events.safe_push (new_event); + + pp_clear_output_area (pp); + + return event_id_t (m_events.length () - 1); +} + +/* Mark the most recent event on this path (which must exist) as being + connected to the next one to be added. */ + +void +test_path::connect_to_next_event () +{ + gcc_assert (m_events.length () > 0); + m_events[m_events.length () - 1]->connect_to_next_event (); +} + +void +test_path::add_entry (const char *callee_name, + int stack_depth, + thread_id_t thread_id) +{ + add_thread_event (thread_id, UNKNOWN_LOCATION, callee_name, stack_depth, + "entering %qs", callee_name); +} + +void +test_path::add_return (const char *caller_name, + int stack_depth, + thread_id_t thread_id) +{ + add_thread_event (thread_id, UNKNOWN_LOCATION, caller_name, stack_depth, + "returning to %qs", caller_name); +} + +void +test_path::add_call (const char *caller_name, + int caller_stack_depth, + const char *callee_name, + thread_id_t thread_id) +{ + add_thread_event (thread_id, UNKNOWN_LOCATION, + caller_name, caller_stack_depth, + "calling %qs", callee_name); + add_entry (callee_name, caller_stack_depth + 1, thread_id); +} + +logical_locations::key +test_path::logical_location_from_funcname (const char *funcname) +{ + return m_test_logical_loc_mgr.logical_location_from_funcname (funcname); +} + +/* struct test_event. */ + +/* test_event's ctor. */ + +test_event:: +test_event (location_t loc, + logical_location logical_loc, + int depth, + const char *desc, + thread_id_t thread_id) +: m_loc (loc), + m_logical_loc (logical_loc), + m_depth (depth), m_desc (xstrdup (desc)), + m_connected_to_next_event (false), + m_thread_id (thread_id) +{ +} + +/* test_event's dtor. */ + +test_event::~test_event () +{ + free (m_desc); +} + +} // namespace diagnostics::paths::selftest +} // namespace diagnostics::paths +} // namespace diagnostics + +#endif /* #if CHECKING_P */ diff --git a/gcc/diagnostics/selftest-paths.h b/gcc/diagnostics/selftest-paths.h new file mode 100644 index 0000000..fe628f6 --- /dev/null +++ b/gcc/diagnostics/selftest-paths.h @@ -0,0 +1,169 @@ +/* Concrete classes for selftests involving diagnostic paths. + Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_SELFTEST_PATHS_H +#define GCC_DIAGNOSTICS_SELFTEST_PATHS_H + +#include "diagnostics/paths.h" +#include "diagnostics/selftest-logical-locations.h" + +/* The selftest code should entirely disappear in a production + configuration, hence we guard all of it with #if CHECKING_P. */ + +#if CHECKING_P + +namespace diagnostics { +namespace paths { +namespace selftest { + +/* Concrete subclasses of the abstract base classes + declared in diagnostic-path.h for use in selftests. + + This code should have no dependency on "tree". */ + +/* An implementation of diagnostics::paths::event. */ + +class test_event : public event +{ + public: + using logical_location = logical_locations::key; + using thread_id_t = paths::thread_id_t; + + test_event (location_t loc, + logical_location logical_loc, + int depth, + const char *desc, + thread_id_t thread_id = 0); + ~test_event (); + + location_t get_location () const final override { return m_loc; } + int get_stack_depth () const final override { return m_depth; } + void print_desc (pretty_printer &pp) const final override + { + pp_string (&pp, m_desc); + } + logical_location get_logical_location () const final override + { + return m_logical_loc; + } + meaning get_meaning () const final override + { + return meaning (); + } + bool connect_to_next_event_p () const final override + { + return m_connected_to_next_event; + } + thread_id_t get_thread_id () const final override + { + return m_thread_id; + } + + void connect_to_next_event () + { + m_connected_to_next_event = true; + } + + private: + location_t m_loc; + logical_location m_logical_loc; + int m_depth; + char *m_desc; // has been formatted; doesn't get i18n-ed + bool m_connected_to_next_event; + thread_id_t m_thread_id; +}; + +/* A simple implementation of diagnostics::paths::thread. */ + +class test_thread : public thread +{ +public: + test_thread (const char *name) : m_name (name) {} + label_text get_name (bool) const final override + { + return label_text::borrow (m_name); + } + +private: + const char *m_name; // has been i18n-ed and formatted +}; + +/* A concrete subclass of diagnostics::paths::path for implementing selftests + - a vector of test_event instances + - adds member functions for adding test event + - does no translation of its events + - has no dependency on "tree". */ + +class test_path : public path +{ + public: + test_path (logical_locations::selftest::test_manager &logical_loc_mgr, + pretty_printer *event_pp); + + unsigned num_events () const final override; + const event & get_event (int idx) const final override; + unsigned num_threads () const final override; + const thread & + get_thread (thread_id_t) const final override; + bool + same_function_p (int event_idx_a, + int event_idx_b) const final override; + + thread_id_t add_thread (const char *name); + + event_id_t add_event (location_t loc, const char *funcname, int depth, + const char *fmt, ...) + ATTRIBUTE_GCC_DIAG(5,6); + event_id_t + add_thread_event (thread_id_t thread_id, + location_t loc, const char *funcname, int depth, + const char *fmt, ...) + ATTRIBUTE_GCC_DIAG(6,7); + + void connect_to_next_event (); + + void add_entry (const char *callee_name, int stack_depth, + thread_id_t thread_id = 0); + void add_return (const char *caller_name, int stack_depth, + thread_id_t thread_id = 0); + void add_call (const char *caller_name, + int caller_stack_depth, + const char *callee_name, + thread_id_t thread_id = 0); + + private: + logical_locations::key + logical_location_from_funcname (const char *funcname); + + logical_locations::selftest::test_manager &m_test_logical_loc_mgr; + auto_delete_vec<test_thread> m_threads; + auto_delete_vec<test_event> m_events; + + /* (for use by add_event). */ + pretty_printer *m_event_pp; +}; + +} // namespace diagnostics::paths::selftest +} // namespace diagnostics::paths +} // namespace diagnostics + +#endif /* #if CHECKING_P */ + +#endif /* ! GCC_DIAGNOSTICS_SELFTEST_PATHS_H */ diff --git a/gcc/diagnostics/selftest-source-printing.h b/gcc/diagnostics/selftest-source-printing.h new file mode 100644 index 0000000..451c120 --- /dev/null +++ b/gcc/diagnostics/selftest-source-printing.h @@ -0,0 +1,85 @@ +/* Support for selftests involving diagnostic_show_locus. + Copyright (C) 1999-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_DIAGNOSTICS_SELFTEST_SOURCE_PRINTING_H +#define GCC_DIAGNOSTICS_SELFTEST_SOURCE_PRINTING_H + +#include "selftest.h" +#include "diagnostics/file-cache.h" + +/* The selftest code should entirely disappear in a production + configuration, hence we guard all of it with #if CHECKING_P. */ + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +/* RAII class for use in selftests involving diagnostic_show_locus. + + Manages creating and cleaning up the following: + - writing out a temporary .c file containing CONTENT + - temporarily override the global "line_table" (using CASE_) and + push a line_map starting at the first line of the temporary file + - provide a file_cache. */ + +struct source_printing_fixture +{ + source_printing_fixture (const ::selftest::line_table_case &case_, + const char *content); + + const char *get_filename () const + { + return m_tmp_source_file.get_filename (); + } + + const char *m_content; + ::selftest::temp_source_file m_tmp_source_file; + ::selftest::line_table_test m_ltt; + file_cache m_fc; +}; + +/* Fixture for one-liner tests exercising multibyte awareness. For + simplicity we stick to using two multibyte characters in the test, U+1F602 + == "\xf0\x9f\x98\x82", which uses 4 bytes and 2 display columns, and U+03C0 + == "\xcf\x80", which uses 2 bytes and 1 display column. + + This works with the following 1-line source file: + + .0000000001111111111222222 display + .1234567890123456789012345 columns + "SS_foo = P_bar.SS_fieldP;\n" + .0000000111111111222222223 byte + .1356789012456789134567891 columns + + Here SS represents the two display columns for the U+1F602 emoji and + P represents the one display column for the U+03C0 pi symbol. */ + +struct source_printing_fixture_one_liner_utf8 + : public source_printing_fixture +{ + source_printing_fixture_one_liner_utf8 (const ::selftest::line_table_case &case_); +}; + +} // namespace diagnostics::selftest +} // namespace diagnostics + +#endif /* #if CHECKING_P */ + +#endif /* GCC_DIAGNOSTICS_SELFTEST_SOURCE_PRINTING_H */ diff --git a/gcc/diagnostics/sink.h b/gcc/diagnostics/sink.h new file mode 100644 index 0000000..ac4e0fb64 --- /dev/null +++ b/gcc/diagnostics/sink.h @@ -0,0 +1,110 @@ +/* Declarations for managing different output formats for diagnostics. + Copyright (C) 2023-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_SINK_H +#define GCC_DIAGNOSTICS_SINK_H + +#include "diagnostic.h" + +namespace diagnostics { + +class per_sink_buffer; + +/* Abstract base class for a particular output format for diagnostics; + each value of -fdiagnostics-output-format= will have its own + implementation. */ + +class sink +{ +public: + virtual ~sink () {} + + virtual void dump (FILE *out, int indent) const; + + /* Vfunc for notifying this format what the primary input file is, + e.g. for titles of HTML, for SARIF's artifact metadata. */ + virtual void set_main_input_filename (const char *) {} + + /* Vfunc for making an appropriate per_sink_buffer + subclass for this format. */ + virtual std::unique_ptr<per_sink_buffer> + make_per_sink_buffer () = 0; + + /* Vfunc to be called when call a diagnostics::buffer is set on + a diagnostics::context, to update this format. The per_sink_buffer + will be one created by make_per_sink_buffer above and thus be + of the correct subclass. */ + virtual void set_buffer (per_sink_buffer *) = 0; + + virtual void on_begin_group () = 0; + virtual void on_end_group () = 0; + + /* Vfunc with responsibility for phase 3 of formatting the message + and "printing" the result. */ + virtual void on_report_diagnostic (const diagnostic_info &, + enum kind orig_diag_kind) = 0; + + virtual void on_report_verbatim (text_info &); + + virtual void on_diagram (const diagram &diag) = 0; + virtual void after_diagnostic (const diagnostic_info &) = 0; + virtual bool machine_readable_stderr_p () const = 0; + virtual bool follows_reference_printer_p () const = 0; + + /* Vfunc called when the diagnostics::context changes its + reference printer (either to a new subclass of pretty_printer + or when color/url options change). + Subclasses should update their m_printer accordingly. */ + virtual void update_printer () = 0; + + virtual void + report_global_digraph (const lazily_created<digraphs::digraph> &) = 0; + + context &get_context () const { return m_context; } + pretty_printer *get_printer () const { return m_printer.get (); } + + text_art::theme *get_diagram_theme () const + { + return m_context.get_diagram_theme (); + } + + void DEBUG_FUNCTION dump () const { dump (stderr, 0); } + +protected: + sink (context &dc) + : m_context (dc), + m_printer (dc.clone_printer ()) + {} + +protected: + context &m_context; + std::unique_ptr<pretty_printer> m_printer; +}; + +extern void +output_format_init (context &, + const char *main_input_filename_, + const char *base_file_name, + enum diagnostics_output_format, + bool json_formatting); + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_SINK_H */ diff --git a/gcc/diagnostics/source-printing-effects.h b/gcc/diagnostics/source-printing-effects.h new file mode 100644 index 0000000..3c4b1bd --- /dev/null +++ b/gcc/diagnostics/source-printing-effects.h @@ -0,0 +1,62 @@ +/* Classes for adding special effects when quoting source code. + Copyright (C) 2024-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_SOURCE_PRINTING_EFFECTS_H +#define GCC_DIAGNOSTICS_SOURCE_PRINTING_EFFECTS_H + +namespace diagnostics { + +/* Abstract base class for describing special effects when printing + a label when quoting source code. */ + +class label_effects +{ +public: + virtual ~label_effects () {} + + /* Adding links between labels, e.g. for visualizing control flow + in execution paths. */ + virtual bool has_in_edge (unsigned range_idx) const = 0; + virtual bool has_out_edge (unsigned range_idx) const = 0; +}; + +/* A class to hold state when quoting a run of lines of source code. */ + +class source_effect_info +{ +public: + source_effect_info () + : m_leading_in_edge_column (-1), + m_trailing_out_edge_column (-1) + { + } + + /* The column for an incoming link to the first label, + or -1 if no such link. */ + int m_leading_in_edge_column; + + /* The column for an outgoing link from the final label, + or -1 if no such link. */ + int m_trailing_out_edge_column; +}; + +} // namespace diagnostics + +#endif /* GCC_DIAGNOSTICS_SOURCE_PRINTING_EFFECTS_H */ diff --git a/gcc/diagnostics/source-printing.cc b/gcc/diagnostics/source-printing.cc new file mode 100644 index 0000000..94b1c2d --- /dev/null +++ b/gcc/diagnostics/source-printing.cc @@ -0,0 +1,7052 @@ +/* Diagnostic subroutines for printing source-code + Copyright (C) 1999-2025 Free Software Foundation, Inc. + Contributed by Gabriel Dos Reis <gdr@codesourcery.com> + +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/>. */ + +#include "config.h" +#define INCLUDE_MAP +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "version.h" +#include "intl.h" +#include "diagnostic.h" +#include "diagnostics/color.h" +#include "gcc-rich-location.h" +#include "text-range-label.h" +#include "selftest.h" +#include "diagnostics/selftest-context.h" +#include "diagnostics/selftest-source-printing.h" +#include "cpplib.h" +#include "text-art/types.h" +#include "text-art/theme.h" +#include "diagnostics/source-printing-effects.h" +#include "diagnostics/file-cache.h" +#include "xml.h" +#include "xml-printer.h" + +#ifdef HAVE_TERMIOS_H +# include <termios.h> +#endif + +#ifdef GWINSZ_IN_SYS_IOCTL +# include <sys/ioctl.h> +#endif + +/* Disable warnings about quoting issues in the pp_xxx calls below + that (intentionally) don't follow GCC diagnostic conventions. */ +#if __GNUC__ >= 10 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-diag" +#endif + +/* Classes for rendering source code and diagnostics, within an + anonymous namespace. + The work is done by "class layout", which embeds and uses + "class colorizer" and "class layout_range" to get things done. */ + +namespace { + +/* The state at a given point of the source code, assuming that we're + in a range: which range are we in, and whether we should draw a caret at + this point. */ + +struct point_state +{ + int range_idx; + bool draw_caret_p; +}; + +/* A class to inject colorization codes when printing the diagnostic locus + as text. + + It has one kind of colorization for each of: + - normal text + - range 0 (the "primary location") + - range 1 + - range 2 + + The class caches the lookup of the color codes for the above. + + The class also has responsibility for tracking which of the above is + active, filtering out unnecessary changes. This allows + layout_printer::print_source_line and layout_printer::print_annotation_line + to simply request a colorization code for *every* character they print, + via this class, and have the filtering be done for them here. */ + +class colorizer +{ + public: + colorizer (pretty_printer &pp, + const rich_location &richloc, + enum diagnostics::kind diagnostic_kind); + ~colorizer (); + + void set_range (int range_idx) + { + /* If we have a specific highlight color for the range, use it. */ + if (pp_show_highlight_colors (&m_pp)) + { + const location_range *const loc_range = m_richloc.get_range (range_idx); + if (loc_range->m_highlight_color) + { + set_named_color (loc_range->m_highlight_color); + return; + } + } + + /* Otherwise, we emphasize the primary location, then alternate between + two colors for the secondary locations. + But if we're printing a run of events in a diagnostic path, that + makes no sense, so print all of them with the same colorization. */ + if (m_diagnostic_kind == diagnostics::kind::path) + set_state (0); + else + set_state (range_idx); + } + void set_cfg_edge () { set_state (0); } + void set_normal_text () { set_state (STATE_NORMAL_TEXT); } + void set_fixit_insert () { set_state (STATE_FIXIT_INSERT); } + void set_fixit_delete () { set_state (STATE_FIXIT_DELETE); } + void set_named_color (const char *color); + + private: + void set_state (int state); + void begin_state (int state); + void finish_state (int state); + const char *get_color_by_name (const char *); + + private: + static const int STATE_NORMAL_TEXT = -1; + static const int STATE_FIXIT_INSERT = -2; + static const int STATE_FIXIT_DELETE = -3; + static const int STATE_NAMED_COLOR = -4; + + pretty_printer &m_pp; + const rich_location &m_richloc; + enum diagnostics::kind m_diagnostic_kind; + int m_current_state; + const char *m_range1; + const char *m_range2; + const char *m_fixit_insert; + const char *m_fixit_delete; + const char *m_stop_color; + std::string m_current_named_color; +}; + +/* In order to handle multibyte sources properly, all of this logic needs to be + aware of the distinction between the number of bytes and the number of + display columns occupied by a character, which are not the same for non-ASCII + characters. For example, the Unicode pi symbol, U+03C0, is encoded in UTF-8 + as "\xcf\x80", and thus occupies 2 bytes of space while only occupying 1 + display column when it is output. A typical emoji, such as U+1F602 (in + UTF-8, "\xf0\x9f\x98\x82"), requires 4 bytes and has a display width of 2. + + The below example line, which is also used for selftests below, shows how the + display column and byte column are related: + + 0000000001111111111222222 display + 1234567890123456789012345 columns + SS_foo = P_bar.SS_fieldP; + 0000000111111111222222223 byte + 1356789012456789134567891 columns + + Here SS represents the two display columns for the U+1F602 emoji, and P + represents the one display column for the U+03C0 pi symbol. As an example, a + diagnostic pointing to the final P on this line is at byte column 29 and + display column 24. This reflects the fact that the three extended characters + before the final P occupy cumulatively 5 more bytes than they do display + columns (a difference of 2 for each of the two SSs, and one for the other P). + + One or the other of the two column units is more useful depending on the + context. For instance, in order to output the caret at the correct location, + we need to count display columns; in order to colorize a source line, we need + to count the bytes. All locations are provided to us as byte counts, which + we augment with the display column on demand so that it can be used when + needed. This is not the most efficient way to do things since it requires + looping over the whole line each time, but it should be fine for the purpose + of outputting diagnostics. + + In order to keep straight which units (byte or display) are in use at a + given time, the following enum lets us specify that explicitly. */ + +enum column_unit { + /* Measured in raw bytes. */ + CU_BYTES = 0, + + /* Measured in display units. */ + CU_DISPLAY_COLS, + + /* For arrays indexed by column_unit. */ + CU_NUM_UNITS +}; + +/* Utility class to augment an exploc with the corresponding display column. */ + +class exploc_with_display_col : public expanded_location +{ + public: + exploc_with_display_col (diagnostics::file_cache &fc, + const expanded_location &exploc, + const cpp_char_column_policy &policy, + enum location_aspect aspect) + : expanded_location (exploc), + m_display_col (location_compute_display_column (fc, exploc, policy)) + { + if (exploc.column > 0) + { + /* m_display_col is now the final column of the byte. + If escaping has happened, we may want the first column instead. */ + if (aspect != LOCATION_ASPECT_FINISH) + { + expanded_location prev_exploc (exploc); + prev_exploc.column--; + int prev_display_col + = (location_compute_display_column (fc, prev_exploc, policy)); + m_display_col = prev_display_col + 1; + } + } + } + + int m_display_col; +}; + + +/* A point within a layout_range; similar to an exploc_with_display_col, + but after filtering on file. */ + +class layout_point +{ + public: + layout_point (const exploc_with_display_col &exploc) + : m_line (exploc.line) + { + m_columns[CU_BYTES] = exploc.column; + m_columns[CU_DISPLAY_COLS] = exploc.m_display_col; + } + + linenum_type m_line; + int m_columns[CU_NUM_UNITS]; +}; + +/* A class for use by "class layout" below: a filtered location_range. */ + +class layout_range +{ + public: + layout_range (const exploc_with_display_col &start_exploc, + const exploc_with_display_col &finish_exploc, + enum range_display_kind range_display_kind, + const exploc_with_display_col &caret_exploc, + unsigned original_idx, + const range_label *label); + + bool contains_point (linenum_type row, int column, + enum column_unit col_unit) const; + bool intersects_line_p (linenum_type row) const; + + bool has_in_edge () const; + bool has_out_edge () const; + + layout_point m_start; + layout_point m_finish; + enum range_display_kind m_range_display_kind; + layout_point m_caret; + unsigned m_original_idx; + const range_label *m_label; +}; + +/* A struct for use by layout::print_source_line for telling + layout::print_annotation_line the extents of the source line that + it printed, so that underlines can be clipped appropriately. Units + are 1-based display columns. */ + +struct line_bounds +{ + int m_first_non_ws_disp_col; + int m_last_non_ws_disp_col; + + line_bounds () + { + m_first_non_ws_disp_col = INT_MAX; + m_last_non_ws_disp_col = 0; + } +}; + +/* A range of contiguous source lines within a layout (e.g. "lines 5-10" + or "line 23"). During the layout ctor, layout::calculate_line_spans + splits the pertinent source lines into a list of disjoint line_span + instances (e.g. lines 5-10, lines 15-20, line 23). */ + +class line_span +{ +public: + line_span (linenum_type first_line, linenum_type last_line) + : m_first_line (first_line), m_last_line (last_line) + { + gcc_assert (first_line <= last_line); + } + linenum_type get_first_line () const { return m_first_line; } + linenum_type get_last_line () const { return m_last_line; } + + bool contains_line_p (linenum_type line) const + { + return line >= m_first_line && line <= m_last_line; + } + + static int comparator (const void *p1, const void *p2) + { + const line_span *ls1 = (const line_span *)p1; + const line_span *ls2 = (const line_span *)p2; + int first_line_cmp = compare (ls1->m_first_line, ls2->m_first_line); + if (first_line_cmp) + return first_line_cmp; + return compare (ls1->m_last_line, ls2->m_last_line); + } + + linenum_type m_first_line; + linenum_type m_last_line; +}; + +#if CHECKING_P + +/* Selftests for line_span. */ + +static void +test_line_span () +{ + line_span line_one (1, 1); + ASSERT_EQ (1, line_one.get_first_line ()); + ASSERT_EQ (1, line_one.get_last_line ()); + ASSERT_FALSE (line_one.contains_line_p (0)); + ASSERT_TRUE (line_one.contains_line_p (1)); + ASSERT_FALSE (line_one.contains_line_p (2)); + + line_span lines_1_to_3 (1, 3); + ASSERT_EQ (1, lines_1_to_3.get_first_line ()); + ASSERT_EQ (3, lines_1_to_3.get_last_line ()); + ASSERT_TRUE (lines_1_to_3.contains_line_p (1)); + ASSERT_TRUE (lines_1_to_3.contains_line_p (3)); + + ASSERT_EQ (0, line_span::comparator (&line_one, &line_one)); + ASSERT_GT (line_span::comparator (&lines_1_to_3, &line_one), 0); + ASSERT_LT (line_span::comparator (&line_one, &lines_1_to_3), 0); + + /* A linenum > 2^31. */ + const linenum_type LARGEST_LINE = 0xffffffff; + line_span largest_line (LARGEST_LINE, LARGEST_LINE); + ASSERT_EQ (LARGEST_LINE, largest_line.get_first_line ()); + ASSERT_EQ (LARGEST_LINE, largest_line.get_last_line ()); + + ASSERT_GT (line_span::comparator (&largest_line, &line_one), 0); + ASSERT_LT (line_span::comparator (&line_one, &largest_line), 0); +} + +#endif /* #if CHECKING_P */ + +/* A bundle of information containing how to print unicode + characters and bytes when quoting source code. + + Provides a unified place to support escaping some subset + of characters to some format. + + Extends char_column_policy; printing is split out to avoid + libcpp having to know about pretty_printer. */ + +struct char_display_policy : public cpp_char_column_policy +{ + public: + char_display_policy (int tabstop, + int (*width_cb) (cppchar_t c), + void (*print_text_cb) (diagnostics::to_text &text_out, + const cpp_decoded_char &cp), + void (*print_html_cb) (diagnostics::to_html &html_out, + const cpp_decoded_char &cp)) + : cpp_char_column_policy (tabstop, width_cb), + m_print_text_cb (print_text_cb), + m_print_html_cb (print_html_cb) + { + } + + void (*m_print_text_cb) (diagnostics::to_text &text_out, + const cpp_decoded_char &cp); + void (*m_print_html_cb) (diagnostics::to_html &html_out, + const cpp_decoded_char &cp); +}; + +template <typename TextOrHtml> class layout_printer; + +} // anonymous namespace + +namespace diagnostics { + +/* This code is written generically to write either: + - text, to a pretty_printer, potentially with colorization codes, or + - html, to an xml::printer, with nested HTML tags. + + This is handled via a "TextOrHtml" template, which is either to_text + or to_html. */ + +/* Writing text output. */ + +struct to_text +{ + friend class layout_printer<to_text>; + + // This is a RAII class for HTML, but is a no-op for text. + struct auto_check_tag_nesting + { + auto_check_tag_nesting (to_text &) {} + }; + + to_text (pretty_printer &pp, + colorizer &colorizer) + : m_pp (pp), + m_colorizer (&colorizer) + { + m_saved_rule = pp_prefixing_rule (&m_pp); + pp_prefixing_rule (&m_pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE; + } + to_text (pretty_printer &pp, + colorizer *colorizer) + : m_pp (pp), + m_colorizer (colorizer) + { + m_saved_rule = pp_prefixing_rule (&m_pp); + pp_prefixing_rule (&m_pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE; + } + ~to_text () + { + + pp_prefixing_rule (&m_pp) = m_saved_rule; + } + + static bool is_text () { return true; } + static bool is_html () { return false; } + + void emit_text_prefix () + { + pp_emit_prefix (&m_pp); + } + + void push_html_tag (std::string, bool) + { + // no-op for text + } + void push_html_tag_with_class (std::string, std::string, bool) + { + // no-op for text + } + void pop_html_tag (const char *) + { + // no-op for text + } + + void add_html_tag_with_class (std::string, std::string, bool) + { + // no-op for text + } + + void add_space () + { + pp_space (&m_pp); + } + + void add_character (cppchar_t ch) + { + pp_unicode_character (&m_pp, ch); + } + + void add_utf8_byte (char b) + { + pp_character (&m_pp, b); + } + + void add_text (const char *text) + { + pp_string (&m_pp, text); + } + + void print_decoded_char (const char_display_policy &char_policy, + cpp_decoded_char cp) + { + char_policy.m_print_text_cb (*this, cp); + } + + void colorize_text_ensure_normal () + { + gcc_assert (m_colorizer); + m_colorizer->set_normal_text (); + } + + void colorize_text_for_range_idx (int range_idx) + { + gcc_assert (m_colorizer); + m_colorizer->set_range (range_idx); + } + + void colorize_text_for_cfg_edge () + { + gcc_assert (m_colorizer); + m_colorizer->set_cfg_edge (); + } + + void colorize_text_for_fixit_insert () + { + gcc_assert (m_colorizer); + m_colorizer->set_fixit_insert (); + } + + void colorize_text_for_fixit_delete () + { + gcc_assert (m_colorizer); + m_colorizer->set_fixit_delete (); + } + + void + invoke_start_span_fn (const source_print_policy &source_policy, + const location_print_policy &loc_policy, + const expanded_location &exploc) + { + source_policy.get_text_start_span_fn () (loc_policy, *this, exploc); + } + + // Text-specific functions + void add_newline () + { + pp_newline (&m_pp); + } + + pretty_printer &m_pp; +private: + colorizer *m_colorizer; + diagnostic_prefixing_rule_t m_saved_rule; +}; + +/* Writing HTML output. */ + +struct to_html +{ + friend class layout_printer<to_html>; + + // RAII class for ensuring that the tags nested correctly + struct auto_check_tag_nesting : public xml::auto_check_tag_nesting + { + public: + auto_check_tag_nesting (to_html &out) + : xml::auto_check_tag_nesting (out.m_xp) + { + } + }; + + to_html (xml::printer &xp, + const rich_location *richloc, + html_label_writer *html_label_writer) + : m_xp (xp), + m_richloc (richloc), + m_html_label_writer (html_label_writer) + {} + + static bool is_text () { return false; } + static bool is_html () { return true; } + + void emit_text_prefix () + { + // no-op for HTML + } + + void push_html_tag (std::string name, + bool preserve_whitespace) + { + m_xp.push_tag (std::move (name), preserve_whitespace); + } + + void push_html_tag_with_class (std::string name, + std::string class_, + bool preserve_whitespace) + { + m_xp.push_tag_with_class (std::move (name), + std::move (class_), + preserve_whitespace); + } + + void pop_html_tag (const char *expected_name) + { + m_xp.pop_tag (expected_name); + } + + void add_html_tag_with_class (std::string name, + std::string class_, + bool preserve_whitespace) + { + auto element = std::make_unique<xml::element> (std::move (name), + preserve_whitespace); + element->set_attr ("class", std::move (class_)); + m_xp.append (std::move (element)); + } + + void add_raw_html (const char *src) + { + m_xp.add_raw (src); + } + + void add_space () + { + m_xp.add_text (" "); + } + + void add_character (cppchar_t ch) + { + pp_clear_output_area (&m_scratch_pp); + pp_unicode_character (&m_scratch_pp, ch); + m_xp.add_text_from_pp (m_scratch_pp); + } + + void add_utf8_byte (char b) + { + m_xp.add_text (std::string (1, b)); + } + + void add_text (const char *text) + { + m_xp.add_text (text); + } + + void print_decoded_char (const char_display_policy &char_policy, + cpp_decoded_char cp) + { + char_policy.m_print_html_cb (*this, cp); + } + + void colorize_text_ensure_normal () + { + // no-op for HTML + } + + void colorize_text_for_cfg_edge () + { + // no-op for HTML + } + + void colorize_text_for_fixit_insert () + { + // no-op for HTML + } + + void colorize_text_for_fixit_delete () + { + // no-op for HTML + } + + void + invoke_start_span_fn (const source_print_policy &source_policy, + const location_print_policy &loc_policy, + const expanded_location &exploc) + { + source_policy.get_html_start_span_fn () (loc_policy, *this, exploc); + } + + const location_range * + get_location_range_by_idx (int range_idx) + { + if (!m_richloc) + return nullptr; + return m_richloc->get_range (range_idx); + } + + const char * + get_highlight_color_for_range_idx (int range_idx) + { + const location_range *const loc_range + = get_location_range_by_idx (range_idx); + if (!loc_range) + return nullptr; + return loc_range->m_highlight_color; + } + + xml::printer &m_xp; + const rich_location *m_richloc; +private: + html_label_writer *m_html_label_writer; + pretty_printer m_scratch_pp; +}; + +void +location_print_policy:: +print_text_span_start (const diagnostics::context &dc, + pretty_printer &pp, + const expanded_location &exploc) +{ + to_text out (pp, nullptr); + source_print_policy source_policy (dc); + source_policy.get_text_start_span_fn () (*this, out, exploc); +} + +void +location_print_policy:: +print_html_span_start (const diagnostics::context &dc, + xml::printer &xp, + const expanded_location &exploc) +{ + to_html out (xp, nullptr, nullptr); + source_print_policy source_policy (dc); + source_policy.get_html_start_span_fn () (*this, out, exploc); +} + +pretty_printer * +get_printer (to_text &out) +{ + return &out.m_pp; +} + +template<> +void +default_start_span_fn<to_text> (const location_print_policy &loc_policy, + to_text &out, + expanded_location exploc) +{ + const column_policy &column_policy_ = loc_policy.get_column_policy (); + label_text text + = column_policy_.get_location_text (exploc, + loc_policy.show_column_p (), + pp_show_color (&out.m_pp)); + pp_string (&out.m_pp, text.get ()); + pp_newline (&out.m_pp); +} + +template<> +void +default_start_span_fn<to_html> (const location_print_policy &loc_policy, + to_html &out, + expanded_location exploc) +{ + const column_policy &column_policy_ + = loc_policy.get_column_policy (); + label_text text + = column_policy_.get_location_text (exploc, + loc_policy.show_column_p (), + false); + out.m_xp.push_tag_with_class ("span", "location", true); + out.m_xp.add_text (text.get ()); + out.m_xp.pop_tag ("span"); +} + +} // namespace diagnostics + +namespace { + +/* A class to control the overall layout when printing a diagnostic. + + The layout is determined within the constructor. + + Printing the layout is handled by class layout_printer. This separation + is to avoid depending on the pretty_printer in the layout. + + We assume we have disjoint ranges. */ + +class layout +{ + public: + friend class layout_printer<diagnostics::to_text>; + friend class layout_printer<diagnostics::to_html>; + + layout (const diagnostics::source_print_policy &source_policy, + const rich_location &richloc, + diagnostics::source_effect_info *effect_info = nullptr); + + bool maybe_add_location_range (const location_range *loc_range, + unsigned original_idx, + bool restrict_to_current_line_spans); + + int get_num_line_spans () const { return m_line_spans.length (); } + const line_span *get_line_span (int idx) const { return &m_line_spans[idx]; } + + int get_linenum_width () const { return m_linenum_width; } + int get_x_offset_display () const { return m_x_offset_display; } + + bool print_heading_for_line_span_index_p (int line_span_idx) const; + + expanded_location get_expanded_location (const line_span *) const; + + void on_bad_codepoint (const char *ptr, cppchar_t ch, size_t ch_sz); + + private: + bool will_show_line_p (linenum_type row) const; + bool should_print_annotation_line_p (linenum_type row) const; + + bool annotation_line_showed_range_p (linenum_type line, int start_column, + int finish_column) const; + bool validate_fixit_hint_p (const fixit_hint *hint); + + void calculate_line_spans (); + void calculate_linenum_width (); + void calculate_x_offset_display (); + + bool + get_state_at_point (/* Inputs. */ + linenum_type row, int column, + int first_non_ws, int last_non_ws, + enum column_unit col_unit, + /* Outputs. */ + point_state *out_state) const; + + int + get_x_bound_for_row (linenum_type row, int caret_column, + int last_non_ws) const; + + private: + bool compatible_locations_p (location_t loc_a, location_t loc_b) const; + + const diagnostics::source_printing_options &m_options; + const line_maps *m_line_table; + diagnostics::file_cache &m_file_cache; + const text_art::ascii_theme m_fallback_theme; + const text_art::theme &m_theme; + diagnostics::source_effect_info *m_effect_info; + char_display_policy m_char_policy; + location_t m_primary_loc; + exploc_with_display_col m_exploc; + auto_vec <layout_range> m_layout_ranges; + auto_vec <const fixit_hint *> m_fixit_hints; + auto_vec <line_span> m_line_spans; + int m_linenum_width; + int m_x_offset_display; + bool m_escape_on_output; +}; + +class line_label; + +enum class margin_kind +{ + normal, + insertion, + ruler +}; + +/* A bundle of state for printing a particular layout + to a particular TextOrHtml (either to_text or to_html). */ +template <typename TextOrHtml> +class layout_printer +{ +public: + layout_printer (TextOrHtml &text_or_html, + const layout &layout, + bool is_diagnostic_path); + + void print (const diagnostics::source_print_policy &source_policy); + +private: + const diagnostics::source_printing_options & + get_options () const { return m_layout.m_options; } + + const text_art::theme & + get_theme () const { return m_layout.m_theme; } + + void show_ruler (int max_column); + void print_gap_in_line_numbering (); + void print_leading_fixits (linenum_type row); + void print_line (linenum_type row); + line_bounds print_source_line (linenum_type row, const char *line, + int line_bytes); + void print_leftmost_column (); + void start_annotation_line (enum margin_kind); + void end_line (); + void print_annotation_line (linenum_type row, const line_bounds lbounds); + void print_any_labels (linenum_type row); + void begin_label (int state_idx, bool is_label_text); + void end_label (int state_idx, bool is_label_text); + void print_trailing_fixits (linenum_type row); + + void + move_to_column (int *column, int dest_column, bool add_left_margin); + + void print_any_right_to_left_edge_lines (); + + void set_in_range (int range_idx); + void set_outside_range (); + +private: + TextOrHtml &m_text_or_html; + const layout &m_layout; + bool m_is_diagnostic_path; + + bool m_was_in_range_p; + int m_last_range_idx; + + /* Fields for handling links between labels (e.g. for showing CFG edges + in execution paths). + Note that the logic for printing such links makes various simplifying + assumptions about the set of labels in the rich_location, and users + of this code will need to split up labels into separate rich_location + instances to respect these assumptions, or the output will look wrong. + See the diagnostic_path-printing code for more information. */ + + /* An enum for describing the state of the leftmost column, + used for showing links between labels. + Consider e.g. + .x0000000001111111111222222222233333333334444444444. + .x1234567890123456789012345678901234567890123456789. + | | <- none + | (9) following ‘false’ branch... ->-+ <- none + | | <- none + | | <- none + |+----------------------------------------+ <- rewinding to lhs + || result->i = i; <- at lhs + || ~~~~~~~~~~^~~ <- at lhs + || | <- at lhs + |+----------->(10) ...to here <- indenting to dest + ^^ + || + |leftmost column ("x" above). + "margin". */ + enum class link_lhs_state { + none, + rewinding_to_lhs, + at_lhs, + indenting_to_dest + } m_link_lhs_state; + + /* The column of the current link on the RHS, if any, or + -1 if there is none. + Consider e.g. + .x0000000001111111111222222222233333333334444444444. + .x1234567890123456789012345678901234567890123456789. + | | <- -1 + | (10) following ‘false’ branch... ->-+ <- 42 + | | <- 42 + | | <- 42 + |+-----------------------------------------+ <- 42 + || result->i = i; <- -1 + || ~~~~~~~~~~^~~ <- -1 + || | <- -1 + |+----------->(11) ...to here <- -1. */ + int m_link_rhs_column; +}; + +/* Implementation of "class colorizer". */ + +/* The constructor for "colorizer". Lookup and store color codes for the + different kinds of things we might need to print. */ + +colorizer::colorizer (pretty_printer &pp, + const rich_location &richloc, + enum diagnostics::kind diagnostic_kind) : + m_pp (pp), + m_richloc (richloc), + m_diagnostic_kind (diagnostic_kind), + m_current_state (STATE_NORMAL_TEXT) +{ + m_range1 = get_color_by_name ("range1"); + m_range2 = get_color_by_name ("range2"); + m_fixit_insert = get_color_by_name ("fixit-insert"); + m_fixit_delete = get_color_by_name ("fixit-delete"); + m_stop_color = colorize_stop (pp_show_color (&m_pp)); +} + +/* The destructor for "colorize". If colorization is on, print a code to + turn it off. */ + +colorizer::~colorizer () +{ + finish_state (m_current_state); +} + +/* Update state, changing to the specific named color and printing its + color codes. */ + +void +colorizer::set_named_color (const char *color) +{ + if (m_current_state == STATE_NAMED_COLOR + && color == m_current_named_color) + return; + finish_state (m_current_state); + m_current_state = STATE_NAMED_COLOR; + pp_string (&m_pp, colorize_start (pp_show_color (&m_pp), color)); + m_current_named_color = color; +} + +/* Update state, printing color codes if necessary if there's a state + change. */ + +void +colorizer::set_state (int new_state) +{ + if (m_current_state != new_state) + { + finish_state (m_current_state); + m_current_state = new_state; + begin_state (new_state); + } +} + +/* Turn on any colorization for STATE. */ + +void +colorizer::begin_state (int state) +{ + switch (state) + { + case STATE_NORMAL_TEXT: + break; + + case STATE_FIXIT_INSERT: + pp_string (&m_pp, m_fixit_insert); + break; + + case STATE_FIXIT_DELETE: + pp_string (&m_pp, m_fixit_delete); + break; + + case STATE_NAMED_COLOR: + /* Should be handled by colorizer::set_named_color. */ + gcc_unreachable (); + + case 0: + /* Make range 0 be the same color as the "kind" text + (error vs warning vs note). */ + pp_string + (&m_pp, + colorize_start (pp_show_color (&m_pp), + get_color_for_kind (m_diagnostic_kind))); + break; + + case 1: + pp_string (&m_pp, m_range1); + break; + + case 2: + pp_string (&m_pp, m_range2); + break; + + default: + /* For ranges beyond 2, alternate between color 1 and color 2. */ + { + gcc_assert (state > 2); + pp_string (&m_pp, + state % 2 ? m_range1 : m_range2); + } + break; + } +} + +/* Turn off any colorization for STATE. */ + +void +colorizer::finish_state (int state) +{ + if (state != STATE_NORMAL_TEXT) + pp_string (&m_pp, m_stop_color); +} + +/* Get the color code for NAME (or the empty string if + colorization is disabled). */ + +const char * +colorizer::get_color_by_name (const char *name) +{ + return colorize_start (pp_show_color (&m_pp), name); +} + +/* Implementation of class layout_range. */ + +/* The constructor for class layout_range. + Initialize various layout_point fields from expanded_location + equivalents; we've already filtered on file. */ + +layout_range::layout_range (const exploc_with_display_col &start_exploc, + const exploc_with_display_col &finish_exploc, + enum range_display_kind range_display_kind, + const exploc_with_display_col &caret_exploc, + unsigned original_idx, + const range_label *label) +: m_start (start_exploc), + m_finish (finish_exploc), + m_range_display_kind (range_display_kind), + m_caret (caret_exploc), + m_original_idx (original_idx), + m_label (label) +{ +} + +/* Is (column, row) within the given range? + We've already filtered on the file. + + Ranges are closed (both limits are within the range). + + Example A: a single-line range: + start: (col=22, line=2) + finish: (col=38, line=2) + + |00000011111111112222222222333333333344444444444 + |34567890123456789012345678901234567890123456789 +--+----------------------------------------------- +01|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +02|bbbbbbbbbbbbbbbbbbbSwwwwwwwwwwwwwwwFaaaaaaaaaaa +03|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + Example B: a multiline range with + start: (col=14, line=3) + finish: (col=08, line=5) + + |00000011111111112222222222333333333344444444444 + |34567890123456789012345678901234567890123456789 +--+----------------------------------------------- +01|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +02|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +03|bbbbbbbbbbbSwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww +04|wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww +05|wwwwwFaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +06|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +--+----------------------------------------------- + + Legend: + - 'b' indicates a point *before* the range + - 'S' indicates the start of the range + - 'w' indicates a point within the range + - 'F' indicates the finish of the range (which is + within it). + - 'a' indicates a subsequent point *after* the range. + + COL_UNIT controls whether we check the byte column or + the display column; one or the other is more convenient + depending on the context. */ + +bool +layout_range::contains_point (linenum_type row, int column, + enum column_unit col_unit) const +{ + gcc_assert (m_start.m_line <= m_finish.m_line); + /* ...but the equivalent isn't true for the columns; + consider example B in the comment above. */ + + if (row < m_start.m_line) + /* Points before the first line of the range are + outside it (corresponding to line 01 in example A + and lines 01 and 02 in example B above). */ + return false; + + if (row == m_start.m_line) + /* On same line as start of range (corresponding + to line 02 in example A and line 03 in example B). */ + { + if (column < m_start.m_columns[col_unit]) + /* Points on the starting line of the range, but + before the column in which it begins. */ + return false; + + if (row < m_finish.m_line) + /* This is a multiline range; the point + is within it (corresponds to line 03 in example B + from column 14 onwards) */ + return true; + else + { + /* This is a single-line range. */ + gcc_assert (row == m_finish.m_line); + return column <= m_finish.m_columns[col_unit]; + } + } + + /* The point is in a line beyond that containing the + start of the range: lines 03 onwards in example A, + and lines 04 onwards in example B. */ + gcc_assert (row > m_start.m_line); + + if (row > m_finish.m_line) + /* The point is beyond the final line of the range + (lines 03 onwards in example A, and lines 06 onwards + in example B). */ + return false; + + if (row < m_finish.m_line) + { + /* The point is in a line that's fully within a multiline + range (e.g. line 04 in example B). */ + gcc_assert (m_start.m_line < m_finish.m_line); + return true; + } + + gcc_assert (row == m_finish.m_line); + + return column <= m_finish.m_columns[col_unit]; +} + +/* Does this layout_range contain any part of line ROW? */ + +bool +layout_range::intersects_line_p (linenum_type row) const +{ + gcc_assert (m_start.m_line <= m_finish.m_line); + if (row < m_start.m_line) + return false; + if (row > m_finish.m_line) + return false; + return true; +} + +/* Return true if this layout_range should have an in-edge. */ + +bool +layout_range::has_in_edge () const +{ + if (!m_label) + return false; + const diagnostics::label_effects *effects + = m_label->get_effects (m_original_idx); + if (!effects) + return false; + + return effects->has_in_edge (m_original_idx); +} + +/* Return true if this layout_range should have an out-edge. */ + +bool +layout_range::has_out_edge () const +{ + if (!m_label) + return false; + const diagnostics::label_effects *effects + = m_label->get_effects (m_original_idx); + if (!effects) + return false; + + return effects->has_out_edge (m_original_idx); +} + +#if CHECKING_P + +/* Default for when we don't care what the tab expansion is set to. */ +static const int def_tabstop = 8; + +static cpp_char_column_policy def_policy () +{ + return cpp_char_column_policy (def_tabstop, cpp_wcwidth); +} + +/* Create some expanded locations for testing layout_range. The filename + member of the explocs is set to the empty string. This member will only be + inspected by the calls to location_compute_display_column() made from the + layout_point constructors. That function will check for an empty filename + argument and not attempt to open it, rather treating the non-existent data + as if the display width were the same as the byte count. Tests exercising a + real difference between byte count and display width are performed later, + e.g. in test_diagnostic_show_locus_one_liner_utf8(). */ + +static layout_range +make_range (diagnostics::file_cache &fc, + int start_line, int start_col, int end_line, int end_col) +{ + const expanded_location start_exploc + = {"", start_line, start_col, nullptr, false}; + const expanded_location finish_exploc + = {"", end_line, end_col, nullptr, false}; + return layout_range (exploc_with_display_col (fc, + start_exploc, def_policy (), + LOCATION_ASPECT_START), + exploc_with_display_col (fc, + finish_exploc, def_policy (), + LOCATION_ASPECT_FINISH), + SHOW_RANGE_WITHOUT_CARET, + exploc_with_display_col (fc, + start_exploc, def_policy (), + LOCATION_ASPECT_CARET), + 0, nullptr); +} + +/* Selftests for layout_range::contains_point and + layout_range::intersects_line_p. */ + +/* Selftest for layout_range, where the layout_range + is a range with start==end i.e. a single point. */ + +static void +test_layout_range_for_single_point () +{ + diagnostics::file_cache fc; + layout_range point = make_range (fc, 7, 10, 7, 10); + + /* Tests for layout_range::contains_point. */ + + for (int i = 0; i != CU_NUM_UNITS; ++i) + { + const enum column_unit col_unit = (enum column_unit) i; + + /* Before the line. */ + ASSERT_FALSE (point.contains_point (6, 1, col_unit)); + + /* On the line, but before start. */ + ASSERT_FALSE (point.contains_point (7, 9, col_unit)); + + /* At the point. */ + ASSERT_TRUE (point.contains_point (7, 10, col_unit)); + + /* On the line, after the point. */ + ASSERT_FALSE (point.contains_point (7, 11, col_unit)); + + /* After the line. */ + ASSERT_FALSE (point.contains_point (8, 1, col_unit)); + } + + /* Tests for layout_range::intersects_line_p. */ + ASSERT_FALSE (point.intersects_line_p (6)); + ASSERT_TRUE (point.intersects_line_p (7)); + ASSERT_FALSE (point.intersects_line_p (8)); +} + +/* Selftest for layout_range, where the layout_range + is the single-line range shown as "Example A" above. */ + +static void +test_layout_range_for_single_line () +{ + diagnostics::file_cache fc; + layout_range example_a = make_range (fc, 2, 22, 2, 38); + + /* Tests for layout_range::contains_point. */ + + for (int i = 0; i != CU_NUM_UNITS; ++i) + { + const enum column_unit col_unit = (enum column_unit) i; + + /* Before the line. */ + ASSERT_FALSE (example_a.contains_point (1, 1, col_unit)); + + /* On the line, but before start. */ + ASSERT_FALSE (example_a.contains_point (2, 21, col_unit)); + + /* On the line, at the start. */ + ASSERT_TRUE (example_a.contains_point (2, 22, col_unit)); + + /* On the line, within the range. */ + ASSERT_TRUE (example_a.contains_point (2, 23, col_unit)); + + /* On the line, at the end. */ + ASSERT_TRUE (example_a.contains_point (2, 38, col_unit)); + + /* On the line, after the end. */ + ASSERT_FALSE (example_a.contains_point (2, 39, col_unit)); + + /* After the line. */ + ASSERT_FALSE (example_a.contains_point (2, 39, col_unit)); + } + + /* Tests for layout_range::intersects_line_p. */ + ASSERT_FALSE (example_a.intersects_line_p (1)); + ASSERT_TRUE (example_a.intersects_line_p (2)); + ASSERT_FALSE (example_a.intersects_line_p (3)); +} + +/* Selftest for layout_range, where the layout_range + is the multi-line range shown as "Example B" above. */ + +static void +test_layout_range_for_multiple_lines () +{ + diagnostics::file_cache fc; + layout_range example_b = make_range (fc, 3, 14, 5, 8); + + /* Tests for layout_range::contains_point. */ + + for (int i = 0; i != CU_NUM_UNITS; ++i) + { + const enum column_unit col_unit = (enum column_unit) i; + + /* Before first line. */ + ASSERT_FALSE (example_b.contains_point (1, 1, col_unit)); + + /* On the first line, but before start. */ + ASSERT_FALSE (example_b.contains_point (3, 13, col_unit)); + + /* At the start. */ + ASSERT_TRUE (example_b.contains_point (3, 14, col_unit)); + + /* On the first line, within the range. */ + ASSERT_TRUE (example_b.contains_point (3, 15, col_unit)); + + /* On an interior line. + The column number should not matter; try various boundary + values. */ + ASSERT_TRUE (example_b.contains_point (4, 1, col_unit)); + ASSERT_TRUE (example_b.contains_point (4, 7, col_unit)); + ASSERT_TRUE (example_b.contains_point (4, 8, col_unit)); + ASSERT_TRUE (example_b.contains_point (4, 9, col_unit)); + ASSERT_TRUE (example_b.contains_point (4, 13, col_unit)); + ASSERT_TRUE (example_b.contains_point (4, 14, col_unit)); + ASSERT_TRUE (example_b.contains_point (4, 15, col_unit)); + + /* On the final line, before the end. */ + ASSERT_TRUE (example_b.contains_point (5, 7, col_unit)); + + /* On the final line, at the end. */ + ASSERT_TRUE (example_b.contains_point (5, 8, col_unit)); + + /* On the final line, after the end. */ + ASSERT_FALSE (example_b.contains_point (5, 9, col_unit)); + + /* After the line. */ + ASSERT_FALSE (example_b.contains_point (6, 1, col_unit)); + } + + /* Tests for layout_range::intersects_line_p. */ + ASSERT_FALSE (example_b.intersects_line_p (2)); + ASSERT_TRUE (example_b.intersects_line_p (3)); + ASSERT_TRUE (example_b.intersects_line_p (4)); + ASSERT_TRUE (example_b.intersects_line_p (5)); + ASSERT_FALSE (example_b.intersects_line_p (6)); +} + +#endif /* #if CHECKING_P */ + +/* Given a source line LINE of length LINE_BYTES bytes, determine the length + (still in bytes, not display cols) without any trailing whitespace. */ + +static int +get_line_bytes_without_trailing_whitespace (const char *line, int line_bytes) +{ + int result = line_bytes; + while (result > 0) + { + char ch = line[result - 1]; + if (ch == ' ' || ch == '\t' || ch == '\r') + result--; + else + break; + } + gcc_assert (result >= 0); + gcc_assert (result <= line_bytes); + gcc_assert (result == 0 || + (line[result - 1] != ' ' + && line[result -1] != '\t' + && line[result -1] != '\r')); + return result; +} + +#if CHECKING_P + +/* A helper function for testing get_line_bytes_without_trailing_whitespace. */ + +static void +assert_eq (const char *line, int expected_bytes) +{ + int actual_value + = get_line_bytes_without_trailing_whitespace (line, strlen (line)); + ASSERT_EQ (actual_value, expected_bytes); +} + +/* Verify that get_line_bytes_without_trailing_whitespace is sane for + various inputs. It is not required to handle newlines. */ + +static void +test_get_line_bytes_without_trailing_whitespace () +{ + assert_eq ("", 0); + assert_eq (" ", 0); + assert_eq ("\t", 0); + assert_eq ("\r", 0); + assert_eq ("hello world", 11); + assert_eq ("hello world ", 11); + assert_eq ("hello world \t\t ", 11); + assert_eq ("hello world\r", 11); +} + +#endif /* #if CHECKING_P */ + +/* Helper function for layout's ctor, for sanitizing locations relative + to the primary location within a diagnostic. + + Compare LOC_A and LOC_B to see if it makes sense to print underlines + connecting their expanded locations. Doing so is only guaranteed to + make sense if the locations share the same macro expansion "history" + i.e. they can be traced through the same macro expansions, eventually + reaching an ordinary map. + + This may be too strong a condition, but it effectively sanitizes + PR c++/70105, which has an example of printing an expression where the + final location of the expression is in a different macro, which + erroneously was leading to hundreds of lines of irrelevant source + being printed. */ + +bool +layout::compatible_locations_p (location_t loc_a, location_t loc_b) const +{ + if (IS_ADHOC_LOC (loc_a)) + loc_a = get_location_from_adhoc_loc (m_line_table, loc_a); + if (IS_ADHOC_LOC (loc_b)) + loc_b = get_location_from_adhoc_loc (m_line_table, loc_b); + + /* If either location is one of the special locations outside of a + linemap, they are only compatible if they are equal. */ + if (loc_a < RESERVED_LOCATION_COUNT + || loc_b < RESERVED_LOCATION_COUNT) + return loc_a == loc_b; + + const line_map *map_a = linemap_lookup (m_line_table, loc_a); + linemap_assert (map_a); + + const line_map *map_b = linemap_lookup (m_line_table, loc_b); + linemap_assert (map_b); + + /* Are they within the same map? */ + if (map_a == map_b) + { + /* Are both within the same macro expansion? */ + if (linemap_macro_expansion_map_p (map_a)) + { + /* If so, then they're only compatible if either both are + from the macro definition, or both from the macro arguments. */ + bool loc_a_from_defn + = linemap_location_from_macro_definition_p (m_line_table, loc_a); + bool loc_b_from_defn + = linemap_location_from_macro_definition_p (m_line_table, loc_b); + if (loc_a_from_defn != loc_b_from_defn) + return false; + + /* Expand each location towards the spelling location, and + recurse. */ + const line_map_macro *macro_map = linemap_check_macro (map_a); + location_t loc_a_toward_spelling + = linemap_macro_map_loc_unwind_toward_spelling (m_line_table, + macro_map, + loc_a); + location_t loc_b_toward_spelling + = linemap_macro_map_loc_unwind_toward_spelling (m_line_table, + macro_map, + loc_b); + return compatible_locations_p (loc_a_toward_spelling, + loc_b_toward_spelling); + } + + /* Otherwise they are within the same ordinary map. */ + return true; + } + else + { + /* Within different maps. */ + + /* If either is within a macro expansion, they are incompatible. */ + if (linemap_macro_expansion_map_p (map_a) + || linemap_macro_expansion_map_p (map_b)) + return false; + + /* Within two different ordinary maps; they are compatible iff they + are in the same file. */ + const line_map_ordinary *ord_map_a = linemap_check_ordinary (map_a); + const line_map_ordinary *ord_map_b = linemap_check_ordinary (map_b); + return ord_map_a->to_file == ord_map_b->to_file; + } +} + +/* Comparator for sorting fix-it hints. */ + +static int +fixit_cmp (const void *p_a, const void *p_b) +{ + const fixit_hint * hint_a = *static_cast<const fixit_hint * const *> (p_a); + const fixit_hint * hint_b = *static_cast<const fixit_hint * const *> (p_b); + return hint_a->get_start_loc () - hint_b->get_start_loc (); +} + +/* Callbacks for use when not escaping the source. */ + +/* The default callback for char_column_policy::m_width_cb is cpp_wcwidth. */ + +/* Callback for char_display_policy::m_print_cb for printing source chars + when not escaping the source. */ + +template <class TextOrHtml> +static void +default_print_decoded_ch (TextOrHtml &text_or_html, + const cpp_decoded_char &decoded_ch) +{ + for (const char *ptr = decoded_ch.m_start_byte; + ptr != decoded_ch.m_next_byte; ptr++) + { + if (*ptr == '\0' || *ptr == '\r') + { + text_or_html.add_space (); + continue; + } + + text_or_html.add_utf8_byte (*ptr); + } +} + +/* Callbacks for use with DIAGNOSTICS_ESCAPE_FORMAT_BYTES. */ + +static const int width_per_escaped_byte = 4; + +/* Callback for char_column_policy::m_width_cb for determining the + display width when escaping with DIAGNOSTICS_ESCAPE_FORMAT_BYTES. */ + +static int +escape_as_bytes_width (cppchar_t ch) +{ + if (ch < 0x80 && ISPRINT (ch)) + return cpp_wcwidth (ch); + else + { + if (ch <= 0x7F) return 1 * width_per_escaped_byte; + if (ch <= 0x7FF) return 2 * width_per_escaped_byte; + if (ch <= 0xFFFF) return 3 * width_per_escaped_byte; + return 4 * width_per_escaped_byte; + } +} + +/* Callback for char_display_policy::m_print_cb for printing source chars + when escaping with DIAGNOSTICS_ESCAPE_FORMAT_BYTES. */ + +template <typename TextOrHtml> +static void +escape_as_bytes_print (TextOrHtml &text_or_html, + const cpp_decoded_char &decoded_ch) +{ + if (!decoded_ch.m_valid_ch) + { + for (const char *iter = decoded_ch.m_start_byte; + iter != decoded_ch.m_next_byte; ++iter) + { + char buf[16]; + sprintf (buf, "<%02x>", (unsigned char)*iter); + text_or_html.add_text (buf); + } + return; + } + + cppchar_t ch = decoded_ch.m_ch; + if (ch < 0x80 && ISPRINT (ch)) + text_or_html.add_character (ch); + else + { + for (const char *iter = decoded_ch.m_start_byte; + iter < decoded_ch.m_next_byte; ++iter) + { + char buf[16]; + sprintf (buf, "<%02x>", (unsigned char)*iter); + text_or_html.add_text (buf); + } + } +} + +/* Callbacks for use with DIAGNOSTICS_ESCAPE_FORMAT_UNICODE. */ + +/* Callback for char_column_policy::m_width_cb for determining the + display width when escaping with DIAGNOSTICS_ESCAPE_FORMAT_UNICODE. */ + +static int +escape_as_unicode_width (cppchar_t ch) +{ + if (ch < 0x80 && ISPRINT (ch)) + return cpp_wcwidth (ch); + else + { + // Width of "<U+%04x>" + if (ch > 0xfffff) + return 10; + else if (ch > 0xffff) + return 9; + else + return 8; + } +} + +/* Callback for char_display_policy::m_print_cb for printing source chars + when escaping with DIAGNOSTICS_ESCAPE_FORMAT_UNICODE. */ + +template <typename TextOrHtml> +static void +escape_as_unicode_print (TextOrHtml &text_or_html, + const cpp_decoded_char &decoded_ch) +{ + if (!decoded_ch.m_valid_ch) + { + escape_as_bytes_print<TextOrHtml> (text_or_html, decoded_ch); + return; + } + + cppchar_t ch = decoded_ch.m_ch; + if (ch < 0x80 && ISPRINT (ch)) + text_or_html.add_character (ch); + else + { + char buf[16]; + sprintf (buf, "<U+%04X>", ch); + text_or_html.add_text (buf); + } +} + +/* Populate a char_display_policy based on SOURCE_POLICY and RICHLOC. */ + +static char_display_policy +make_char_policy (const diagnostics::source_print_policy &source_policy, + const rich_location &richloc) +{ + /* The default is to not escape non-ASCII bytes. */ + char_display_policy result + (source_policy.get_column_policy ().get_tabstop (), + cpp_wcwidth, + default_print_decoded_ch<diagnostics::to_text>, + default_print_decoded_ch<diagnostics::to_html>); + + /* If the diagnostic suggests escaping non-ASCII bytes, then + use policy from user-supplied options. */ + if (richloc.escape_on_output_p ()) + { + result.m_undecoded_byte_width = width_per_escaped_byte; + switch (source_policy.get_escape_format ()) + { + default: + gcc_unreachable (); + case DIAGNOSTICS_ESCAPE_FORMAT_UNICODE: + result.m_width_cb = escape_as_unicode_width; + result.m_print_text_cb + = escape_as_unicode_print<diagnostics::to_text>; + result.m_print_html_cb + = escape_as_unicode_print<diagnostics::to_html>; + break; + case DIAGNOSTICS_ESCAPE_FORMAT_BYTES: + result.m_width_cb = escape_as_bytes_width; + result.m_print_text_cb = escape_as_bytes_print<diagnostics::to_text>; + result.m_print_html_cb = escape_as_bytes_print<diagnostics::to_html>; + break; + } + } + + return result; +} + +/* Implementation of class layout. */ + +/* Constructor for class layout. + + Filter the ranges from the rich_location to those that we can + sanely print, populating m_layout_ranges and m_fixit_hints. + Determine the range of lines that we will print, splitting them + up into an ordered list of disjoint spans of contiguous line numbers. + Determine m_x_offset_display, to ensure that the primary caret + will fit within the max_width provided by the diagnostics::context. */ + +layout::layout (const diagnostics::source_print_policy &source_policy, + const rich_location &richloc, + diagnostics::source_effect_info *effect_info) +: m_options (source_policy.get_options ()), + m_line_table (richloc.get_line_table ()), + m_file_cache (source_policy.get_file_cache ()), + /* Ensure we have a non-null m_theme. */ + m_theme (source_policy.get_diagram_theme () + ? *source_policy.get_diagram_theme () + : *static_cast <const text_art::theme *> (&m_fallback_theme)), + m_effect_info (effect_info), + m_char_policy (make_char_policy (source_policy, richloc)), + m_primary_loc (richloc.get_range (0)->m_loc), + m_exploc (m_file_cache, + richloc.get_expanded_location (0), m_char_policy, + LOCATION_ASPECT_CARET), + m_layout_ranges (richloc.get_num_locations ()), + m_fixit_hints (richloc.get_num_fixit_hints ()), + m_line_spans (1 + richloc.get_num_locations ()), + m_linenum_width (0), + m_x_offset_display (0), + m_escape_on_output (richloc.escape_on_output_p ()) +{ + for (unsigned int idx = 0; idx < richloc.get_num_locations (); idx++) + { + /* This diagnostic printer can only cope with "sufficiently sane" ranges. + Ignore any ranges that are awkward to handle. */ + const location_range *loc_range = richloc.get_range (idx); + maybe_add_location_range (loc_range, idx, false); + } + + /* Populate m_fixit_hints, filtering to only those that are in the + same file. */ + for (unsigned int i = 0; i < richloc.get_num_fixit_hints (); i++) + { + const fixit_hint *hint = richloc.get_fixit_hint (i); + if (validate_fixit_hint_p (hint)) + m_fixit_hints.safe_push (hint); + } + + /* Sort m_fixit_hints. */ + m_fixit_hints.qsort (fixit_cmp); + + /* Populate the indicated members. */ + calculate_line_spans (); + calculate_linenum_width (); + calculate_x_offset_display (); +} + + +/* Attempt to add LOC_RANGE to m_layout_ranges, filtering them to + those that we can sanely print. + + ORIGINAL_IDX is the index of LOC_RANGE within its rich_location, + (for use as extrinsic state by label ranges). + + If RESTRICT_TO_CURRENT_LINE_SPANS is true, then LOC_RANGE is also + filtered against this layout instance's current line spans: it + will only be added if the location is fully within the lines + already specified by other locations. + + Return true iff LOC_RANGE was added. */ + +bool +layout::maybe_add_location_range (const location_range *loc_range, + unsigned original_idx, + bool restrict_to_current_line_spans) +{ + gcc_assert (loc_range); + + /* Split the "range" into caret and range information. */ + source_range src_range = get_range_from_loc (m_line_table, loc_range->m_loc); + + /* Expand the various locations. */ + expanded_location start + = linemap_client_expand_location_to_spelling_point + (m_line_table, src_range.m_start, LOCATION_ASPECT_START); + expanded_location finish + = linemap_client_expand_location_to_spelling_point + (m_line_table, src_range.m_finish, LOCATION_ASPECT_FINISH); + expanded_location caret + = linemap_client_expand_location_to_spelling_point + (m_line_table, loc_range->m_loc, LOCATION_ASPECT_CARET); + + /* If any part of the range isn't in the same file as the primary + location of this diagnostic, ignore the range. */ + if (start.file != m_exploc.file) + return false; + if (finish.file != m_exploc.file) + return false; + if (loc_range->m_range_display_kind == SHOW_RANGE_WITH_CARET) + if (caret.file != m_exploc.file) + return false; + + /* Sanitize the caret location for non-primary ranges. */ + if (m_layout_ranges.length () > 0) + if (loc_range->m_range_display_kind == SHOW_RANGE_WITH_CARET) + if (!compatible_locations_p (loc_range->m_loc, m_primary_loc)) + /* Discard any non-primary ranges that can't be printed + sanely relative to the primary location. */ + return false; + + /* If there's no column information, then don't try to print + annotation lines for this range. */ + enum range_display_kind range_display_kind + = loc_range->m_range_display_kind; + if (start.column == 0 + || finish.column == 0 + || caret.column == 0) + range_display_kind = SHOW_LINES_WITHOUT_RANGE; + + /* Everything is now known to be in the correct source file, + but it may require further sanitization. */ + layout_range ri (exploc_with_display_col (m_file_cache, + start, m_char_policy, + LOCATION_ASPECT_START), + exploc_with_display_col (m_file_cache, + finish, m_char_policy, + LOCATION_ASPECT_FINISH), + range_display_kind, + exploc_with_display_col (m_file_cache, + caret, m_char_policy, + LOCATION_ASPECT_CARET), + original_idx, loc_range->m_label); + + /* If we have a range that finishes before it starts (perhaps + from something built via macro expansion), printing the + range is likely to be nonsensical. Also, attempting to do so + breaks assumptions within the printing code (PR c/68473). + Similarly, don't attempt to print ranges if one or both ends + of the range aren't sane to print relative to the + primary location (PR c++/70105). */ + if (start.line > finish.line + || !compatible_locations_p (src_range.m_start, m_primary_loc) + || !compatible_locations_p (src_range.m_finish, m_primary_loc)) + { + /* Is this the primary location? */ + if (m_layout_ranges.length () == 0) + { + /* We want to print the caret for the primary location, but + we must sanitize away m_start and m_finish. */ + ri.m_start = ri.m_caret; + ri.m_finish = ri.m_caret; + } + else + /* This is a non-primary range; ignore it. */ + return false; + } + + /* Potentially filter to just the lines already specified by other + locations. This is for use by gcc_rich_location::add_location_if_nearby. + The layout ctor doesn't use it, and can't because m_line_spans + hasn't been set up at that point. */ + if (restrict_to_current_line_spans) + { + if (!will_show_line_p (start.line)) + return false; + if (!will_show_line_p (finish.line)) + return false; + if (loc_range->m_range_display_kind == SHOW_RANGE_WITH_CARET) + if (!will_show_line_p (caret.line)) + return false; + } + + /* Passed all the tests; add the range to m_layout_ranges so that + it will be printed. */ + m_layout_ranges.safe_push (ri); + return true; +} + +/* Return true iff ROW is within one of the line spans for this layout. */ + +bool +layout::will_show_line_p (linenum_type row) const +{ + for (int line_span_idx = 0; line_span_idx < get_num_line_spans (); + line_span_idx++) + { + const line_span *line_span = get_line_span (line_span_idx); + if (line_span->contains_line_p (row)) + return true; + } + return false; +} + +/* Print a line showing a gap in the line numbers, for showing the boundary + between two line spans. */ + +template<> +void +layout_printer<diagnostics::to_text>::print_gap_in_line_numbering () +{ + gcc_assert (m_layout.m_options.show_line_numbers_p); + + m_text_or_html.emit_text_prefix (); + + for (int i = 0; i < m_layout.get_linenum_width () + 1; i++) + m_text_or_html.add_character ('.'); + + m_text_or_html.add_newline (); +} + +template<> +void +layout_printer<diagnostics::to_html>::print_gap_in_line_numbering () +{ + gcc_assert (m_layout.m_options.show_line_numbers_p); + + m_text_or_html.add_raw_html + ("<tbody class=\"line-span-jump\">\n" + "<tr class=\"line-span-jump-row\">" + "<td class=\"linenum-gap\">[...]</td>" + "<td class=\"source-gap\"/></tr>\n" + "</tbody>\n"); +} + +/* Return true iff we should print a heading when starting the + line span with the given index. */ + +bool +layout::print_heading_for_line_span_index_p (int line_span_idx) const +{ + /* We print a heading for every change of line span, hence for every + line span after the initial one. */ + if (line_span_idx > 0) + return true; + + /* We also do it for the initial span if the primary location of the + diagnostic is in a different span. */ + if (m_exploc.line > (int)get_line_span (0)->m_last_line) + return true; + + return false; +} + +/* Get an expanded_location for the first location of interest within + the given line_span. + Used when printing a heading to indicate a new line span. */ + +expanded_location +layout::get_expanded_location (const line_span *line_span) const +{ + /* Whenever possible, use the caret location. */ + if (line_span->contains_line_p (m_exploc.line)) + return m_exploc; + + /* Otherwise, use the start of the first range that's present + within the line_span. */ + for (unsigned int i = 0; i < m_layout_ranges.length (); i++) + { + const layout_range *lr = &m_layout_ranges[i]; + if (line_span->contains_line_p (lr->m_start.m_line)) + { + expanded_location exploc = m_exploc; + exploc.line = lr->m_start.m_line; + exploc.column = lr->m_start.m_columns[CU_BYTES]; + return exploc; + } + } + + /* Otherwise, use the location of the first fixit-hint present within + the line_span. */ + for (unsigned int i = 0; i < m_fixit_hints.length (); i++) + { + const fixit_hint *hint = m_fixit_hints[i]; + location_t loc = hint->get_start_loc (); + expanded_location exploc = expand_location (loc); + if (line_span->contains_line_p (exploc.line)) + return exploc; + } + + /* It should not be possible to have a line span that didn't + contain any of the layout_range or fixit_hint instances. */ + gcc_unreachable (); + return m_exploc; +} + +/* Determine if HINT is meaningful to print within this layout. */ + +bool +layout::validate_fixit_hint_p (const fixit_hint *hint) +{ + if (LOCATION_FILE (hint->get_start_loc ()) != m_exploc.file) + return false; + if (LOCATION_FILE (hint->get_next_loc ()) != m_exploc.file) + return false; + + return true; +} + +/* Determine the range of lines affected by HINT. + This assumes that HINT has already been filtered by + validate_fixit_hint_p, and so affects the correct source file. */ + +static line_span +get_line_span_for_fixit_hint (const fixit_hint *hint) +{ + gcc_assert (hint); + + int start_line = LOCATION_LINE (hint->get_start_loc ()); + + /* For line-insertion fix-it hints, add the previous line to the + span, to give the user more context on the proposed change. */ + if (hint->ends_with_newline_p ()) + if (start_line > 1) + start_line--; + + return line_span (start_line, + LOCATION_LINE (hint->get_next_loc ())); +} + +/* We want to print the pertinent source code at a diagnostic. The + rich_location can contain multiple locations. This will have been + filtered into m_exploc (the caret for the primary location) and + m_layout_ranges, for those ranges within the same source file. + + We will print a subset of the lines within the source file in question, + as a collection of "spans" of lines. + + This function populates m_line_spans with an ordered, disjoint list of + the line spans of interest. + + Printing a gap between line spans takes one line, so, when printing + line numbers, we allow a gap of up to one line between spans when + merging, since it makes more sense to print the source line rather than a + "gap-in-line-numbering" line. When not printing line numbers, it's + better to be more explicit about what's going on, so keeping them as + separate spans is preferred. + + For example, if the primary range is on lines 8-10, with secondary ranges + covering lines 5-6 and lines 13-15: + + 004 + 005 |RANGE 1 + 006 |RANGE 1 + 007 + 008 |PRIMARY RANGE + 009 |PRIMARY CARET + 010 |PRIMARY RANGE + 011 + 012 + 013 |RANGE 2 + 014 |RANGE 2 + 015 |RANGE 2 + 016 + + With line numbering on, we want two spans: lines 5-10 and lines 13-15. + + With line numbering off (with span headers), we want three spans: lines 5-6, + lines 8-10, and lines 13-15. */ + +void +layout::calculate_line_spans () +{ + /* This should only be called once, by the ctor. */ + gcc_assert (m_line_spans.length () == 0); + + /* Populate tmp_spans with individual spans, for each of + m_exploc, and for m_layout_ranges. */ + auto_vec<line_span> tmp_spans (1 + m_layout_ranges.length ()); + tmp_spans.safe_push (line_span (m_exploc.line, m_exploc.line)); + for (unsigned int i = 0; i < m_layout_ranges.length (); i++) + { + const layout_range *lr = &m_layout_ranges[i]; + gcc_assert (lr->m_start.m_line <= lr->m_finish.m_line); + tmp_spans.safe_push (line_span (lr->m_start.m_line, + lr->m_finish.m_line)); + } + + /* Also add spans for any fix-it hints, in case they cover other lines. */ + for (unsigned int i = 0; i < m_fixit_hints.length (); i++) + { + const fixit_hint *hint = m_fixit_hints[i]; + gcc_assert (hint); + tmp_spans.safe_push (get_line_span_for_fixit_hint (hint)); + } + + /* Sort them. */ + tmp_spans.qsort(line_span::comparator); + + /* Now iterate through tmp_spans, copying into m_line_spans, and + combining where possible. */ + gcc_assert (tmp_spans.length () > 0); + m_line_spans.safe_push (tmp_spans[0]); + for (unsigned int i = 1; i < tmp_spans.length (); i++) + { + line_span *current = &m_line_spans[m_line_spans.length () - 1]; + const line_span *next = &tmp_spans[i]; + gcc_assert (next->m_first_line >= current->m_first_line); + const int merger_distance = m_options.show_line_numbers_p ? 1 : 0; + if ((linenum_arith_t)next->m_first_line + <= (linenum_arith_t)current->m_last_line + 1 + merger_distance) + { + /* We can merge them. */ + if (next->m_last_line > current->m_last_line) + current->m_last_line = next->m_last_line; + } + else + { + /* No merger possible. */ + m_line_spans.safe_push (*next); + } + } + + /* Verify the result, in m_line_spans. */ + gcc_assert (m_line_spans.length () > 0); + for (unsigned int i = 1; i < m_line_spans.length (); i++) + { + const line_span *prev = &m_line_spans[i - 1]; + const line_span *next = &m_line_spans[i]; + /* The individual spans must be sane. */ + gcc_assert (prev->m_first_line <= prev->m_last_line); + gcc_assert (next->m_first_line <= next->m_last_line); + /* The spans must be ordered. */ + gcc_assert (prev->m_first_line < next->m_first_line); + /* There must be a gap of at least one line between separate spans. */ + gcc_assert ((prev->m_last_line + 1) < next->m_first_line); + } +} + +/* Determine how many display columns need to be reserved for line numbers, + based on the largest line number that will be needed, and populate + m_linenum_width. */ + +void +layout::calculate_linenum_width () +{ + gcc_assert (m_line_spans.length () > 0); + const line_span *last_span = &m_line_spans[m_line_spans.length () - 1]; + int highest_line = last_span->m_last_line; + if (highest_line < 0) + highest_line = 0; + m_linenum_width = diagnostics::num_digits (highest_line); + /* If we're showing jumps in the line-numbering, allow at least 3 chars. */ + if (m_line_spans.length () > 1) + m_linenum_width = MAX (m_linenum_width, 3); + /* If there's a minimum margin width, apply it (subtracting 1 for the space + after the line number. */ + m_linenum_width = MAX (m_linenum_width, m_options.min_margin_width - 1); +} + +/* Calculate m_x_offset_display, which improves readability in case the source + line of interest is longer than the user's display. All lines output will be + shifted to the left (so that their beginning is no longer displayed) by + m_x_offset_display display columns, so that the caret is in a reasonable + location. */ + +void +layout::calculate_x_offset_display () +{ + m_x_offset_display = 0; + + const int max_width = m_options.max_width; + if (!max_width) + { + /* Nothing to do, the width is not capped. */ + return; + } + + const diagnostics::char_span line + = m_file_cache.get_source_line (m_exploc.file, + m_exploc.line); + if (!line) + { + /* Nothing to do, we couldn't find the source line. */ + return; + } + int caret_display_column = m_exploc.m_display_col; + const int line_bytes + = get_line_bytes_without_trailing_whitespace (line.get_buffer (), + line.length ()); + int eol_display_column + = cpp_display_width (line.get_buffer (), line_bytes, m_char_policy); + if (caret_display_column > eol_display_column + || !caret_display_column) + { + /* This does not make sense, so don't try to do anything in this case. */ + return; + } + + /* Adjust caret and eol positions to include the left margin. If we are + outputting line numbers, then the left margin is equal to m_linenum_width + plus three for the " | " which follows it. Otherwise the left margin width + is equal to 1, because layout::print_source_line() will prefix each line + with a space. */ + const int source_display_cols = eol_display_column; + int left_margin_size = 1; + if (m_options.show_line_numbers_p) + left_margin_size = m_linenum_width + 3; + caret_display_column += left_margin_size; + eol_display_column += left_margin_size; + + if (eol_display_column <= max_width) + { + /* Nothing to do, everything fits in the display. */ + return; + } + + /* The line is too long for the display. Calculate an offset such that the + caret is not too close to the right edge of the screen. It will be + CARET_LINE_MARGIN display columns from the right edge, unless it is closer + than that to the end of the source line anyway. */ + int right_margin_size = CARET_LINE_MARGIN; + right_margin_size = MIN (eol_display_column - caret_display_column, + right_margin_size); + if (right_margin_size + left_margin_size >= max_width) + { + /* The max_width is very small, so anything we try to do will not be very + effective; just punt in this case and output with no offset. */ + return; + } + const int max_caret_display_column = max_width - right_margin_size; + if (caret_display_column > max_caret_display_column) + { + m_x_offset_display = caret_display_column - max_caret_display_column; + /* Make sure we don't offset the line into oblivion. */ + static const int min_cols_visible = 2; + if (source_display_cols - m_x_offset_display < min_cols_visible) + m_x_offset_display = 0; + } +} + +/* Print line ROW of source code, potentially colorized at any ranges, and + return the line bounds. LINE is the source line (not necessarily + 0-terminated) and LINE_BYTES is its length in bytes. In order to handle both + colorization and tab expansion, this function tracks the line position in + both byte and display column units. */ + +template<typename TextOrHtml> +line_bounds +layout_printer<TextOrHtml>::print_source_line (linenum_type row, + const char *line, + int line_bytes) +{ + m_text_or_html.colorize_text_ensure_normal (); + m_text_or_html.push_html_tag ("tr", true); + m_text_or_html.emit_text_prefix (); + if (m_layout.m_options.show_line_numbers_p) + { + m_text_or_html.push_html_tag_with_class ("td", "linenum", true); + int width = diagnostics::num_digits (row); + for (int i = 0; i < m_layout.get_linenum_width () - width; i++) + m_text_or_html.add_space (); + char buf[20]; + sprintf (buf, "%i", row); + m_text_or_html.add_text (buf); + if (TextOrHtml::is_text ()) + m_text_or_html.add_text (" |"); + m_text_or_html.pop_html_tag ("td"); + } + + m_text_or_html.push_html_tag_with_class ("td", "left-margin", true); + print_leftmost_column (); + m_text_or_html.pop_html_tag ("td"); + + /* We will stop printing the source line at any trailing whitespace. */ + line_bytes = get_line_bytes_without_trailing_whitespace (line, + line_bytes); + + /* This object helps to keep track of which display column we are at, which is + necessary for computing the line bounds in display units, for doing + tab expansion, and for implementing m_x_offset_display. */ + cpp_display_width_computation dw (line, line_bytes, m_layout.m_char_policy); + + m_text_or_html.push_html_tag_with_class ("td", "source", true); + + /* Skip the first m_x_offset_display display columns. In case the leading + portion that will be skipped ends with a character with wcwidth > 1, then + it is possible we skipped too much, so account for that by padding with + spaces. Note that this does the right thing too in case a tab was the last + character to be skipped over; the tab is effectively replaced by the + correct number of trailing spaces needed to offset by the desired number of + display columns. */ + for (int skipped_display_cols + = dw.advance_display_cols (m_layout.m_x_offset_display); + skipped_display_cols > m_layout.m_x_offset_display; --skipped_display_cols) + m_text_or_html.add_space (); + + /* Print the line and compute the line_bounds. */ + line_bounds lbounds; + while (!dw.done ()) + { + /* Assuming colorization is enabled for the caret and underline + characters, we may also colorize the associated characters + within the source line. + + For frontends that generate range information, we color the + associated characters in the source line the same as the + carets and underlines in the annotation line, to make it easier + for the reader to see the pertinent code. + + For frontends that only generate carets, we don't colorize the + characters above them, since this would look strange (e.g. + colorizing just the first character in a token). */ + if (m_layout.m_options.colorize_source_p) + { + bool in_range_p; + point_state state; + const int start_byte_col = dw.bytes_processed () + 1; + in_range_p = m_layout.get_state_at_point (row, start_byte_col, + 0, INT_MAX, + CU_BYTES, + &state); + if (in_range_p) + set_in_range (state.range_idx); + else + set_outside_range (); + } + + /* Get the display width of the next character to be output, expanding + tabs and replacing some control bytes with spaces as necessary. */ + const char *c = dw.next_byte (); + const int start_disp_col = dw.display_cols_processed () + 1; + cpp_decoded_char cp; + const int this_display_width = dw.process_next_codepoint (&cp); + if (*c == '\t') + { + /* The returned display width is the number of spaces into which the + tab should be expanded. */ + for (int i = 0; i != this_display_width; ++i) + m_text_or_html.add_space (); + continue; + } + + /* We have a (possibly multibyte) character to output; update the line + bounds if it is not whitespace. */ + if (*c != ' ') + { + lbounds.m_last_non_ws_disp_col = dw.display_cols_processed (); + if (lbounds.m_first_non_ws_disp_col == INT_MAX) + lbounds.m_first_non_ws_disp_col = start_disp_col; + } + + /* Output the character. */ + m_text_or_html.print_decoded_char (m_layout.m_char_policy, cp); + c = dw.next_byte (); + } + set_outside_range (); + end_line (); + return lbounds; +} + +/* Determine if we should print an annotation line for ROW. + i.e. if any of m_layout_ranges contains ROW. */ + +bool +layout::should_print_annotation_line_p (linenum_type row) const +{ + layout_range *range; + int i; + FOR_EACH_VEC_ELT (m_layout_ranges, i, range) + { + if (range->m_range_display_kind == SHOW_LINES_WITHOUT_RANGE) + return false; + if (range->intersects_line_p (row)) + return true; + } + return false; +} + +/* Print the leftmost column after the margin, which is used for showing + links between labels (e.g. for CFG edges in execution paths). */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::print_leftmost_column () +{ + if (!get_options ().show_event_links_p) + gcc_assert (m_link_lhs_state == link_lhs_state::none); + + switch (m_link_lhs_state) + { + default: + gcc_unreachable (); + case link_lhs_state::none: + m_text_or_html.add_space (); + break; + case link_lhs_state::rewinding_to_lhs: + { + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t ch = get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_FROM_LEFT_TO_DOWN); + m_text_or_html.add_character (ch); + m_text_or_html.colorize_text_ensure_normal (); + } + break; + case link_lhs_state::at_lhs: + { + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t ch = get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_DOWN); + m_text_or_html.add_character (ch); + m_text_or_html.colorize_text_ensure_normal (); + } + break; + case link_lhs_state::indenting_to_dest: + { + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t ch = get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_FROM_DOWN_TO_RIGHT); + m_text_or_html.add_character (ch); + m_text_or_html.colorize_text_ensure_normal (); + } + break; + } +} + +/* Begin an annotation line for either text or html output + + If m_show_line_numbers_p, print the left margin, which is empty + for annotation lines. + After any left margin, print a leftmost column, which is used for + showing links between labels (e.g. for CFG edges in execution paths). + + For text output, this also first prints the text prefix. + For html output, this also pushes <tr> and <td> open tags, where the + <td> is for the coming annotations. */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::start_annotation_line (enum margin_kind margin) +{ + m_text_or_html.emit_text_prefix (); + m_text_or_html.push_html_tag ("tr", true); + + char margin_char = (margin == margin_kind::insertion + ? '+' + : ' '); + + if (get_options ().show_line_numbers_p) + { + /* Print the margin. If MARGIN_CHAR != ' ', then print up to 3 + of it, right-aligned, padded with spaces. */ + m_text_or_html.push_html_tag_with_class ("td", "linenum", true); + int i; + for (i = 0; i < m_layout.m_linenum_width - 3; i++) + m_text_or_html.add_space (); + for (; i < m_layout.m_linenum_width; i++) + m_text_or_html.add_character (margin_char); + if (TextOrHtml::is_text ()) + m_text_or_html.add_text (" |"); + m_text_or_html.pop_html_tag ("td"); + } + + m_text_or_html.push_html_tag_with_class ("td", "left-margin", true); + if (margin == margin_kind::insertion) + m_text_or_html.add_character (margin_char); + else + print_leftmost_column (); + m_text_or_html.pop_html_tag ("td"); + + m_text_or_html.push_html_tag_with_class ("td", + (margin == margin_kind::ruler + ? "ruler" + : "annotation"), + true); +} + +/* End a source or annotation line: text implementation. + Reset any colorization and emit a newline. */ + +template<> +void +layout_printer<diagnostics::to_text>::end_line () +{ + m_text_or_html.colorize_text_ensure_normal (); + m_text_or_html.add_newline (); +} + +/* End a source or annotation line: HTML implementation. + Close the <td> and <tr> tags. */ + +template<> +void +layout_printer<diagnostics::to_html>::end_line () +{ + m_text_or_html.pop_html_tag ("td"); + m_text_or_html.pop_html_tag ("tr"); +} + +/* Handle the various transitions between being-in-range and + not-being-in-a-range, and between ranges. */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::set_in_range (int range_idx) +{ + if (m_was_in_range_p) + { + if (m_last_range_idx != range_idx) + { + /* transition between ranges. */ + end_label (m_last_range_idx, false); + begin_label (range_idx, false); + } + } + else + { + /* transition from "not in a range" to "in a range". */ + begin_label (range_idx, false); + m_was_in_range_p = true; + } + m_last_range_idx = range_idx; +} + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::set_outside_range () +{ + if (m_was_in_range_p) + /* transition from "in a range" to "not in a range". */ + end_label (m_last_range_idx, false); + m_was_in_range_p = false; +} + +/* Print a line consisting of the caret/underlines for the given + source line. */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::print_annotation_line (linenum_type row, + const line_bounds lbounds) +{ + int x_bound = m_layout.get_x_bound_for_row (row, + m_layout.m_exploc.m_display_col, + lbounds.m_last_non_ws_disp_col); + + start_annotation_line (margin_kind::normal); + + for (int column = 1 + m_layout.m_x_offset_display; column < x_bound; column++) + { + bool in_range_p; + point_state state; + in_range_p = m_layout.get_state_at_point (row, column, + lbounds.m_first_non_ws_disp_col, + lbounds.m_last_non_ws_disp_col, + CU_DISPLAY_COLS, + &state); + if (in_range_p) + set_in_range (state.range_idx); + else + set_outside_range (); + + if (in_range_p) + { + /* Within a range. Draw either the caret or an underline. */ + if (state.draw_caret_p) + { + /* Draw the caret. */ + char caret_char; + if (state.range_idx < rich_location::STATICALLY_ALLOCATED_RANGES) + caret_char = get_options ().caret_chars[state.range_idx]; + else + caret_char = '^'; + m_text_or_html.add_character (caret_char); + } + else + m_text_or_html.add_character ('~'); + } + else + { + /* Not in a range. */ + m_text_or_html.add_character (' '); + } + } + + set_outside_range (); + + end_line (); +} + +/* A version of label_text that can live inside a vec. + Requires manual cleanup via maybe_free. */ + +struct pod_label_text +{ + pod_label_text () + : m_buffer (nullptr), m_caller_owned (false) + {} + + pod_label_text (label_text &&other) + : m_buffer (const_cast<char*> (other.get ())), + m_caller_owned (other.is_owner ()) + { + other.release (); + } + + void maybe_free () + { + if (m_caller_owned) + free (m_buffer); + } + + char *m_buffer; + bool m_caller_owned; +}; + +/* Implementation detail of layout::print_any_labels. + + A label within the given row of source. */ + +class line_label +{ +public: + line_label (unsigned original_range_idx, + int state_idx, int column, + label_text text, + bool has_in_edge, + bool has_out_edge) + : m_original_range_idx (original_range_idx), + m_state_idx (state_idx), m_column (column), + m_text (std::move (text)), m_label_line (0), m_has_vbar (true), + m_has_in_edge (has_in_edge), + m_has_out_edge (has_out_edge) + { + /* Using styled_string rather than cpp_display_width here + lets us skip SGR formatting characters for color and URLs. + It doesn't handle tabs and unicode escaping, but we don't + expect to see either of those in labels. */ + text_art::style_manager sm; + text_art::styled_string str (sm, m_text.m_buffer); + m_display_width = str.calc_canvas_width (); + } + + /* Sorting is primarily by column, then by state index. */ + static int comparator (const void *p1, const void *p2) + { + const line_label *ll1 = (const line_label *)p1; + const line_label *ll2 = (const line_label *)p2; + int column_cmp = compare (ll1->m_column, ll2->m_column); + if (column_cmp) + return column_cmp; + /* Order by reverse state index, so that labels are printed + in order of insertion into the rich_location when the + sorted list is walked backwards. */ + return -compare (ll1->m_state_idx, ll2->m_state_idx); + } + + unsigned m_original_range_idx; + int m_state_idx; + int m_column; + pod_label_text m_text; + size_t m_display_width; + int m_label_line; + bool m_has_vbar; + bool m_has_in_edge; + bool m_has_out_edge; +}; + +/* Implementations of layout_printer::{begin,end}_label for + to_text and to_html. + + RANGE_IDX is the index of the range within the rich_location. + + IS_LABEL_TEXT is true for the text of the label, + false when quoting the source code, underlining the source + code, and for the vertical bars connecting the underlines + to the text of the label. */ + +template<> +void +layout_printer<diagnostics::to_text>::begin_label (int range_idx, + bool is_label_text) +{ + /* Colorize the text, unless it's for labels for events in a + diagnostic path. */ + if (is_label_text && m_is_diagnostic_path) + return; + + gcc_assert (m_text_or_html.m_colorizer); + m_text_or_html.m_colorizer->set_range (range_idx); +} + +template<> +void +layout_printer<diagnostics::to_html>::begin_label (int range_idx, + bool is_label_text) +{ + if (is_label_text && m_text_or_html.m_html_label_writer) + m_text_or_html.m_html_label_writer->begin_label (); + + if (const char *highlight_color + = m_text_or_html.get_highlight_color_for_range_idx (range_idx)) + m_text_or_html.m_xp.push_tag_with_class ("span", highlight_color); +} + +template<> +void +layout_printer<diagnostics::to_text>::end_label (int, bool) +{ + m_text_or_html.colorize_text_ensure_normal (); +} + +template<> +void +layout_printer<diagnostics::to_html>::end_label (int range_idx, + bool is_label_text) +{ + if (m_text_or_html.get_highlight_color_for_range_idx (range_idx)) + m_text_or_html.m_xp.pop_tag ("span"); + + if (is_label_text && m_text_or_html.m_html_label_writer) + m_text_or_html.m_html_label_writer->end_label (); +} + +/* Print any labels in this row. */ +template <typename TextOrHtml> +void +layout_printer<TextOrHtml>::print_any_labels (linenum_type row) +{ + int i; + auto_vec<line_label> labels; + + /* Gather the labels that are to be printed into "labels". */ + { + layout_range *range; + FOR_EACH_VEC_ELT (m_layout.m_layout_ranges, i, range) + { + /* Most ranges don't have labels, so reject this first. */ + if (range->m_label == nullptr) + continue; + + /* The range's caret must be on this line. */ + if (range->m_caret.m_line != row) + continue; + + /* Reject labels that aren't fully visible due to clipping + by m_x_offset_display. */ + const int disp_col = range->m_caret.m_columns[CU_DISPLAY_COLS]; + if (disp_col <= m_layout.m_x_offset_display) + continue; + + label_text text; + text = range->m_label->get_text (range->m_original_idx); + + /* Allow for labels that return nullptr from their get_text + implementation (so e.g. such labels can control their own + visibility). */ + if (text.get () == nullptr) + continue; + + labels.safe_push (line_label (range->m_original_idx, + i, disp_col, std::move (text), + range->has_in_edge (), + range->has_out_edge ())); + } + } + + /* Bail out if there are no labels on this row. */ + if (labels.length () == 0) + return; + + /* Sort them. */ + labels.qsort(line_label::comparator); + + /* Figure out how many "label lines" we need, and which + one each label is printed in. + + For example, if the labels aren't too densely packed, + we can fit them on the same line, giving two "label lines": + + foo + bar + ~~~ ~~~ + | | : label line 0 + l0 l1 : label line 1 + + If they would touch each other or overlap, then we need + additional "label lines": + + foo + bar + ~~~ ~~~ + | | : label line 0 + | label 1 : label line 1 + label 0 : label line 2 + + Place the final label on label line 1, and work backwards, adding + label lines as needed. + + If multiple labels are at the same place, put them on separate + label lines: + + foo + bar + ^ : label line 0 + | : label line 1 + label 0 : label line 2 + label 1 : label line 3. */ + + int max_label_line = 1; + int label_line_with_in_edge = -1; + { + int next_column = INT_MAX; + line_label *label; + FOR_EACH_VEC_ELT_REVERSE (labels, i, label) + { + /* Would this label "touch" or overlap the next label? */ + if (label->m_column + label->m_display_width >= (size_t)next_column) + { + max_label_line++; + + /* If we've already seen labels with the same column, suppress the + vertical bar for subsequent ones in this backwards iteration; + hence only the one with the highest label_line has m_has_vbar set. */ + if (label->m_column == next_column) + label->m_has_vbar = false; + } + + label->m_label_line = max_label_line; + if (get_options ().show_event_links_p) + if (label->m_has_in_edge) + label_line_with_in_edge = max_label_line; + next_column = label->m_column; + } + } + + gcc_assert (labels.length () > 0); + + /* Print the "label lines". For each label within the line, print + either a vertical bar ('|') for the labels that are lower down, or the + labels themselves once we've reached their line. */ + { + for (int label_line = 0; label_line <= max_label_line; label_line++) + { + if (label_line == label_line_with_in_edge) + { + gcc_assert (get_options ().show_event_links_p); + m_link_lhs_state = link_lhs_state::indenting_to_dest; + } + start_annotation_line (margin_kind::normal); + + int column = 1 + m_layout.m_x_offset_display; + line_label *label; + FOR_EACH_VEC_ELT (labels, i, label) + { + if (label_line > label->m_label_line) + /* We've printed all the labels for this label line. */ + break; + + if (label_line == label->m_label_line) + { + gcc_assert (column <= label->m_column); + + if (label_line == label_line_with_in_edge) + { + /* Print a prefix showing an incoming + link from another label. + .|+----------->(10) ...to here + . ^~~~~~~~~~~~~ + . this text. */ + gcc_assert (get_options ().show_event_links_p); + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t right= get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_RIGHT); + while (column < label->m_column - 1) + { + m_text_or_html.add_character (right); + column++; + } + if (column == label->m_column - 1) + { + m_text_or_html.add_character ('>'); + column++; + } + m_text_or_html.colorize_text_ensure_normal (); + m_link_lhs_state = link_lhs_state::none; + label_line_with_in_edge = -1; + } + else + move_to_column (&column, label->m_column, true); + gcc_assert (column == label->m_column); + + begin_label (label->m_state_idx, true); + m_text_or_html.add_text (label->m_text.m_buffer); + end_label (label->m_state_idx, true); + + column += label->m_display_width; + if (get_options ().show_event_links_p && label->m_has_out_edge) + { + /* Print a suffix showing the start of a linkage + to another label e.g. " ->-+" which will be the + first part of e.g. + . (9) following ‘false’ branch... ->-+ <- HERE + . | + . | + . */ + const cppchar_t right= get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_RIGHT); + const cppchar_t from_right_to_down= get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_FROM_RIGHT_TO_DOWN); + m_text_or_html.colorize_text_for_cfg_edge (); + m_text_or_html.add_space (); + m_text_or_html.add_character (right); + m_text_or_html.add_character ('>'); + m_text_or_html.add_character (right); + m_text_or_html.add_character (from_right_to_down); + m_text_or_html.colorize_text_ensure_normal (); + column += 5; + m_link_rhs_column = column - 1; + } + } + else if (label->m_has_vbar) + { + gcc_assert (column <= label->m_column); + move_to_column (&column, label->m_column, true); + begin_label (label->m_state_idx, false); + m_text_or_html.add_character ('|'); + end_label (label->m_state_idx, false); + column++; + } + } + + /* If we have a vertical link line on the RHS, print the + '|' on this annotation line after the labels. */ + if (m_link_rhs_column != -1 && column < m_link_rhs_column) + { + move_to_column (&column, m_link_rhs_column, true); + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t down= get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_DOWN); + m_text_or_html.add_character (down); + m_text_or_html.colorize_text_ensure_normal (); + } + + end_line (); + } + } + + /* If we have a vertical link line on the RHS, print a trailing + annotation line showing the vertical line. */ + if (m_link_rhs_column != -1) + { + int column = 1 + m_layout.m_x_offset_display; + start_annotation_line (margin_kind::normal); + move_to_column (&column, m_link_rhs_column, true); + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t down= get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_DOWN); + m_text_or_html.add_character (down); + end_line (); + } + + /* Clean up. */ + { + line_label *label; + FOR_EACH_VEC_ELT (labels, i, label) + label->m_text.maybe_free (); + } +} + +/* If there are any fixit hints inserting new lines before source line ROW, + print them. + + They are printed on lines of their own, before the source line + itself, with a leading '+'. */ + +template <typename TextOrHtml> +void +layout_printer<TextOrHtml>::print_leading_fixits (linenum_type row) +{ + for (unsigned int i = 0; i < m_layout.m_fixit_hints.length (); i++) + { + const fixit_hint *hint = m_layout.m_fixit_hints[i]; + + if (!hint->ends_with_newline_p ()) + /* Not a newline fixit; print it in print_trailing_fixits. */ + continue; + + gcc_assert (hint->insertion_p ()); + + if (hint->affects_line_p (m_layout.m_line_table, + m_layout.m_exploc.file, + row)) + { + /* Printing the '+' with normal colorization + and the inserted line with "insert" colorization + helps them stand out from each other, and from + the surrounding text. */ + m_text_or_html.colorize_text_ensure_normal (); + start_annotation_line (margin_kind::insertion); + m_text_or_html.colorize_text_for_fixit_insert (); + /* Print all but the trailing newline of the fix-it hint. + We have to print the newline separately to avoid + getting additional pp prefixes printed. */ + for (size_t i = 0; i < hint->get_length () - 1; i++) + m_text_or_html.add_character (hint->get_string ()[i]); + end_line (); + } + } +} + +/* Subroutine of layout::print_trailing_fixits. + + Determine if the annotation line printed for LINE contained + the exact range from START_COLUMN to FINISH_COLUMN (in display units). */ + +bool +layout::annotation_line_showed_range_p (linenum_type line, int start_column, + int finish_column) const +{ + layout_range *range; + int i; + FOR_EACH_VEC_ELT (m_layout_ranges, i, range) + if (range->m_start.m_line == line + && range->m_start.m_columns[CU_DISPLAY_COLS] == start_column + && range->m_finish.m_line == line + && range->m_finish.m_columns[CU_DISPLAY_COLS] == finish_column) + return true; + return false; +} + +/* Classes for printing trailing fix-it hints i.e. those that + don't add new lines. + + For insertion, these can look like: + + new_text + + For replacement, these can look like: + + ------------- : underline showing affected range + new_text + + For deletion, these can look like: + + ------------- : underline showing affected range + + This can become confusing if they overlap, and so we need + to do some preprocessing to decide what to print. + We use the list of fixit_hint instances affecting the line + to build a list of "correction" instances, and print the + latter. + + For example, consider a set of fix-its for converting + a C-style cast to a C++ const_cast. + + Given: + + ..000000000111111111122222222223333333333. + ..123456789012345678901234567890123456789. + foo *f = (foo *)ptr->field; + ^~~~~ + + and the fix-it hints: + - replace col 10 (the open paren) with "const_cast<" + - replace col 16 (the close paren) with "> (" + - insert ")" before col 27 + + then we would get odd-looking output: + + foo *f = (foo *)ptr->field; + ^~~~~ + - + const_cast< + - + > ( ) + + It would be better to detect when fixit hints are going to + overlap (those that require new lines), and to consolidate + the printing of such fixits, giving something like: + + foo *f = (foo *)ptr->field; + ^~~~~ + ----------------- + const_cast<foo *> (ptr->field) + + This works by detecting when the printing would overlap, and + effectively injecting no-op replace hints into the gaps between + such fix-its, so that the printing joins up. + + In the above example, the overlap of: + - replace col 10 (the open paren) with "const_cast<" + and: + - replace col 16 (the close paren) with "> (" + is fixed by injecting a no-op: + - replace cols 11-15 with themselves ("foo *") + and consolidating these, making: + - replace cols 10-16 with "const_cast<" + "foo *" + "> (" + i.e.: + - replace cols 10-16 with "const_cast<foo *> (" + + This overlaps with the final fix-it hint: + - insert ")" before col 27 + and so we repeat the consolidation process, by injecting + a no-op: + - replace cols 17-26 with themselves ("ptr->field") + giving: + - replace cols 10-26 with "const_cast<foo *> (" + "ptr->field" + ")" + i.e.: + - replace cols 10-26 with "const_cast<foo *> (ptr->field)" + + and is thus printed as desired. */ + +/* A range of (byte or display) columns within a line. */ + +class column_range +{ +public: + column_range (int start_, int finish_) : start (start_), finish (finish_) + { + gcc_assert (valid_p (start, finish)); + } + + bool operator== (const column_range &other) const + { + return start == other.start && finish == other.finish; + } + + static bool valid_p (int start, int finish) + { + /* We must have either a range, or an insertion. */ + return (start <= finish || finish == start - 1); + } + + int start; + int finish; +}; + +/* Get the range of bytes or display columns that HINT would affect. */ +static column_range +get_affected_range (diagnostics::file_cache &fc, + const cpp_char_column_policy &policy, + const fixit_hint *hint, enum column_unit col_unit) +{ + expanded_location exploc_start = expand_location (hint->get_start_loc ()); + expanded_location exploc_finish = expand_location (hint->get_next_loc ()); + --exploc_finish.column; + + int start_column; + int finish_column; + if (col_unit == CU_DISPLAY_COLS) + { + start_column = location_compute_display_column (fc, exploc_start, policy); + if (hint->insertion_p ()) + finish_column = start_column - 1; + else + finish_column + = location_compute_display_column (fc, exploc_finish, policy); + } + else + { + start_column = exploc_start.column; + finish_column = exploc_finish.column; + } + return column_range (start_column, finish_column); +} + +/* Get the range of display columns that would be printed for HINT. */ + +static column_range +get_printed_columns (diagnostics::file_cache &fc, + const cpp_char_column_policy &policy, + const fixit_hint *hint) +{ + expanded_location exploc = expand_location (hint->get_start_loc ()); + int start_column = location_compute_display_column (fc, exploc, policy); + int hint_width = cpp_display_width (hint->get_string (), hint->get_length (), + policy); + int final_hint_column = start_column + hint_width - 1; + if (hint->insertion_p ()) + { + return column_range (start_column, final_hint_column); + } + else + { + exploc = expand_location (hint->get_next_loc ()); + --exploc.column; + int finish_column = location_compute_display_column (fc, exploc, policy); + return column_range (start_column, + MAX (finish_column, final_hint_column)); + } +} + +/* A correction on a particular line. + This describes a plan for how to print one or more fixit_hint + instances that affected the line, potentially consolidating hints + into corrections to make the result easier for the user to read. */ + +class correction +{ +public: + correction (column_range affected_bytes, + column_range affected_columns, + column_range printed_columns, + const char *new_text, size_t new_text_len, + const cpp_char_column_policy &policy) + : m_affected_bytes (affected_bytes), + m_affected_columns (affected_columns), + m_printed_columns (printed_columns), + m_text (xstrdup (new_text)), + m_byte_length (new_text_len), + m_policy (policy), + m_alloc_sz (new_text_len + 1) + { + compute_display_cols (); + } + + ~correction () { free (m_text); } + + bool insertion_p () const + { + return m_affected_bytes.start == m_affected_bytes.finish + 1; + } + + void ensure_capacity (size_t len); + void ensure_terminated (); + + void compute_display_cols () + { + m_display_cols = cpp_display_width (m_text, m_byte_length, m_policy); + } + + void overwrite (int dst_offset, const diagnostics::char_span &src_span) + { + gcc_assert (dst_offset >= 0); + gcc_assert (dst_offset + src_span.length () < m_alloc_sz); + memcpy (m_text + dst_offset, src_span.get_buffer (), + src_span.length ()); + } + + /* If insert, then start: the column before which the text + is to be inserted, and finish is offset by the length of + the replacement. + If replace, then the range of columns affected. */ + column_range m_affected_bytes; + column_range m_affected_columns; + + /* If insert, then start: the column before which the text + is to be inserted, and finish is offset by the length of + the replacement. + If replace, then the range of columns affected. */ + column_range m_printed_columns; + + /* The text to be inserted/used as replacement. */ + char *m_text; + size_t m_byte_length; /* Not including null-terminator. */ + int m_display_cols; + const cpp_char_column_policy &m_policy; + size_t m_alloc_sz; +}; + +/* Ensure that m_text can hold a string of length LEN + (plus 1 for 0-termination). */ + +void +correction::ensure_capacity (size_t len) +{ + /* Allow 1 extra byte for 0-termination. */ + if (m_alloc_sz < (len + 1)) + { + size_t new_alloc_sz = (len + 1) * 2; + m_text = (char *)xrealloc (m_text, new_alloc_sz); + m_alloc_sz = new_alloc_sz; + } +} + +/* Ensure that m_text is 0-terminated. */ + +void +correction::ensure_terminated () +{ + /* 0-terminate the buffer. */ + gcc_assert (m_byte_length < m_alloc_sz); + m_text[m_byte_length] = '\0'; +} + +/* A list of corrections affecting a particular line. + This is used by layout::print_trailing_fixits for planning + how to print the fix-it hints affecting the line. */ + +class line_corrections +{ +public: + line_corrections (diagnostics::file_cache &fc, + const char_display_policy &policy, + const char *filename, + linenum_type row) + : m_file_cache (fc), + m_policy (policy), m_filename (filename), m_row (row) + {} + ~line_corrections (); + + void add_hint (const fixit_hint *hint); + + diagnostics::file_cache &m_file_cache; + const char_display_policy &m_policy; + const char *m_filename; + linenum_type m_row; + auto_vec <correction *> m_corrections; +}; + +/* struct line_corrections. */ + +line_corrections::~line_corrections () +{ + unsigned i; + correction *c; + FOR_EACH_VEC_ELT (m_corrections, i, c) + delete c; +} + +/* A struct wrapping a particular source line, allowing + run-time bounds-checking of accesses in a checked build. */ + +class source_line +{ +public: + source_line (diagnostics::file_cache &fc, const char *filename, int line); + + diagnostics::char_span as_span () + { + return diagnostics::char_span (chars, width); + } + + const char *chars; + int width; +}; + +/* source_line's ctor. */ + +source_line::source_line (diagnostics::file_cache &fc, + const char *filename, + int line) +{ + diagnostics::char_span span = fc.get_source_line (filename, line); + chars = span.get_buffer (); + width = span.length (); +} + +/* Add HINT to the corrections for this line. + Attempt to consolidate nearby hints so that they will not + overlap with printed. */ + +void +line_corrections::add_hint (const fixit_hint *hint) +{ + column_range affected_bytes + = get_affected_range (m_file_cache, m_policy, hint, CU_BYTES); + column_range affected_columns + = get_affected_range (m_file_cache, m_policy, hint, CU_DISPLAY_COLS); + column_range printed_columns + = get_printed_columns (m_file_cache, m_policy, hint); + + /* Potentially consolidate. */ + if (!m_corrections.is_empty ()) + { + correction *last_correction + = m_corrections[m_corrections.length () - 1]; + + /* The following consolidation code assumes that the fix-it hints + have been sorted by start (done within layout's ctor). */ + gcc_assert (affected_bytes.start + >= last_correction->m_affected_bytes.start); + gcc_assert (printed_columns.start + >= last_correction->m_printed_columns.start); + + if (printed_columns.start <= last_correction->m_printed_columns.finish + && column_range::valid_p (last_correction->m_affected_bytes.finish + 1, + affected_bytes.start - 1)) + { + /* We have two hints for which the printed forms of the hints + would touch or overlap, so we need to consolidate them to avoid + confusing the user. + Attempt to inject a "replace" correction from immediately + after the end of the last hint to immediately before the start + of the next hint. */ + column_range between (last_correction->m_affected_bytes.finish + 1, + affected_bytes.start - 1); + + /* Try to read the source. */ + source_line line (m_file_cache, m_filename, m_row); + if (line.chars && between.finish < line.width) + { + /* Consolidate into the last correction: + add a no-op "replace" of the "between" text, and + add the text from the new hint. */ + int old_byte_len = last_correction->m_byte_length; + gcc_assert (old_byte_len >= 0); + int between_byte_len = between.finish + 1 - between.start; + gcc_assert (between_byte_len >= 0); + int new_byte_len + = old_byte_len + between_byte_len + hint->get_length (); + gcc_assert (new_byte_len >= 0); + last_correction->ensure_capacity (new_byte_len); + last_correction->overwrite + (old_byte_len, + line.as_span ().subspan (between.start - 1, + between.finish + 1 - between.start)); + last_correction->overwrite + (old_byte_len + between_byte_len, + diagnostics::char_span (hint->get_string (), + hint->get_length ())); + last_correction->m_byte_length = new_byte_len; + last_correction->ensure_terminated (); + last_correction->m_affected_bytes.finish + = affected_bytes.finish; + last_correction->m_affected_columns.finish + = affected_columns.finish; + int prev_display_cols = last_correction->m_display_cols; + last_correction->compute_display_cols (); + last_correction->m_printed_columns.finish + += last_correction->m_display_cols - prev_display_cols; + return; + } + } + } + + /* If no consolidation happened, add a new correction instance. */ + m_corrections.safe_push (new correction (affected_bytes, + affected_columns, + printed_columns, + hint->get_string (), + hint->get_length (), + m_policy)); +} + +/* If there are any fixit hints on source line ROW, print them. + They are printed in order, attempting to combine them onto lines, but + starting new lines if necessary. + Fix-it hints that insert new lines are handled separately, + in layout::print_leading_fixits. */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::print_trailing_fixits (linenum_type row) +{ + typename TextOrHtml::auto_check_tag_nesting sentinel (m_text_or_html); + + /* Build a list of correction instances for the line, + potentially consolidating hints (for the sake of readability). */ + line_corrections corrections (m_layout.m_file_cache, m_layout.m_char_policy, + m_layout.m_exploc.file, row); + for (unsigned int i = 0; i < m_layout.m_fixit_hints.length (); i++) + { + const fixit_hint *hint = m_layout.m_fixit_hints[i]; + + /* Newline fixits are handled by layout::print_leading_fixits. */ + if (hint->ends_with_newline_p ()) + continue; + + if (hint->affects_line_p (m_layout.m_line_table, + m_layout.m_exploc.file, + row)) + corrections.add_hint (hint); + } + + /* Now print the corrections. */ + unsigned i; + correction *c; + int column = 1 + m_layout.m_x_offset_display; + + if (!corrections.m_corrections.is_empty ()) + start_annotation_line (margin_kind::normal); + + FOR_EACH_VEC_ELT (corrections.m_corrections, i, c) + { + /* For now we assume each fixit hint can only touch one line. */ + if (c->insertion_p ()) + { + /* This assumes the insertion just affects one line. */ + int start_column = c->m_printed_columns.start; + move_to_column (&column, start_column, true); + m_text_or_html.colorize_text_for_fixit_insert (); + m_text_or_html.add_text (c->m_text); + m_text_or_html.colorize_text_ensure_normal (); + column += c->m_display_cols; + } + else + { + /* If the range of the replacement wasn't printed in the + annotation line, then print an extra underline to + indicate exactly what is being replaced. + Always show it for removals. */ + int start_column = c->m_affected_columns.start; + int finish_column = c->m_affected_columns.finish; + if (!m_layout.annotation_line_showed_range_p (row, start_column, + finish_column) + || c->m_byte_length == 0) + { + move_to_column (&column, start_column, true); + m_text_or_html.colorize_text_for_fixit_delete (); + for (; column <= finish_column; column++) + m_text_or_html.add_character ('-'); + m_text_or_html.colorize_text_ensure_normal (); + } + /* Print the replacement text. REPLACE also covers + removals, so only do this extra work (potentially starting + a new line) if we have actual replacement text. */ + if (c->m_byte_length > 0) + { + move_to_column (&column, start_column, true); + m_text_or_html.colorize_text_for_fixit_insert (); + m_text_or_html.add_text (c->m_text); + m_text_or_html.colorize_text_ensure_normal (); + column += c->m_display_cols; + } + } + } + + /* Add a trailing newline, if necessary. */ + move_to_column (&column, 1 + m_layout.m_x_offset_display, false); +} + +/* Return true if (ROW/COLUMN) is within a range of the layout. + If it returns true, OUT_STATE is written to, with the + range index, and whether we should draw the caret at + (ROW/COLUMN) (as opposed to an underline). COL_UNIT controls + whether all inputs and outputs are in bytes or display column units. */ + +bool +layout::get_state_at_point (/* Inputs. */ + linenum_type row, int column, + int first_non_ws, int last_non_ws, + enum column_unit col_unit, + /* Outputs. */ + point_state *out_state) const +{ + layout_range *range; + int i; + FOR_EACH_VEC_ELT (m_layout_ranges, i, range) + { + if (range->m_range_display_kind == SHOW_LINES_WITHOUT_RANGE) + /* Bail out early, so that such ranges don't affect underlining or + source colorization. */ + continue; + + if (range->contains_point (row, column, col_unit)) + { + out_state->range_idx = i; + + /* Are we at the range's caret? is it visible? */ + out_state->draw_caret_p = false; + if (range->m_range_display_kind == SHOW_RANGE_WITH_CARET + && row == range->m_caret.m_line + && column == range->m_caret.m_columns[col_unit]) + out_state->draw_caret_p = true; + + /* Within a multiline range, don't display any underline + in any leading or trailing whitespace on a line. + We do display carets, however. */ + if (!out_state->draw_caret_p) + if (column < first_non_ws || column > last_non_ws) + return false; + + /* We are within a range. */ + return true; + } + } + + return false; +} + +/* Helper function for use by layout::print_line when printing the + annotation line under the source line. + Get the display column beyond the rightmost one that could contain a caret + or range marker, given that we stop rendering at trailing whitespace. + ROW is the source line within the given file. + CARET_COLUMN is the display column of range 0's caret. + LAST_NON_WS_COLUMN is the last display column containing a non-whitespace + character of source (as determined when printing the source line). */ + +int +layout::get_x_bound_for_row (linenum_type row, int caret_column, + int last_non_ws_column) const +{ + int result = caret_column + 1; + + layout_range *range; + int i; + FOR_EACH_VEC_ELT (m_layout_ranges, i, range) + { + if (row >= range->m_start.m_line) + { + if (range->m_finish.m_line == row) + { + /* On the final line within a range; ensure that + we render up to the end of the range. */ + const int disp_col = range->m_finish.m_columns[CU_DISPLAY_COLS]; + if (result <= disp_col) + result = disp_col + 1; + } + else if (row < range->m_finish.m_line) + { + /* Within a multiline range; ensure that we render up to the + last non-whitespace column. */ + if (result <= last_non_ws_column) + result = last_non_ws_column + 1; + } + } + } + + return result; +} + +/* Given *COLUMN as an x-coordinate, print spaces to position + successive output at DEST_COLUMN, printing a newline if necessary, + and updating *COLUMN. If ADD_LEFT_MARGIN, then print the (empty) + left margin after any newline. */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::move_to_column (int *column, + int dest_column, + bool add_left_margin) +{ + /* Start a new line if we need to. */ + if (*column > dest_column) + { + end_line (); + if (add_left_margin) + start_annotation_line (margin_kind::normal); + *column = 1 + m_layout.m_x_offset_display; + } + + while (*column < dest_column) + { + /* For debugging column issues, it can be helpful to replace this + add_space call with + m_text_or_html.add_character ('0' + (*column % 10)); + to visualize the changing value of "*column". */ + m_text_or_html.add_space (); + (*column)++; + } +} + +/* For debugging layout issues, render a ruler giving column numbers + (after the 1-column indent). */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::show_ruler (int max_column) +{ + m_text_or_html.push_html_tag_with_class("thead", "ruler", false); + + /* Hundreds. */ + if (max_column > 99) + { + start_annotation_line (margin_kind::ruler); + for (int column = 1 + m_layout.m_x_offset_display; + column <= max_column; + ++column) + if (column % 10 == 0) + m_text_or_html.add_character ('0' + (column / 100) % 10); + else + m_text_or_html.add_space (); + end_line (); + } + + /* Tens. */ + start_annotation_line (margin_kind::ruler); + for (int column = 1 + m_layout.m_x_offset_display; + column <= max_column; + ++column) + if (column % 10 == 0) + m_text_or_html.add_character ('0' + (column / 10) % 10); + else + m_text_or_html.add_space (); + end_line (); + + /* Units. */ + start_annotation_line (margin_kind::ruler); + for (int column = 1 + m_layout.m_x_offset_display; + column <= max_column; + ++column) + m_text_or_html.add_character ('0' + (column % 10)); + end_line (); + + m_text_or_html.pop_html_tag("thead"); // thead +} + +/* Print leading fix-its (for new lines inserted before the source line) + then the source line, followed by an annotation line + consisting of any caret/underlines, then any fixits. + If the source line can't be read, print nothing. */ +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::print_line (linenum_type row) +{ + typename TextOrHtml::auto_check_tag_nesting sentinel (m_text_or_html); + + diagnostics::char_span line + = m_layout.m_file_cache.get_source_line (m_layout.m_exploc.file, row); + if (!line) + return; + + print_any_right_to_left_edge_lines (); + print_leading_fixits (row); + const line_bounds lbounds + = print_source_line (row, line.get_buffer (), line.length ()); + if (m_layout.should_print_annotation_line_p (row)) + print_annotation_line (row, lbounds); + if (get_options ().show_labels_p) + print_any_labels (row); + print_trailing_fixits (row); +} + +/* If there's a link column in the RHS, print something like this: + " │\n" + "┌──────────────────────────────────────────┘\n" + showing the link entering at the top right and emerging + at the bottom left. */ + +template<typename TextOrHtml> +void +layout_printer<TextOrHtml>::print_any_right_to_left_edge_lines () +{ + if (m_link_rhs_column == -1) + /* Can also happen if the out-edge had UNKNOWN_LOCATION. */ + return; + + gcc_assert (get_options ().show_event_links_p); + + /* Print the line with "|". */ + start_annotation_line (margin_kind::normal); + + int column = 1 + m_layout.m_x_offset_display; + move_to_column (&column, m_link_rhs_column, true); + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t down= get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_DOWN); + m_text_or_html.add_character (down); + end_line (); + + /* Print the line with "┌──────────────────────────────────────────┘". */ + m_link_lhs_state = link_lhs_state::rewinding_to_lhs; + start_annotation_line (margin_kind::normal); + m_text_or_html.colorize_text_for_cfg_edge (); + const cppchar_t left= get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_LEFT); + for (int column = 1 + m_layout.m_x_offset_display; + column < m_link_rhs_column; + ++column) + m_text_or_html.add_character (left); + const cppchar_t from_down_to_left = get_theme ().get_cppchar + (text_art::theme::cell_kind::CFG_FROM_DOWN_TO_LEFT); + m_text_or_html.add_character (from_down_to_left); + end_line (); + + /* We now have a link line on the LHS, + and no longer have one on the RHS. */ + m_link_lhs_state = link_lhs_state::at_lhs; + m_link_rhs_column = -1; +} + +template<typename TextOrHtml> +layout_printer<TextOrHtml>::layout_printer (TextOrHtml &text_or_html, + const layout &layout, + bool is_diagnostic_path) +: m_text_or_html (text_or_html), + m_layout (layout), + m_is_diagnostic_path (is_diagnostic_path), + m_was_in_range_p (false), + m_last_range_idx (0), + m_link_lhs_state (link_lhs_state::none), + m_link_rhs_column (-1) +{ + if (get_options ().show_event_links_p) + if (auto effect_info = m_layout.m_effect_info) + if (effect_info->m_leading_in_edge_column) + m_link_rhs_column = effect_info->m_leading_in_edge_column; +} + +} /* End of anonymous namespace. */ + +/* If LOC is within the spans of lines that will already be printed for + this gcc_rich_location, then add it as a secondary location and return true. + + Otherwise return false. + + Use POLICY for determining how spans of lines would be printed. */ + +bool +gcc_rich_location:: +add_location_if_nearby (const diagnostics::source_print_policy &policy, + location_t loc, + bool restrict_to_current_line_spans, + const range_label *label) +{ + /* Use the layout location-handling logic to sanitize LOC, + filtering it to the current line spans within a temporary + layout instance. */ + + layout layout (policy, *this); + location_range loc_range; + loc_range.m_loc = loc; + loc_range.m_range_display_kind = SHOW_RANGE_WITHOUT_CARET; + loc_range.m_label = nullptr; + if (!layout.maybe_add_location_range (&loc_range, 0, + restrict_to_current_line_spans)) + return false; + + add_range (loc, SHOW_RANGE_WITHOUT_CARET, label); + return true; +} + +bool +gcc_rich_location:: +add_location_if_nearby (const diagnostics::context &dc, + location_t loc, + bool restrict_to_current_line_spans, + const range_label *label) +{ + diagnostics::source_print_policy + source_policy (dc, + dc.get_source_printing_options ()); + return add_location_if_nearby (source_policy, loc, + restrict_to_current_line_spans, label); +} + +namespace diagnostics { + +/* As per diagnostics::source_print_policy::print, but don't print anything + if source printing is disabled, or if the location hasn't changed. */ + +void +context::maybe_show_locus (const rich_location &richloc, + const source_printing_options &opts, + enum kind diagnostic_kind, + pretty_printer &pp, + source_effect_info *effects) +{ + const location_t loc = richloc.get_loc (); + /* Do nothing if source-printing has been disabled. */ + if (!opts.enabled) + return; + + /* Don't attempt to print source for UNKNOWN_LOCATION and for builtins. */ + if (loc <= BUILTINS_LOCATION) + return; + + /* Don't print the same source location twice in a row, unless we have + fix-it hints, or multiple locations, or a label. */ + if (loc == m_last_location + && richloc.get_num_fixit_hints () == 0 + && richloc.get_num_locations () == 1 + && richloc.get_range (0)->m_label == nullptr) + return; + + m_last_location = loc; + + source_print_policy source_policy (*this, opts); + source_policy.print (pp, richloc, diagnostic_kind, effects); +} + +/* As above, but print in HTML form to XP. + If non-null, use LABEL_WRITER when writing labelled ranges. */ + +void +context::maybe_show_locus_as_html (const rich_location &richloc, + const source_printing_options &opts, + enum kind diagnostic_kind, + xml::printer &xp, + source_effect_info *effects, + html_label_writer *label_writer) +{ + const location_t loc = richloc.get_loc (); + /* Do nothing if source-printing has been disabled. */ + if (!opts.enabled) + return; + + /* Don't attempt to print source for UNKNOWN_LOCATION and for builtins. */ + if (loc <= BUILTINS_LOCATION) + return; + + /* Don't print the same source location twice in a row, unless we have + fix-it hints, or multiple locations, or a label. */ + if (loc == m_last_location + && richloc.get_num_fixit_hints () == 0 + && richloc.get_num_locations () == 1 + && richloc.get_range (0)->m_label == nullptr) + return; + + m_last_location = loc; + + source_print_policy source_policy (*this, opts); + source_policy.print_as_html (xp, richloc, diagnostic_kind, effects, + label_writer); +} + +source_print_policy:: +source_print_policy (const context &dc) +: m_options (dc.m_source_printing), + m_location_policy (dc), + m_text_start_span_cb (dc.m_text_callbacks.m_text_start_span), + m_html_start_span_cb (dc.m_text_callbacks.m_html_start_span), + m_file_cache (dc.get_file_cache ()), + m_diagram_theme (dc.get_diagram_theme ()), + m_escape_format (dc.get_escape_format ()) +{ +} + +source_print_policy::source_print_policy (const context &dc, + const source_printing_options &opts) +: m_options (opts), + m_location_policy (dc), + m_text_start_span_cb (dc.m_text_callbacks.m_text_start_span), + m_html_start_span_cb (dc.m_text_callbacks.m_html_start_span), + m_file_cache (dc.get_file_cache ()), + m_diagram_theme (dc.get_diagram_theme ()), + m_escape_format (dc.get_escape_format ()) +{ +} + +/* Print to PP the physical source code corresponding to the location(s) + in RICHLOC, with additional annotations, as if for a diagnostic of the + given DIAGNOSTIC_KIND. + If EFFECTS is non-null, then use and update it. */ + +void +source_print_policy::print (pretty_printer &pp, + const rich_location &richloc, + enum kind diagnostic_kind, + source_effect_info *effects) const +{ + layout layout (*this, richloc, effects); + colorizer col (pp, richloc, diagnostic_kind); + to_text text_or_html (pp, col); + layout_printer<to_text> lp (text_or_html, layout, + diagnostic_kind == diagnostics::kind::path); + lp.print (*this); +} + +/* As above, but print in HTML form to XP. + If non-null, use LABEL_WRITER when writing labelled ranges. */ + +void +source_print_policy::print_as_html (xml::printer &xp, + const rich_location &richloc, + enum kind diagnostic_kind, + source_effect_info *effects, + html_label_writer *label_writer) const +{ + layout layout (*this, richloc, effects); + to_html text_or_html (xp, &richloc, label_writer); + layout_printer<to_html> lp (text_or_html, layout, + diagnostic_kind == diagnostics::kind::path); + xml::auto_check_tag_nesting sentinel (xp); + lp.print (*this); +} + +} // namespace diagnostics + +template <typename TextOrHtml> +void +layout_printer<TextOrHtml>:: +print (const diagnostics::source_print_policy &source_policy) +{ + typename TextOrHtml::auto_check_tag_nesting sentinel (m_text_or_html); + + m_text_or_html.push_html_tag_with_class ("table", "locus", false); + + if (get_options ().show_ruler_p) + show_ruler (m_layout.m_x_offset_display + get_options ().max_width); + + for (int line_span_idx = 0; line_span_idx < m_layout.get_num_line_spans (); + line_span_idx++) + { + const line_span *line_span = m_layout.get_line_span (line_span_idx); + if (get_options ().show_line_numbers_p) + { + /* With line numbers, we should show whenever the line-numbering + "jumps". */ + if (line_span_idx > 0) + print_gap_in_line_numbering (); + } + else + { + /* Without line numbers, we print headings for some line spans. */ + if (m_layout.print_heading_for_line_span_index_p (line_span_idx)) + { + expanded_location exploc + = m_layout.get_expanded_location (line_span); + const diagnostics::location_print_policy & + loc_policy = source_policy.get_location_policy (); + m_text_or_html.invoke_start_span_fn (source_policy, loc_policy, exploc); + } + } + + m_text_or_html.push_html_tag_with_class ("tbody", "line-span", false); + + /* Iterate over the lines within this span (using linenum_arith_t to + avoid overflow with 0xffffffff causing an infinite loop). */ + linenum_arith_t last_line = line_span->get_last_line (); + for (linenum_arith_t row = line_span->get_first_line (); + row <= last_line; row++) + print_line (row); + + m_text_or_html.pop_html_tag ("tbody"); + } + + if (auto effect_info = m_layout.m_effect_info) + effect_info->m_trailing_out_edge_column = m_link_rhs_column; + + m_text_or_html.pop_html_tag ("table"); +} + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +using test_context = diagnostics::selftest::test_context; + +using line_table_case = ::selftest::line_table_case; +using line_table_test = ::selftest::line_table_test; +using temp_source_file = ::selftest::temp_source_file; + +static std::unique_ptr<xml::node> +make_element_for_locus (const rich_location &rich_loc, + enum kind kind, + diagnostics::context &dc) +{ + dc.m_last_location = UNKNOWN_LOCATION; + + xml::element wrapper ("wrapper", false); + xml::printer xp (wrapper); + dc.maybe_show_locus_as_html (rich_loc, + dc.get_source_printing_options (), + kind, + xp, + nullptr, + nullptr); // label_writer + if (wrapper.m_children.size () > 0) + return std::move (wrapper.m_children[0]); + else + return nullptr; +} + +static label_text +make_raw_html_for_locus (const rich_location &rich_loc, + enum kind kind, + diagnostics::context &dc) +{ + auto node = make_element_for_locus (rich_loc, kind, dc); + pretty_printer pp; + if (node) + node->write_as_xml (&pp, 0, true); + return label_text::take (xstrdup (pp_formatted_text (&pp))); +} + +/* Selftests for diagnostic_show_locus. */ + +source_printing_fixture:: +source_printing_fixture (const line_table_case &case_, + const char *content) +: m_content (content), + m_tmp_source_file (SELFTEST_LOCATION, ".c", content), + m_ltt (case_), + m_fc () +{ + linemap_add (line_table, LC_ENTER, false, + m_tmp_source_file.get_filename (), 1); +} + +/* Populate a char_display_policy based on DC and RICHLOC. */ + +static char_display_policy +make_char_policy (const diagnostics::context &dc, + const rich_location &richloc) +{ + diagnostics::source_print_policy source_policy (dc); + return ::make_char_policy (source_policy, richloc); +} + +/* Verify that cpp_display_width correctly handles escaping. */ + +static void +test_display_widths () +{ + gcc_rich_location richloc (UNKNOWN_LOCATION); + + /* U+03C0 "GREEK SMALL LETTER PI". */ + const char *pi = "\xCF\x80"; + /* U+1F642 "SLIGHTLY SMILING FACE". */ + const char *emoji = "\xF0\x9F\x99\x82"; + /* Stray trailing byte of a UTF-8 character. */ + const char *stray = "\xBF"; + /* U+10FFFF. */ + const char *max_codepoint = "\xF4\x8F\xBF\xBF"; + + /* No escaping. */ + { + test_context dc; + char_display_policy policy (make_char_policy (dc, richloc)); + ASSERT_EQ (cpp_display_width (pi, strlen (pi), policy), 1); + ASSERT_EQ (cpp_display_width (emoji, strlen (emoji), policy), 2); + ASSERT_EQ (cpp_display_width (stray, strlen (stray), policy), 1); + /* Don't check width of U+10FFFF; it's in a private use plane. */ + } + + richloc.set_escape_on_output (true); + + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_UNICODE); + char_display_policy policy (make_char_policy (dc, richloc)); + ASSERT_EQ (cpp_display_width (pi, strlen (pi), policy), 8); + ASSERT_EQ (cpp_display_width (emoji, strlen (emoji), policy), 9); + ASSERT_EQ (cpp_display_width (stray, strlen (stray), policy), 4); + ASSERT_EQ (cpp_display_width (max_codepoint, strlen (max_codepoint), + policy), + strlen ("<U+10FFFF>")); + } + + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_BYTES); + char_display_policy policy (make_char_policy (dc, richloc)); + ASSERT_EQ (cpp_display_width (pi, strlen (pi), policy), 8); + ASSERT_EQ (cpp_display_width (emoji, strlen (emoji), policy), 16); + ASSERT_EQ (cpp_display_width (stray, strlen (stray), policy), 4); + ASSERT_EQ (cpp_display_width (max_codepoint, strlen (max_codepoint), + policy), + 16); + } +} + +/* For precise tests of the layout, make clear where the source line will + start. test_left_margin sets the total byte count from the left side of the + screen to the start of source lines, after the line number and the separator, + which consists of the three characters " | ". */ +static const int test_linenum_sep = 3; +static const int test_left_margin = 7; + +/* Helper function for test_layout_x_offset_display_utf8(). */ +static void +test_offset_impl (int caret_byte_col, int max_width, + int expected_x_offset_display, + int left_margin = test_left_margin) +{ + test_context dc; + auto &source_printing_opts = dc.get_source_printing_options (); + source_printing_opts.max_width = max_width; + /* min_margin_width sets the minimum space reserved for + the line number plus one space after. */ + source_printing_opts.min_margin_width = left_margin - test_linenum_sep + 1; + dc.show_line_numbers (true); + diagnostics::source_print_policy source_policy (dc); + rich_location richloc (line_table, + linemap_position_for_column (line_table, + caret_byte_col)); + layout test_layout (source_policy, richloc, nullptr); + ASSERT_EQ (left_margin - test_linenum_sep, + test_layout.get_linenum_width ()); + ASSERT_EQ (expected_x_offset_display, + test_layout.get_x_offset_display ()); +} + +/* Test that layout::calculate_x_offset_display() works. */ +static void +test_layout_x_offset_display_utf8 (const line_table_case &case_) +{ + + const char *content + = "This line is very long, so that we can use it to test the logic for " + "clipping long lines. Also this: \xf0\x9f\x98\x82\xf0\x9f\x98\x82 is a " + "pair of emojis that occupies 8 bytes and 4 display columns, starting at " + "column #102.\n"; + + /* Number of bytes in the line, subtracting one to remove the newline. */ + const int line_bytes = strlen (content) - 1; + + /* Number of display columns occupied by the line; each of the 2 emojis + takes up 2 fewer display columns than it does bytes. */ + const int line_display_cols = line_bytes - 2*2; + + /* The column of the first emoji. Byte or display is the same as there are + no multibyte characters earlier on the line. */ + const int emoji_col = 102; + + source_printing_fixture f (case_, content); + + linemap_add (line_table, LC_ENTER, false, f.get_filename (), 1); + + location_t line_end = linemap_position_for_column (line_table, line_bytes); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + ASSERT_STREQ (f.get_filename (), LOCATION_FILE (line_end)); + ASSERT_EQ (1, LOCATION_LINE (line_end)); + ASSERT_EQ (line_bytes, LOCATION_COLUMN (line_end)); + + diagnostics::char_span lspan = f.m_fc.get_source_line (f.get_filename (), 1); + ASSERT_EQ (line_display_cols, + cpp_display_width (lspan.get_buffer (), lspan.length (), + def_policy ())); + ASSERT_EQ (line_display_cols, + location_compute_display_column (f.m_fc, + expand_location (line_end), + def_policy ())); + ASSERT_EQ (0, memcmp (lspan.get_buffer () + (emoji_col - 1), + "\xf0\x9f\x98\x82\xf0\x9f\x98\x82", 8)); + + /* (caret_byte, max_width, expected_x_offset_display, [left_margin]) */ + + /* No constraint on the width -> no offset. */ + test_offset_impl (emoji_col, 0, 0); + + /* Caret is before the beginning -> no offset. */ + test_offset_impl (0, 100, 0); + + /* Caret is past the end of the line -> no offset. */ + test_offset_impl (line_bytes+1, 100, 0); + + /* Line fits in the display -> no offset. */ + test_offset_impl (line_bytes, line_display_cols + test_left_margin, 0); + test_offset_impl (emoji_col, line_display_cols + test_left_margin, 0); + + /* Line is too long for the display but caret location is OK + anyway -> no offset. */ + static const int small_width = 24; + test_offset_impl (1, small_width, 0); + + /* Width constraint is very small -> no offset. */ + test_offset_impl (emoji_col, CARET_LINE_MARGIN, 0); + + /* Line would be offset, but due to large line numbers, offsetting + would remove the whole line -> no offset. */ + static const int huge_left_margin = 100; + test_offset_impl (emoji_col, huge_left_margin, 0, huge_left_margin); + + /* Line is the same length as the display, but the line number makes it too + long, so offset is required. Caret is at the end so padding on the right + is not in effect. */ + for (int excess = 1; excess <= 3; ++excess) + test_offset_impl (line_bytes, line_display_cols + test_left_margin - excess, + excess); + + /* Line is much too long for the display, caret is near the end -> + offset should be such that the line fits in the display and caret + remains the same distance from the end that it was. */ + for (int caret_offset = 0, max_offset = MIN (CARET_LINE_MARGIN, 10); + caret_offset <= max_offset; ++caret_offset) + test_offset_impl (line_bytes - caret_offset, small_width, + line_display_cols + test_left_margin - small_width); + + /* As previous case but caret is closer to the middle; now we want it to end + up CARET_LINE_MARGIN bytes from the end. */ + ASSERT_GT (line_display_cols - emoji_col, CARET_LINE_MARGIN); + test_offset_impl (emoji_col, small_width, + emoji_col + test_left_margin + - (small_width - CARET_LINE_MARGIN)); + + /* Test that the source line is offset as expected when printed. */ + { + test_context dc; + auto &source_printing_opts = dc.get_source_printing_options (); + source_printing_opts.max_width = small_width - 6; + source_printing_opts.min_margin_width + = test_left_margin - test_linenum_sep + 1; + dc.show_line_numbers (true); + dc.show_ruler (true); + diagnostics::source_print_policy policy (dc); + rich_location richloc (line_table, + linemap_position_for_column (line_table, + emoji_col)); + layout test_layout (policy, richloc, nullptr); + colorizer col (*dc.get_reference_printer (), + richloc, diagnostics::kind::error); + diagnostics::to_text text_or_html (*dc.get_reference_printer (), col); + layout_printer<diagnostics::to_text> lp (text_or_html, test_layout, false); + lp.print (policy); + ASSERT_STREQ (" | 1 \n" + " | 1 \n" + " | 234567890123456789\n" + " 1 | \xf0\x9f\x98\x82\xf0\x9f\x98\x82 is a pair of emojis " + "that occupies 8 bytes and 4 display columns, starting at " + "column #102.\n" + " | ^\n", + pp_formatted_text (dc.get_reference_printer ())); + } + + /* Similar to the previous example, but now the offset called for would split + the first emoji in the middle of the UTF-8 sequence. Check that we replace + it with a padding space in this case. */ + { + test_context dc; + auto &source_printing_opts = dc.get_source_printing_options (); + source_printing_opts.max_width = small_width - 5; + source_printing_opts.min_margin_width + = test_left_margin - test_linenum_sep + 1; + dc.show_line_numbers (true); + dc.show_ruler (true); + diagnostics::source_print_policy policy (dc); + rich_location richloc (line_table, + linemap_position_for_column (line_table, + emoji_col + 2)); + layout test_layout (dc, richloc, nullptr); + colorizer col (*dc.get_reference_printer (), + richloc, diagnostics::kind::error); + diagnostics::to_text text_or_html (*dc.get_reference_printer (), col); + layout_printer<diagnostics::to_text> lp (text_or_html, test_layout, false); + lp.print (policy); + ASSERT_STREQ (" | 1 1 \n" + " | 1 2 \n" + " | 3456789012345678901\n" + " 1 | \xf0\x9f\x98\x82 is a pair of emojis " + "that occupies 8 bytes and 4 display columns, starting at " + "column #102.\n" + " | ^\n", + pp_formatted_text (dc.get_reference_printer ())); + } + +} + +static void +test_layout_x_offset_display_tab (const line_table_case &case_) +{ + const char *content + = "This line is very long, so that we can use it to test the logic for " + "clipping long lines. Also this: `\t' is a tab that occupies 1 byte and " + "a variable number of display columns, starting at column #103.\n"; + + /* Number of bytes in the line, subtracting one to remove the newline. */ + const int line_bytes = strlen (content) - 1; + + /* The column where the tab begins. Byte or display is the same as there are + no multibyte characters earlier on the line. */ + const int tab_col = 103; + + /* Effective extra size of the tab beyond what a single space would have taken + up, indexed by tabstop. */ + static const int num_tabstops = 11; + int extra_width[num_tabstops]; + for (int tabstop = 1; tabstop != num_tabstops; ++tabstop) + { + const int this_tab_size = tabstop - (tab_col - 1) % tabstop; + extra_width[tabstop] = this_tab_size - 1; + } + /* Example of this calculation: if tabstop is 10, the tab starting at column + #103 has to expand into 8 spaces, covering columns 103-110, so that the + next character is at column #111. So it takes up 7 more columns than + a space would have taken up. */ + ASSERT_EQ (7, extra_width[10]); + + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + diagnostics::file_cache fc; + line_table_test ltt (case_); + + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 1); + + location_t line_end = linemap_position_for_column (line_table, line_bytes); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Check that cpp_display_width handles the tabs as expected. */ + diagnostics::char_span lspan = fc.get_source_line (tmp.get_filename (), 1); + ASSERT_EQ ('\t', *(lspan.get_buffer () + (tab_col - 1))); + for (int tabstop = 1; tabstop != num_tabstops; ++tabstop) + { + cpp_char_column_policy policy (tabstop, cpp_wcwidth); + ASSERT_EQ (line_bytes + extra_width[tabstop], + cpp_display_width (lspan.get_buffer (), lspan.length (), + policy)); + ASSERT_EQ (line_bytes + extra_width[tabstop], + location_compute_display_column (fc, + expand_location (line_end), + policy)); + } + + /* Check that the tab is expanded to the expected number of spaces. */ + rich_location richloc (line_table, + linemap_position_for_column (line_table, + tab_col + 1)); + for (int tabstop = 1; tabstop != num_tabstops; ++tabstop) + { + test_context dc; + dc.m_tabstop = tabstop; + diagnostics::source_print_policy policy (dc); + layout test_layout (policy, richloc, nullptr); + colorizer col (*dc.get_reference_printer (), + richloc, diagnostics::kind::error); + diagnostics::to_text text_or_html (*dc.get_reference_printer (), col); + layout_printer<diagnostics::to_text> lp + (text_or_html, test_layout, false); + lp.print (policy); + const char *out = pp_formatted_text (dc.get_reference_printer ()); + ASSERT_EQ (nullptr, strchr (out, '\t')); + const char *left_quote = strchr (out, '`'); + const char *right_quote = strchr (out, '\''); + ASSERT_NE (nullptr, left_quote); + ASSERT_NE (nullptr, right_quote); + ASSERT_EQ (right_quote - left_quote, extra_width[tabstop] + 2); + } + + /* Check that the line is offset properly and that the tab is broken up + into the expected number of spaces when it is the last character skipped + over. */ + for (int tabstop = 1; tabstop != num_tabstops; ++tabstop) + { + test_context dc; + dc.m_tabstop = tabstop; + static const int small_width = 24; + auto &source_printing_opts = dc.get_source_printing_options (); + source_printing_opts.max_width = small_width - 4; + source_printing_opts.min_margin_width + = test_left_margin - test_linenum_sep + 1; + dc.show_line_numbers (true); + diagnostics::source_print_policy policy (dc); + layout test_layout (policy, richloc, nullptr); + colorizer col (*dc.get_reference_printer (), + richloc, diagnostics::kind::error); + diagnostics::to_text text_or_html (*dc.get_reference_printer (), col); + layout_printer<diagnostics::to_text> lp + (text_or_html, test_layout, false); + lp.print (policy); + + /* We have arranged things so that two columns will be printed before + the caret. If the tab results in more than one space, this should + produce two spaces in the output; otherwise, it will be a single space + preceded by the opening quote before the tab character. */ + const char *output1 + = " 1 | ' is a tab that occupies 1 byte and a variable number of " + "display columns, starting at column #103.\n" + " | ^\n"; + const char *output2 + = " 1 | ` ' is a tab that occupies 1 byte and a variable number of " + "display columns, starting at column #103.\n" + " | ^\n"; + const char *expected_output = (extra_width[tabstop] ? output1 : output2); + ASSERT_STREQ (expected_output, pp_formatted_text (dc.get_reference_printer ())); + } +} + + +/* Verify that diagnostic_show_locus works sanely on UNKNOWN_LOCATION. */ + +static void +test_diagnostic_show_locus_unknown_location () +{ + test_context dc; + rich_location richloc (line_table, UNKNOWN_LOCATION); + ASSERT_STREQ ("", dc.test_show_locus (richloc)); +} + +/* Verify that diagnostic_show_locus works sanely for various + single-line cases. + + All of these work on the following 1-line source file: + .0000000001111111 + .1234567890123456 + "foo = bar.field;\n" + which is set up by test_diagnostic_show_locus_one_liner and calls + them. */ + +/* Just a caret. */ + +static void +test_one_liner_simple_caret () +{ + test_context dc; + location_t caret = linemap_position_for_column (line_table, 10); + rich_location richloc (line_table, caret); + ASSERT_STREQ (" foo = bar.field;\n" + " ^\n", + dc.test_show_locus (richloc)); +} + +/* No column information (column == 0). + No annotation line should be printed. */ + +static void +test_one_liner_no_column () +{ + test_context dc; + location_t caret = linemap_position_for_column (line_table, 0); + rich_location richloc (line_table, caret); + ASSERT_STREQ (" foo = bar.field;\n", + dc.test_show_locus (richloc)); +} + +/* Caret and range. */ + +static void +test_one_liner_caret_and_range () +{ + test_context dc; + location_t caret = linemap_position_for_column (line_table, 10); + location_t start = linemap_position_for_column (line_table, 7); + location_t finish = linemap_position_for_column (line_table, 15); + location_t loc = make_location (caret, start, finish); + rich_location richloc (line_table, loc); + ASSERT_STREQ (" foo = bar.field;\n" + " ~~~^~~~~~\n", + dc.test_show_locus (richloc)); + + { + test_context dc; + auto out = make_raw_html_for_locus (richloc, diagnostics::kind::error, dc); + ASSERT_STREQ + ("<table class=\"locus\">\n" + " <tbody class=\"line-span\">\n" + " <tr><td class=\"left-margin\"> </td><td class=\"source\">foo = bar.field;</td></tr>\n" + " <tr><td class=\"left-margin\"> </td><td class=\"annotation\"> ~~~^~~~~~</td></tr>\n" + " </tbody>\n" + "</table>\n", + out.get ()); + } + { + test_context dc; + dc.show_line_numbers (true); + auto out = make_raw_html_for_locus (richloc, diagnostics::kind::error, dc); + ASSERT_STREQ + ("<table class=\"locus\">\n" + " <tbody class=\"line-span\">\n" + " <tr><td class=\"linenum\"> 1</td><td class=\"left-margin\"> </td><td class=\"source\">foo = bar.field;</td></tr>\n" + " <tr><td class=\"linenum\"> </td><td class=\"left-margin\"> </td><td class=\"annotation\"> ~~~^~~~~~</td></tr>\n" + " </tbody>\n" + "</table>\n", + out.get ()); + } +} + +/* Multiple ranges and carets. */ + +static void +test_one_liner_multiple_carets_and_ranges () +{ + test_context dc; + location_t foo + = make_location (linemap_position_for_column (line_table, 2), + linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 3)); + dc.set_caret_char (0, 'A'); + + location_t bar + = make_location (linemap_position_for_column (line_table, 8), + linemap_position_for_column (line_table, 7), + linemap_position_for_column (line_table, 9)); + dc.set_caret_char (1, 'B'); + + location_t field + = make_location (linemap_position_for_column (line_table, 13), + linemap_position_for_column (line_table, 11), + linemap_position_for_column (line_table, 15)); + dc.set_caret_char (2, 'C'); + + rich_location richloc (line_table, foo); + richloc.add_range (bar, SHOW_RANGE_WITH_CARET); + richloc.add_range (field, SHOW_RANGE_WITH_CARET); + ASSERT_STREQ (" foo = bar.field;\n" + " ~A~ ~B~ ~~C~~\n", + dc.test_show_locus (richloc)); +} + +/* Insertion fix-it hint: adding an "&" to the front of "bar.field". */ + +static void +test_one_liner_fixit_insert_before () +{ + test_context dc; + location_t caret = linemap_position_for_column (line_table, 7); + rich_location richloc (line_table, caret); + richloc.add_fixit_insert_before ("&"); + ASSERT_STREQ (" foo = bar.field;\n" + " ^\n" + " &\n", + dc.test_show_locus (richloc)); +} + +/* Insertion fix-it hint: adding a "[0]" after "foo". */ + +static void +test_one_liner_fixit_insert_after () +{ + test_context dc; + location_t start = linemap_position_for_column (line_table, 1); + location_t finish = linemap_position_for_column (line_table, 3); + location_t foo = make_location (start, start, finish); + rich_location richloc (line_table, foo); + richloc.add_fixit_insert_after ("[0]"); + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~\n" + " [0]\n", + dc.test_show_locus (richloc)); +} + +/* Removal fix-it hint: removal of the ".field". + Also verify the interaction of pp_set_prefix with rulers and + fix-it hints. */ + +static void +test_one_liner_fixit_remove () +{ + location_t start = linemap_position_for_column (line_table, 10); + location_t finish = linemap_position_for_column (line_table, 15); + location_t dot = make_location (start, start, finish); + rich_location richloc (line_table, dot); + richloc.add_fixit_remove (); + + /* Normal. */ + { + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~~~~\n" + " ------\n", + dc.test_show_locus (richloc)); + } + + /* Test of adding a prefix. */ + { + test_context dc; + pp_prefixing_rule (dc.get_reference_printer ()) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE; + pp_set_prefix (dc.get_reference_printer (), xstrdup ("TEST PREFIX:")); + ASSERT_STREQ ("TEST PREFIX: foo = bar.field;\n" + "TEST PREFIX: ^~~~~~\n" + "TEST PREFIX: ------\n", + dc.test_show_locus (richloc)); + } + + /* Normal, with ruler. */ + { + test_context dc; + auto &source_printing_opts = dc.get_source_printing_options (); + dc.show_ruler (true); + source_printing_opts.max_width = 104; + ASSERT_STREQ (" 0 0 0 0 0 0 0 0 0 1 \n" + " 1 2 3 4 5 6 7 8 9 0 \n" + " 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234\n" + " foo = bar.field;\n" + " ^~~~~~\n" + " ------\n", + dc.test_show_locus (richloc)); + } + + /* Test of adding a prefix, with ruler. */ + { + test_context dc; + auto &source_printing_opts = dc.get_source_printing_options (); + dc.show_ruler (true); + source_printing_opts.max_width = 50; + pp_prefixing_rule (dc.get_reference_printer ()) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE; + pp_set_prefix (dc.get_reference_printer (), xstrdup ("TEST PREFIX:")); + ASSERT_STREQ ("TEST PREFIX: 1 2 3 4 5\n" + "TEST PREFIX: 12345678901234567890123456789012345678901234567890\n" + "TEST PREFIX: foo = bar.field;\n" + "TEST PREFIX: ^~~~~~\n" + "TEST PREFIX: ------\n", + dc.test_show_locus (richloc)); + } + + /* Test of adding a prefix, with ruler and line numbers. */ + { + test_context dc; + auto &source_printing_opts = dc.get_source_printing_options (); + dc.show_ruler (true); + source_printing_opts.max_width = 50; + dc.show_line_numbers (true); + pp_prefixing_rule (dc.get_reference_printer ()) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE; + pp_set_prefix (dc.get_reference_printer (), xstrdup ("TEST PREFIX:")); + ASSERT_STREQ ("TEST PREFIX: | 1 2 3 4 5\n" + "TEST PREFIX: | 12345678901234567890123456789012345678901234567890\n" + "TEST PREFIX: 1 | foo = bar.field;\n" + "TEST PREFIX: | ^~~~~~\n" + "TEST PREFIX: | ------\n", + dc.test_show_locus (richloc)); + } +} + +/* Replace fix-it hint: replacing "field" with "m_field". */ + +static void +test_one_liner_fixit_replace () +{ + test_context dc; + location_t start = linemap_position_for_column (line_table, 11); + location_t finish = linemap_position_for_column (line_table, 15); + location_t field = make_location (start, start, finish); + rich_location richloc (line_table, field); + richloc.add_fixit_replace ("m_field"); + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~~~\n" + " m_field\n", + dc.test_show_locus (richloc)); +} + +/* Replace fix-it hint: replacing "field" with "m_field", + but where the caret was elsewhere. */ + +static void +test_one_liner_fixit_replace_non_equal_range () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 5); + location_t start = linemap_position_for_column (line_table, 11); + location_t finish = linemap_position_for_column (line_table, 15); + rich_location richloc (line_table, equals); + source_range range; + range.m_start = start; + range.m_finish = finish; + richloc.add_fixit_replace (range, "m_field"); + /* The replacement range is not indicated in the annotation line, so + it should be indicated via an additional underline. */ + ASSERT_STREQ (" foo = bar.field;\n" + " ^\n" + " -----\n" + " m_field\n", + dc.test_show_locus (richloc)); +} + +/* Replace fix-it hint: replacing "field" with "m_field", + where the caret was elsewhere, but where a secondary range + exactly covers "field". */ + +static void +test_one_liner_fixit_replace_equal_secondary_range () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 5); + location_t start = linemap_position_for_column (line_table, 11); + location_t finish = linemap_position_for_column (line_table, 15); + rich_location richloc (line_table, equals); + location_t field = make_location (start, start, finish); + richloc.add_range (field); + richloc.add_fixit_replace (field, "m_field"); + /* The replacement range is indicated in the annotation line, + so it shouldn't be indicated via an additional underline. */ + ASSERT_STREQ (" foo = bar.field;\n" + " ^ ~~~~~\n" + " m_field\n", + dc.test_show_locus (richloc)); +} + +/* Verify that we can use ad-hoc locations when adding fixits to a + rich_location. */ + +static void +test_one_liner_fixit_validation_adhoc_locations () +{ + /* Generate a range that's too long to be packed, so must + be stored as an ad-hoc location (given the defaults + of 5 or 7 bits or 0 bits of packed range); 150 columns > 2**7. */ + const location_t c7 = linemap_position_for_column (line_table, 7); + const location_t c157 = linemap_position_for_column (line_table, 157); + const location_t loc = make_location (c7, c7, c157); + + if (c157 > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + ASSERT_TRUE (IS_ADHOC_LOC (loc)); + + /* Insert. */ + { + rich_location richloc (line_table, loc); + richloc.add_fixit_insert_before (loc, "test"); + /* It should not have been discarded by the validator. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~~~~~~~~ " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " \n" + " test\n", + dc.test_show_locus (richloc)); + } + + /* Remove. */ + { + rich_location richloc (line_table, loc); + source_range range = source_range::from_locations (loc, c157); + richloc.add_fixit_remove (range); + /* It should not have been discarded by the validator. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~~~~~~~~ " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " \n" + " -----------------------------------------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------\n", + dc.test_show_locus (richloc)); + } + + /* Replace. */ + { + rich_location richloc (line_table, loc); + source_range range = source_range::from_locations (loc, c157); + richloc.add_fixit_replace (range, "test"); + /* It should not have been discarded by the validator. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~~~~~~~~ " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " \n" + " test\n", + dc.test_show_locus (richloc)); + } +} + +/* Test of consolidating insertions at the same location. */ + +static void +test_one_liner_many_fixits_1 () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 5); + rich_location richloc (line_table, equals); + for (int i = 0; i < 19; i++) + richloc.add_fixit_insert_before ("a"); + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + ASSERT_STREQ (" foo = bar.field;\n" + " ^\n" + " aaaaaaaaaaaaaaaaaaa\n", + dc.test_show_locus (richloc)); +} + +/* Ensure that we can add an arbitrary number of fix-it hints to a + rich_location, even if they are not consolidated. */ + +static void +test_one_liner_many_fixits_2 () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 5); + rich_location richloc (line_table, equals); + for (int i = 0; i < 19; i++) + { + location_t loc = linemap_position_for_column (line_table, (i * 2) + 1); + richloc.add_fixit_insert_before (loc, "a"); + } + ASSERT_EQ (19, richloc.get_num_fixit_hints ()); + ASSERT_STREQ (" foo = bar.field;\n" + " ^\n" + " a a a a a a a a a a a a a a a a a a a\n", + dc.test_show_locus (richloc)); +} + +/* Test of labeling the ranges within a rich_location. */ + +static void +test_one_liner_labels () +{ + location_t foo + = make_location (linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 3)); + location_t bar + = make_location (linemap_position_for_column (line_table, 7), + linemap_position_for_column (line_table, 7), + linemap_position_for_column (line_table, 9)); + location_t field + = make_location (linemap_position_for_column (line_table, 11), + linemap_position_for_column (line_table, 11), + linemap_position_for_column (line_table, 15)); + + /* Example where all the labels fit on one line. */ + { + text_range_label label0 ("0"); + text_range_label label1 ("1"); + text_range_label label2 ("2"); + gcc_rich_location richloc (foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + + { + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~ ~~~ ~~~~~\n" + " | | |\n" + " 0 1 2\n", + dc.test_show_locus (richloc)); + } + + /* Verify that we can disable label-printing. */ + { + test_context dc; + dc.show_labels (false); + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~ ~~~ ~~~~~\n", + dc.test_show_locus (richloc)); + } + } + + /* Example where the labels need extra lines. */ + { + text_range_label label0 ("label 0"); + text_range_label label1 ("label 1"); + text_range_label label2 ("label 2"); + gcc_rich_location richloc (foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~ ~~~ ~~~~~\n" + " | | |\n" + " | | label 2\n" + " | label 1\n" + " label 0\n", + dc.test_show_locus (richloc)); + + { + test_context dc; + dc.show_line_numbers (true); + auto out + = make_raw_html_for_locus (richloc, diagnostics::kind::error, dc); + ASSERT_STREQ + ("<table class=\"locus\">\n" + " <tbody class=\"line-span\">\n" + " <tr><td class=\"linenum\"> 1</td><td class=\"left-margin\"> </td><td class=\"source\">foo = bar.field;</td></tr>\n" + " <tr><td class=\"linenum\"> </td><td class=\"left-margin\"> </td><td class=\"annotation\">^~~ ~~~ ~~~~~</td></tr>\n" + " <tr><td class=\"linenum\"> </td><td class=\"left-margin\"> </td><td class=\"annotation\">| | |</td></tr>\n" + " <tr><td class=\"linenum\"> </td><td class=\"left-margin\"> </td><td class=\"annotation\">| | label 2</td></tr>\n" + " <tr><td class=\"linenum\"> </td><td class=\"left-margin\"> </td><td class=\"annotation\">| label 1</td></tr>\n" + " <tr><td class=\"linenum\"> </td><td class=\"left-margin\"> </td><td class=\"annotation\">label 0</td></tr>\n" + " </tbody>\n" + "</table>\n", + out.get ()); + } + } + + /* Example of boundary conditions: label 0 and 1 have just enough clearance, + but label 1 just touches label 2. */ + { + text_range_label label0 ("aaaaa"); + text_range_label label1 ("bbbb"); + text_range_label label2 ("c"); + gcc_rich_location richloc (foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~ ~~~ ~~~~~\n" + " | | |\n" + " | | c\n" + " aaaaa bbbb\n", + dc.test_show_locus (richloc)); + } + + /* Example of out-of-order ranges (thus requiring a sort). */ + { + text_range_label label0 ("0"); + text_range_label label1 ("1"); + text_range_label label2 ("2"); + gcc_rich_location richloc (field, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (foo, SHOW_RANGE_WITHOUT_CARET, &label2); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ~~~ ~~~ ^~~~~\n" + " | | |\n" + " 2 1 0\n", + dc.test_show_locus (richloc)); + } + + /* Ensure we don't ICE if multiple ranges with labels are on + the same point. */ + { + text_range_label label0 ("label 0"); + text_range_label label1 ("label 1"); + text_range_label label2 ("label 2"); + gcc_rich_location richloc (bar, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label2); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~\n" + " |\n" + " label 0\n" + " label 1\n" + " label 2\n", + dc.test_show_locus (richloc)); + } + + /* Example of out-of-order ranges (thus requiring a sort), where + they overlap, and there are multiple ranges on the same point. */ + { + text_range_label label_0a ("label 0a"); + text_range_label label_1a ("label 1a"); + text_range_label label_2a ("label 2a"); + text_range_label label_0b ("label 0b"); + text_range_label label_1b ("label 1b"); + text_range_label label_2b ("label 2b"); + text_range_label label_0c ("label 0c"); + text_range_label label_1c ("label 1c"); + text_range_label label_2c ("label 2c"); + gcc_rich_location richloc (field, &label_0a, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label_1a); + richloc.add_range (foo, SHOW_RANGE_WITHOUT_CARET, &label_2a); + + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label_0b); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label_1b); + richloc.add_range (foo, SHOW_RANGE_WITHOUT_CARET, &label_2b); + + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label_0c); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label_1c); + richloc.add_range (foo, SHOW_RANGE_WITHOUT_CARET, &label_2c); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ~~~ ~~~ ^~~~~\n" + " | | |\n" + " | | label 0a\n" + " | | label 0b\n" + " | | label 0c\n" + " | label 1a\n" + " | label 1b\n" + " | label 1c\n" + " label 2a\n" + " label 2b\n" + " label 2c\n", + dc.test_show_locus (richloc)); + } + + /* Verify that a nullptr result from range_label::get_text is + handled gracefully. */ + { + text_range_label label (nullptr); + gcc_rich_location richloc (bar, &label, nullptr); + + test_context dc; + ASSERT_STREQ (" foo = bar.field;\n" + " ^~~\n", + dc.test_show_locus (richloc)); + } + + /* TODO: example of formatted printing (needs to be in + gcc-rich-location.cc due to Makefile.in issues). */ +} + +/* Run the various one-liner tests. */ + +static void +test_diagnostic_show_locus_one_liner (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + ....................0000000001111111. + ....................1234567890123456. */ + const char *content = "foo = bar.field;\n"; + + source_printing_fixture f (case_, content); + + location_t line_end = linemap_position_for_column (line_table, 16); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + ASSERT_STREQ (f.get_filename (), LOCATION_FILE (line_end)); + ASSERT_EQ (1, LOCATION_LINE (line_end)); + ASSERT_EQ (16, LOCATION_COLUMN (line_end)); + + test_one_liner_simple_caret (); + test_one_liner_no_column (); + test_one_liner_caret_and_range (); + test_one_liner_multiple_carets_and_ranges (); + test_one_liner_fixit_insert_before (); + test_one_liner_fixit_insert_after (); + test_one_liner_fixit_remove (); + test_one_liner_fixit_replace (); + test_one_liner_fixit_replace_non_equal_range (); + test_one_liner_fixit_replace_equal_secondary_range (); + test_one_liner_fixit_validation_adhoc_locations (); + test_one_liner_many_fixits_1 (); + test_one_liner_many_fixits_2 (); + test_one_liner_labels (); +} + +/* Version of all one-liner tests exercising multibyte awareness. + These are all called from test_diagnostic_show_locus_one_liner, + which uses source_printing_fixture_one_liner_utf8 to create + the test file; see the notes in diagnostic-show-locus-selftest.h. + + Note: all of the below asserts would be easier to read if we used UTF-8 + directly in the string constants, but it seems better not to demand the + host compiler support this, when it isn't otherwise necessary. Instead, + whenever an extended character appears in a string, we put a line break + after it so that all succeeding characters can appear visually at the + correct display column. */ + +/* Just a caret. */ + +static void +test_one_liner_simple_caret_utf8 () +{ + test_context dc; + location_t caret = linemap_position_for_column (line_table, 18); + rich_location richloc (line_table, caret); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^\n", + dc.test_show_locus (richloc)); +} + +/* Caret and range. */ +static void +test_one_liner_caret_and_range_utf8 () +{ + test_context dc; + location_t caret = linemap_position_for_column (line_table, 18); + location_t start = linemap_position_for_column (line_table, 12); + location_t finish = linemap_position_for_column (line_table, 30); + location_t loc = make_location (caret, start, finish); + rich_location richloc (line_table, loc); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ~~~~~^~~~~~~~~~\n", + dc.test_show_locus (richloc)); +} + +/* Multiple ranges and carets. */ + +static void +test_one_liner_multiple_carets_and_ranges_utf8 () +{ + test_context dc; + location_t foo + = make_location (linemap_position_for_column (line_table, 7), + linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 8)); + dc.set_caret_char (0, 'A'); + + location_t bar + = make_location (linemap_position_for_column (line_table, 16), + linemap_position_for_column (line_table, 12), + linemap_position_for_column (line_table, 17)); + dc.set_caret_char (1, 'B'); + + location_t field + = make_location (linemap_position_for_column (line_table, 26), + linemap_position_for_column (line_table, 19), + linemap_position_for_column (line_table, 30)); + dc.set_caret_char (2, 'C'); + rich_location richloc (line_table, foo); + richloc.add_range (bar, SHOW_RANGE_WITH_CARET); + richloc.add_range (field, SHOW_RANGE_WITH_CARET); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ~~~~A~ ~~~B~ ~~~~~C~~~\n", + dc.test_show_locus (richloc)); +} + +/* Insertion fix-it hint: adding an "&" to the front of "P_bar.field". */ + +static void +test_one_liner_fixit_insert_before_utf8 () +{ + test_context dc; + location_t caret = linemap_position_for_column (line_table, 12); + rich_location richloc (line_table, caret); + richloc.add_fixit_insert_before ("&"); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^\n" + " &\n", + dc.test_show_locus (richloc)); +} + +/* Insertion fix-it hint: adding a "[0]" after "SS_foo". */ + +static void +test_one_liner_fixit_insert_after_utf8 () +{ + test_context dc; + location_t start = linemap_position_for_column (line_table, 1); + location_t finish = linemap_position_for_column (line_table, 8); + location_t foo = make_location (start, start, finish); + rich_location richloc (line_table, foo); + richloc.add_fixit_insert_after ("[0]"); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~\n" + " [0]\n", + dc.test_show_locus (richloc)); +} + +/* Removal fix-it hint: removal of the ".SS_fieldP". */ + +static void +test_one_liner_fixit_remove_utf8 () +{ + test_context dc; + location_t start = linemap_position_for_column (line_table, 18); + location_t finish = linemap_position_for_column (line_table, 30); + location_t dot = make_location (start, start, finish); + rich_location richloc (line_table, dot); + richloc.add_fixit_remove (); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~~~~~\n" + " ----------\n", + dc.test_show_locus (richloc)); +} + +/* Replace fix-it hint: replacing "SS_fieldP" with "m_SSfieldP". */ + +static void +test_one_liner_fixit_replace_utf8 () +{ + test_context dc; + location_t start = linemap_position_for_column (line_table, 19); + location_t finish = linemap_position_for_column (line_table, 30); + location_t field = make_location (start, start, finish); + rich_location richloc (line_table, field); + richloc.add_fixit_replace ("m_\xf0\x9f\x98\x82_field\xcf\x80"); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~~~~\n" + " m_\xf0\x9f\x98\x82" + "_field\xcf\x80\n", + dc.test_show_locus (richloc)); +} + +/* Replace fix-it hint: replacing "SS_fieldP" with "m_SSfieldP", + but where the caret was elsewhere. */ + +static void +test_one_liner_fixit_replace_non_equal_range_utf8 () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 10); + location_t start = linemap_position_for_column (line_table, 19); + location_t finish = linemap_position_for_column (line_table, 30); + rich_location richloc (line_table, equals); + source_range range; + range.m_start = start; + range.m_finish = finish; + richloc.add_fixit_replace (range, "m_\xf0\x9f\x98\x82_field\xcf\x80"); + /* The replacement range is not indicated in the annotation line, so + it should be indicated via an additional underline. */ + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^\n" + " ---------\n" + " m_\xf0\x9f\x98\x82" + "_field\xcf\x80\n", + dc.test_show_locus (richloc)); +} + +/* Replace fix-it hint: replacing "SS_fieldP" with "m_SSfieldP", + where the caret was elsewhere, but where a secondary range + exactly covers "field". */ + +static void +test_one_liner_fixit_replace_equal_secondary_range_utf8 () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 10); + location_t start = linemap_position_for_column (line_table, 19); + location_t finish = linemap_position_for_column (line_table, 30); + rich_location richloc (line_table, equals); + location_t field = make_location (start, start, finish); + richloc.add_range (field); + richloc.add_fixit_replace (field, "m_\xf0\x9f\x98\x82_field\xcf\x80"); + /* The replacement range is indicated in the annotation line, + so it shouldn't be indicated via an additional underline. */ + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^ ~~~~~~~~~\n" + " m_\xf0\x9f\x98\x82" + "_field\xcf\x80\n", + dc.test_show_locus (richloc)); +} + +/* Verify that we can use ad-hoc locations when adding fixits to a + rich_location. */ + +static void +test_one_liner_fixit_validation_adhoc_locations_utf8 () +{ + /* Generate a range that's too long to be packed, so must + be stored as an ad-hoc location (given the defaults + of 5 bits or 7 bits or 0 bits of packed range); 150 columns > 2**7. */ + const location_t c12 = linemap_position_for_column (line_table, 12); + const location_t c162 = linemap_position_for_column (line_table, 162); + const location_t loc = make_location (c12, c12, c162); + + if (c162 > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + ASSERT_TRUE (IS_ADHOC_LOC (loc)); + + /* Insert. */ + { + rich_location richloc (line_table, loc); + richloc.add_fixit_insert_before (loc, "test"); + /* It should not have been discarded by the validator. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + test_context dc; + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~~~~~~~~~~~ " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " \n" + " test\n", + dc.test_show_locus (richloc)); + } + + /* Remove. */ + { + rich_location richloc (line_table, loc); + source_range range = source_range::from_locations (loc, c162); + richloc.add_fixit_remove (range); + /* It should not have been discarded by the validator. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + test_context dc; + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~~~~~~~~~~~ " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " \n" + " -------------------------------------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------" + "----------\n", + dc.test_show_locus (richloc)); + } + + /* Replace. */ + { + rich_location richloc (line_table, loc); + source_range range = source_range::from_locations (loc, c162); + richloc.add_fixit_replace (range, "test"); + /* It should not have been discarded by the validator. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + test_context dc; + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~~~~~~~~~~~ " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " \n" + " test\n", + dc.test_show_locus (richloc)); + } +} + +/* Test of consolidating insertions at the same location. */ + +static void +test_one_liner_many_fixits_1_utf8 () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 10); + rich_location richloc (line_table, equals); + for (int i = 0; i < 19; i++) + richloc.add_fixit_insert_before (i & 1 ? "@" : "\xcf\x80"); + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^\n" + " \xcf\x80@\xcf\x80@\xcf\x80@\xcf\x80@\xcf\x80@" + "\xcf\x80@\xcf\x80@\xcf\x80@\xcf\x80@\xcf\x80\n", + dc.test_show_locus (richloc)); +} + +/* Ensure that we can add an arbitrary number of fix-it hints to a + rich_location, even if they are not consolidated. */ + +static void +test_one_liner_many_fixits_2_utf8 () +{ + test_context dc; + location_t equals = linemap_position_for_column (line_table, 10); + rich_location richloc (line_table, equals); + const int nlocs = 19; + int locs[nlocs] = {1, 5, 7, 9, 11, 14, 16, 18, 23, 25, 27, 29, 32, + 34, 36, 38, 40, 42, 44}; + for (int i = 0; i != nlocs; ++i) + { + location_t loc = linemap_position_for_column (line_table, locs[i]); + richloc.add_fixit_insert_before (loc, i & 1 ? "@" : "\xcf\x80"); + } + + ASSERT_EQ (nlocs, richloc.get_num_fixit_hints ()); + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^\n" + " \xcf\x80 @ \xcf\x80 @ \xcf\x80 @ \xcf\x80 @ \xcf\x80 @" + " \xcf\x80 @ \xcf\x80 @ \xcf\x80 @ \xcf\x80 @ \xcf\x80\n", + dc.test_show_locus (richloc)); +} + +/* Test of labeling the ranges within a rich_location. */ + +static void +test_one_liner_labels_utf8 () +{ + location_t foo + = make_location (linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 1), + linemap_position_for_column (line_table, 8)); + location_t bar + = make_location (linemap_position_for_column (line_table, 12), + linemap_position_for_column (line_table, 12), + linemap_position_for_column (line_table, 17)); + location_t field + = make_location (linemap_position_for_column (line_table, 19), + linemap_position_for_column (line_table, 19), + linemap_position_for_column (line_table, 30)); + + /* Example where all the labels fit on one line. */ + { + /* These three labels contain multibyte characters such that their byte + lengths are respectively (12, 10, 18), but their display widths are only + (6, 5, 9). All three fit on the line when considering the display + widths, but not when considering the byte widths, so verify that we do + indeed put them all on one line. */ + text_range_label label0 + ("\xcf\x80\xcf\x80\xcf\x80\xcf\x80\xcf\x80\xcf\x80"); + text_range_label label1 + ("\xf0\x9f\x98\x82\xf0\x9f\x98\x82\xcf\x80"); + text_range_label label2 + ("\xf0\x9f\x98\x82\xcf\x80\xf0\x9f\x98\x82\xf0\x9f\x98\x82\xcf\x80" + "\xcf\x80"); + gcc_rich_location richloc (foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + + { + test_context dc; + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~ ~~~~~ ~~~~~~~~~\n" + " | | |\n" + " \xcf\x80\xcf\x80\xcf\x80\xcf\x80\xcf\x80\xcf\x80" + " \xf0\x9f\x98\x82\xf0\x9f\x98\x82\xcf\x80" + " \xf0\x9f\x98\x82\xcf\x80\xf0\x9f\x98\x82" + "\xf0\x9f\x98\x82\xcf\x80\xcf\x80\n", + dc.test_show_locus (richloc)); + } + + } + + /* Example where the labels need extra lines. */ + { + text_range_label label0 ("label 0\xf0\x9f\x98\x82"); + text_range_label label1 ("label 1\xcf\x80"); + text_range_label label2 ("label 2\xcf\x80"); + gcc_rich_location richloc (foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + + test_context dc; + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~ ~~~~~ ~~~~~~~~~\n" + " | | |\n" + " | | label 2\xcf\x80\n" + " | label 1\xcf\x80\n" + " label 0\xf0\x9f\x98\x82\n", + dc.test_show_locus (richloc)); + } + + /* Example of boundary conditions: label 0 and 1 have just enough clearance, + but label 1 just touches label 2. */ + { + text_range_label label0 ("aaaaa\xf0\x9f\x98\x82\xcf\x80"); + text_range_label label1 ("bb\xf0\x9f\x98\x82\xf0\x9f\x98\x82"); + text_range_label label2 ("c"); + gcc_rich_location richloc (foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + + test_context dc; + ASSERT_STREQ (" \xf0\x9f\x98\x82" + "_foo = \xcf\x80" + "_bar.\xf0\x9f\x98\x82" + "_field\xcf\x80" + ";\n" + " ^~~~~~ ~~~~~ ~~~~~~~~~\n" + " | | |\n" + " | | c\n" + " aaaaa\xf0\x9f\x98\x82\xcf\x80" + " bb\xf0\x9f\x98\x82\xf0\x9f\x98\x82\n", + dc.test_show_locus (richloc)); + } + + /* Example of escaping the source lines. */ + { + text_range_label label0 ("label 0\xf0\x9f\x98\x82"); + text_range_label label1 ("label 1\xcf\x80"); + text_range_label label2 ("label 2\xcf\x80"); + gcc_rich_location richloc (foo, &label0, nullptr); + richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1); + richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2); + richloc.set_escape_on_output (true); + + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_UNICODE); + ASSERT_STREQ (" <U+1F602>_foo = <U+03C0>_bar.<U+1F602>_field<U+03C0>;\n" + " ^~~~~~~~~~~~~ ~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~\n" + " | | |\n" + " label 0\xf0\x9f\x98\x82" + /* ... */ " label 1\xcf\x80" + /* ...................*/ " label 2\xcf\x80\n", + dc.test_show_locus (richloc)); + } + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_BYTES); + ASSERT_STREQ + (" <f0><9f><98><82>_foo = <cf><80>_bar.<f0><9f><98><82>_field<cf><80>;\n" + " ^~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + " | | |\n" + " label 0\xf0\x9f\x98\x82" + /* ... */ " label 1\xcf\x80" + /* ..........................*/ " label 2\xcf\x80\n", + dc.test_show_locus (richloc)); + } + } +} + +/* Make sure that colorization codes don't interrupt a multibyte + sequence, which would corrupt it. */ +static void +test_one_liner_colorized_utf8 () +{ + test_context dc; + dc.colorize_source (true); + diagnostic_color_init (&dc, DIAGNOSTICS_COLOR_YES); + const location_t pi = linemap_position_for_column (line_table, 12); + rich_location richloc (line_table, pi); + + /* In order to avoid having the test depend on exactly how the colorization + was effected, just confirm there are two pi characters in the output. */ + const char *result = dc.test_show_locus (richloc); + const char *null_term = result + strlen (result); + const char *first_pi = strstr (result, "\xcf\x80"); + ASSERT_TRUE (first_pi && first_pi <= null_term - 2); + ASSERT_STR_CONTAINS (first_pi + 2, "\xcf\x80"); +} + +static const char * const one_liner_utf8_content + /* Display columns. + 0000000000000000000000011111111111111111111111111111112222222222222 + 1111111122222222345678900000000123456666666677777777890123444444445 */ + = "\xf0\x9f\x98\x82_foo = \xcf\x80_bar.\xf0\x9f\x98\x82_field\xcf\x80;\n"; + /* 0000000000000000000001111111111111111111222222222222222222222233333 + 1111222233334444567890122223333456789999000011112222345678999900001 + Byte columns. */ + +source_printing_fixture_one_liner_utf8:: +source_printing_fixture_one_liner_utf8 (const line_table_case &case_) +: source_printing_fixture (case_, one_liner_utf8_content) +{ +} + +/* Run the various one-liner tests. */ + +static void +test_diagnostic_show_locus_one_liner_utf8 (const line_table_case &case_) +{ + source_printing_fixture_one_liner_utf8 f (case_); + + location_t line_end = linemap_position_for_column (line_table, 31); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + ASSERT_STREQ (f.get_filename (), LOCATION_FILE (line_end)); + ASSERT_EQ (1, LOCATION_LINE (line_end)); + ASSERT_EQ (31, LOCATION_COLUMN (line_end)); + + diagnostics::char_span lspan = f.m_fc.get_source_line (f.get_filename (), 1); + ASSERT_EQ (25, cpp_display_width (lspan.get_buffer (), lspan.length (), + def_policy ())); + ASSERT_EQ (25, location_compute_display_column (f.m_fc, + expand_location (line_end), + def_policy ())); + + test_one_liner_simple_caret_utf8 (); + test_one_liner_caret_and_range_utf8 (); + test_one_liner_multiple_carets_and_ranges_utf8 (); + test_one_liner_fixit_insert_before_utf8 (); + test_one_liner_fixit_insert_after_utf8 (); + test_one_liner_fixit_remove_utf8 (); + test_one_liner_fixit_replace_utf8 (); + test_one_liner_fixit_replace_non_equal_range_utf8 (); + test_one_liner_fixit_replace_equal_secondary_range_utf8 (); + test_one_liner_fixit_validation_adhoc_locations_utf8 (); + test_one_liner_many_fixits_1_utf8 (); + test_one_liner_many_fixits_2_utf8 (); + test_one_liner_labels_utf8 (); + test_one_liner_colorized_utf8 (); +} + +/* Verify that gcc_rich_location::add_location_if_nearby works. */ + +static void +test_add_location_if_nearby (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333. + ...123456789012345678901234567890123456789. */ + const char *content + = ("struct same_line { double x; double y; ;\n" /* line 1. */ + "struct different_line\n" /* line 2. */ + "{\n" /* line 3. */ + " double x;\n" /* line 4. */ + " double y;\n" /* line 5. */ + ";\n"); /* line 6. */ + temp_source_file tmp (SELFTEST_LOCATION, ".c", content, nullptr); + line_table_test ltt (case_); + + const line_map_ordinary *ord_map + = linemap_check_ordinary (linemap_add (line_table, LC_ENTER, false, + tmp.get_filename (), 0)); + + linemap_line_start (line_table, 1, 100); + + const location_t final_line_end + = linemap_position_for_line_and_column (line_table, ord_map, 6, 7); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (final_line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Test of add_location_if_nearby on the same line as the + primary location. */ + { + test_context dc; + const location_t missing_close_brace_1_39 + = linemap_position_for_line_and_column (line_table, ord_map, 1, 39); + const location_t matching_open_brace_1_18 + = linemap_position_for_line_and_column (line_table, ord_map, 1, 18); + gcc_rich_location richloc (missing_close_brace_1_39); + bool added = richloc.add_location_if_nearby (dc, + matching_open_brace_1_18); + ASSERT_TRUE (added); + ASSERT_EQ (2, richloc.get_num_locations ()); + ASSERT_STREQ (" struct same_line { double x; double y; ;\n" + " ~ ^\n", + dc.test_show_locus (richloc)); + } + + /* Test of add_location_if_nearby on a different line to the + primary location. */ + { + test_context dc; + const location_t missing_close_brace_6_1 + = linemap_position_for_line_and_column (line_table, ord_map, 6, 1); + const location_t matching_open_brace_3_1 + = linemap_position_for_line_and_column (line_table, ord_map, 3, 1); + gcc_rich_location richloc (missing_close_brace_6_1); + bool added = richloc.add_location_if_nearby (dc, + matching_open_brace_3_1); + ASSERT_FALSE (added); + ASSERT_EQ (1, richloc.get_num_locations ()); + } +} + +/* Verify that we print fixits even if they only affect lines + outside those covered by the ranges in the rich_location. */ + +static void +test_diagnostic_show_locus_fixit_lines (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333. + ...123456789012345678901234567890123456789. */ + const char *content + = ("struct point { double x; double y; };\n" /* line 1. */ + "struct point origin = {x: 0.0,\n" /* line 2. */ + " y\n" /* line 3. */ + "\n" /* line 4. */ + "\n" /* line 5. */ + " : 0.0};\n"); /* line 6. */ + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + line_table_test ltt (case_); + + const line_map_ordinary *ord_map + = linemap_check_ordinary (linemap_add (line_table, LC_ENTER, false, + tmp.get_filename (), 0)); + + linemap_line_start (line_table, 1, 100); + + const location_t final_line_end + = linemap_position_for_line_and_column (line_table, ord_map, 6, 36); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (final_line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* A pair of tests for modernizing the initializers to C99-style. */ + + /* The one-liner case (line 2). */ + { + test_context dc; + const location_t x + = linemap_position_for_line_and_column (line_table, ord_map, 2, 24); + const location_t colon + = linemap_position_for_line_and_column (line_table, ord_map, 2, 25); + rich_location richloc (line_table, colon); + richloc.add_fixit_insert_before (x, "."); + richloc.add_fixit_replace (colon, "="); + ASSERT_STREQ (" struct point origin = {x: 0.0,\n" + " ^\n" + " .=\n", + dc.test_show_locus (richloc)); + } + + /* The multiline case. The caret for the rich_location is on line 6; + verify that insertion fixit on line 3 is still printed (and that + span starts are printed due to the gap between the span at line 3 + and that at line 6). */ + { + test_context dc; + const location_t y + = linemap_position_for_line_and_column (line_table, ord_map, 3, 24); + const location_t colon + = linemap_position_for_line_and_column (line_table, ord_map, 6, 25); + rich_location richloc (line_table, colon); + richloc.add_fixit_insert_before (y, "."); + richloc.add_fixit_replace (colon, "="); + ASSERT_STREQ ("FILENAME:3:24:\n" + " y\n" + " .\n" + "FILENAME:6:25:\n" + " : 0.0};\n" + " ^\n" + " =\n", + dc.test_show_locus (richloc)); + } + + /* As above, but verify the behavior of multiple line spans + with line-numbering enabled. */ + { + const location_t y + = linemap_position_for_line_and_column (line_table, ord_map, 3, 24); + const location_t colon + = linemap_position_for_line_and_column (line_table, ord_map, 6, 25); + rich_location richloc (line_table, colon); + richloc.add_fixit_insert_before (y, "."); + richloc.add_fixit_replace (colon, "="); + test_context dc; + dc.show_line_numbers (true); + ASSERT_STREQ (" 3 | y\n" + " | .\n" + "......\n" + " 6 | : 0.0};\n" + " | ^\n" + " | =\n", + dc.test_show_locus (richloc)); + } +} + + +/* Verify that fix-it hints are appropriately consolidated. + + If any fix-it hints in a rich_location involve locations beyond + LINE_MAP_MAX_LOCATION_WITH_COLS, then we can't reliably apply + the fix-it as a whole, so there should be none. + + Otherwise, verify that consecutive "replace" and "remove" fix-its + are merged, and that other fix-its remain separate. */ + +static void +test_fixit_consolidation (const line_table_case &case_) +{ + line_table_test ltt (case_); + + linemap_add (line_table, LC_ENTER, false, "test.c", 1); + + const location_t c10 = linemap_position_for_column (line_table, 10); + const location_t c15 = linemap_position_for_column (line_table, 15); + const location_t c16 = linemap_position_for_column (line_table, 16); + const location_t c17 = linemap_position_for_column (line_table, 17); + const location_t c20 = linemap_position_for_column (line_table, 20); + const location_t c21 = linemap_position_for_column (line_table, 21); + const location_t caret = c10; + + /* Insert + insert. */ + { + rich_location richloc (line_table, caret); + richloc.add_fixit_insert_before (c10, "foo"); + richloc.add_fixit_insert_before (c15, "bar"); + + if (c15 > LINE_MAP_MAX_LOCATION_WITH_COLS) + /* Bogus column info for 2nd fixit, so no fixits. */ + ASSERT_EQ (0, richloc.get_num_fixit_hints ()); + else + /* They should not have been merged. */ + ASSERT_EQ (2, richloc.get_num_fixit_hints ()); + } + + /* Insert + replace. */ + { + rich_location richloc (line_table, caret); + richloc.add_fixit_insert_before (c10, "foo"); + richloc.add_fixit_replace (source_range::from_locations (c15, c17), + "bar"); + + if (c17 > LINE_MAP_MAX_LOCATION_WITH_COLS) + /* Bogus column info for 2nd fixit, so no fixits. */ + ASSERT_EQ (0, richloc.get_num_fixit_hints ()); + else + /* They should not have been merged. */ + ASSERT_EQ (2, richloc.get_num_fixit_hints ()); + } + + /* Replace + non-consecutive insert. */ + { + rich_location richloc (line_table, caret); + richloc.add_fixit_replace (source_range::from_locations (c10, c15), + "bar"); + richloc.add_fixit_insert_before (c17, "foo"); + + if (c17 > LINE_MAP_MAX_LOCATION_WITH_COLS) + /* Bogus column info for 2nd fixit, so no fixits. */ + ASSERT_EQ (0, richloc.get_num_fixit_hints ()); + else + /* They should not have been merged. */ + ASSERT_EQ (2, richloc.get_num_fixit_hints ()); + } + + /* Replace + non-consecutive replace. */ + { + rich_location richloc (line_table, caret); + richloc.add_fixit_replace (source_range::from_locations (c10, c15), + "foo"); + richloc.add_fixit_replace (source_range::from_locations (c17, c20), + "bar"); + + if (c20 > LINE_MAP_MAX_LOCATION_WITH_COLS) + /* Bogus column info for 2nd fixit, so no fixits. */ + ASSERT_EQ (0, richloc.get_num_fixit_hints ()); + else + /* They should not have been merged. */ + ASSERT_EQ (2, richloc.get_num_fixit_hints ()); + } + + /* Replace + consecutive replace. */ + { + rich_location richloc (line_table, caret); + richloc.add_fixit_replace (source_range::from_locations (c10, c15), + "foo"); + richloc.add_fixit_replace (source_range::from_locations (c16, c20), + "bar"); + + if (c20 > LINE_MAP_MAX_LOCATION_WITH_COLS) + /* Bogus column info for 2nd fixit, so no fixits. */ + ASSERT_EQ (0, richloc.get_num_fixit_hints ()); + else + { + /* They should have been merged into a single "replace". */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + const fixit_hint *hint = richloc.get_fixit_hint (0); + ASSERT_STREQ ("foobar", hint->get_string ()); + ASSERT_EQ (c10, hint->get_start_loc ()); + ASSERT_EQ (c21, hint->get_next_loc ()); + } + } + + /* Replace + consecutive removal. */ + { + rich_location richloc (line_table, caret); + richloc.add_fixit_replace (source_range::from_locations (c10, c15), + "foo"); + richloc.add_fixit_remove (source_range::from_locations (c16, c20)); + + if (c20 > LINE_MAP_MAX_LOCATION_WITH_COLS) + /* Bogus column info for 2nd fixit, so no fixits. */ + ASSERT_EQ (0, richloc.get_num_fixit_hints ()); + else + { + /* They should have been merged into a single replace, with the + range extended to cover that of the removal. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + const fixit_hint *hint = richloc.get_fixit_hint (0); + ASSERT_STREQ ("foo", hint->get_string ()); + ASSERT_EQ (c10, hint->get_start_loc ()); + ASSERT_EQ (c21, hint->get_next_loc ()); + } + } + + /* Consecutive removals. */ + { + rich_location richloc (line_table, caret); + richloc.add_fixit_remove (source_range::from_locations (c10, c15)); + richloc.add_fixit_remove (source_range::from_locations (c16, c20)); + + if (c20 > LINE_MAP_MAX_LOCATION_WITH_COLS) + /* Bogus column info for 2nd fixit, so no fixits. */ + ASSERT_EQ (0, richloc.get_num_fixit_hints ()); + else + { + /* They should have been merged into a single "replace-with-empty". */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + const fixit_hint *hint = richloc.get_fixit_hint (0); + ASSERT_STREQ ("", hint->get_string ()); + ASSERT_EQ (c10, hint->get_start_loc ()); + ASSERT_EQ (c21, hint->get_next_loc ()); + } + } +} + +/* Verify that the line_corrections machinery correctly prints + overlapping fixit-hints. */ + +static void +test_overlapped_fixit_printing (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333. + ...123456789012345678901234567890123456789. */ + const char *content + = (" foo *f = (foo *)ptr->field;\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".C", content); + diagnostics::file_cache fc; + line_table_test ltt (case_); + + const line_map_ordinary *ord_map + = linemap_check_ordinary (linemap_add (line_table, LC_ENTER, false, + tmp.get_filename (), 0)); + + linemap_line_start (line_table, 1, 100); + + const location_t final_line_end + = linemap_position_for_line_and_column (line_table, ord_map, 6, 36); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (final_line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* A test for converting a C-style cast to a C++-style cast. */ + const location_t open_paren + = linemap_position_for_line_and_column (line_table, ord_map, 1, 12); + const location_t close_paren + = linemap_position_for_line_and_column (line_table, ord_map, 1, 18); + const location_t expr_start + = linemap_position_for_line_and_column (line_table, ord_map, 1, 19); + const location_t expr_finish + = linemap_position_for_line_and_column (line_table, ord_map, 1, 28); + const location_t expr = make_location (expr_start, expr_start, expr_finish); + + /* Various examples of fix-it hints that aren't themselves consolidated, + but for which the *printing* may need consolidation. */ + + /* Example where 3 fix-it hints are printed as one. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_replace (open_paren, "const_cast<"); + richloc.add_fixit_replace (close_paren, "> ("); + richloc.add_fixit_insert_after (")"); + + ASSERT_STREQ (" foo *f = (foo *)ptr->field;\n" + " ^~~~~~~~~~\n" + " -----------------\n" + " const_cast<foo *> (ptr->field)\n", + dc.test_show_locus (richloc)); + + /* Unit-test the line_corrections machinery. */ + char_display_policy policy (make_char_policy (dc, richloc)); + ASSERT_EQ (3, richloc.get_num_fixit_hints ()); + const fixit_hint *hint_0 = richloc.get_fixit_hint (0); + ASSERT_EQ (column_range (12, 12), + get_affected_range (fc, policy, hint_0, CU_BYTES)); + ASSERT_EQ (column_range (12, 12), + get_affected_range (fc, policy, hint_0, CU_DISPLAY_COLS)); + ASSERT_EQ (column_range (12, 22), get_printed_columns (fc, policy, hint_0)); + const fixit_hint *hint_1 = richloc.get_fixit_hint (1); + ASSERT_EQ (column_range (18, 18), + get_affected_range (fc, policy, hint_1, CU_BYTES)); + ASSERT_EQ (column_range (18, 18), + get_affected_range (fc, policy, hint_1, CU_DISPLAY_COLS)); + ASSERT_EQ (column_range (18, 20), get_printed_columns (fc, policy, hint_1)); + const fixit_hint *hint_2 = richloc.get_fixit_hint (2); + ASSERT_EQ (column_range (29, 28), + get_affected_range (fc, policy, hint_2, CU_BYTES)); + ASSERT_EQ (column_range (29, 28), + get_affected_range (fc, policy, hint_2, CU_DISPLAY_COLS)); + ASSERT_EQ (column_range (29, 29), get_printed_columns (fc, policy, hint_2)); + + /* Add each hint in turn to a line_corrections instance, + and verify that they are consolidated into one correction instance + as expected. */ + line_corrections lc (fc, policy, tmp.get_filename (), 1); + + /* The first replace hint by itself. */ + lc.add_hint (hint_0); + ASSERT_EQ (1, lc.m_corrections.length ()); + ASSERT_EQ (column_range (12, 12), lc.m_corrections[0]->m_affected_bytes); + ASSERT_EQ (column_range (12, 12), lc.m_corrections[0]->m_affected_columns); + ASSERT_EQ (column_range (12, 22), lc.m_corrections[0]->m_printed_columns); + ASSERT_STREQ ("const_cast<", lc.m_corrections[0]->m_text); + + /* After the second replacement hint, they are printed together + as a replacement (along with the text between them). */ + lc.add_hint (hint_1); + ASSERT_EQ (1, lc.m_corrections.length ()); + ASSERT_STREQ ("const_cast<foo *> (", lc.m_corrections[0]->m_text); + ASSERT_EQ (column_range (12, 18), lc.m_corrections[0]->m_affected_bytes); + ASSERT_EQ (column_range (12, 18), lc.m_corrections[0]->m_affected_columns); + ASSERT_EQ (column_range (12, 30), lc.m_corrections[0]->m_printed_columns); + + /* After the final insertion hint, they are all printed together + as a replacement (along with the text between them). */ + lc.add_hint (hint_2); + ASSERT_STREQ ("const_cast<foo *> (ptr->field)", + lc.m_corrections[0]->m_text); + ASSERT_EQ (1, lc.m_corrections.length ()); + ASSERT_EQ (column_range (12, 28), lc.m_corrections[0]->m_affected_bytes); + ASSERT_EQ (column_range (12, 28), lc.m_corrections[0]->m_affected_columns); + ASSERT_EQ (column_range (12, 41), lc.m_corrections[0]->m_printed_columns); + } + + /* Example where two are consolidated during printing. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_replace (open_paren, "CAST ("); + richloc.add_fixit_replace (close_paren, ") ("); + richloc.add_fixit_insert_after (")"); + + ASSERT_STREQ (" foo *f = (foo *)ptr->field;\n" + " ^~~~~~~~~~\n" + " -\n" + " CAST (-\n" + " ) ( )\n", + dc.test_show_locus (richloc)); + } + + /* Example where none are consolidated during printing. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_replace (open_paren, "CST ("); + richloc.add_fixit_replace (close_paren, ") ("); + richloc.add_fixit_insert_after (")"); + + ASSERT_STREQ (" foo *f = (foo *)ptr->field;\n" + " ^~~~~~~~~~\n" + " -\n" + " CST ( -\n" + " ) ( )\n", + dc.test_show_locus (richloc)); + } + + /* Example of deletion fix-it hints. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_insert_before (open_paren, "(bar *)"); + source_range victim = {open_paren, close_paren}; + richloc.add_fixit_remove (victim); + + /* This case is actually handled by fixit-consolidation, + rather than by line_corrections. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + ASSERT_STREQ (" foo *f = (foo *)ptr->field;\n" + " ^~~~~~~~~~\n" + " -------\n" + " (bar *)\n", + dc.test_show_locus (richloc)); + } + + /* Example of deletion fix-it hints that would overlap. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_insert_before (open_paren, "(longer *)"); + source_range victim = {expr_start, expr_finish}; + richloc.add_fixit_remove (victim); + + /* These fixits are not consolidated. */ + ASSERT_EQ (2, richloc.get_num_fixit_hints ()); + + /* But the corrections are. */ + ASSERT_STREQ (" foo *f = (foo *)ptr->field;\n" + " ^~~~~~~~~~\n" + " -----------------\n" + " (longer *)(foo *)\n", + dc.test_show_locus (richloc)); + } + + /* Example of insertion fix-it hints that would overlap. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_insert_before (open_paren, "LONGER THAN THE CAST"); + richloc.add_fixit_insert_after (close_paren, "TEST"); + + /* The first insertion is long enough that if printed naively, + it would overlap with the second. + Verify that they are printed as a single replacement. */ + ASSERT_STREQ (" foo *f = (foo *)ptr->field;\n" + " ^~~~~~~~~~\n" + " -------\n" + " LONGER THAN THE CAST(foo *)TEST\n", + dc.test_show_locus (richloc)); + } +} + +/* Multibyte-aware version of preceding tests. See comments above + test_one_liner_simple_caret_utf8() too, we use the same two multibyte + characters here. */ + +static void +test_overlapped_fixit_printing_utf8 (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. */ + + const char *content + /* Display columns. + 00000000000000000000000111111111111111111111111222222222222222223 + 12344444444555555556789012344444444555555556789012345678999999990 */ + = " f\xf0\x9f\x98\x82 *f = (f\xf0\x9f\x98\x82 *)ptr->field\xcf\x80;\n"; + /* 00000000000000000000011111111111111111111112222222222333333333333 + 12344445555666677778901234566667777888899990123456789012333344445 + Byte columns. */ + + temp_source_file tmp (SELFTEST_LOCATION, ".C", content); + line_table_test ltt (case_); + + const line_map_ordinary *ord_map + = linemap_check_ordinary (linemap_add (line_table, LC_ENTER, false, + tmp.get_filename (), 0)); + + linemap_line_start (line_table, 1, 100); + + const location_t final_line_end + = linemap_position_for_line_and_column (line_table, ord_map, 6, 50); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (final_line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* A test for converting a C-style cast to a C++-style cast. */ + const location_t open_paren + = linemap_position_for_line_and_column (line_table, ord_map, 1, 14); + const location_t close_paren + = linemap_position_for_line_and_column (line_table, ord_map, 1, 22); + const location_t expr_start + = linemap_position_for_line_and_column (line_table, ord_map, 1, 23); + const location_t expr_finish + = linemap_position_for_line_and_column (line_table, ord_map, 1, 34); + const location_t expr = make_location (expr_start, expr_start, expr_finish); + + /* Various examples of fix-it hints that aren't themselves consolidated, + but for which the *printing* may need consolidation. */ + + /* Example where 3 fix-it hints are printed as one. */ + { + test_context dc; + diagnostics::file_cache &fc = dc.get_file_cache (); + rich_location richloc (line_table, expr); + richloc.add_fixit_replace (open_paren, "const_cast<"); + richloc.add_fixit_replace (close_paren, "> ("); + richloc.add_fixit_insert_after (")"); + + ASSERT_STREQ (" f\xf0\x9f\x98\x82" + " *f = (f\xf0\x9f\x98\x82" + " *)ptr->field\xcf\x80" + ";\n" + " ^~~~~~~~~~~\n" + " ------------------\n" + " const_cast<f\xf0\x9f\x98\x82" + " *> (ptr->field\xcf\x80" + ")\n", + dc.test_show_locus (richloc)); + + /* Unit-test the line_corrections machinery. */ + char_display_policy policy (make_char_policy (dc, richloc)); + ASSERT_EQ (3, richloc.get_num_fixit_hints ()); + const fixit_hint *hint_0 = richloc.get_fixit_hint (0); + ASSERT_EQ (column_range (14, 14), + get_affected_range (fc, policy, hint_0, CU_BYTES)); + ASSERT_EQ (column_range (12, 12), + get_affected_range (fc, policy, hint_0, CU_DISPLAY_COLS)); + ASSERT_EQ (column_range (12, 22), get_printed_columns (fc, policy, hint_0)); + const fixit_hint *hint_1 = richloc.get_fixit_hint (1); + ASSERT_EQ (column_range (22, 22), + get_affected_range (fc, policy, hint_1, CU_BYTES)); + ASSERT_EQ (column_range (18, 18), + get_affected_range (fc, policy, hint_1, CU_DISPLAY_COLS)); + ASSERT_EQ (column_range (18, 20), get_printed_columns (fc, policy, hint_1)); + const fixit_hint *hint_2 = richloc.get_fixit_hint (2); + ASSERT_EQ (column_range (35, 34), + get_affected_range (fc, policy, hint_2, CU_BYTES)); + ASSERT_EQ (column_range (30, 29), + get_affected_range (fc, policy, hint_2, CU_DISPLAY_COLS)); + ASSERT_EQ (column_range (30, 30), get_printed_columns (fc, policy, hint_2)); + + /* Add each hint in turn to a line_corrections instance, + and verify that they are consolidated into one correction instance + as expected. */ + line_corrections lc (fc, policy, tmp.get_filename (), 1); + + /* The first replace hint by itself. */ + lc.add_hint (hint_0); + ASSERT_EQ (1, lc.m_corrections.length ()); + ASSERT_EQ (column_range (14, 14), lc.m_corrections[0]->m_affected_bytes); + ASSERT_EQ (column_range (12, 12), lc.m_corrections[0]->m_affected_columns); + ASSERT_EQ (column_range (12, 22), lc.m_corrections[0]->m_printed_columns); + ASSERT_STREQ ("const_cast<", lc.m_corrections[0]->m_text); + + /* After the second replacement hint, they are printed together + as a replacement (along with the text between them). */ + lc.add_hint (hint_1); + ASSERT_EQ (1, lc.m_corrections.length ()); + ASSERT_STREQ ("const_cast<f\xf0\x9f\x98\x82 *> (", + lc.m_corrections[0]->m_text); + ASSERT_EQ (column_range (14, 22), lc.m_corrections[0]->m_affected_bytes); + ASSERT_EQ (column_range (12, 18), lc.m_corrections[0]->m_affected_columns); + ASSERT_EQ (column_range (12, 30), lc.m_corrections[0]->m_printed_columns); + + /* After the final insertion hint, they are all printed together + as a replacement (along with the text between them). */ + lc.add_hint (hint_2); + ASSERT_STREQ ("const_cast<f\xf0\x9f\x98\x82 *> (ptr->field\xcf\x80)", + lc.m_corrections[0]->m_text); + ASSERT_EQ (1, lc.m_corrections.length ()); + ASSERT_EQ (column_range (14, 34), lc.m_corrections[0]->m_affected_bytes); + ASSERT_EQ (column_range (12, 29), lc.m_corrections[0]->m_affected_columns); + ASSERT_EQ (column_range (12, 42), lc.m_corrections[0]->m_printed_columns); + } + + /* Example where two are consolidated during printing. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_replace (open_paren, "CAST ("); + richloc.add_fixit_replace (close_paren, ") ("); + richloc.add_fixit_insert_after (")"); + + ASSERT_STREQ (" f\xf0\x9f\x98\x82" + " *f = (f\xf0\x9f\x98\x82" + " *)ptr->field\xcf\x80" + ";\n" + " ^~~~~~~~~~~\n" + " -\n" + " CAST (-\n" + " ) ( )\n", + dc.test_show_locus (richloc)); + } + + /* Example where none are consolidated during printing. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_replace (open_paren, "CST ("); + richloc.add_fixit_replace (close_paren, ") ("); + richloc.add_fixit_insert_after (")"); + + ASSERT_STREQ (" f\xf0\x9f\x98\x82" + " *f = (f\xf0\x9f\x98\x82" + " *)ptr->field\xcf\x80" + ";\n" + " ^~~~~~~~~~~\n" + " -\n" + " CST ( -\n" + " ) ( )\n", + dc.test_show_locus (richloc)); + } + + /* Example of deletion fix-it hints. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_insert_before (open_paren, "(bar\xf0\x9f\x98\x82 *)"); + source_range victim = {open_paren, close_paren}; + richloc.add_fixit_remove (victim); + + /* This case is actually handled by fixit-consolidation, + rather than by line_corrections. */ + ASSERT_EQ (1, richloc.get_num_fixit_hints ()); + + ASSERT_STREQ (" f\xf0\x9f\x98\x82" + " *f = (f\xf0\x9f\x98\x82" + " *)ptr->field\xcf\x80" + ";\n" + " ^~~~~~~~~~~\n" + " -------\n" + " (bar\xf0\x9f\x98\x82" + " *)\n", + dc.test_show_locus (richloc)); + } + + /* Example of deletion fix-it hints that would overlap. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_insert_before (open_paren, "(long\xf0\x9f\x98\x82 *)"); + source_range victim = {expr_start, expr_finish}; + richloc.add_fixit_remove (victim); + + /* These fixits are not consolidated. */ + ASSERT_EQ (2, richloc.get_num_fixit_hints ()); + + /* But the corrections are. */ + ASSERT_STREQ (" f\xf0\x9f\x98\x82" + " *f = (f\xf0\x9f\x98\x82" + " *)ptr->field\xcf\x80" + ";\n" + " ^~~~~~~~~~~\n" + " ------------------\n" + " (long\xf0\x9f\x98\x82" + " *)(f\xf0\x9f\x98\x82" + " *)\n", + dc.test_show_locus (richloc)); + } + + /* Example of insertion fix-it hints that would overlap. */ + { + test_context dc; + rich_location richloc (line_table, expr); + richloc.add_fixit_insert_before + (open_paren, "L\xf0\x9f\x98\x82NGER THAN THE CAST"); + richloc.add_fixit_insert_after (close_paren, "TEST"); + + /* The first insertion is long enough that if printed naively, + it would overlap with the second. + Verify that they are printed as a single replacement. */ + ASSERT_STREQ (" f\xf0\x9f\x98\x82" + " *f = (f\xf0\x9f\x98\x82" + " *)ptr->field\xcf\x80" + ";\n" + " ^~~~~~~~~~~\n" + " -------\n" + " L\xf0\x9f\x98\x82" + "NGER THAN THE CAST(f\xf0\x9f\x98\x82" + " *)TEST\n", + dc.test_show_locus (richloc)); + } +} + +/* Verify that the line_corrections machinery correctly prints + overlapping fixit-hints that have been added in the wrong + order. + Adapted from PR c/81405 seen on gcc.dg/init-excess-1.c*/ + +static void +test_overlapped_fixit_printing_2 (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + ...000000000111111111122222222223333333333. + ...123456789012345678901234567890123456789. */ + const char *content + = ("int a5[][0][0] = { 1, 2 };\n"); + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + line_table_test ltt (case_); + + const line_map_ordinary *ord_map + = linemap_check_ordinary (linemap_add (line_table, LC_ENTER, false, + tmp.get_filename (), 0)); + + linemap_line_start (line_table, 1, 100); + + const location_t final_line_end + = linemap_position_for_line_and_column (line_table, ord_map, 1, 100); + + /* Don't attempt to run the tests if column data might be unavailable. */ + if (final_line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + const location_t col_1 + = linemap_position_for_line_and_column (line_table, ord_map, 1, 1); + const location_t col_20 + = linemap_position_for_line_and_column (line_table, ord_map, 1, 20); + const location_t col_21 + = linemap_position_for_line_and_column (line_table, ord_map, 1, 21); + const location_t col_23 + = linemap_position_for_line_and_column (line_table, ord_map, 1, 23); + const location_t col_25 + = linemap_position_for_line_and_column (line_table, ord_map, 1, 25); + + /* Two insertions, in the wrong order. */ + { + test_context dc; + diagnostics::file_cache &fc = dc.get_file_cache (); + + rich_location richloc (line_table, col_20); + richloc.add_fixit_insert_before (col_23, "{"); + richloc.add_fixit_insert_before (col_21, "}"); + + /* These fixits should be accepted; they can't be consolidated. */ + char_display_policy policy (make_char_policy (dc, richloc)); + ASSERT_EQ (2, richloc.get_num_fixit_hints ()); + const fixit_hint *hint_0 = richloc.get_fixit_hint (0); + ASSERT_EQ (column_range (23, 22), + get_affected_range (fc, policy, hint_0, CU_BYTES)); + ASSERT_EQ (column_range (23, 23), get_printed_columns (fc, policy, hint_0)); + const fixit_hint *hint_1 = richloc.get_fixit_hint (1); + ASSERT_EQ (column_range (21, 20), + get_affected_range (fc, policy, hint_1, CU_BYTES)); + ASSERT_EQ (column_range (21, 21), get_printed_columns (fc, policy, hint_1)); + + /* Verify that they're printed correctly. */ + ASSERT_STREQ (" int a5[][0][0] = { 1, 2 };\n" + " ^\n" + " } {\n", + dc.test_show_locus (richloc)); + } + + /* Various overlapping insertions, some occurring "out of order" + (reproducing the fix-it hints from PR c/81405). */ + { + test_context dc; + rich_location richloc (line_table, col_20); + + richloc.add_fixit_insert_before (col_20, "{{"); + richloc.add_fixit_insert_before (col_21, "}}"); + richloc.add_fixit_insert_before (col_23, "{"); + richloc.add_fixit_insert_before (col_21, "}"); + richloc.add_fixit_insert_before (col_23, "{{"); + richloc.add_fixit_insert_before (col_25, "}"); + richloc.add_fixit_insert_before (col_21, "}"); + richloc.add_fixit_insert_before (col_1, "{"); + richloc.add_fixit_insert_before (col_25, "}"); + + ASSERT_STREQ (" int a5[][0][0] = { 1, 2 };\n" + " ^\n" + " { -----\n" + " {{1}}}}, {{{2 }}\n", + dc.test_show_locus (richloc)); + } +} + +/* Insertion fix-it hint: adding a "break;" on a line by itself. */ + +static void +test_fixit_insert_containing_newline (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = (" case 'a':\n" /* line 1. */ + " x = a;\n" /* line 2. */ + " case 'b':\n" /* line 3. */ + " x = b;\n");/* line 4. */ + + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 3); + + location_t case_start = linemap_position_for_column (line_table, 5); + location_t case_finish = linemap_position_for_column (line_table, 13); + location_t case_loc = make_location (case_start, case_start, case_finish); + location_t line_start = linemap_position_for_column (line_table, 1); + + if (case_finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Add a "break;" on a line by itself before line 3 i.e. before + column 1 of line 3. */ + { + rich_location richloc (line_table, case_loc); + richloc.add_fixit_insert_before (line_start, " break;\n"); + + /* Without line numbers. */ + { + test_context dc; + ASSERT_STREQ (" x = a;\n" + "+ break;\n" + " case 'b':\n" + " ^~~~~~~~~\n", + dc.test_show_locus (richloc)); + } + + /* With line numbers. */ + { + test_context dc; + dc.show_line_numbers (true); + ASSERT_STREQ (" 2 | x = a;\n" + " +++ |+ break;\n" + " 3 | case 'b':\n" + " | ^~~~~~~~~\n", + dc.test_show_locus (richloc)); + } + } + + /* Verify that attempts to add text with a newline fail when the + insertion point is *not* at the start of a line. */ + { + rich_location richloc (line_table, case_loc); + richloc.add_fixit_insert_before (case_start, "break;\n"); + ASSERT_TRUE (richloc.seen_impossible_fixit_p ()); + test_context dc; + ASSERT_STREQ (" case 'b':\n" + " ^~~~~~~~~\n", + dc.test_show_locus (richloc)); + } +} + +/* Insertion fix-it hint: adding a "#include <stdio.h>\n" to the top + of the file, where the fix-it is printed in a different line-span + to the primary range of the diagnostic. */ + +static void +test_fixit_insert_containing_newline_2 (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111111. + .........................1234567890123456. */ + const char *old_content = ("test (int ch)\n" /* line 1. */ + "{\n" /* line 2. */ + " putchar (ch);\n" /* line 3. */ + "}\n"); /* line 4. */ + + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + line_table_test ltt (case_); + + const line_map_ordinary *ord_map = linemap_check_ordinary + (linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 0)); + linemap_line_start (line_table, 1, 100); + + /* The primary range is the "putchar" token. */ + location_t putchar_start + = linemap_position_for_line_and_column (line_table, ord_map, 3, 2); + location_t putchar_finish + = linemap_position_for_line_and_column (line_table, ord_map, 3, 8); + location_t putchar_loc + = make_location (putchar_start, putchar_start, putchar_finish); + rich_location richloc (line_table, putchar_loc); + + /* Add a "#include <stdio.h>" on a line by itself at the top of the file. */ + location_t file_start + = linemap_position_for_line_and_column (line_table, ord_map, 1, 1); + richloc.add_fixit_insert_before (file_start, "#include <stdio.h>\n"); + + if (putchar_finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + { + test_context dc; + ASSERT_STREQ ("FILENAME:1:1:\n" + "+#include <stdio.h>\n" + " test (int ch)\n" + "FILENAME:3:2:\n" + " putchar (ch);\n" + " ^~~~~~~\n", + dc.test_show_locus (richloc)); + } + + /* With line-numbering, the line spans are close enough to be + consolidated, since it makes little sense to skip line 2. */ + { + test_context dc; + dc.show_line_numbers (true); + ASSERT_STREQ (" +++ |+#include <stdio.h>\n" + " 1 | test (int ch)\n" + " 2 | {\n" + " 3 | putchar (ch);\n" + " | ^~~~~~~\n", + dc.test_show_locus (richloc)); + } +} + +/* Replacement fix-it hint containing a newline. + This will fail, as newlines are only supported when inserting at the + beginning of a line. */ + +static void +test_fixit_replace_containing_newline (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + .........................0000000001111. + .........................1234567890123. */ + const char *old_content = "foo = bar ();\n"; + + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 1); + + /* Replace the " = " with "\n = ", as if we were reformatting an + overly long line. */ + location_t start = linemap_position_for_column (line_table, 4); + location_t finish = linemap_position_for_column (line_table, 6); + location_t loc = linemap_position_for_column (line_table, 13); + rich_location richloc (line_table, loc); + source_range range = source_range::from_locations (start, finish); + richloc.add_fixit_replace (range, "\n ="); + + /* Arbitrary newlines are not yet supported within fix-it hints, so + the fix-it should not be displayed. */ + ASSERT_TRUE (richloc.seen_impossible_fixit_p ()); + + if (finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + test_context dc; + ASSERT_STREQ (" foo = bar ();\n" + " ^\n", + dc.test_show_locus (richloc)); +} + +/* Fix-it hint, attempting to delete a newline. + This will fail, as we currently only support fix-it hints that + affect one line at a time. */ + +static void +test_fixit_deletion_affecting_newline (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. + ..........................0000000001111. + ..........................1234567890123. */ + const char *old_content = ("foo = bar (\n" + " );\n"); + + temp_source_file tmp (SELFTEST_LOCATION, ".c", old_content); + line_table_test ltt (case_); + const line_map_ordinary *ord_map = linemap_check_ordinary + (linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 0)); + linemap_line_start (line_table, 1, 100); + + /* Attempt to delete the " (\n...)". */ + location_t start + = linemap_position_for_line_and_column (line_table, ord_map, 1, 10); + location_t caret + = linemap_position_for_line_and_column (line_table, ord_map, 1, 11); + location_t finish + = linemap_position_for_line_and_column (line_table, ord_map, 2, 7); + location_t loc = make_location (caret, start, finish); + rich_location richloc (line_table, loc); + richloc. add_fixit_remove (); + + /* Fix-it hints that affect more than one line are not yet supported, so + the fix-it should not be displayed. */ + ASSERT_TRUE (richloc.seen_impossible_fixit_p ()); + + if (finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + test_context dc; + ASSERT_STREQ (" foo = bar (\n" + " ~^\n" + " );\n" + " ~ \n", + dc.test_show_locus (richloc)); +} + +static void +test_tab_expansion (const line_table_case &case_) +{ + /* Create a tempfile and write some text to it. This example uses a tabstop + of 8, as the column numbers attempt to indicate: + + .....................000.01111111111.22222333333 display + .....................123.90123456789.56789012345 columns */ + const char *content = " \t This: `\t' is a tab.\n"; + /* ....................000 00000011111 11111222222 byte + ....................123 45678901234 56789012345 columns */ + + const int tabstop = 8; + cpp_char_column_policy policy (tabstop, cpp_wcwidth); + const int first_non_ws_byte_col = 7; + const int right_quote_byte_col = 15; + const int last_byte_col = 25; + ASSERT_EQ (35, cpp_display_width (content, last_byte_col, policy)); + + temp_source_file tmp (SELFTEST_LOCATION, ".c", content); + line_table_test ltt (case_); + linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 1); + + /* Don't attempt to run the tests if column data might be unavailable. */ + location_t line_end = linemap_position_for_column (line_table, last_byte_col); + if (line_end > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Check that the leading whitespace with mixed tabs and spaces is expanded + into 11 spaces. Recall that print_line() also puts one space before + everything too. */ + { + test_context dc; + dc.m_tabstop = tabstop; + rich_location richloc (line_table, + linemap_position_for_column (line_table, + first_non_ws_byte_col)); + ASSERT_STREQ (" This: ` ' is a tab.\n" + " ^\n", + dc.test_show_locus (richloc)); + } + + /* Confirm the display width was tracked correctly across the internal tab + as well. */ + { + test_context dc; + dc.m_tabstop = tabstop; + rich_location richloc (line_table, + linemap_position_for_column (line_table, + right_quote_byte_col)); + ASSERT_STREQ (" This: ` ' is a tab.\n" + " ^\n", + dc.test_show_locus (richloc)); + } +} + +/* Verify that the escaping machinery can cope with a variety of different + invalid bytes. */ + +static void +test_escaping_bytes_1 (const line_table_case &case_) +{ + const char content[] = "before\0\1\2\3\v\x80\xff""after\n"; + const size_t sz = sizeof (content); + temp_source_file tmp (SELFTEST_LOCATION, ".c", content, sz); + line_table_test ltt (case_); + const line_map_ordinary *ord_map = linemap_check_ordinary + (linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 0)); + linemap_line_start (line_table, 1, 100); + + location_t finish + = linemap_position_for_line_and_column (line_table, ord_map, 1, + strlen (content)); + + if (finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Locations of the NUL and \v bytes. */ + location_t nul_loc + = linemap_position_for_line_and_column (line_table, ord_map, 1, 7); + location_t v_loc + = linemap_position_for_line_and_column (line_table, ord_map, 1, 11); + gcc_rich_location richloc (nul_loc); + richloc.add_range (v_loc); + + { + test_context dc; + ASSERT_STREQ (" before \1\2\3\v\x80\xff""after\n" + " ^ ~\n", + dc.test_show_locus (richloc)); + } + richloc.set_escape_on_output (true); + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_UNICODE); + ASSERT_STREQ + (" before<U+0000><U+0001><U+0002><U+0003><U+000B><80><ff>after\n" + " ^~~~~~~~ ~~~~~~~~\n", + dc.test_show_locus (richloc)); + } + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_BYTES); + ASSERT_STREQ (" before<00><01><02><03><0b><80><ff>after\n" + " ^~~~ ~~~~\n", + dc.test_show_locus (richloc)); + } +} + +/* As above, but verify that we handle the initial byte of a line + correctly. */ + +static void +test_escaping_bytes_2 (const line_table_case &case_) +{ + const char content[] = "\0after\n"; + const size_t sz = sizeof (content); + temp_source_file tmp (SELFTEST_LOCATION, ".c", content, sz); + line_table_test ltt (case_); + const line_map_ordinary *ord_map = linemap_check_ordinary + (linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 0)); + linemap_line_start (line_table, 1, 100); + + location_t finish + = linemap_position_for_line_and_column (line_table, ord_map, 1, + strlen (content)); + + if (finish > LINE_MAP_MAX_LOCATION_WITH_COLS) + return; + + /* Location of the NUL byte. */ + location_t nul_loc + = linemap_position_for_line_and_column (line_table, ord_map, 1, 1); + gcc_rich_location richloc (nul_loc); + + { + test_context dc; + ASSERT_STREQ (" after\n" + " ^\n", + dc.test_show_locus (richloc)); + } + richloc.set_escape_on_output (true); + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_UNICODE); + ASSERT_STREQ (" <U+0000>after\n" + " ^~~~~~~~\n", + dc.test_show_locus (richloc)); + } + { + test_context dc; + dc.set_escape_format (DIAGNOSTICS_ESCAPE_FORMAT_BYTES); + ASSERT_STREQ (" <00>after\n" + " ^~~~\n", + dc.test_show_locus (richloc)); + } +} + +/* Verify that line numbers are correctly printed for the case of + a multiline range in which the width of the line numbers changes + (e.g. from "9" to "10"). */ + +static void +test_line_numbers_multiline_range () +{ + /* Create a tempfile and write some text to it. */ + pretty_printer pp; + for (int i = 0; i < 20; i++) + /* .........0000000001111111. + .............1234567890123456. */ + pp_printf (&pp, "this is line %i\n", i + 1); + temp_source_file tmp (SELFTEST_LOCATION, ".txt", pp_formatted_text (&pp)); + line_table_test ltt; + + const line_map_ordinary *ord_map = linemap_check_ordinary + (linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 0)); + linemap_line_start (line_table, 1, 100); + + /* Create a multi-line location, starting at the "line" of line 9, with + a caret on the "is" of line 10, finishing on the "this" line 11. */ + + location_t start + = linemap_position_for_line_and_column (line_table, ord_map, 9, 9); + location_t caret + = linemap_position_for_line_and_column (line_table, ord_map, 10, 6); + location_t finish + = linemap_position_for_line_and_column (line_table, ord_map, 11, 4); + location_t loc = make_location (caret, start, finish); + + test_context dc; + dc.show_line_numbers (true); + auto &source_printing_opts = dc.get_source_printing_options (); + source_printing_opts.min_margin_width = 0; + gcc_rich_location richloc (loc); + ASSERT_STREQ (" 9 | this is line 9\n" + " | ~~~~~~\n" + "10 | this is line 10\n" + " | ~~~~~^~~~~~~~~~\n" + "11 | this is line 11\n" + " | ~~~~ \n", + dc.test_show_locus (richloc)); +} + +/* Run all of the selftests within this file. */ + +void +source_printing_cc_tests () +{ + test_line_span (); + + test_layout_range_for_single_point (); + test_layout_range_for_single_line (); + test_layout_range_for_multiple_lines (); + + test_display_widths (); + + for_each_line_table_case (test_layout_x_offset_display_utf8); + for_each_line_table_case (test_layout_x_offset_display_tab); + + test_get_line_bytes_without_trailing_whitespace (); + + test_diagnostic_show_locus_unknown_location (); + + for_each_line_table_case (test_diagnostic_show_locus_one_liner); + for_each_line_table_case (test_diagnostic_show_locus_one_liner_utf8); + for_each_line_table_case (test_add_location_if_nearby); + for_each_line_table_case (test_diagnostic_show_locus_fixit_lines); + for_each_line_table_case (test_fixit_consolidation); + for_each_line_table_case (test_overlapped_fixit_printing); + for_each_line_table_case (test_overlapped_fixit_printing_utf8); + for_each_line_table_case (test_overlapped_fixit_printing_2); + for_each_line_table_case (test_fixit_insert_containing_newline); + for_each_line_table_case (test_fixit_insert_containing_newline_2); + for_each_line_table_case (test_fixit_replace_containing_newline); + for_each_line_table_case (test_fixit_deletion_affecting_newline); + for_each_line_table_case (test_tab_expansion); + for_each_line_table_case (test_escaping_bytes_1); + for_each_line_table_case (test_escaping_bytes_2); + + test_line_numbers_multiline_range (); +} + +} // namespace diagnostics::selftest +} // namespace diagnostics + +#endif /* #if CHECKING_P */ + +#if __GNUC__ >= 10 +# pragma GCC diagnostic pop +#endif diff --git a/gcc/diagnostics/state-graphs-to-dot.cc b/gcc/diagnostics/state-graphs-to-dot.cc new file mode 100644 index 0000000..2d80e6b --- /dev/null +++ b/gcc/diagnostics/state-graphs-to-dot.cc @@ -0,0 +1,551 @@ +/* Creating GraphViz .dot files from diagnostic state graphs. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + +#define INCLUDE_ALGORITHM +#define INCLUDE_MAP +#define INCLUDE_SET +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "config.h" +#include "system.h" +#include "coretypes.h" + +#include "diagnostics/state-graphs.h" +#include "graphviz.h" +#include "xml.h" +#include "xml-printer.h" +#include "intl.h" + +using namespace diagnostics; +using namespace diagnostics::state_graphs; + +static int +get_depth (const digraphs::node &n) +{ + int deepest_child = 0; + for (size_t i = 0; i < n.get_num_children (); ++i) + deepest_child = std::max (deepest_child, + get_depth (n.get_child (i))); + return deepest_child + 1; +} + +static const char * +get_color_for_dynalloc_state (enum node_dynalloc_state dynalloc_st) +{ + switch (dynalloc_st) + { + default: + gcc_unreachable (); + break; + case node_dynalloc_state::unknown: + case node_dynalloc_state::nonnull: + return nullptr; + + case node_dynalloc_state::unchecked: + return "#ec7a08"; // pf-orange-400 + + case node_dynalloc_state::freed: + return "#cc0000"; // pf-red-100 + } +} + +static void +set_color_for_dynalloc_state (dot::attr_list &attrs, + enum node_dynalloc_state state) +{ + if (const char *color = get_color_for_dynalloc_state (state)) + attrs.add (dot::id ("color"), dot::id (color)); +} + +class state_diagram : public dot::graph +{ +public: + state_diagram (const diagnostics::digraphs::digraph &input_state_graph, + const logical_locations::manager &logical_loc_mgr) + : m_logical_loc_mgr (logical_loc_mgr) + { + // "node [shape=plaintext]\n" + { + auto attr_stmt + = std::make_unique<dot::attr_stmt> (dot::attr_stmt::kind::node); + attr_stmt->m_attrs.add (dot::id ("shape"), dot::id ("plaintext")); + add_stmt (std::move (attr_stmt)); + } + + /* Determine which nodes are involved in edges. */ + for (size_t i = 0; i < input_state_graph.get_num_edges (); ++i) + { + auto &edge = input_state_graph.get_edge (i); + m_src_nodes.insert (&edge.get_src_node ()); + m_dst_nodes.insert (&edge.get_dst_node ()); + } + + /* Recurse down the nodes in the state graph, creating subgraphs + and then eventually creating nodes, and recursively + creating XML tables, and adding ports for the endpoints of edges + where needed. */ + + auto root_cluster + = std::make_unique<dot::subgraph> (dot::id ("cluster_memory_regions")); + for (size_t i = 0; i < input_state_graph.get_num_nodes (); ++i) + on_input_state_node (*root_cluster, + state_node_ref (input_state_graph.get_node (i))); + add_stmt (std::move (root_cluster)); + + /* Now create dot edges for edges in input_stage_graph. */ + for (size_t i = 0; i < input_state_graph.get_num_edges (); ++i) + { + auto &edge = input_state_graph.get_edge (i); + auto &src_node = edge.get_src_node (); + auto &dst_node = edge.get_dst_node (); + + auto src_port_id = m_src_node_to_port_id.find (&src_node); + if (src_port_id == m_src_node_to_port_id.end ()) + continue; + auto dst_port_id = m_dst_node_to_port_id.find (&dst_node); + if (dst_port_id == m_dst_node_to_port_id.end ()) + continue; + + auto e = std::make_unique<dot::edge_stmt> (src_port_id->second, + dst_port_id->second); + set_color_for_dynalloc_state + (e->m_attrs, state_node_ref (dst_node).get_dynalloc_state ()); + + add_stmt (std::move (e)); + } + } + +private: + struct pending_edge + { + dot::node_id m_src_node_id; + std::string m_dst_region_id; + }; + + dot::id + get_id_for_region (const char *region_id) + { + gcc_assert (region_id); + return std::string ("cluster_region_") + region_id; + } + + dot::id + make_id (state_node_ref state_node, bool cluster) + { + std::string input_node_id = state_node.m_node.get_id (); + if (cluster) + return std::string ("cluster_") + input_node_id; + else + return input_node_id; + } + + bool + starts_node_p (state_node_ref state_node) + { + switch (state_node.get_node_kind ()) + { + default: + return false; + + case node_kind::stack: + /* We want all frames in the stack in the same table, + so they are grouped. */ + case node_kind::dynalloc_buffer: + case node_kind::variable: + return true; + } + } + + const char * + get_label_for_node (state_node_ref state_node) + { + switch (state_node.get_node_kind ()) + { + default: + return nullptr; + + case node_kind::globals: + return _("Globals"); + case node_kind::code: + return _("Code"); + case node_kind::stack: + return _("Stack"); + case node_kind::heap_: + return _("Heap"); + } + } + + void + on_input_state_node (dot::subgraph &parent_subgraph, + state_node_ref state_node) + { + dot::id sg_id = make_id (state_node, true); + + if (starts_node_p (state_node)) + { + // Create node with table + xml::element table ("table", false); + xml::printer xp (table); + xp.set_attr ("border", "0"); + xp.set_attr ("cellborder", "1"); + xp.set_attr ("cellspacing", "0"); + + const int max_depth = get_depth (state_node.m_node); + const int num_columns = max_depth + 2; + + dot::id id_of_dot_node = make_id (state_node, false); + on_node_in_table (id_of_dot_node, xp, state_node, + max_depth, 0, num_columns); + + auto node = std::make_unique<dot::node_stmt> (std::move (id_of_dot_node)); + node->m_attrs.add (dot::id ("shape"), + dot::id ("plaintext")); + + // xml must be done by now + + node->m_attrs.add (dot::id ("label"), + dot::id (table)); + + parent_subgraph.m_stmt_list.add_stmt (std::move (node)); + } + else + { + auto child_subgraph = std::make_unique<dot::subgraph> (std::move (sg_id)); + + if (const char *label = get_label_for_node (state_node)) + child_subgraph->add_attr (dot::id ("label"), dot::id (label)); + + // recurse: + for (size_t i = 0; i < state_node.m_node.get_num_children (); ++i) + on_input_state_node (*child_subgraph, + state_node.m_node.get_child (i)); + parent_subgraph.m_stmt_list.add_stmt (std::move (child_subgraph)); + } + } + + enum class style { h1, h2 }; + + void + add_title_tr (const dot::id &id_of_dot_node, + xml::printer &xp, + int num_columns, + state_node_ref state_node, + std::string heading, + enum style styl, + enum node_dynalloc_state dynalloc_state) + { + xp.push_tag ("tr", true); + xp.push_tag ("td", false); + xp.set_attr ("colspan", std::to_string (num_columns)); + xp.set_attr ("cellpadding", "5"); + + const char *bgcolor; + const char *color; + if (const char *c = get_color_for_dynalloc_state (dynalloc_state)) + { + bgcolor = c; + color = "white"; + } + else + switch (styl) + { + default: + gcc_unreachable (); + case style::h1: + // from diagnostics/html-sink.cc: HTML_STYLE .linenum + bgcolor = "#0088ce"; + color = "white"; + break; + case style::h2: + // from diagnostics/html-sink.cc: HTML_STYLE .events-hdr + bgcolor = "#393f44"; // pf-black-800 + color = "white"; + break; + } + + xp.set_attr ("bgcolor", bgcolor); + xp.push_tag ("font", false); + xp.set_attr ("color", color); + if (heading == "") + heading = " "; + xp.add_text (std::move (heading)); + xp.pop_tag ("font"); + + maybe_add_dst_port (id_of_dot_node, xp, state_node); + + xp.pop_tag ("td"); + xp.pop_tag ("tr"); + } + + /* Recursively add <TR> to XP for STATE_NODE and its descendents. */ + void + on_node_in_table (const dot::id &id_of_dot_node, + xml::printer &xp, + state_node_ref state_node, + int max_depth, + int depth, + int num_columns) + { + bool recurse = true; + auto input_node_kind = state_node.get_node_kind (); + + switch (input_node_kind) + { + case node_kind::padding: + case node_kind::other: + return; + + case node_kind::stack: + add_title_tr (id_of_dot_node, xp, num_columns, state_node, "Stack", + style::h1, + node_dynalloc_state::unknown); + break; + case node_kind::stack_frame: + if (auto logical_loc = state_node.get_logical_loc ()) + if (const char *function + = m_logical_loc_mgr.get_short_name (logical_loc)) + add_title_tr (id_of_dot_node, xp, num_columns, state_node, + std::string ("Frame: ") + function, + style::h2, + node_dynalloc_state::unknown); + break; + case node_kind::dynalloc_buffer: + { + enum node_dynalloc_state dynalloc_st + = state_node.get_dynalloc_state (); + const char *extents = state_node.get_dynamic_extents (); + const char *type = state_node.get_type (); + pretty_printer pp; + switch (dynalloc_st) + { + default: + gcc_unreachable (); + + case node_dynalloc_state::unknown: + case node_dynalloc_state::nonnull: + if (type) + { + if (extents) + pp_printf (&pp, "%s (%s byte allocation)", + type, extents); + else + pp_printf (&pp, "%s", type); + } + else + { + if (extents) + pp_printf (&pp, "%s byte allocation", + extents); + } + break; + + case node_dynalloc_state::unchecked: + if (type) + { + if (extents) + pp_printf (&pp, "%s (unchecked %s byte allocation)", + type, extents); + } + else + { + if (extents) + pp_printf (&pp, "Unchecked %s byte allocation", + extents); + } + break; + + case node_dynalloc_state::freed: + // TODO: show deallocator + // TODO: show deallocation event + pp_printf (&pp, "Freed buffer"); + break; + } + maybe_add_dst_port (id_of_dot_node, xp, state_node); + add_title_tr (id_of_dot_node, xp, num_columns, state_node, + pp_formatted_text (&pp), + style::h2, + dynalloc_st); + } + break; + + default: + { + xp.push_tag ("tr", true); + + maybe_add_dst_port (id_of_dot_node, xp, state_node); + + if (depth > 0) + { + /* Indent, by create a <td> spanning "depth" columns. */ + xp.push_tag ("td", false); + xp.set_attr ("colspan", std::to_string (depth)); + xp.add_text (" "); // graphviz doesn't like <td/> + xp.pop_tag ("td"); + } + + switch (input_node_kind) + { + default: + break; + case node_kind::variable: + { + const char *name = state_node.get_name (); + gcc_assert (name); + xp.push_tag ("td", false); + maybe_add_dst_port (id_of_dot_node, xp, state_node); + push_src_text (xp); + xp.add_text (name); + pop_src_text (xp); + xp.pop_tag ("td"); + } + break; + case node_kind::element: + { + const char *index = state_node.get_index (); + gcc_assert (index); + xp.push_tag ("td", false); + maybe_add_dst_port (id_of_dot_node, xp, state_node); + push_src_text (xp); + xp.add_text ("["); + xp.add_text (index); + xp.add_text ("]"); + pop_src_text (xp); + xp.pop_tag ("td"); + } + break; + case node_kind::field: + { + const char *name = state_node.get_name (); + gcc_assert (name); + xp.push_tag ("td", false); + maybe_add_dst_port (id_of_dot_node, xp, state_node); + push_src_text (xp); + xp.add_text ("."); + xp.add_text (name); + pop_src_text (xp); + xp.pop_tag ("td"); + } + break; + } + + if (const char *type = state_node.get_type ()) + { + xp.push_tag ("td", false); + xp.set_attr ("align", "right"); + push_src_text (xp); + xp.add_text (type); + pop_src_text (xp); + xp.pop_tag ("td"); + } + + if (const char *value = state_node.get_value ()) + { + xp.push_tag ("td", false); + xp.set_attr ("align", "left"); + maybe_add_src_port (id_of_dot_node, xp, state_node); + push_src_text (xp); + xp.add_text (value); + pop_src_text (xp); + xp.pop_tag ("td"); + recurse = false; + } + xp.pop_tag ("tr"); + } + break; + } + + if (recurse) + for (size_t i = 0; i < state_node.m_node.get_num_children (); ++i) + on_node_in_table (id_of_dot_node, xp, + state_node.m_node.get_child (i), + max_depth, depth + 1, num_columns); + } + + void + push_src_text (xml::printer &xp) + { + xp.push_tag ("font"); + xp.set_attr ("color", "blue"); + } + + void + pop_src_text (xml::printer &xp) + { + xp.pop_tag ("font"); + } + + /* If STATE_NODE is in m_src_nodes, add a port to XP for possible + incoming edges to use. */ + + void + maybe_add_src_port (const dot::id &id_of_dot_node, + xml::printer &xp, + state_node_ref state_node) + { + auto iter = m_src_nodes.find (&state_node.m_node); + if (iter == m_src_nodes.end ()) + return; + + dot::id src_id = make_id (state_node, false); + dot::node_id node_id (id_of_dot_node, + dot::port (src_id, + dot::compass_pt::e)); + m_src_node_to_port_id.insert ({&state_node.m_node, node_id}); + xp.set_attr ("port", src_id.m_str); + } + + /* If STATE_NODE is in m_dst_nodes, add a port to XP for possible + incoming edges to use. */ + + void + maybe_add_dst_port (const dot::id &id_of_dot_node, + xml::printer &xp, + state_node_ref state_node) + { + auto iter = m_dst_nodes.find (&state_node.m_node); + if (iter == m_dst_nodes.end ()) + return; + + dot::id dst_id = make_id (state_node, false); + dot::node_id node_id (id_of_dot_node, + dot::port (dst_id/*, + dot::compass_pt::w*/)); + m_dst_node_to_port_id.insert ({&state_node.m_node, node_id}); + xp.set_attr ("port", dst_id.m_str); + } + +private: + const logical_locations::manager &m_logical_loc_mgr; + + /* All nodes involved in edges (and thus will need a port). */ + std::set<digraphs::node *> m_src_nodes; + std::set<digraphs::node *> m_dst_nodes; + + std::map<digraphs::node *, dot::node_id> m_src_node_to_port_id; + std::map<digraphs::node *, dot::node_id> m_dst_node_to_port_id; +}; + +std::unique_ptr<dot::graph> +state_graphs:: +make_dot_graph (const digraphs::digraph &state_graph, + const logical_locations::manager &logical_loc_mgr) +{ + return std::make_unique<state_diagram> (state_graph, logical_loc_mgr); +} diff --git a/gcc/diagnostics/state-graphs.cc b/gcc/diagnostics/state-graphs.cc new file mode 100644 index 0000000..5941c41 --- /dev/null +++ b/gcc/diagnostics/state-graphs.cc @@ -0,0 +1,156 @@ +/* Extensions to diagnostics::digraphs to support state graphs. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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/>. */ + +#define INCLUDE_ALGORITHM +#define INCLUDE_MAP +#define INCLUDE_SET +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "config.h" +#include "system.h" +#include "coretypes.h" + +#include "diagnostics/state-graphs.h" +#include "selftest.h" + +using namespace diagnostics::state_graphs; + +const char * const node_kind_strs[] = { + "globals", + "code", + "function", + "stack", + "stack-frame", + "heap", + "thread-local", + "dynalloc-buffer", + "variable", + "field", + "padding", + "element", + "other", +}; + +const char * +diagnostics::state_graphs::node_kind_to_str (enum node_kind k) +{ + return node_kind_strs[static_cast<int> (k)]; +} + +// struct state_node_ref + +enum node_kind +state_node_ref::get_node_kind () const +{ + const char *value = get_attr ("kind"); + if (!value) + return node_kind::other; + + for (size_t i = 0; i < ARRAY_SIZE (node_kind_strs); ++i) + if (!strcmp (node_kind_strs[i], value)) + return static_cast<enum node_kind> (i); + + return node_kind::other; +} + +void +state_node_ref::set_node_kind (enum node_kind k) +{ + set_attr ("kind", node_kind_to_str (k)); +} + +const char * const dynalloc_state_strs[] = { + "unknown", + "nonnull", + "unchecked", + "freed" +}; + +enum node_dynalloc_state +state_node_ref::get_dynalloc_state () const +{ + const char *value = get_attr ("dynalloc-state"); + if (!value) + return node_dynalloc_state::unknown; + + for (size_t i = 0; i < ARRAY_SIZE (dynalloc_state_strs); ++i) + if (!strcmp (dynalloc_state_strs[i], value)) + return static_cast<enum node_dynalloc_state> (i); + + return node_dynalloc_state::unknown; +} + +void +state_node_ref::set_dynalloc_state (enum node_dynalloc_state s) const +{ + set_attr ("dynalloc-state", + dynalloc_state_strs[static_cast <size_t> (s)]); +} + +const char * +state_node_ref::get_dynamic_extents () const +{ + return m_node.get_attr (STATE_NODE_PREFIX, "dynamic-extents"); +} + +void +state_node_ref::set_json_attr (const char *key, + std::unique_ptr<json::value> value) const +{ + m_node.set_json_attr (STATE_NODE_PREFIX, key, std::move (value)); +} + +#if CHECKING_P + +namespace diagnostics { +namespace selftest { + +static void +test_node_attrs () +{ + digraphs::digraph g; + digraphs::node n (g, "a"); + state_node_ref node_ref (n); + + ASSERT_EQ (node_ref.get_node_kind (), node_kind::other); + node_ref.set_node_kind (node_kind::stack); + ASSERT_EQ (node_ref.get_node_kind (), node_kind::stack); + + ASSERT_EQ (node_ref.get_dynalloc_state (), node_dynalloc_state::unknown); + node_ref.set_dynalloc_state (node_dynalloc_state::freed); + ASSERT_EQ (node_ref.get_dynalloc_state (), node_dynalloc_state::freed); + + ASSERT_EQ (node_ref.get_type (), nullptr); + node_ref.set_type ("const char *"); + ASSERT_STREQ (node_ref.get_type (), "const char *"); +} + +/* Run all of the selftests within this file. */ + +void +state_graphs_cc_tests () +{ + test_node_attrs (); +} + +} // namespace diagnostics::selftest +} // namespace diagnostics + +#endif /* CHECKING_P */ diff --git a/gcc/diagnostics/state-graphs.h b/gcc/diagnostics/state-graphs.h new file mode 100644 index 0000000..ad18f82 --- /dev/null +++ b/gcc/diagnostics/state-graphs.h @@ -0,0 +1,156 @@ +/* Extensions to diagnostics::digraphs to support state graphs. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com> + +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_DIAGNOSTICS_STATE_GRAPHS_H +#define GCC_DIAGNOSTICS_STATE_GRAPHS_H + +#include "diagnostics/digraphs.h" +#include "diagnostics/logical-locations.h" + +/* diagnostics::digraphs provides support for directed graphs. + + diagnostics::state_graphs provides a way to extend these graphs + for representing "state graphs" i.e. a representation of the state + of memory inside a program, for use e.g. by -fanalyzer. + + Specifically, nodes represent memory regions, and we use property bags + in these nodes to stash extra properties (e.g. what kind of memory region + a node is e.g. stack vs heap). */ + +class sarif_graph; +namespace dot { class graph; } + +namespace diagnostics { +namespace state_graphs { + +enum class node_kind +{ + // Memory regions + globals, + code, + function, // code within a particular function + stack, + stack_frame, + heap_, + thread_local_, + + /* Dynamically-allocated buffer, + on heap or stack (depending on parent). */ + dynalloc_buffer, + + variable, + + field, // field within a struct or union + padding, // padding bits in a struct or union + element, // element within an array + + other // anything else +}; + +extern const char * +node_kind_to_str (enum node_kind); + +enum class node_dynalloc_state +{ + unknown, + nonnull, + unchecked, + freed +}; + +/* Prefixes to use in SARIF property bags. */ +#define STATE_GRAPH_PREFIX "gcc/diagnostic_state_graph/" +#define STATE_NODE_PREFIX "gcc/diagnostic_state_node/" +#define STATE_EDGE_PREFIX "gcc/diagnostic_state_edge/" + +/* A wrapper around a node that gets/sets attributes, using + the node's property bag for storage, so that the data roundtrips + through SARIF. */ + +struct state_node_ref +{ + state_node_ref (diagnostics::digraphs::node &node) + : m_node (node) + {} + + enum node_kind + get_node_kind () const; + void + set_node_kind (enum node_kind); + + // For node_kind::stack_frame, this will be the function + logical_locations::key + get_logical_loc () const + { + return m_node.get_logical_loc (); + } + + // For node_kind::dynalloc_buffer + enum node_dynalloc_state + get_dynalloc_state () const; + + void + set_dynalloc_state (enum node_dynalloc_state) const; + + const char * + get_dynamic_extents () const; + + const char * + get_name () const { return get_attr ("name"); } + void + set_name (const char *name) const { set_attr ("name", name); } + + const char * + get_type () const { return get_attr ("type"); } + void + set_type (const char *type) const { set_attr ("type", type); } + + const char * + get_value () const { return get_attr ("value"); } + + const char * + get_index () const { return get_attr ("index"); } + + const char * + get_attr (const char *key) const + { + return m_node.get_attr (STATE_NODE_PREFIX, key); + } + + void + set_attr (const char *key, const char *value) const + { + return m_node.set_attr (STATE_NODE_PREFIX, key, value); + } + + void + set_json_attr (const char *key, std::unique_ptr<json::value> value) const; + + diagnostics::digraphs::node &m_node; +}; + +extern std::unique_ptr<dot::graph> +make_dot_graph (const diagnostics::digraphs::digraph &state_graph, + const logical_locations::manager &logical_loc_mgr); + +} // namespace state_graphs +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_STATE_GRAPHS_H */ diff --git a/gcc/diagnostics/text-sink.cc b/gcc/diagnostics/text-sink.cc new file mode 100644 index 0000000..bcf91cf --- /dev/null +++ b/gcc/diagnostics/text-sink.cc @@ -0,0 +1,728 @@ +/* Classic text-based output of diagnostics. + Copyright (C) 1999-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/>. */ + + +#include "config.h" +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "version.h" +#include "intl.h" +#include "diagnostic.h" +#include "diagnostics/color.h" +#include "diagnostics/url.h" +#include "diagnostics/metadata.h" +#include "diagnostics/paths.h" +#include "diagnostics/client-data-hooks.h" +#include "diagnostics/diagram.h" +#include "diagnostics/text-sink.h" +#include "diagnostics/buffering.h" +#include "text-art/theme.h" + +/* Disable warnings about quoting issues in the pp_xxx calls below + that (intentionally) don't follow GCC diagnostic conventions. */ +#if __GNUC__ >= 10 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-diag" +#endif + +namespace diagnostics { + +/* Concrete buffering implementation subclass for text output. */ + +class text_sink_buffer : public per_sink_buffer +{ +public: + friend class text_sink; + + text_sink_buffer (sink &sink_); + + void dump (FILE *out, int indent) const final override; + + bool empty_p () const final override; + void move_to (per_sink_buffer &dest) final override; + void clear () final override; + void flush () final override; + +private: + sink &m_sink; + output_buffer m_output_buffer; +}; + +/* class text_sink_buffer : public per_sink_buffer. */ + +text_sink_buffer::text_sink_buffer (sink &sink_) +: m_sink (sink_) +{ + m_output_buffer.m_flush_p = false; +} + +void +text_sink_buffer::dump (FILE *out, int indent) const +{ + fprintf (out, "%*stext_sink_buffer:\n", indent, ""); + m_output_buffer.dump (out, indent + 2); +} + +bool +text_sink_buffer::empty_p () const +{ + return output_buffer_last_position_in_text (&m_output_buffer) == nullptr; +} + +void +text_sink_buffer::move_to (per_sink_buffer &base_dest) +{ + text_sink_buffer &dest + = static_cast<text_sink_buffer &> (base_dest); + const char *str = output_buffer_formatted_text (&m_output_buffer); + output_buffer_append_r (&dest.m_output_buffer, str, strlen (str)); + + obstack_free (m_output_buffer.m_obstack, + obstack_base (m_output_buffer.m_obstack)); + m_output_buffer.m_line_length = 0; +} + +void +text_sink_buffer::clear () +{ + pretty_printer *const pp = m_sink.get_printer (); + output_buffer *const old_output_buffer = pp_buffer (pp); + + pp_buffer (pp) = &m_output_buffer; + + pp_clear_output_area (pp); + gcc_assert (empty_p ()); + + pp_buffer (pp) = old_output_buffer; +} + +void +text_sink_buffer::flush () +{ + pretty_printer *const pp = m_sink.get_printer (); + output_buffer *const old_output_buffer = pp_buffer (pp); + + pp_buffer (pp) = &m_output_buffer; + + pp_really_flush (pp); + gcc_assert (empty_p ()); + + pp_buffer (pp) = old_output_buffer; +} + +/* class diagnostics::text_sink : public diagnostics::sink. */ + +text_sink::~text_sink () +{ + /* Some of the errors may actually have been warnings. */ + if (m_context.diagnostic_count (kind::werror)) + { + pretty_printer *pp = get_printer (); + /* -Werror was given. */ + if (m_context.warning_as_error_requested_p ()) + pp_verbatim (pp, + _("%s: all warnings being treated as errors"), + progname); + /* At least one -Werror= was given. */ + else + pp_verbatim (pp, + _("%s: some warnings being treated as errors"), + progname); + pp_newline_and_flush (pp); + } + + if (m_includes_seen) + { + delete m_includes_seen; + m_includes_seen = nullptr; + } +} + +void +text_sink::dump (FILE *out, int indent) const +{ + fprintf (out, "%*stext_sink\n", indent, ""); + fprintf (out, "%*sm_follows_reference_printer: %s\n", + indent, "", + m_follows_reference_printer ? "true" : "false"); + sink::dump (out, indent); + fprintf (out, "%*ssaved_output_buffer:\n", indent + 2, ""); + if (m_saved_output_buffer) + m_saved_output_buffer->dump (out, indent + 4); + else + fprintf (out, "%*s(none):\n", indent + 4, ""); +} + +void +text_sink::set_buffer (per_sink_buffer *base) +{ + text_sink_buffer * const buffer + = static_cast<text_sink_buffer *> (base); + + pretty_printer *const pp = get_printer (); + + if (!m_saved_output_buffer) + m_saved_output_buffer = pp_buffer (pp); + + if (buffer) + pp_buffer (pp) = &buffer->m_output_buffer; + else + { + gcc_assert (m_saved_output_buffer); + pp_buffer (pp) = m_saved_output_buffer; + } +} + +std::unique_ptr<per_sink_buffer> +text_sink::make_per_sink_buffer () +{ + return std::make_unique<text_sink_buffer> (*this); +} + +/* Implementation of diagnostics::sink::on_report_diagnostic vfunc + for GCC's standard textual output. */ + +void +text_sink::on_report_diagnostic (const diagnostic_info &diagnostic, + enum kind orig_diag_kind) +{ + pretty_printer *pp = get_printer (); + + (*text_starter (&m_context)) (*this, &diagnostic); + + pp_output_formatted_text (pp, m_context.get_urlifier ()); + + if (m_context.m_show_cwe) + print_any_cwe (diagnostic); + + if (m_context.m_show_rules) + print_any_rules (diagnostic); + + if (m_context.m_show_option_requested) + print_option_information (diagnostic, orig_diag_kind); + + /* If we're showing nested diagnostics, then print the location + on a new line, indented. */ + if (m_show_nesting && m_show_locations_in_nesting) + { + const int nesting_level = get_context ().get_diagnostic_nesting_level (); + if (nesting_level > 0) + { + location_t loc = diagnostic_location (&diagnostic); + pp_set_prefix (pp, nullptr); + char *indent_prefix = build_indent_prefix (false); + /* Only print changes of location. */ + if (loc != get_context ().m_last_location + && loc > BUILTINS_LOCATION) + { + const expanded_location s + = diagnostic_expand_location (&diagnostic); + label_text location_text = get_location_text (s); + pp_newline (pp); + pp_printf (pp, "%s%s", indent_prefix, location_text.get ()); + } + pp_set_prefix (pp, indent_prefix); + } + } + + (*text_finalizer (&m_context)) (*this, + &diagnostic, + orig_diag_kind); + + if (m_show_nesting && m_show_locations_in_nesting) + get_context ().m_last_location = diagnostic_location (&diagnostic); +} + +void +text_sink::on_report_verbatim (text_info &text) +{ + pp_format_verbatim (get_printer (), &text); + pp_newline_and_flush (get_printer ()); +} + +void +text_sink::on_diagram (const diagnostics::diagram &d) +{ + pretty_printer *const pp = get_printer (); + + char *saved_prefix = pp_take_prefix (pp); + pp_set_prefix (pp, nullptr); + /* Use a newline before and after and a two-space indent + to make the diagram stand out a little from the wall of text. */ + pp_newline (pp); + d.get_canvas ().print_to_pp (pp, " "); + pp_newline (pp); + pp_set_prefix (pp, saved_prefix); + pp_flush (pp); +} + +void +text_sink:: +after_diagnostic (const diagnostic_info &diagnostic) +{ + if (const paths::path *path = diagnostic.m_richloc->get_path ()) + print_path (*path); +} + +/* Return a malloc'd string describing a location and the severity of the + diagnostic, e.g. "foo.c:42:10: error: ". + + If m_show_nesting, then the above will be preceded by indentation to show + the level, and a bullet point. + + The caller is responsible for freeing the memory. */ +char * +text_sink::build_prefix (const diagnostic_info &diagnostic) const +{ + gcc_assert (diagnostic.m_kind < kind::last_diagnostic_kind); + + const char *text = _(get_text_for_kind (diagnostic.m_kind)); + const char *text_cs = "", *text_ce = ""; + pretty_printer *pp = get_printer (); + + if (const char *color_name = get_color_for_kind (diagnostic.m_kind)) + { + text_cs = colorize_start (pp_show_color (pp), color_name); + text_ce = colorize_stop (pp_show_color (pp)); + } + + const int nesting_level = get_context ().get_diagnostic_nesting_level (); + if (m_show_nesting && nesting_level > 0) + { + char *indent_prefix = build_indent_prefix (true); + + /* Reduce verbosity of nested diagnostics by not printing "note: " + all the time. */ + if (diagnostic.m_kind == kind::note) + return indent_prefix; + + char *result = build_message_string ("%s%s%s%s", indent_prefix, + text_cs, text, text_ce); + free (indent_prefix); + return result; + } + else + { + const expanded_location s = diagnostic_expand_location (&diagnostic); + label_text location_text = get_location_text (s); + return build_message_string ("%s %s%s%s", location_text.get (), + text_cs, text, text_ce); + } +} + +/* Same as build_prefix, but only the source FILE is given. */ +char * +text_sink::file_name_as_prefix (const char *f) const +{ + pretty_printer *const pp = get_printer (); + const char *locus_cs + = colorize_start (pp_show_color (pp), "locus"); + const char *locus_ce = colorize_stop (pp_show_color (pp)); + return build_message_string ("%s%s:%s ", locus_cs, f, locus_ce); +} + +/* Get the unicode code point for bullet points when showing + nested diagnostics. */ + +static unsigned +get_bullet_point_unichar (bool unicode) +{ + if (unicode) + return 0x2022; /* U+2022: Bullet */ + else + return '*'; +} + +/* Return true if DC's theme supports unicode characters. */ + +static bool +use_unicode_p (const context &dc) +{ + if (text_art::theme *theme = dc.get_diagram_theme ()) + return theme->unicode_p (); + else + return false; +} + +/* Get the unicode code point for bullet points when showing + nested diagnostics. */ + +static unsigned +get_bullet_point_unichar (context &dc) +{ + return get_bullet_point_unichar (use_unicode_p (dc)); +} + +/* Return a malloc'd string for use as a prefix to show indentation. + If m_show_nesting is false, or we're at the top-level, then the + result will be the empty string. + + If m_show_nesting, then the result will contain indentation to show + the nesting level, then either a bullet point (if WITH_BULLET is true), + or a space. + + The caller is responsible for freeing the memory. */ + +char * +text_sink::build_indent_prefix (bool with_bullet) const +{ + if (!m_show_nesting) + return xstrdup (""); + + const int nesting_level = get_context ().get_diagnostic_nesting_level (); + if (nesting_level == 0) + return xstrdup (""); + + pretty_printer pp; + for (int i = 0; i < nesting_level; i++) + pp_string (&pp, " "); + if (with_bullet) + pp_unicode_character (&pp, get_bullet_point_unichar (get_context ())); + else + pp_space (&pp); + pp_space (&pp); + if (m_show_nesting_levels) + pp_printf (&pp, "(level %i):", nesting_level); + return xstrdup (pp_formatted_text (&pp)); +} + +/* Add a purely textual note with text GMSGID and with LOCATION. */ + +void +text_sink::append_note (location_t location, + const char * gmsgid, ...) +{ + context *dc = &get_context (); + + diagnostic_info diagnostic; + va_list ap; + rich_location richloc (line_table, location); + + va_start (ap, gmsgid); + diagnostic_set_info (&diagnostic, gmsgid, &ap, &richloc, kind::note); + if (dc->m_inhibit_notes_p) + { + va_end (ap); + return; + } + pretty_printer *pp = get_printer (); + char *saved_prefix = pp_take_prefix (pp); + pp_set_prefix (pp, build_prefix (diagnostic)); + pp_format (pp, &diagnostic.m_message); + pp_output_formatted_text (pp); + pp_destroy_prefix (pp); + pp_set_prefix (pp, saved_prefix); + pp_newline (pp); + diagnostic_show_locus (dc, get_source_printing_options (), + &richloc, kind::note, pp); + va_end (ap); +} + +bool +text_sink::follows_reference_printer_p () const +{ + return m_follows_reference_printer; +} + +void +text_sink::update_printer () +{ + pretty_printer *copy_from_pp + = (m_follows_reference_printer + ? get_context ().get_reference_printer () + : m_printer.get ()); + const bool show_color = pp_show_color (copy_from_pp); + const diagnostic_url_format url_format = copy_from_pp->get_url_format (); + + m_printer = get_context ().clone_printer (); + + pp_show_color (m_printer.get ()) = show_color; + m_printer->set_url_format (url_format); + // ...etc + + m_source_printing = get_context ().m_source_printing; +} + +/* If DIAGNOSTIC has a CWE identifier, print it. + + For example, if the diagnostic metadata associates it with CWE-119, + " [CWE-119]" will be printed, suitably colorized, and with a URL of a + description of the security issue. */ + +void +text_sink::print_any_cwe (const diagnostic_info &diagnostic) +{ + if (!diagnostic.m_metadata) + return; + + int cwe = diagnostic.m_metadata->get_cwe (); + if (cwe) + { + pretty_printer * const pp = get_printer (); + char *saved_prefix = pp_take_prefix (pp); + pp_string (pp, " ["); + const char *kind_color = get_color_for_kind (diagnostic.m_kind); + pp_string (pp, colorize_start (pp_show_color (pp), kind_color)); + if (pp->supports_urls_p ()) + { + char *cwe_url = get_cwe_url (cwe); + pp_begin_url (pp, cwe_url); + free (cwe_url); + } + pp_printf (pp, "CWE-%i", cwe); + pp_set_prefix (pp, saved_prefix); + if (pp->supports_urls_p ()) + pp_end_url (pp); + pp_string (pp, colorize_stop (pp_show_color (pp))); + pp_character (pp, ']'); + } +} + +/* If DIAGNOSTIC has any rules associated with it, print them. + + For example, if the diagnostic metadata associates it with a rule + named "STR34-C", then " [STR34-C]" will be printed, suitably colorized, + with any URL provided by the rule. */ + +void +text_sink::print_any_rules (const diagnostic_info &diagnostic) +{ + if (!diagnostic.m_metadata) + return; + + for (unsigned idx = 0; idx < diagnostic.m_metadata->get_num_rules (); idx++) + { + const diagnostics::metadata::rule &rule + = diagnostic.m_metadata->get_rule (idx); + if (char *desc = rule.make_description ()) + { + pretty_printer * const pp = get_printer (); + char *saved_prefix = pp_take_prefix (pp); + pp_string (pp, " ["); + const char *kind_color = get_color_for_kind (diagnostic.m_kind); + pp_string (pp, colorize_start (pp_show_color (pp), kind_color)); + char *url = nullptr; + if (pp->supports_urls_p ()) + { + url = rule.make_url (); + if (url) + pp_begin_url (pp, url); + } + pp_string (pp, desc); + pp_set_prefix (pp, saved_prefix); + if (pp->supports_urls_p ()) + if (url) + pp_end_url (pp); + free (url); + pp_string (pp, colorize_stop (pp_show_color (pp))); + pp_character (pp, ']'); + free (desc); + } + } +} + +/* Print any metadata about the option used to control DIAGNOSTIC to + the context's printer, e.g. " [-Werror=uninitialized]". */ + +void +text_sink::print_option_information (const diagnostic_info &diagnostic, + enum kind orig_diag_kind) +{ + if (char *option_text + = m_context.make_option_name (diagnostic.m_option_id, + orig_diag_kind, diagnostic.m_kind)) + { + char *option_url = nullptr; + pretty_printer * const pp = get_printer (); + if (pp->supports_urls_p ()) + option_url = m_context.make_option_url (diagnostic.m_option_id); + pp_string (pp, " ["); + const char *kind_color = get_color_for_kind (diagnostic.m_kind); + pp_string (pp, colorize_start (pp_show_color (pp), kind_color)); + if (option_url) + pp_begin_url (pp, option_url); + pp_string (pp, option_text); + if (option_url) + { + pp_end_url (pp); + free (option_url); + } + pp_string (pp, colorize_stop (pp_show_color (pp))); + pp_character (pp, ']'); + free (option_text); + } +} + +/* Only dump the "In file included from..." stack once for each file. */ + +bool +text_sink::includes_seen_p (const line_map_ordinary *map) +{ + /* No include path for main. */ + if (MAIN_FILE_P (map)) + return true; + + /* Always identify C++ modules, at least for now. */ + auto probe = map; + if (linemap_check_ordinary (map)->reason == LC_RENAME) + /* The module source file shows up as LC_RENAME inside LC_MODULE. */ + probe = linemap_included_from_linemap (line_table, map); + if (MAP_MODULE_P (probe)) + return false; + + if (!m_includes_seen) + m_includes_seen = new hash_set<location_t, false, location_hash>; + + /* Hash the location of the #include directive to better handle files + that are included multiple times with different macros defined. */ + return m_includes_seen->add (linemap_included_from (map)); +} + +label_text +text_sink::get_location_text (const expanded_location &s) const +{ + column_policy column_policy_ (get_context ()); + return column_policy_.get_location_text (s, + show_column_p (), + pp_show_color (get_printer ())); +} + +/* Helpers for writing lang-specific starters/finalizers for text output. */ + +/* Return a formatted line and column ':%line:%column'. Elided if + line == 0 or col < 0. (A column of 0 may be valid due to the + -fdiagnostics-column-origin option.) + The result is a statically allocated buffer. */ + +const char * +maybe_line_and_column (int line, int col) +{ + static char result[32]; + + if (line) + { + size_t l + = snprintf (result, sizeof (result), + col >= 0 ? ":%d:%d" : ":%d", line, col); + gcc_checking_assert (l < sizeof (result)); + } + else + result[0] = 0; + return result; +} + +void +text_sink::report_current_module (location_t where) +{ + pretty_printer *pp = get_printer (); + const line_map_ordinary *map = nullptr; + + if (pp_needs_newline (pp)) + { + pp_newline (pp); + pp_needs_newline (pp) = false; + } + + if (where <= BUILTINS_LOCATION) + return; + + linemap_resolve_location (line_table, where, + LRK_MACRO_DEFINITION_LOCATION, + &map); + + if (map && m_last_module != map) + { + m_last_module = map; + if (!includes_seen_p (map)) + { + bool first = true, need_inc = true, was_module = MAP_MODULE_P (map); + expanded_location s = {}; + do + { + where = linemap_included_from (map); + map = linemap_included_from_linemap (line_table, map); + bool is_module = MAP_MODULE_P (map); + s.file = LINEMAP_FILE (map); + s.line = SOURCE_LINE (map, where); + int col = -1; + if (first && show_column_p ()) + { + s.column = SOURCE_COLUMN (map, where); + col = get_column_policy ().converted_column (s); + } + const char *line_col = maybe_line_and_column (s.line, col); + static const char *const msgs[] = + { + nullptr, + N_(" from"), + N_("In file included from"), /* 2 */ + N_(" included from"), + N_("In module"), /* 4 */ + N_("of module"), + N_("In module imported at"), /* 6 */ + N_("imported at"), + }; + + unsigned index = (was_module ? 6 : is_module ? 4 + : need_inc ? 2 : 0) + !first; + + pp_verbatim (pp, "%s%s %r%s%s%R", + first ? "" : was_module ? ", " : ",\n", + _(msgs[index]), + "locus", s.file, line_col); + first = false, need_inc = was_module, was_module = is_module; + } + while (!includes_seen_p (map)); + pp_verbatim (pp, ":"); + pp_newline (pp); + } + } +} + +void +default_text_starter (text_sink &text_output, + const diagnostic_info *diagnostic) +{ + text_output.report_current_module (diagnostic_location (diagnostic)); + pretty_printer *const pp = text_output.get_printer (); + pp_set_prefix (pp, text_output.build_prefix (*diagnostic)); +} + +void +default_text_finalizer (text_sink &text_output, + const diagnostic_info *diagnostic, + enum kind) +{ + pretty_printer *const pp = text_output.get_printer (); + char *saved_prefix = pp_take_prefix (pp); + pp_set_prefix (pp, nullptr); + pp_newline (pp); + diagnostic_show_locus (&text_output.get_context (), + text_output.get_source_printing_options (), + diagnostic->m_richloc, diagnostic->m_kind, pp); + pp_set_prefix (pp, saved_prefix); + pp_flush (pp); +} + +#if __GNUC__ >= 10 +# pragma GCC diagnostic pop +#endif + +} // namespace diagnostics diff --git a/gcc/diagnostics/text-sink.h b/gcc/diagnostics/text-sink.h new file mode 100644 index 0000000..5c60976 --- /dev/null +++ b/gcc/diagnostics/text-sink.h @@ -0,0 +1,176 @@ +/* Classic text-based output of diagnostics. + Copyright (C) 2023-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_TEXT_SINK_H +#define GCC_DIAGNOSTICS_TEXT_SINK_H + +#include "diagnostics/sink.h" + +namespace diagnostics { + +/* Subclass of diagnostics::sink for classic text-based output + to stderr. + + Uses diagnostics::context.m_text_callbacks to provide client-specific + textual output (e.g. include paths, macro expansions, etc). */ + +class text_sink : public sink +{ +public: + text_sink (context &dc, + source_printing_options *source_printing = nullptr, + bool follows_reference_printer = false) + : sink (dc), + m_saved_output_buffer (nullptr), + m_column_policy (dc), + m_last_module (nullptr), + m_includes_seen (nullptr), + m_source_printing (source_printing + ? *source_printing + : dc.get_source_printing_options ()), + m_follows_reference_printer (follows_reference_printer), + m_show_nesting (false), + m_show_nesting_levels (false) + {} + ~text_sink (); + + void dump (FILE *out, int indent) const override; + + std::unique_ptr<per_sink_buffer> + make_per_sink_buffer () final override; + void set_buffer (per_sink_buffer *) final override; + + void on_begin_group () override {} + void on_end_group () override {} + void on_report_diagnostic (const diagnostic_info &, + enum kind orig_diag_kind) override; + void on_report_verbatim (text_info &) final override; + void on_diagram (const diagram &d) override; + void after_diagnostic (const diagnostic_info &) override; + bool machine_readable_stderr_p () const final override + { + return false; + } + bool follows_reference_printer_p () const final override; + + void update_printer () override; + + void + report_global_digraph (const lazily_created<digraphs::digraph> &) + final override + { + // no-op for text + } + + /* Helpers for writing lang-specific starters/finalizers for text output. */ + char *build_prefix (const diagnostic_info &) const; + void report_current_module (location_t where); + void append_note (location_t location, + const char * gmsgid, ...) ATTRIBUTE_GCC_DIAG(3,4); + + + char *file_name_as_prefix (const char *) const; + + char *build_indent_prefix (bool with_bullet) const; + + void print_path (const paths::path &path); + + bool show_column_p () const { return get_context ().m_show_column; } + + const column_policy &get_column_policy () const + { + return m_column_policy; + } + location_print_policy get_location_print_policy () const; + + bool show_nesting_p () const { return m_show_nesting; } + bool show_locations_in_nesting_p () const + { + return m_show_locations_in_nesting; + } + + void set_show_nesting (bool show_nesting) { m_show_nesting = show_nesting; } + void set_show_locations_in_nesting (bool val) + { + m_show_locations_in_nesting = val; + } + void set_show_nesting_levels (bool show_nesting_levels) + { + m_show_nesting_levels = show_nesting_levels; + } + + label_text get_location_text (const expanded_location &s) const; + + source_printing_options &get_source_printing_options () + { + return m_source_printing; + } + const source_printing_options &get_source_printing_options () const + { + return m_source_printing; + } + +protected: + void print_any_cwe (const diagnostic_info &diagnostic); + void print_any_rules (const diagnostic_info &diagnostic); + void print_option_information (const diagnostic_info &diagnostic, + enum kind orig_diag_kind); + + bool includes_seen_p (const line_map_ordinary *map); + + /* For handling diagnostics::buffer. */ + output_buffer *m_saved_output_buffer; + + column_policy m_column_policy; + + /* Used to detect when the input file stack has changed since last + described. */ + const line_map_ordinary *m_last_module; + + /* Include files that report_current_module has already listed the + include path for. */ + hash_set<location_t, false, location_hash> *m_includes_seen; + + source_printing_options &m_source_printing; + + /* If true, this is the initial default text output format created + when the diagnostics::context was created, and, in particular, before + initializations of color and m_url_format. Hence this should follow + the dc's reference printer for these. + If false, this text output was created after the dc was created, and + thus tracks its own values for color and m_url_format. */ + bool m_follows_reference_printer; + + /* If true, then use indentation to show the nesting structure of + nested diagnostics, and print locations on separate lines after the + diagnostic message, rather than as a prefix to the message. */ + bool m_show_nesting; + + /* Set to false to suppress location-printing when showing nested + diagnostics, for use in DejaGnu tests. */ + bool m_show_locations_in_nesting; + + /* If true, then add "(level N):" when printing nested diagnostics. */ + bool m_show_nesting_levels; +}; + +} // namespace diagnostics + +#endif /* ! GCC_DIAGNOSTICS_TEXT_SINK_H */ diff --git a/gcc/diagnostics/url.h b/gcc/diagnostics/url.h new file mode 100644 index 0000000..89efa36 --- /dev/null +++ b/gcc/diagnostics/url.h @@ -0,0 +1,52 @@ +/* Copyright (C) 2019-2025 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +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_DIAGNOSTICS_URL_H +#define GCC_DIAGNOSTICS_URL_H + +/* Whether to add URLs to diagnostics: + - DIAGNOSTICS_URL_NO: never + - DIAGNOSTICS_URL_YES: always + - DIAGNOSTICS_URL_AUTO: depending on the output stream. */ +typedef enum +{ + DIAGNOSTICS_URL_NO = 0, + DIAGNOSTICS_URL_YES = 1, + DIAGNOSTICS_URL_AUTO = 2 +} diagnostic_url_rule_t; + +/* Tells whether URLs should be emitted, and, if so, how to + terminate strings within the escape sequence. */ +enum diagnostic_url_format +{ + /* No URLs shall be emitted. */ + URL_FORMAT_NONE, + + /* Use ST string termination. */ + URL_FORMAT_ST, + + /* Use BEL string termination. */ + URL_FORMAT_BEL +}; + +const diagnostic_url_format URL_FORMAT_DEFAULT = URL_FORMAT_BEL; + +extern diagnostic_url_format determine_url_format (diagnostic_url_rule_t); + +#endif /* ! GCC_DIAGNOSTICS_URL_H */ |