Previously the diagnostic subsystem supported a one-deep
hierarchy via auto_diagnostic_group, for associating
notes with the warning/error they annotate; this only
affects SARIF output, not text output.

This patch adds support to the diagnostics subsystem for
capturing arbitrarily deep nesting structure within
diagnostic messages.

This patch:
* adds the ability to express nesting internally when
  building diagnostics
* captures the nesting in SARIF output in the form documented
  in SG15's P3358R0 ("SARIF for Structured Diagnostics") via
  a "nestingLevel" property
* adds a new experimental mode to text output to see the
  hierarchy, via:
  -fdiagnostics-set-output=text:experimental-nesting=yes
* adds test coverage via a plugin, which with the above
  option emits:
  • note: child 0
    • note: grandchild 0 0
    • note: grandchild 0 1
    • note: grandchild 0 2
  • note: child 1
    • note: grandchild 1 0
    • note: grandchild 1 1
    • note: grandchild 1 2
  • note: child 2
    • note: grandchild 2 0
    • note: grandchild 2 1
    • note: grandchild 2 2
  using '*' rather than '•' if the text_art::theme is ascii-only.

My hope is to eventually:
(a) use this to improve C++'s template diagnostics
(b) remove the "experimental" caveat from the the text output mode

but this patch doesn't touch the C++ frontend, leaving both of these
to followup work.

gcc/c-family/ChangeLog:
        PR other/116253
        * c-opts.cc (c_diagnostic_text_finalizer): Use
        text_output.build_indent_prefix for prefix to
        diagnostic_show_locus.

gcc/ChangeLog:
        PR other/116253
        * diagnostic-core.h (class auto_diagnostic_nesting_level): New.
        * diagnostic-format-sarif.cc (class sarif_builder): Update leading
        comment re nesting of diagnostics.
        (sarif_result::on_nested_diagnostic): Add nestingLevel property.
        * diagnostic-format-text.cc (on_report_diagnostic): If we're
        showing nested diagnostics, then print changes of location on a
        new line, indented, and update m_last_location.
        (diagnostic_text_output_format::build_prefix): If m_show_nesting,
        then potentially add indentation and a bullet point.
        (get_bullet_point_unichar): New.
        (use_unicode_p): New.
        (diagnostic_text_output_format::build_indent_prefix): New.
        * diagnostic-format-text.h
        (diagnostic_text_output_format::diagnostic_text_output_format):
        Initialize m_show_nesting and m_show_nesting_levels.
        (diagnostic_text_output_format::build_indent_prefix): New decl.
        (diagnostic_text_output_format::show_nesting_p): New accessor
        (diagnostic_text_output_format::show_locations_in_nesting_p):
        Likewise.
        (diagnostic_text_output_format::set_show_nesting): New.
        (diagnostic_text_output_format::set_show_locations_in_nesting):
        New.
        (diagnostic_text_output_format::set_show_nesting_levels): New.
        (diagnostic_text_output_format::m_show_nesting): New field.
        (diagnostic_text_output_format::m_show_locations_in_nesting): New
        field.
        (diagnostic_text_output_format::m_show_nesting_levels): New field.
        * diagnostic-global-context.cc
        (auto_diagnostic_nesting_level::auto_diagnostic_nesting_level):
        New.
        (auto_diagnostic_nesting_level::~auto_diagnostic_nesting_level):
        New.
        * diagnostic-show-locus.cc (layout_printer::print): Temporarily
        set DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE.
        * diagnostic.cc (diagnostic_context::initialize): Update for
        renaming of m_nesting_depth to m_group_nesting_depth and
        initialize m_diagnostic_nesting_level.
        (diagnostic_context::finish): Update for renaming of
        m_nesting_depth to m_group_nesting_depth.
        (diagnostic_context::report_diagnostic): Likewise.
        (diagnostic_context::begin_group): Likewise.
        (diagnostic_context::end_group): Likewise.
        (diagnostic_context::push_nesting_level): New.
        (diagnostic_context::pop_nesting_level): New.
        (diagnostic_context::set_diagnostic_buffer): Update for renaming
        of m_nesting_depth to m_group_nesting_depth.  Assert that we don't
        have nested diagnostics.
        * diagnostic.h (diagnostic_context::push_nesting_level): New decl.
        (diagnostic_context::pop_nesting_level): New decl.
        (diagnostic_context::get_diagnostic_nesting_level): New accessor.
        (diagnostic_context::build_indent_prefix): New decl.
        (diagnostic_context::m_diagnostic_groups): Rename m_nesting_depth
        to m_group_nesting_depth and add field m_diagnostic_nesting_level.
        * doc/invoke.texi (fdiagnostics-add-output): Add note about
        "experimental" schemes, keys, and values.  Add keys
        "experimental-nesting", "experimental-nesting-show-locations",
        and "experimental-nesting-show-levels" to text scheme.
        * opts-diagnostic.cc (text_scheme_handler::make_sink): Add keys
        "experimental-nesting", "experimental-nesting-show-locations",
        and "experimental-nesting-show-levels".

gcc/testsuite/ChangeLog:
        PR other/116253
        * gcc.dg/plugin/diagnostic-test-nesting-sarif.c: New test.
        * gcc.dg/plugin/diagnostic-test-nesting-sarif.py: New test.
        * gcc.dg/plugin/diagnostic-test-nesting-text-indented-show-levels.c:
        New test.
        * gcc.dg/plugin/diagnostic-test-nesting-text-indented-unicode.c:
        New test.
        * gcc.dg/plugin/diagnostic-test-nesting-text-indented.c: New test.
        * gcc.dg/plugin/diagnostic-test-nesting-text-plain.c: New test.
        * gcc.dg/plugin/diagnostic_plugin_test_nesting.c: New test plugin.
        * gcc.dg/plugin/plugin.exp: Add the above.

Signed-off-by: David Malcolm <dmalc...@redhat.com>
---
 gcc/c-family/c-opts.cc                        |   2 +-
 gcc/diagnostic-core.h                         |  12 ++
 gcc/diagnostic-format-sarif.cc                |  16 +-
 gcc/diagnostic-format-text.cc                 | 121 ++++++++++++++-
 gcc/diagnostic-format-text.h                  |  34 +++-
 gcc/diagnostic-global-context.cc              |  12 ++
 gcc/diagnostic-show-locus.cc                  |   5 +
 gcc/diagnostic.cc                             |  29 +++-
 gcc/diagnostic.h                              |  15 +-
 gcc/doc/invoke.texi                           |  18 +++
 gcc/opts-diagnostic.cc                        |  32 +++-
 .../plugin/diagnostic-test-nesting-sarif.c    |  16 ++
 .../plugin/diagnostic-test-nesting-sarif.py   |  39 +++++
 ...c-test-nesting-text-indented-show-levels.c |  24 +++
 ...ostic-test-nesting-text-indented-unicode.c |  24 +++
 .../diagnostic-test-nesting-text-indented.c   |  24 +++
 .../diagnostic-test-nesting-text-plain.c      |   8 +
 .../plugin/diagnostic_plugin_test_nesting.c   | 145 ++++++++++++++++++
 gcc/testsuite/gcc.dg/plugin/plugin.exp        |   6 +
 19 files changed, 560 insertions(+), 22 deletions(-)
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.py
 create mode 100644 
gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-show-levels.c
 create mode 100644 
gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-unicode.c
 create mode 100644 
gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented.c
 create mode 100644 
gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-plain.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_nesting.c

diff --git a/gcc/c-family/c-opts.cc b/gcc/c-family/c-opts.cc
index b920b7d347d1..9a49874bb5e9 100644
--- a/gcc/c-family/c-opts.cc
+++ b/gcc/c-family/c-opts.cc
@@ -176,7 +176,7 @@ c_diagnostic_text_finalizer (diagnostic_text_output_format 
&text_output,
 {
   pretty_printer *const pp = text_output.get_printer ();
   char *saved_prefix = pp_take_prefix (pp);
-  pp_set_prefix (pp, NULL);
+  pp_set_prefix (pp, text_output.build_indent_prefix (false));
   pp_newline (pp);
   diagnostic_show_locus (&text_output.get_context (),
                         diagnostic->richloc, diagnostic->kind, pp);
diff --git a/gcc/diagnostic-core.h b/gcc/diagnostic-core.h
index 0fddf25403ce..46f33d6553e4 100644
--- a/gcc/diagnostic-core.h
+++ b/gcc/diagnostic-core.h
@@ -48,6 +48,18 @@ class auto_diagnostic_group
   ~auto_diagnostic_group ();
 };
 
+/* RAII-style class for nesting hierarchical diagnostics.
+   Any diagnostics emitted within the lifetime of this object
+   will be treated as one level of nesting deeper than diagnostics
+   emitted outside the lifetime of the object.  */
+
+class auto_diagnostic_nesting_level
+{
+ public:
+  auto_diagnostic_nesting_level ();
+  ~auto_diagnostic_nesting_level ();
+};
+
 /* Forward decl.  */
 class diagnostic_metadata; /* See diagnostic-metadata.h.  */
 
diff --git a/gcc/diagnostic-format-sarif.cc b/gcc/diagnostic-format-sarif.cc
index 5a3fd3f5dac8..0d56475dc6da 100644
--- a/gcc/diagnostic-format-sarif.cc
+++ b/gcc/diagnostic-format-sarif.cc
@@ -658,9 +658,13 @@ private:
    - secondary ranges without labels (as related locations)
 
    Known limitations:
-   - GCC supports one-deep nesting of diagnostics (via auto_diagnostic_group),
-     but we only capture location and message information from such nested
-     diagnostics (e.g. we ignore fix-it hints on them)
+   - 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
@@ -1222,6 +1226,12 @@ sarif_result::on_nested_diagnostic (const 
diagnostic_info &diagnostic,
   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);
 }
 
diff --git a/gcc/diagnostic-format-text.cc b/gcc/diagnostic-format-text.cc
index fd47ca4a15b2..217733784e8d 100644
--- a/gcc/diagnostic-format-text.cc
+++ b/gcc/diagnostic-format-text.cc
@@ -220,9 +220,35 @@ on_report_diagnostic (const diagnostic_info &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)
+       {
+         pp_set_prefix (pp, nullptr);
+         char *indent_prefix = build_indent_prefix (false);
+         /* Only print changes of location.  */
+         if (diagnostic_location (&diagnostic)
+             != get_context ().m_last_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);
+       }
+    }
+
   (*diagnostic_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
@@ -257,8 +283,12 @@ after_diagnostic (const diagnostic_info &diagnostic)
 }
 
 /* Return a malloc'd string describing a location and the severity of the
-   diagnostic, e.g. "foo.c:42:10: error: ".  The caller is responsible for
-   freeing the memory.  */
+   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 *
 diagnostic_text_output_format::
 build_prefix (const diagnostic_info &diagnostic) const
@@ -275,12 +305,22 @@ build_prefix (const diagnostic_info &diagnostic) const
       text_ce = colorize_stop (pp_show_color (pp));
     }
 
-  const expanded_location s = diagnostic_expand_location (&diagnostic);
-  label_text location_text = get_location_text (s);
-
-  char *result = build_message_string ("%s %s%s%s", location_text.get (),
-                                      text_cs, text, text_ce);
-  return result;
+  const int nesting_level = get_context ().get_diagnostic_nesting_level ();
+  if (m_show_nesting && nesting_level > 0)
+    {
+      char *indent_prefix = build_indent_prefix (true);
+      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.  */
@@ -294,6 +334,71 @@ diagnostic_text_output_format::file_name_as_prefix (const 
char *f) const
   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 diagnostic_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 (diagnostic_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 *
+diagnostic_text_output_format::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
diff --git a/gcc/diagnostic-format-text.h b/gcc/diagnostic-format-text.h
index 78d1b2da0a0c..f01d8c118e62 100644
--- a/gcc/diagnostic-format-text.h
+++ b/gcc/diagnostic-format-text.h
@@ -39,7 +39,9 @@ public:
     m_column_policy (context),
     m_last_module (nullptr),
     m_includes_seen (nullptr),
-    m_follows_reference_printer (follows_reference_printer)
+    m_follows_reference_printer (follows_reference_printer),
+    m_show_nesting (false),
+    m_show_nesting_levels (false)
   {}
   ~diagnostic_text_output_format ();
 
@@ -73,6 +75,8 @@ public:
 
   char *file_name_as_prefix (const char *) const;
 
+  char *build_indent_prefix (bool with_bullet) const;
+
   void print_path (const diagnostic_path &path);
 
   bool show_column_p () const { return get_context ().m_show_column; }
@@ -83,6 +87,22 @@ public:
   }
   diagnostic_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;
+  }
+
 protected:
   void print_any_cwe (const diagnostic_info &diagnostic);
   void print_any_rules (const diagnostic_info &diagnostic);
@@ -112,6 +132,18 @@ protected:
      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;
 };
 
 #endif /* ! GCC_DIAGNOSTIC_FORMAT_TEXT_H */
diff --git a/gcc/diagnostic-global-context.cc b/gcc/diagnostic-global-context.cc
index bd299d08429b..f8b2f7fb52ce 100644
--- a/gcc/diagnostic-global-context.cc
+++ b/gcc/diagnostic-global-context.cc
@@ -576,3 +576,15 @@ auto_diagnostic_group::~auto_diagnostic_group ()
 {
   global_dc->end_group ();
 }
+
+/* class auto_diagnostic_nesting_level.  */
+
+auto_diagnostic_nesting_level::auto_diagnostic_nesting_level ()
+{
+  global_dc->push_nesting_level ();
+}
+
+auto_diagnostic_nesting_level::~auto_diagnostic_nesting_level ()
+{
+  global_dc->pop_nesting_level ();
+}
diff --git a/gcc/diagnostic-show-locus.cc b/gcc/diagnostic-show-locus.cc
index a2a4b047ff9c..4e695c557a77 100644
--- a/gcc/diagnostic-show-locus.cc
+++ b/gcc/diagnostic-show-locus.cc
@@ -3323,6 +3323,9 @@ diagnostic_source_print_policy::print (pretty_printer &pp,
 void
 layout_printer::print (const diagnostic_source_print_policy &source_policy)
 {
+  diagnostic_prefixing_rule_t saved_rule = pp_prefixing_rule (&m_pp);
+  pp_prefixing_rule (&m_pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE;
+
   if (get_options ().show_ruler_p)
     show_ruler (m_layout.m_x_offset_display + get_options ().max_width);
 
@@ -3359,6 +3362,8 @@ layout_printer::print (const 
diagnostic_source_print_policy &source_policy)
 
   if (auto effect_info = m_layout.m_effect_info)
     effect_info->m_trailing_out_edge_column = m_link_rhs_column;
+
+  pp_prefixing_rule (&m_pp) = saved_rule;
 }
 
 #if CHECKING_P
diff --git a/gcc/diagnostic.cc b/gcc/diagnostic.cc
index ecc4bd94eba1..cc642b017d22 100644
--- a/gcc/diagnostic.cc
+++ b/gcc/diagnostic.cc
@@ -281,7 +281,8 @@ diagnostic_context::initialize (int n_opts)
   m_tabstop = 8;
   m_escape_format = DIAGNOSTICS_ESCAPE_FORMAT_UNICODE;
   m_edit_context_ptr = nullptr;
-  m_diagnostic_groups.m_nesting_depth = 0;
+  m_diagnostic_groups.m_group_nesting_depth = 0;
+  m_diagnostic_groups.m_diagnostic_nesting_level = 0;
   m_diagnostic_groups.m_emission_count = 0;
   m_output_sinks.safe_push (new diagnostic_text_output_format (*this, true));
   m_set_locations_cb = nullptr;
@@ -384,7 +385,7 @@ diagnostic_context::finish ()
   /* We might be handling a fatal error.
      Close any active diagnostic groups, which may trigger flushing
      sinks.  */
-  while (m_diagnostic_groups.m_nesting_depth > 0)
+  while (m_diagnostic_groups.m_group_nesting_depth > 0)
     end_group ();
 
   set_diagnostic_buffer (nullptr);
@@ -1322,7 +1323,7 @@ diagnostic_context::report_diagnostic (diagnostic_info 
*diagnostic)
   /* 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_nesting_depth > 0);
+  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.  */
@@ -1701,13 +1702,13 @@ fancy_abort (const char *file, int line, const char 
*function)
 void
 diagnostic_context::begin_group ()
 {
-  m_diagnostic_groups.m_nesting_depth++;
+  m_diagnostic_groups.m_group_nesting_depth++;
 }
 
 void
 diagnostic_context::end_group ()
 {
-  if (--m_diagnostic_groups.m_nesting_depth == 0)
+  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
@@ -1719,6 +1720,18 @@ diagnostic_context::end_group ()
     }
 }
 
+void
+diagnostic_context::push_nesting_level ()
+{
+  ++m_diagnostic_groups.m_diagnostic_nesting_level;
+}
+
+void
+diagnostic_context::pop_nesting_level ()
+{
+  --m_diagnostic_groups.m_diagnostic_nesting_level;
+}
+
 void
 diagnostic_output_format::dump (FILE *out, int indent) const
 {
@@ -1824,7 +1837,11 @@ diagnostic_context::set_diagnostic_buffer 
(diagnostic_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_nesting_depth == 0);
+  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;
 
diff --git a/gcc/diagnostic.h b/gcc/diagnostic.h
index 5b71523cf89b..f32d404c7a4d 100644
--- a/gcc/diagnostic.h
+++ b/gcc/diagnostic.h
@@ -557,6 +557,9 @@ public:
   void begin_group ();
   void end_group ();
 
+  void push_nesting_level ();
+  void pop_nesting_level ();
+
   bool warning_enabled_at (location_t loc, diagnostic_option_id option_id);
 
   bool option_unspecified_p (diagnostic_option_id option_id) const
@@ -723,6 +726,13 @@ public:
                          const char *, const char *, va_list *,
                          diagnostic_t) 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)
   {
@@ -933,7 +943,10 @@ private:
   /* Fields relating to diagnostic groups.  */
   struct {
     /* How many diagnostic_group instances are currently alive.  */
-    int m_nesting_depth;
+    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.  */
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index 93e9af3791c8..2f84bb9cf502 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -5937,6 +5937,9 @@ by @code{:} and one or more @var{KEY}=@var{VALUE} pairs, 
in this form:
 
 etc.
 
+Schemes, keys, or values with a name prefixed ``experimental'' may change
+or be removed without notice.
+
 @var{SCHEME} can be
 
 @table @gcctabopt
@@ -5952,6 +5955,21 @@ Supported keys are:
 Override colorization settings from @option{-fdiagnostics-color} for this
 text output.
 
+@item experimental-nesting=@r{[}yes@r{|}no@r{]}
+Enable an experimental mode that emphasizes hierarchical relationships
+within diagnostics messages, displaying location information on separate
+lines.
+
+@item experimental-nesting-show-locations=@r{[}yes@r{|}no@r{]}
+If @code{experimental-nesting=yes}, then by default locations are
+shown; set this key to @code{no} to disable printing such locations.
+This exists for use by GCC developers, for writing DejaGnu test cases.
+
+@item experimental-nesting-show-levels=@r{[}yes@r{|}no@r{]}
+This is a debugging option for use with @code{experimental-nesting=yes}.
+Set this key to @code{yes} to print explicit nesting levels in the output.
+This exists for use by GCC developers.
+
 @end table
 
 @item sarif
diff --git a/gcc/opts-diagnostic.cc b/gcc/opts-diagnostic.cc
index 54040ba8d431..2c7e95260f70 100644
--- a/gcc/opts-diagnostic.cc
+++ b/gcc/opts-diagnostic.cc
@@ -361,6 +361,9 @@ text_scheme_handler::make_sink (const context &ctxt,
                                const scheme_name_and_params &parsed_arg) const
 {
   bool show_color = pp_show_color (ctxt.m_dc.get_reference_printer ());
+  bool show_nesting = false;
+  bool show_locations_in_nesting = true;
+  bool show_levels = false;
   for (auto& iter : parsed_arg.m_kvs)
     {
       const std::string &key = iter.first;
@@ -371,17 +374,42 @@ text_scheme_handler::make_sink (const context &ctxt,
            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;
     }
 
-  std::unique_ptr<diagnostic_output_format> sink;
-  sink = ::make_unique<diagnostic_text_output_format> (ctxt.m_dc);
+  auto sink = ::make_unique<diagnostic_text_output_format> (ctxt.m_dc);
+  sink->set_show_nesting (show_nesting);
+  sink->set_show_locations_in_nesting (show_locations_in_nesting);
+  sink->set_show_nesting_levels (show_levels);
   return sink;
 }
 
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.c 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.c
new file mode 100644
index 000000000000..11d9933da248
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.c
@@ -0,0 +1,16 @@
+/* { dg-do compile } */
+/* { dg-options "-fdiagnostics-add-output=sarif" } */
+
+extern void foo (void);
+
+void test_nesting (void)
+{
+  foo (); /* { dg-error "top-level error" } */
+}
+
+/* Verify that some JSON was written to a file with the expected name.  */
+/* { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest diagnostic-test-nesting-sarif.c 
"diagnostic-test-nesting-sarif.py" } } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.py 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.py
new file mode 100644
index 000000000000..7791147de3d6
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-sarif.py
@@ -0,0 +1,39 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_basics(sarif):
+    schema = sarif['$schema']
+    assert schema == 
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
+
+    version = sarif['version']
+    assert version == "2.1.0"
+
+def test_nested_result(sarif):
+    runs = sarif['runs']
+    run = runs[0]
+    results = run['results']
+
+    assert len(results) == 1
+
+    result = results[0]
+    assert result['level'] == 'error'
+    assert result['message']['text'] == "top-level error"
+
+    relatedLocations = result['relatedLocations']
+    assert len(relatedLocations) == 12
+
+    for i in range(12):
+        note = relatedLocations[i]
+        text = note['message']['text']
+        nestingLevel = note['properties']['nestingLevel']
+        if i % 4 == 0:
+            assert text == 'child %i' % (i / 4)
+            assert nestingLevel == 1
+        else:
+            assert text == 'grandchild %i %i' % ((i / 4), (i % 4) - 1)
+            assert nestingLevel == 2
diff --git 
a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-show-levels.c
 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-show-levels.c
new file mode 100644
index 000000000000..91b1056d9557
--- /dev/null
+++ 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-show-levels.c
@@ -0,0 +1,24 @@
+/* { dg-do compile } */
+/* { dg-options 
"-fdiagnostics-set-output=text:experimental-nesting=yes,experimental-nesting-show-levels=yes"
 } */
+
+extern void foo (void);
+
+void test_nesting (void)
+{
+  foo (); /* { dg-error "top-level error" } */
+}
+
+/* { dg-begin-multiline-output "" }
+  * (level 1):note: child 0
+    * (level 2):note: grandchild 0 0
+    * (level 2):note: grandchild 0 1
+    * (level 2):note: grandchild 0 2
+  * (level 1):note: child 1
+    * (level 2):note: grandchild 1 0
+    * (level 2):note: grandchild 1 1
+    * (level 2):note: grandchild 1 2
+  * (level 1):note: child 2
+    * (level 2):note: grandchild 2 0
+    * (level 2):note: grandchild 2 1
+    * (level 2):note: grandchild 2 2
+   { dg-end-multiline-output "" } */
diff --git 
a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-unicode.c 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-unicode.c
new file mode 100644
index 000000000000..e843d196aaf1
--- /dev/null
+++ 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented-unicode.c
@@ -0,0 +1,24 @@
+/* { dg-do compile } */
+/* { dg-options "-fdiagnostics-set-output=text:experimental-nesting=yes 
-fdiagnostics-text-art-charset=unicode" } */
+
+extern void foo (void);
+
+void test_nesting (void)
+{
+  foo (); /* { dg-error "top-level error" } */
+}
+
+/* { dg-begin-multiline-output "" }
+  • note: child 0
+    • note: grandchild 0 0
+    • note: grandchild 0 1
+    • note: grandchild 0 2
+  • note: child 1
+    • note: grandchild 1 0
+    • note: grandchild 1 1
+    • note: grandchild 1 2
+  • note: child 2
+    • note: grandchild 2 0
+    • note: grandchild 2 1
+    • note: grandchild 2 2
+   { dg-end-multiline-output "" } */
diff --git 
a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented.c 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented.c
new file mode 100644
index 000000000000..687acf98fece
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-indented.c
@@ -0,0 +1,24 @@
+/* { dg-do compile } */
+/* { dg-options "-fdiagnostics-set-output=text:experimental-nesting=yes" } */
+
+extern void foo (void);
+
+void test_nesting (void)
+{
+  foo (); /* { dg-error "top-level error" } */
+}
+
+/* { dg-begin-multiline-output "" }
+  * note: child 0
+    * note: grandchild 0 0
+    * note: grandchild 0 1
+    * note: grandchild 0 2
+  * note: child 1
+    * note: grandchild 1 0
+    * note: grandchild 1 1
+    * note: grandchild 1 2
+  * note: child 2
+    * note: grandchild 2 0
+    * note: grandchild 2 1
+    * note: grandchild 2 2
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-plain.c 
b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-plain.c
new file mode 100644
index 000000000000..042081d35c7b
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-nesting-text-plain.c
@@ -0,0 +1,8 @@
+/* { dg-do compile } */
+
+extern void foo (void);
+
+void test_nesting (void)
+{
+  foo (); /* { dg-error "top-level error" } */
+}
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_nesting.c 
b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_nesting.c
new file mode 100644
index 000000000000..17c3574dbf7c
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_nesting.c
@@ -0,0 +1,145 @@
+/* This plugin exercises diagnostic nesting.  */
+
+#include "gcc-plugin.h"
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "tm.h"
+#include "tree.h"
+#include "stringpool.h"
+#include "toplev.h"
+#include "basic-block.h"
+#include "hash-table.h"
+#include "vec.h"
+#include "ggc.h"
+#include "basic-block.h"
+#include "tree-ssa-alias.h"
+#include "internal-fn.h"
+#include "gimple.h"
+#include "gimple-iterator.h"
+#include "gimple-fold.h"
+#include "tree-eh.h"
+#include "gimple-expr.h"
+#include "is-a.h"
+#include "tree.h"
+#include "tree-pass.h"
+#include "intl.h"
+#include "plugin-version.h"
+#include "diagnostic.h"
+#include "context.h"
+#include "gcc-rich-location.h"
+
+int plugin_is_GPL_compatible;
+
+const pass_data pass_data_test_nesting =
+{
+  GIMPLE_PASS, /* type */
+  "test_nesting", /* name */
+  OPTGROUP_NONE, /* optinfo_flags */
+  TV_NONE, /* tv_id */
+  PROP_ssa, /* properties_required */
+  0, /* properties_provided */
+  0, /* properties_destroyed */
+  0, /* todo_flags_start */
+  0, /* todo_flags_finish */
+};
+
+class pass_test_nesting : public gimple_opt_pass
+{
+public:
+  pass_test_nesting(gcc::context *ctxt)
+    : gimple_opt_pass(pass_data_test_nesting, ctxt)
+  {}
+
+  /* opt_pass methods: */
+  bool gate (function *) { return true; }
+  virtual unsigned int execute (function *);
+
+}; // class pass_test_nesting
+
+/* Determine if STMT is a call with NUM_ARGS arguments to a function
+   named FUNCNAME.
+   If so, return STMT as a gcall *.  Otherwise return NULL.  */
+
+static gcall *
+check_for_named_call (gimple *stmt,
+                     const char *funcname, unsigned int num_args)
+{
+  gcc_assert (funcname);
+
+  gcall *call = dyn_cast <gcall *> (stmt);
+  if (!call)
+    return NULL;
+
+  tree fndecl = gimple_call_fndecl (call);
+  if (!fndecl)
+    return NULL;
+
+  if (strcmp (IDENTIFIER_POINTER (DECL_NAME (fndecl)), funcname))
+    return NULL;
+
+  if (gimple_call_num_args (call) != num_args)
+    {
+      error_at (stmt->location, "expected number of args: %i (got %i)",
+               num_args, gimple_call_num_args (call));
+      return NULL;
+    }
+
+  return call;
+}
+
+/* Exercise diagnostic_nesting.  */
+
+unsigned int
+pass_test_nesting::execute (function *fun)
+{
+  gimple_stmt_iterator gsi;
+  basic_block bb;
+
+  FOR_EACH_BB_FN (bb, fun)
+    for (gsi = gsi_start_bb (bb); !gsi_end_p (gsi); gsi_next (&gsi))
+      {
+       gimple *stmt = gsi_stmt (gsi);
+
+       if (gcall *call = check_for_named_call (stmt, "foo", 0))
+         {
+           location_t loc = gimple_location (call);
+           auto_diagnostic_group g;
+           error_at (loc, "top-level error");
+           for (int i = 0; i < 3; i++)
+             {
+               auto_diagnostic_nesting_level sentinel;
+               inform (loc, "child %i", i);
+               for (int j = 0; j < 3; j++)
+                 {
+                   auto_diagnostic_nesting_level sentinel;
+                   inform (loc, "grandchild %i %i", i, j);
+                 }
+             }
+         }
+      }
+
+  return 0;
+}
+
+int
+plugin_init (struct plugin_name_args *plugin_info,
+            struct plugin_gcc_version *version)
+{
+  struct register_pass_info pass_info;
+  const char *plugin_name = plugin_info->base_name;
+  int argc = plugin_info->argc;
+  struct plugin_argument *argv = plugin_info->argv;
+
+  if (!plugin_default_version_check (version, &gcc_version))
+    return 1;
+
+  pass_info.pass = new pass_test_nesting (g);
+  pass_info.reference_pass_name = "ssa";
+  pass_info.ref_pass_instance_number = 1;
+  pass_info.pos_op = PASS_POS_INSERT_AFTER;
+  register_callback (plugin_name, PLUGIN_PASS_MANAGER_SETUP, NULL,
+                    &pass_info);
+
+  return 0;
+}
diff --git a/gcc/testsuite/gcc.dg/plugin/plugin.exp 
b/gcc/testsuite/gcc.dg/plugin/plugin.exp
index 0b53e06d8352..dee8a2bfecb5 100644
--- a/gcc/testsuite/gcc.dg/plugin/plugin.exp
+++ b/gcc/testsuite/gcc.dg/plugin/plugin.exp
@@ -108,6 +108,12 @@ set plugin_test_list [list \
     { diagnostic_plugin_test_metadata.c \
          diagnostic-test-metadata.c \
          diagnostic-test-metadata-sarif.c } \
+    { diagnostic_plugin_test_nesting.c \
+         diagnostic-test-nesting-text-plain.c \
+         diagnostic-test-nesting-text-indented.c \
+         diagnostic-test-nesting-text-indented-show-levels.c \
+         diagnostic-test-nesting-text-indented-unicode.c \
+         diagnostic-test-nesting-sarif.c } \
     { diagnostic_plugin_test_paths.c \
          diagnostic-test-paths-1.c \
          diagnostic-test-paths-2.c \
-- 
2.26.3

Reply via email to