aboutsummaryrefslogtreecommitdiff
path: root/gcc/diagnostics
diff options
context:
space:
mode:
Diffstat (limited to 'gcc/diagnostics')
-rw-r--r--gcc/diagnostics/buffering.cc199
-rw-r--r--gcc/diagnostics/buffering.h113
-rw-r--r--gcc/diagnostics/changes.cc1874
-rw-r--r--gcc/diagnostics/changes.h78
-rw-r--r--gcc/diagnostics/client-data-hooks.h125
-rw-r--r--gcc/diagnostics/color.cc536
-rw-r--r--gcc/diagnostics/color.h65
-rw-r--r--gcc/diagnostics/context-options.h116
-rw-r--r--gcc/diagnostics/context.cc2152
-rw-r--r--gcc/diagnostics/context.h959
-rw-r--r--gcc/diagnostics/diagnostic-info.h75
-rw-r--r--gcc/diagnostics/diagnostics-selftests.cc59
-rw-r--r--gcc/diagnostics/diagnostics-selftests.h55
-rw-r--r--gcc/diagnostics/diagram.h55
-rw-r--r--gcc/diagnostics/digraphs.cc464
-rw-r--r--gcc/diagnostics/digraphs.h379
-rw-r--r--gcc/diagnostics/event-id.h83
-rw-r--r--gcc/diagnostics/file-cache.cc1083
-rw-r--r--gcc/diagnostics/file-cache.h125
-rw-r--r--gcc/diagnostics/html-sink.cc1708
-rw-r--r--gcc/diagnostics/html-sink.h68
-rw-r--r--gcc/diagnostics/kinds.def55
-rw-r--r--gcc/diagnostics/kinds.h45
-rw-r--r--gcc/diagnostics/lazy-paths.cc236
-rw-r--r--gcc/diagnostics/lazy-paths.h70
-rw-r--r--gcc/diagnostics/logical-locations.h180
-rw-r--r--gcc/diagnostics/macro-unwinding.cc227
-rw-r--r--gcc/diagnostics/macro-unwinding.h33
-rw-r--r--gcc/diagnostics/metadata.h124
-rw-r--r--gcc/diagnostics/option-classifier.cc222
-rw-r--r--gcc/diagnostics/option-classifier.h110
-rw-r--r--gcc/diagnostics/option-id.h49
-rw-r--r--gcc/diagnostics/output-file.h111
-rw-r--r--gcc/diagnostics/output-spec.cc852
-rw-r--r--gcc/diagnostics/output-spec.h118
-rw-r--r--gcc/diagnostics/paths-output.cc2742
-rw-r--r--gcc/diagnostics/paths.cc233
-rw-r--r--gcc/diagnostics/paths.h250
-rw-r--r--gcc/diagnostics/sarif-sink.cc5078
-rw-r--r--gcc/diagnostics/sarif-sink.h189
-rw-r--r--gcc/diagnostics/selftest-context.cc104
-rw-r--r--gcc/diagnostics/selftest-context.h93
-rw-r--r--gcc/diagnostics/selftest-logical-locations.cc122
-rw-r--r--gcc/diagnostics/selftest-logical-locations.h86
-rw-r--r--gcc/diagnostics/selftest-paths.cc247
-rw-r--r--gcc/diagnostics/selftest-paths.h169
-rw-r--r--gcc/diagnostics/selftest-source-printing.h85
-rw-r--r--gcc/diagnostics/sink.h110
-rw-r--r--gcc/diagnostics/source-printing-effects.h62
-rw-r--r--gcc/diagnostics/source-printing.cc7052
-rw-r--r--gcc/diagnostics/state-graphs-to-dot.cc551
-rw-r--r--gcc/diagnostics/state-graphs.cc156
-rw-r--r--gcc/diagnostics/state-graphs.h156
-rw-r--r--gcc/diagnostics/text-sink.cc728
-rw-r--r--gcc/diagnostics/text-sink.h176
-rw-r--r--gcc/diagnostics/url.h52
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>"
+ "&apos;: `"
+ "<span class=\"gcc-quoted-text\">"
+ "bar"
+ "</span>"
+ "&apos;"
+ "</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>&apos;</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 */