]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
Add libdiagnostics (v4)
authorDavid Malcolm <dmalcolm@redhat.com>
Mon, 18 Nov 2024 22:08:36 +0000 (17:08 -0500)
committerDavid Malcolm <dmalcolm@redhat.com>
Mon, 18 Nov 2024 22:08:36 +0000 (17:08 -0500)
This patch adds a new libdiagnostics shared library available as
part of the GCC build via --enable-libdiagnostics when
configuring GCC.

It combines the following patches from:
  https://gcc.gnu.org/pipermail/gcc-patches/2024-November/668632.html
    [PATCH 1/8] libdiagnostics v4: header
    [PATCH 2/8] libdiagnostics v4: implementation
    [PATCH 3/8] libdiagnostics: add API docs
    [PATCH 4/8] libdiagnostics v4: add C++ wrapper API
    [PATCH 6/8] libdiagnostics v4: test suite

ChangeLog:
* configure.ac (--enable-libdiagnostics): New.
* configure: Regenerate.

gcc/ChangeLog:
* configure.ac (check_languages): Add check-libdiagnostics.
(--enable-libdiagnostics): New.
* configure: Regenerate.
* Makefile.in (enable_libdiagnostics): New.
(lang_checks): If libdiagnostics is enabled, add
check-libdiagnostics.
(ALL_HOST_OBJS): If libdiagnostics is enabled, add
$(libdiagnostics_OBJS).
(start.encap): Add LIBDIAGNOSTICS.
(libdiagnostics_OBJS): New.
(LIBDIAGNOSTICS_VERSION_NUM): New, adapted from code in
jit/Make-lang.in.
(LIBDIAGNOSTICS_MINOR_NUM): Likewise.
(LIBDIAGNOSTICS_RELEASE_NUM): Likewise.
(LIBDIAGNOSTICS_FILENAME): Likewise.
(LIBDIAGNOSTICS_IMPORT_LIB): Likewise.
(libdiagnostics): Likewise.
(LIBDIAGNOSTICS_AGE): Likewise.
(LIBDIAGNOSTICS_BASENAME): Likewise.
(LIBDIAGNOSTICS_SONAME): Likewise.
(LIBDIAGNOSTICS_LINKER_NAME): Likewise.
(LIBDIAGNOSTICS_COMMA): Likewise.
(LIBDIAGNOSTICS_VERSION_SCRIPT_OPTION): Likewise.
(LIBDIAGNOSTICS_SONAME_OPTION): Likewise.
(LIBDIAGNOSTICS_SONAME_SYMLINK): Likewise.
(LIBDIAGNOSTICS_LINKER_NAME_SYMLINK): Likewise.
(LIBDIAGNOSTICS_FILENAME): Likewise.
(libdiagnostics.serial): Likewise.
(LIBDIAGNOSTICS_EXTRA_OPTS): Likewise.
(install): If libdiagnostics is enabled, add
install-libdiagnostics.
(libdiagnostics.install-headers): New.
(libdiagnostics.install-common): New, adapted from code in
jit/Make-lang.in.
(install-libdiagnostics): New.
* diagnostic-format-text.h
(diagnostic_text_output_format::get_location_text): Make public.
* doc/install.texi (--enable-libdiagnostics): New.
* doc/libdiagnostics/Makefile: New file.
* doc/libdiagnostics/conf.py: New file.
* doc/libdiagnostics/index.rst: New file.
* doc/libdiagnostics/make.bat: New file.
* doc/libdiagnostics/topics/diagnostic-manager.rst: New file.
* doc/libdiagnostics/topics/diagnostics.rst: New file.
* doc/libdiagnostics/topics/execution-paths.rst: New file.
* doc/libdiagnostics/topics/fix-it-hints.rst: New file.
* doc/libdiagnostics/topics/index.rst: New file.
* doc/libdiagnostics/topics/logical-locations.rst: New file.
* doc/libdiagnostics/topics/message-formatting.rst: New file.
* doc/libdiagnostics/topics/metadata.rst: New file.
* doc/libdiagnostics/topics/physical-locations.rst: New file.
* doc/libdiagnostics/topics/retrofitting.rst: New file.
* doc/libdiagnostics/topics/sarif.rst: New file.
* doc/libdiagnostics/topics/text-output.rst: New file.
* doc/libdiagnostics/topics/ux.rst: New file.
* doc/libdiagnostics/tutorial/01-hello-world.rst: New file.
* doc/libdiagnostics/tutorial/02-physical-locations.rst: New file.
* doc/libdiagnostics/tutorial/03-logical-locations.rst: New file.
* doc/libdiagnostics/tutorial/04-notes.rst: New file.
* doc/libdiagnostics/tutorial/05-warnings.rst: New file.
* doc/libdiagnostics/tutorial/06-fix-it-hints.rst: New file.
* doc/libdiagnostics/tutorial/07-execution-paths.rst: New file.
* doc/libdiagnostics/tutorial/index.rst: New file.
* libdiagnostics++.h: New file.
* libdiagnostics.cc: New file.
* libdiagnostics.h: New file.
* libdiagnostics.map: New file.

gcc/testsuite/ChangeLog:
* libdiagnostics.dg/libdiagnostics.exp: New, adapted from jit.exp.
* libdiagnostics.dg/sarif.py: New.
* libdiagnostics.dg/test-dump.c: New test.
* libdiagnostics.dg/test-error-c.py: New test.
* libdiagnostics.dg/test-error-with-note-c.py: New test.
* libdiagnostics.dg/test-error-with-note.c: New test.
* libdiagnostics.dg/test-error-with-note.cc: New test.
* libdiagnostics.dg/test-error.c: New test.
* libdiagnostics.dg/test-error.cc: New test.
* libdiagnostics.dg/test-example-1.c: New test.
* libdiagnostics.dg/test-fix-it-hint-c.py: New test.
* libdiagnostics.dg/test-fix-it-hint.c: New test.
* libdiagnostics.dg/test-fix-it-hint.cc: New test.
* libdiagnostics.dg/test-helpers++.h: New test.
* libdiagnostics.dg/test-helpers.h: New test.
* libdiagnostics.dg/test-labelled-ranges.c: New test.
* libdiagnostics.dg/test-labelled-ranges.cc: New test.
* libdiagnostics.dg/test-labelled-ranges.py: New test.
* libdiagnostics.dg/test-logical-location-c.py: New test.
* libdiagnostics.dg/test-logical-location.c: New test.
* libdiagnostics.dg/test-metadata-c.py: New test.
* libdiagnostics.dg/test-metadata.c: New test.
* libdiagnostics.dg/test-multiple-lines-c.py: New test.
* libdiagnostics.dg/test-multiple-lines.c: New test.
* libdiagnostics.dg/test-no-column-c.py: New test.
* libdiagnostics.dg/test-no-column.c: New test.
* libdiagnostics.dg/test-no-diagnostics-c.py: New test.
* libdiagnostics.dg/test-no-diagnostics.c: New test.
* libdiagnostics.dg/test-note-with-fix-it-hint-c.py: New test.
* libdiagnostics.dg/test-note-with-fix-it-hint.c: New test.
* libdiagnostics.dg/test-text-sink-options.c: New test.
* libdiagnostics.dg/test-warning-c.py: New test.
* libdiagnostics.dg/test-warning-with-path-c.py: New test.
* libdiagnostics.dg/test-warning-with-path.c: New test.
* libdiagnostics.dg/test-warning.c: New test.
* libdiagnostics.dg/test-write-sarif-to-file-c.py: New test.
* libdiagnostics.dg/test-write-sarif-to-file.c: New test.
* libdiagnostics.dg/test-write-text-to-file.c: New test.

Signed-off-by: David Malcolm <dmalcolm@redhat.com>
75 files changed:
configure
configure.ac
gcc/Makefile.in
gcc/configure
gcc/configure.ac
gcc/diagnostic-format-text.h
gcc/doc/install.texi
gcc/doc/libdiagnostics/Makefile [new file with mode: 0644]
gcc/doc/libdiagnostics/conf.py [new file with mode: 0644]
gcc/doc/libdiagnostics/index.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/make.bat [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/diagnostic-manager.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/diagnostics.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/execution-paths.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/fix-it-hints.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/index.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/logical-locations.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/message-formatting.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/metadata.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/physical-locations.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/retrofitting.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/sarif.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/text-output.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/topics/ux.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/01-hello-world.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/02-physical-locations.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/03-logical-locations.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/04-notes.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/05-warnings.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/06-fix-it-hints.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/07-execution-paths.rst [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/example-1.png [new file with mode: 0644]
gcc/doc/libdiagnostics/tutorial/index.rst [new file with mode: 0644]
gcc/libdiagnostics++.h [new file with mode: 0644]
gcc/libdiagnostics.cc [new file with mode: 0644]
gcc/libdiagnostics.h [new file with mode: 0644]
gcc/libdiagnostics.map [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/sarif.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-dump.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-error-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-error-with-note.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-error.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-error.cc [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-example-1.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-helpers++.h [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-helpers.h [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-logical-location.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-metadata-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-metadata.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-no-column-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-no-column.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-warning-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-warning.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c [new file with mode: 0644]
gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c [new file with mode: 0644]

index a2e86731b0850aefb1beb2a4f27720c25f822dcd..10c1589d473cb8d55ab5193c1935917304d05324 100755 (executable)
--- a/configure
+++ b/configure
@@ -691,6 +691,7 @@ extra_host_libiberty_configure_flags
 stage1_languages
 host_libs_picflag
 CRAB1_LIBS
+enable_libdiagnostics
 PICFLAG
 host_shared
 gcc_host_pie
@@ -844,6 +845,7 @@ enable_linker_plugin_configure_flags
 enable_linker_plugin_flags
 enable_host_pie
 enable_host_shared
+enable_libdiagnostics
 enable_stage1_languages
 enable_objc_gc
 with_target_bdw_gc
@@ -1578,6 +1580,7 @@ Optional Features:
                           plugins [none]
   --enable-host-pie       build position independent host executables
   --enable-host-shared    build host code as shared libraries
+  --enable-libdiagnostics build libdiagnostics shared library
   --enable-stage1-languages[=all]
                           choose additional languages to build during stage1.
                           Mostly useful for compiler development
@@ -9603,6 +9606,45 @@ fi
 
 
 
+
+# Check for libdiagnostics support.
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable libdiagnostics" >&5
+$as_echo_n "checking whether to enable libdiagnostics... " >&6; }
+# Check whether --enable-libdiagnostics was given.
+if test "${enable_libdiagnostics+set}" = set; then :
+  enableval=$enable_libdiagnostics; enable_libdiagnostics=$enableval
+else
+  enable_libdiagnostics=no
+fi
+
+
+if test x$enable_libdiagnostics = xyes; then
+  # Disable libdiagnostics if -enable-host-shared not specified
+  # but not if building for Mingw. All code in Windows
+  # is position independent code (PIC).
+  case $target in
+     *mingw*) ;;
+     *)
+       if test x$host_shared != xyes; then
+        as_fn_error $? "
+Enabling libdiagnostics requires --enable-host-shared.
+
+--enable-host-shared typically slows the rest of the compiler down by
+a few %, so you must explicitly enable it.
+
+If you want to build both libdiagnostics and the regular compiler, it is often
+best to do this via two separate configure/builds, in separate
+directories, to avoid imposing the performance cost of
+--enable-host-shared on the regular compiler." "$LINENO" 5
+       fi
+       ;;
+   esac
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $enable_libdiagnostics" >&5
+$as_echo "$enable_libdiagnostics" >&6; }
+
+
+
 # Rust requires -ldl and -lpthread if you are using an old glibc that does not include them by
 # default, so we check for them here
 # We are doing the test here and not in the gcc/configure to be able to nicely disable the
index 25419a1d2ab825fd2f70dc6ef68bfa220ad6c7cc..fb61550dba7b5bfa0511069a1dca74100fbff4bc 100644 (file)
@@ -2044,6 +2044,41 @@ fi
 
 AC_SUBST(PICFLAG)
 
+
+# Check for libdiagnostics support.
+AC_MSG_CHECKING([whether to enable libdiagnostics])
+AC_ARG_ENABLE(libdiagnostics,
+[AS_HELP_STRING([--enable-libdiagnostics],
+               [build libdiagnostics shared library])],
+enable_libdiagnostics=$enableval,
+enable_libdiagnostics=no)
+
+if test x$enable_libdiagnostics = xyes; then
+  # Disable libdiagnostics if -enable-host-shared not specified
+  # but not if building for Mingw. All code in Windows
+  # is position independent code (PIC).
+  case $target in
+     *mingw*) ;;
+     *)
+       if test x$host_shared != xyes; then
+        AC_MSG_ERROR([
+Enabling libdiagnostics requires --enable-host-shared.
+
+--enable-host-shared typically slows the rest of the compiler down by
+a few %, so you must explicitly enable it.
+
+If you want to build both libdiagnostics and the regular compiler, it is often
+best to do this via two separate configure/builds, in separate
+directories, to avoid imposing the performance cost of
+--enable-host-shared on the regular compiler.])
+       fi
+       ;;
+   esac
+fi
+AC_MSG_RESULT($enable_libdiagnostics)
+AC_SUBST(enable_libdiagnostics)
+
+
 # Rust requires -ldl and -lpthread if you are using an old glibc that does not include them by
 # default, so we check for them here
 # We are doing the test here and not in the gcc/configure to be able to nicely disable the
index e8b2a38c8b37550ffd727502cf2a5b923bb85ba6..41b0c2d0248eec9220413aa8e41a6413c48cbe9b 100644 (file)
@@ -436,6 +436,8 @@ endif
 
 enable_host_shared = @enable_host_shared@
 
+enable_libdiagnostics = @enable_libdiagnostics@
+
 enable_as_accelerator = @enable_as_accelerator@
 
 CPPLIB = ../libcpp/libcpp.a
@@ -618,6 +620,9 @@ xm_include_list=@xm_include_list@
 xm_defines=@xm_defines@
 lang_checks=
 lang_checks_parallelized=
+ifeq (@enable_libdiagnostics@,yes)
+lang_checks += check-libdiagnostics
+endif
 lang_opt_files=@lang_opt_files@ $(srcdir)/c-family/c.opt $(srcdir)/common.opt $(srcdir)/params.opt $(srcdir)/analyzer/analyzer.opt
 lang_specs_files=@lang_specs_files@
 lang_tree_files=@lang_tree_files@
@@ -1880,6 +1885,10 @@ endif
 # compilation or not.
 ALL_HOST_OBJS = $(ALL_HOST_FRONTEND_OBJS) $(ALL_HOST_BACKEND_OBJS)
 
+ifeq (@enable_libdiagnostics@,yes)
+ALL_HOST_OBJS += $(libdiagnostics_OBJS)
+endif
+
 BACKEND = libbackend.a main.o libcommon-target.a libcommon.a \
        $(CPPLIB) $(LIBDECNUMBER)
 
@@ -2186,7 +2195,7 @@ all.cross: native gcc-cross$(exeext) cpp$(exeext) specs \
        libgcc-support lang.all.cross doc selftest @GENINSRC@ srcextra
 # This is what must be made before installing GCC and converting libraries.
 start.encap: native xgcc$(exeext) cpp$(exeext) specs \
-       libgcc-support lang.start.encap @GENINSRC@ srcextra
+       libgcc-support lang.start.encap @LIBDIAGNOSTICS@ @GENINSRC@ srcextra
 # These can't be made until after GCC can run.
 rest.encap: lang.rest.encap
 # This is what is made with the host's compiler
@@ -2275,6 +2284,129 @@ cpp$(exeext): $(GCC_OBJS) c-family/cppspec.o libcommon-target.a $(LIBDEPS) \
          c-family/cppspec.o $(EXTRA_GCC_OBJS) libcommon-target.a \
          $(EXTRA_GCC_LIBS) $(LIBS)
 
+
+libdiagnostics_OBJS = libdiagnostics.o \
+       libcommon.a
+
+# libdiagnostics
+
+LIBDIAGNOSTICS_VERSION_NUM = 0
+LIBDIAGNOSTICS_MINOR_NUM = 0
+LIBDIAGNOSTICS_RELEASE_NUM = 1
+
+ifneq (,$(findstring mingw,$(target)))
+LIBDIAGNOSTICS_FILENAME = libdiagnostics-$(LIBDIAGNOSTICS_VERSION_NUM).dll
+LIBDIAGNOSTICS_IMPORT_LIB = libdiagnostics.dll.a
+
+libdiagnostics: $(LIBDIAGNOSTICS_FILENAME)
+
+else
+
+ifneq (,$(findstring darwin,$(host)))
+
+LIBDIAGNOSTICS_AGE = 1
+LIBDIAGNOSTICS_BASENAME = libdiagnostics
+
+LIBDIAGNOSTICS_SONAME = \
+  ${libdir}/$(LIBDIAGNOSTICS_BASENAME).$(LIBDIAGNOSTICS_VERSION_NUM).dylib
+LIBDIAGNOSTICS_FILENAME = $(LIBDIAGNOSTICS_BASENAME).$(LIBDIAGNOSTICS_VERSION_NUM).dylib
+LIBDIAGNOSTICS_LINKER_NAME = $(LIBDIAGNOSTICS_BASENAME).dylib
+
+# Conditionalize the use of the LD_VERSION_SCRIPT_OPTION and
+# LD_SONAME_OPTION depending if configure found them, using $(if)
+# We have to define a LIBDIAGNOSTICS_COMMA here, otherwise the commas in the "true"
+# result are treated as separators by the $(if).
+LIBDIAGNOSTICS_COMMA := ,
+LIBDIAGNOSTICS_VERSION_SCRIPT_OPTION = \
+       $(if $(LD_VERSION_SCRIPT_OPTION),\
+         -Wl$(LIBDIAGNOSTICS_COMMA)$(LD_VERSION_SCRIPT_OPTION)$(LIBDIAGNOSTICS_COMMA)$(srcdir)/libdiagnostics.map)
+
+LIBDIAGNOSTICS_SONAME_OPTION = \
+       $(if $(LD_SONAME_OPTION), \
+            -Wl$(LIBDIAGNOSTICS_COMMA)$(LD_SONAME_OPTION)$(LIBDIAGNOSTICS_COMMA)$(LIBDIAGNOSTICS_SONAME))
+
+LIBDIAGNOSTICS_SONAME_SYMLINK = $(LIBDIAGNOSTICS_FILENAME)
+LIBDIAGNOSTICS_LINKER_NAME_SYMLINK = $(LIBDIAGNOSTICS_LINKER_NAME)
+
+libdiagnostics: $(LIBDIAGNOSTICS_FILENAME) \
+       $(LIBDIAGNOSTICS_SYMLINK) \
+       $(LIBDIAGNOSTICS_LINKER_NAME_SYMLINK)
+
+else
+
+LIBDIAGNOSTICS_LINKER_NAME = libdiagnostics.so
+LIBDIAGNOSTICS_SONAME = $(LIBDIAGNOSTICS_LINKER_NAME).$(LIBDIAGNOSTICS_VERSION_NUM)
+LIBDIAGNOSTICS_FILENAME = \
+  $(LIBDIAGNOSTICS_SONAME).$(LIBDIAGNOSTICS_MINOR_NUM).$(LIBDIAGNOSTICS_RELEASE_NUM)
+
+LIBDIAGNOSTICS_LINKER_NAME_SYMLINK = $(LIBDIAGNOSTICS_LINKER_NAME)
+LIBDIAGNOSTICS_SONAME_SYMLINK = $(LIBDIAGNOSTICS_SONAME)
+
+# Conditionalize the use of the LD_VERSION_SCRIPT_OPTION and
+# LD_SONAME_OPTION depending if configure found them, using $(if)
+# We have to define a LIBDIAGNOSTICS_COMMA here, otherwise the commas in the "true"
+# result are treated as separators by the $(if).
+LIBDIAGNOSTICS_COMMA := ,
+LIBDIAGNOSTICS_VERSION_SCRIPT_OPTION = \
+       $(if $(LD_VERSION_SCRIPT_OPTION),\
+         -Wl$(LIBDIAGNOSTICS_COMMA)$(LD_VERSION_SCRIPT_OPTION)$(LIBDIAGNOSTICS_COMMA)$(srcdir)/libdiagnostics.map)
+
+LIBDIAGNOSTICS_SONAME_OPTION = \
+       $(if $(LD_SONAME_OPTION), \
+            -Wl$(LIBDIAGNOSTICS_COMMA)$(LD_SONAME_OPTION)$(LIBDIAGNOSTICS_COMMA)$(LIBDIAGNOSTICS_SONAME))
+
+libdiagnostics: $(LIBDIAGNOSTICS_FILENAME) \
+       $(LIBDIAGNOSTICS_SYMLINK) \
+       $(LIBDIAGNOSTICS_LINKER_NAME_SYMLINK)
+
+endif
+endif
+
+libdiagnostics.serial = $(LIBDIAGNOSTICS_FILENAME)
+
+# Tell GNU make to ignore these if they exist.
+.PHONY: libdiagnostics
+
+ifneq (,$(findstring mingw,$(target)))
+# Create import library
+LIBDIAGNOSTICS_EXTRA_OPTS = -Wl,--out-implib,$(LIBDIAGNOSTICS_IMPORT_LIB)
+else
+
+ifneq (,$(findstring darwin,$(host)))
+# TODO : Construct a Darwin-style symbol export file.
+LIBDIAGNOSTICS_EXTRA_OPTS = -Wl,-compatibility_version,$(LIBDIAGNOSTICS_VERSION_NUM) \
+       -Wl,-current_version,$(LIBDIAGNOSTICS_VERSION_NUM).$(LIBDIAGNOSTICS_MINOR_NUM).$(LIBDIAGNOSTICS_AGE) \
+       $(LIBDIAGNOSTICS_VERSION_SCRIPT_OPTION) \
+       $(LIBDIAGNOSTICS_SONAME_OPTION)
+else
+
+LIBDIAGNOSTICS_EXTRA_OPTS = $(LIBDIAGNOSTICS_VERSION_SCRIPT_OPTION) \
+       $(LIBDIAGNOSTICS_SONAME_OPTION)
+endif
+endif
+
+$(LIBDIAGNOSTICS_FILENAME): $(libdiagnostics_OBJS) $(CPPLIB) $(EXTRA_GCC_LIBS) $(LIBS) \
+       $(LIBDEPS) $(srcdir)/libdiagnostics.map
+       @$(call LINK_PROGRESS,$(INDEX.libdiagnostics),start)
+       +$(LLINKER) $(ALL_LINKERFLAGS) $(LDFLAGS) -o $@ -shared \
+            $(libdiagnostics_OBJS) \
+            $(CPPLIB) $(EXTRA_GCC_LIBS) $(LIBS) \
+            $(LIBDIAGNOSTICS_EXTRA_OPTS)
+       @$(call LINK_PROGRESS,$(INDEX.libdiagnostics),end)
+
+# Create symlinks when not building for Windows
+ifeq (,$(findstring mingw,$(target)))
+
+ifeq (,$(findstring darwin,$(host)))
+# but only one level for Darwin, version info is embedded.
+$(LIBDIAGNOSTICS_SONAME_SYMLINK): $(LIBDIAGNOSTICS_FILENAME)
+       ln -sf $(LIBDIAGNOSTICS_FILENAME) $(LIBDIAGNOSTICS_SONAME_SYMLINK)
+endif
+
+$(LIBDIAGNOSTICS_LINKER_NAME_SYMLINK): $(LIBDIAGNOSTICS_SONAME_SYMLINK)
+       ln -sf $(LIBDIAGNOSTICS_SONAME_SYMLINK) $(LIBDIAGNOSTICS_LINKER_NAME_SYMLINK)
+endif
+
 # Dump a specs file to make -B./ read these specs over installed ones.
 $(SPECS): xgcc$(exeext)
        $(GCC_FOR_TARGET) -dumpspecs > tmp-specs
@@ -3816,6 +3948,10 @@ ifeq ($(enable_plugin),yes)
 install: install-plugin
 endif
 
+ifeq ($(enable_libdiagnostics),yes)
+install: install-libdiagnostics
+endif
+
 install-strip: override INSTALL_PROGRAM = $(INSTALL_STRIP_PROGRAM)
 ifneq ($(STRIP),)
 install-strip: STRIPPROG = $(STRIP)
@@ -3992,6 +4128,47 @@ install-driver: installdirs xgcc$(exeext)
          fi; \
        fi
 
+libdiagnostics.install-headers: installdirs
+       $(INSTALL_DATA) $(srcdir)/libdiagnostics.h \
+         $(DESTDIR)$(includedir)/libdiagnostics.h
+       $(INSTALL_DATA) $(srcdir)/libdiagnostics++.h \
+         $(DESTDIR)$(includedir)/libdiagnostics++.h
+
+ifneq (,$(findstring mingw,$(target)))
+libdiagnostics.install-common: installdirs libdiagnostics.install-headers
+# Install import library
+       $(INSTALL_PROGRAM) $(LIBDIAGNOSTICS_IMPORT_LIB) \
+         $(DESTDIR)$(libdir)/$(LIBDIAGNOSTICS_IMPORT_LIB)
+# Install DLL file
+       $(INSTALL_PROGRAM) $(LIBDIAGNOSTICS_FILENAME) \
+         $(DESTDIR)$(bindir)/$(LIBDIAGNOSTICS_FILENAME)
+
+else
+ifneq (,$(findstring darwin,$(host)))
+# but only one level for Darwin
+
+libdiagnostics.install-common: installdirs libdiagnostics.install-headers
+       $(INSTALL_PROGRAM) $(LIBDIAGNOSTICS_FILENAME) \
+         $(DESTDIR)$(libdir)/$(LIBDIAGNOSTICS_FILENAME)
+       ln -sf \
+         $(LIBDIAGNOSTICS_SONAME_SYMLINK)\
+         $(DESTDIR)$(libdir)/$(LIBDIAGNOSTICS_LINKER_NAME_SYMLINK)
+
+else
+libdiagnostics.install-common: installdirs libdiagnostics.install-headers
+       $(INSTALL_PROGRAM) $(LIBDIAGNOSTICS_FILENAME) \
+         $(DESTDIR)$(libdir)/$(LIBDIAGNOSTICS_FILENAME)
+       ln -sf \
+         $(LIBDIAGNOSTICS_FILENAME) \
+         $(DESTDIR)$(libdir)/$(LIBDIAGNOSTICS_SONAME_SYMLINK)
+       ln -sf \
+         $(LIBDIAGNOSTICS_SONAME_SYMLINK)\
+         $(DESTDIR)$(libdir)/$(LIBDIAGNOSTICS_LINKER_NAME_SYMLINK)
+endif
+endif
+
+install-libdiagnostics: libdiagnostics.install-common
+
 # Install the info files.
 # $(INSTALL_DATA) might be a relative pathname, so we can't cd into srcdir
 # to do the install.
index 19cb5d0695900fda5e154747d5408938af5d5030..b2eceb967f8f46ad9eefaab1b97caeca9644ec19 100755 (executable)
@@ -637,6 +637,8 @@ LD_PICFLAG
 PICFLAG
 enable_default_pie
 enable_host_bind_now
+LIBDIAGNOSTICS
+enable_libdiagnostics
 enable_host_pie
 enable_host_shared
 enable_plugin
@@ -1051,6 +1053,7 @@ enable_version_specific_runtime_libs
 enable_plugin
 enable_host_shared
 enable_host_pie
+enable_libdiagnostics
 enable_host_bind_now
 enable_libquadmath_support
 with_linker_hash_style
@@ -1826,6 +1829,7 @@ Optional Features:
   --enable-plugin         enable plugin support
   --enable-host-shared    build host code as shared libraries
   --enable-host-pie       build host code as PIE
+  --enable-libdiagnostics build libdiagnostics shared library
   --enable-host-bind-now  link host code as BIND_NOW
   --disable-libquadmath-support
                           disable libquadmath support for Fortran
@@ -21456,7 +21460,7 @@ else
   lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
   lt_status=$lt_dlunknown
   cat > conftest.$ac_ext <<_LT_EOF
-#line 21459 "configure"
+#line 21463 "configure"
 #include "confdefs.h"
 
 #if HAVE_DLFCN_H
@@ -21562,7 +21566,7 @@ else
   lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
   lt_status=$lt_dlunknown
   cat > conftest.$ac_ext <<_LT_EOF
-#line 21565 "configure"
+#line 21569 "configure"
 #include "confdefs.h"
 
 #if HAVE_DLFCN_H
@@ -33838,6 +33842,9 @@ for language in $all_selected_languages
 do
        check_languages="$check_languages check-$language"
 done
+if test x$enable_libdiagnostics = xyes; then
+       check_languages="$check_languages check-libdiagnostics"
+fi
 
 selftest_languages=
 for language in $all_selected_languages
 
 
 
+# Check whether --enable-libdiagnostics was given.
+if test "${enable_libdiagnostics+set}" = set; then :
+  enableval=$enable_libdiagnostics;
+fi
+
+
+
+if test "$enable_libdiagnostics" = "yes"; then
+  LIBDIAGNOSTICS='libdiagnostics'
+else
+  LIBDIAGNOSTICS=''
+fi
+
+
+
 # Enable --enable-host-bind-now
 # Check whether --enable-host-bind-now was given.
 if test "${enable_host_bind_now+set}" = set; then :
index bdd8cb0002f38813614cbffb42a20e32e6464d35..2dda8f2a5707a51b6a179d033591ba2d33c00dd7 100644 (file)
@@ -7380,6 +7380,9 @@ for language in $all_selected_languages
 do
        check_languages="$check_languages check-$language"
 done
+if test x$enable_libdiagnostics = xyes; then
+       check_languages="$check_languages check-libdiagnostics"
+fi
 
 selftest_languages=
 for language in $all_selected_languages
@@ -7612,6 +7615,19 @@ AC_ARG_ENABLE(host-pie,
                [build host code as PIE])])
 AC_SUBST(enable_host_pie)
 
+AC_ARG_ENABLE(libdiagnostics,
+[AS_HELP_STRING([--enable-libdiagnostics],
+               [build libdiagnostics shared library])])
+AC_SUBST(enable_libdiagnostics)
+
+if test "$enable_libdiagnostics" = "yes"; then
+  LIBDIAGNOSTICS='libdiagnostics'
+else
+  LIBDIAGNOSTICS=''
+fi
+AC_SUBST(LIBDIAGNOSTICS)
+
+
 # Enable --enable-host-bind-now
 AC_ARG_ENABLE(host-bind-now,
 [AS_HELP_STRING([--enable-host-bind-now],
index f01d8c118e62727c52874d0280225f8742233b0b..2ca3bb227ef76451393b4ec0ca6e716aeef76b7d 100644 (file)
@@ -103,13 +103,14 @@ public:
     m_show_nesting_levels = show_nesting_levels;
   }
 
+  label_text get_location_text (const expanded_location &s) const;
+
 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,
                                 diagnostic_t orig_diag_kind);
 
-  label_text get_location_text (const expanded_location &s) const;
   bool includes_seen_p (const line_map_ordinary *map);
 
   /* For handling diagnostic_buffer.  */
index 03bbfece0ec65055df63803c44ea37a781794164..d365c07987d4a8f173857d387b46691bcf176e43 100644 (file)
@@ -1231,6 +1231,13 @@ virtual calls in verifiable mode at all.  However the libvtv library will
 still be built (see @option{--disable-libvtv} to turn off building libvtv).
 @option{--disable-vtable-verify} is the default.
 
+@item --enable-libdiagnostics
+Specify whether to build @code{libdiagnostics}, a shared library exposing
+GCC's diagnostics capabilities via a C API, and a C++ wrapper API adding
+``syntactic sugar''.
+
+This option requires @option{--enable-host-shared} on non-Windows hosts.
+
 @item --disable-gcov
 Specify that the run-time library used for coverage analysis
 and associated host tools should not be built.
diff --git a/gcc/doc/libdiagnostics/Makefile b/gcc/doc/libdiagnostics/Makefile
new file mode 100644 (file)
index 0000000..d4bb2cb
--- /dev/null
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+       @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+       @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/gcc/doc/libdiagnostics/conf.py b/gcc/doc/libdiagnostics/conf.py
new file mode 100644 (file)
index 0000000..1ff7552
--- /dev/null
@@ -0,0 +1,27 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = 'libdiagnostics'
+copyright = '2024, David Malcolm'
+author = 'David Malcolm'
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = []
+
+templates_path = ['_templates']
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'alabaster'
+html_static_path = ['_static']
diff --git a/gcc/doc/libdiagnostics/index.rst b/gcc/doc/libdiagnostics/index.rst
new file mode 100644 (file)
index 0000000..a05eb4e
--- /dev/null
@@ -0,0 +1,113 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+libdiagnostics
+==============
+
+This document describes `libdiagnostics <https://gcc.gnu.org/wiki/libdiagnostics>`_,
+an API for programs to use to emit diagnostics (such as for "lint"-style checker
+tools), supporting:
+
+* text output similar to GCC's errors and warnings::
+
+    test-typo.c:19:13: error: unknown field 'colour'
+       19 |   return p->colour;
+          |             ^~~~~~
+
+  quoting pertinent source code (with a cache), and underlining
+  :doc:`points and ranges in the files being tested <tutorial/02-physical-locations>`,
+  possibly with labels::
+
+   test-labelled-ranges.c:9:6: error: mismatching types: 'int' and 'const char *'
+      19 |   42 + "foo"
+         |   ~~ ^ ~~~~~
+         |   |    |
+         |   int  const char *
+
+* emitting :doc:`fix-it hints <tutorial/06-fix-it-hints>`::
+
+   test-fix-it-hint.c:19:13: error: unknown field 'colour'; did you mean 'color'
+      19 |   return p->colour;
+         |             ^~~~~~
+         |             color
+
+  and generating patches from them::
+
+   @@ -16,7 +16,7 @@
+    struct rgb
+    get_color (struct object *p)
+    {
+   -  return p->colour;
+   +  return p->color;
+    }
+
+* capturing :doc:`execution paths<tutorial/07-execution-paths>` through code::
+
+   In function 'make_a_list_of_random_ints_badly':
+   test-warning-with-path.c:30:5: warning: passing NULL as argument 1 to 'PyList_Append' which requires a non-NULL parameter"
+      30 |     PyList_Append(list, item);
+         |     ^~~~~~~~~~~~~~~~~~~~~~~~~
+   make_a_list_of_random_ints_badly': events 1-3
+      26 |   list = PyList_New(0);
+         |          ^~~~~~~~~~~~~
+         |          |
+         |          (1) when 'PyList_New' fails, returning NULL
+      27 |
+      28 |   for (i = 0; i < count; i++) {
+         |               ~~~~~~~~~
+         |               |
+         |               (2) when 'i < count'
+      29 |     item = PyLong_FromLong(random());
+      30 |     PyList_Append(list, item);
+         |     ~~~~~~~~~~~~~~~~~~~~~~~~~
+         |     |
+         |     (3) when calling 'PyList_Append', passing NULL from (1) as argument 1
+
+* support for emitting machine-readable representations of the above
+  using the :doc:`SARIF file format <topics/sarif>`
+
+There are actually two APIs for the library:
+
+* a pure C API: ``libdiagnostics.h``
+
+* a C++ wrapper API: ``libdiagnostics+.h``.  This is a header-only
+  collection of wrapper classes around the C API to give a less
+  verbose API.
+
+This documentation covers the C API.
+
+Contents
+********
+
+.. toctree::
+   :maxdepth: 2
+
+   tutorial/index.rst
+   topics/index.rst
+
+libdiagnostics 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 of the License, or
+(at your option) any later version.
+
+
+Indices and tables
+******************
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/gcc/doc/libdiagnostics/make.bat b/gcc/doc/libdiagnostics/make.bat
new file mode 100644 (file)
index 0000000..954237b
--- /dev/null
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+       set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+       echo.
+       echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+       echo.installed, then set the SPHINXBUILD environment variable to point
+       echo.to the full path of the 'sphinx-build' executable. Alternatively you
+       echo.may add the Sphinx directory to PATH.
+       echo.
+       echo.If you don't have Sphinx installed, grab it from
+       echo.https://www.sphinx-doc.org/
+       exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/gcc/doc/libdiagnostics/topics/diagnostic-manager.rst b/gcc/doc/libdiagnostics/topics/diagnostic-manager.rst
new file mode 100644 (file)
index 0000000..7f86f6b
--- /dev/null
@@ -0,0 +1,58 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Diagnostic Managers
+===================
+
+.. type:: diagnostic_manager;
+
+A :type:`diagnostic_manager` is an opaque bundle of state for a client of
+libdiagnostics.
+
+It has zero of more "output sinks" to which diagnostics are emitted.
+
+Responsibilities include:
+
+* location-management
+
+* caching of source file content
+
+* patch generation
+
+.. function:: diagnostic_manager *diagnostic_manager_new (void)
+
+   Create a new diagnostic_manager.
+   The caller will need to call :func:`diagnostic_release_manager`
+   on it at some point.
+
+   .. note:: No output sinks are created by default; so you will want
+      to create one with something like:
+
+      .. code-block::
+
+       diagnostic_manager_add_text_sink (diag_mgr, stderr,
+                                          DIAGNOSTIC_COLORIZE_IF_TTY);
+
+.. function::  void diagnostic_manager_release (diagnostic_manager *diag_mgr)
+
+   Release a diagnostic_manager.
+
+   This will flush output to all of the output sinks, and clean up.
+
+   The parameter must be non-NULL.
diff --git a/gcc/doc/libdiagnostics/topics/diagnostics.rst b/gcc/doc/libdiagnostics/topics/diagnostics.rst
new file mode 100644 (file)
index 0000000..66f0a25
--- /dev/null
@@ -0,0 +1,127 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Diagnostics
+===========
+
+.. type:: diagnostic
+
+A :type:`diagnostic` is an opaque bundle of state for a particular
+diagnostic that is being constructed in memory.
+
+
+Lifecycle of a diagnostic
+*************************
+
+Diagnostics are
+
+* *created* from a :type:`diagnostic_manager` by using
+  :func:`diagnostic_begin`, then
+
+* *populated* with data, such as physical locations, logical locations,
+  metadata, execution paths, or fix-it hints, then
+
+* *finished*, in which a formatting string and arguments are given,
+  via a call to :func:`diagnostic_finish` or :func:`diagnostic_finish_va`.
+  The :type:`diagnostic_manager` will emit the diagnostic to all of the
+  manager's output sinks (either immediately, or at some later time,
+  depending on the sink).
+
+  Once a :type:`diagnostic` has had one of these "finish" functions called
+  on it, it is freed, and is no longer valid for use.
+
+  The formatting strings use their own syntax; see :doc:`message-formatting`.
+
+.. function::  diagnostic *diagnostic_begin (diagnostic_manager *diag_mgr, \
+                                             enum diagnostic_level level)
+
+   Create a new :type:`diagnostic` associated with the given
+   :type:`diagnostic_manager`.
+
+   The parameter ``diag_mgr`` must be non-NULL.
+
+   The parameter ``level`` describes the severity of the diagnostic.
+
+.. enum:: diagnostic_level
+
+   This enum describes the severity of a particular diagnostic.
+
+   .. macro:: DIAGNOSTIC_LEVEL_ERROR
+
+      A problem sufficiently severe that the program cannot successfully
+      complete, or where the input being analyzed is definitely wrong
+      (e.g. malformed).
+
+   .. macro:: DIAGNOSTIC_LEVEL_WARNING
+
+      A problem where the input is technically correct, but is likely
+      not what the user intended, such as common mistakes, or other
+      unusual conditions that *may* indicate trouble, such as use of
+      obsolete features.
+
+   .. macro:: DIAGNOSTIC_LEVEL_NOTE
+
+      A supplementary message added to another :type:`diagnostic`, giving
+      extra information that may help the user understand it.
+
+   .. macro:: DIAGNOSTIC_LEVEL_SORRY
+
+      A problem where the input is valid, but the tool isn't
+      able to handle it.
+
+.. function:: void diagnostic_finish (diagnostic *diag, const char *fmt, ...)
+
+   Emit ``diag`` to all sinks of its manager, and release ``diag``.  It is not
+   valid to use ``diag`` after this call.
+
+   Use parameter ``fmt`` for the message.
+   Note that this uses gcc's pretty-print format, which is *not* printf.
+   See :doc:`message-formatting`.
+
+   Both ``diag`` and ``fmt`` must be non-NULL.
+
+   TODO: who is responsible for putting FMT through gettext?
+
+.. function:: void diagnostic_finish_va (diagnostic *diag, const char *fmt, va_list *args)
+
+   This is equivalent to :func:`diagnostic_finish`, but using a
+   :type:`va_list` rather than directly taking variadic arguments.
+
+   All three parameters must be non-NULL.
+
+
+Diagnostic groups
+*****************
+
+See :doc:`the "adding notes" section of the tutorial <../tutorial/04-notes>`
+for an example of a diagnostic group.
+
+.. function:: void diagnostic_manager_begin_group (diagnostic_manager *diag_mgr)
+
+  Begin a diagnostic group.  All diagnostics emitted within
+  ``diag_mgr`` after the first one will be treated as additional information
+  relating to the initial diagnostic.
+
+  The parameter ``diag_mgr`` must be non-NULL.
+
+.. function:: void diagnostic_manager_end_group (diagnostic_manager *diag_mgr)
+
+   Finish a diagnostic group.
+
+   The parameter ``diag_mgr`` must be non-NULL.
diff --git a/gcc/doc/libdiagnostics/topics/execution-paths.rst b/gcc/doc/libdiagnostics/topics/execution-paths.rst
new file mode 100644 (file)
index 0000000..3f4109c
--- /dev/null
@@ -0,0 +1,93 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Execution paths
+===============
+
+.. type:: diagnostic_execution_path
+
+A :type:`diagnostic` can optionally contain a :type:`diagnostic_execution_path`
+describing a path of execution through code.
+
+.. function:: diagnostic_execution_path * diagnostic_add_execution_path (diagnostic *diag)
+
+   Create and borrow a pointer to an execution path for ``diag``.
+
+   The path is automatically cleaned up when ``diag`` is finished.
+
+   ``diag`` must be non-NULL.
+
+.. function:: diagnostic_execution_path * diagnostic_manager_new_execution_path (diagnostic_manager *diag_mgr)
+
+   Create a new execution path. This is owned by the caller and must have either
+   :func:`diagnostic_take_execution_path` or
+   :func:`diagnostic_execution_path_release` called on it.
+
+   ``diag_mgr`` must be non-NULL.
+
+.. function:: void diagnostic_take_execution_path (diagnostic *diag, diagnostic_execution_path *path)
+
+   Set ``diag`` to use ``path`` as its execution path, taking ownership of ``path``.
+
+   Both parameters must be non-NULL.
+
+.. function:: void diagnostic_execution_path_release (diagnostic_execution_path *path)
+
+   Release ownership of ``path``, which must not have been taken by a diagnostic.
+
+.. type:: diagnostic_event_id
+
+A :type:`diagnostic_event_id` identifies a particular event within a
+:type:`diagnostic_execution_path` and can be used for expressing
+cross-references between events.  In particular FIXME
+
+.. function:: diagnostic_event_id diagnostic_execution_path_add_event (diagnostic_execution_path *path, \
+                                                                       const diagnostic_physical_location *physical_loc, \
+                                                                       const diagnostic_logical_location *logical_loc, \
+                                                                       unsigned stack_depth, \
+                                                                       const char *fmt, ...)
+
+   Append an event to the end of ``path``, which must be non-NULL.
+
+   ``physical_loc`` can be NULL, or non-NULL to associate the event
+   with a :type:`diagnostic_physical_location`.
+
+   ``logical_loc`` can be NULL, or non-NULL to associate the event
+   with a :type:`diagnostic_logical_location`.
+
+   ``stack_depth`` is for use in interprocedural paths and identifies the
+   depth of the stack at the event.  Purely intraprocedural paths should
+   use a stack depth of 1 for their events
+
+   ``fmt`` must be non-NULL.  See :doc:`message-formatting` for details of
+   how to use it.
+
+.. function:: diagnostic_event_id diagnostic_execution_path_add_event_va (diagnostic_execution_path *path, \
+                                                                          const diagnostic_physical_location *physical_loc, \
+                                                                          const diagnostic_logical_location *logical_loc, \
+                                                                          unsigned stack_depth, \
+                                                                          const char *fmt, \
+                                                                          va_list *args)
+
+   Equivalent to :func:`diagnostic_execution_path_add_event`, but using a
+   :type:`va_list` rather than directly taking variadic arguments.
+
+Paths are printed to text sinks, and for SARIF sinks each path is added as
+a ``codeFlow`` object (see SARIF 2.1.0
+`3.36 codeFlow object <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790990>`_).
diff --git a/gcc/doc/libdiagnostics/topics/fix-it-hints.rst b/gcc/doc/libdiagnostics/topics/fix-it-hints.rst
new file mode 100644 (file)
index 0000000..08acb71
--- /dev/null
@@ -0,0 +1,135 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Fix-it hints
+============
+
+Adding fix-it hints to a diagnostic
+***********************************
+
+A :type:`diagnostic` can contain "fix-it hints", giving suggestions
+for the user on how to edit their code to fix a problem.  These
+can be expressed as insertions, replacements, and removals of text.
+
+There is only limited support for newline characters in fix-it hints:
+only hints with newlines which insert an entire new line are permitted,
+inserting at the start of a line, and finishing with a newline
+(with no interior newline characters).  Other attempts to add
+fix-it hints containing newline characters will fail.
+Similarly, attempts to delete or replace a range *affecting* multiple
+lines will fail.
+
+The API handles these failures gracefully, so that diagnostics can attempt
+to add fix-it hints without each needing extensive checking.
+
+Fix-it hints are printed to text sinks, and are emitted by SARIF sinks
+as ``fix`` objects (see SARIF 2.1.0
+`3.55 fix object <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141791131>`_).
+
+Fix-it hints within a :type:`diagnostic` are "atomic": if any hints can't
+be applied, none of them will be, and no fix-its hints will be displayed
+for that diagnostic.  This implies that diagnostic messages need to be worded
+in such a way that they make sense whether or not the fix-it hints
+are displayed.
+
+All fix-it hints within one :type:`diagnostic` must affect the same
+:type:`diagnostic_file`.
+
+.. function:: void diagnostic_add_fix_it_hint_insert_before (diagnostic *diag, \
+                                                             const diagnostic_physical_location *loc, \
+                                                             const char *addition)
+
+   Add a fix-it hint to ``diag`` suggesting the insertion of the string
+   ``addition`` before ``LOC``.
+
+   For example::
+
+     ptr = arr[0];
+           ^~~~~~
+           &
+
+   This :type:`diagnostic` has a single location covering ``arr[0]``,
+   with the caret at the start.  It has a single insertion fix-it hint,
+   inserting ``&`` before the start of ``loc``.
+
+.. function:: void diagnostic_add_fix_it_hint_insert_after (diagnostic *diag, \
+                                                            const diagnostic_physical_location *loc, \
+                                                            const char *addition)
+
+   Add a fix-it hint to ``diag`` suggesting the insertion of the string
+   ``addition`` after the end of ``LOC``.
+
+   For example, in::
+
+      #define FN(ARG0, ARG1, ARG2) fn(ARG0, ARG1, ARG2)
+                                      ^~~~  ^~~~  ^~~~
+                                      (   ) (   ) (   )
+
+
+   the :type:`diagnostic` has three physical locations, covering ``ARG0``,
+   ``ARG1``, and ``ARG2``, and 6 insertion fix-it hints: each arg
+   has a pair of insertion fix-it hints, suggesting wrapping
+   them with parentheses: one a '(' inserted before,
+   the other a ')' inserted after.
+
+.. function:: void diagnostic_add_fix_it_hint_replace (diagnostic *diag, \
+                                                       const diagnostic_physical_location *loc, \
+                                                       const char *replacement)
+
+   Add a fix-it hint to ``diag`` suggesting the replacement of the text
+   at ``LOC`` with the string ``replacement``.
+
+   For example, in::
+
+      c = s.colour;
+           ^~~~~~
+           color
+
+   This :type:`diagnostic` has a single physical location covering ``colour``,
+   and a single "replace" fix-it hint, covering the same range, suggesting
+   replacing it with ``color``.
+
+.. function:: void diagnostic_add_fix_it_hint_delete (diagnostic *diag, \
+                                                      const diagnostic_physical_location *loc)
+
+   Add a fix-it hint to ``diag`` suggesting the deletion of the text
+   at ``LOC``.
+
+
+   For example, in::
+
+     struct s {int i};;
+                     ^
+                     -
+
+   This :type:`diagnostic` has a single physical location at the stray
+   trailing semicolon, along with a single removal fix-it hint, covering
+   the same location.
+
+
+Generating patches
+******************
+
+.. function:: void diagnostic_manager_write_patch (diagnostic_manager *diag_mgr, \
+                                                   FILE *dst_stream)
+
+   Write a patch to ``dst_stream`` consisting of the effect of all fix-it hints
+   on all diagnostics that have been finished on ``diag_mgr``.
+
+   Both parameters must be non-NULL.
diff --git a/gcc/doc/libdiagnostics/topics/index.rst b/gcc/doc/libdiagnostics/topics/index.rst
new file mode 100644 (file)
index 0000000..064340b
--- /dev/null
@@ -0,0 +1,38 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+
+Topic reference
+===============
+
+.. toctree::
+   :maxdepth: 2
+
+   retrofitting.rst
+   diagnostic-manager.rst
+   diagnostics.rst
+   message-formatting.rst
+   physical-locations.rst
+   logical-locations.rst
+   metadata.rst
+   fix-it-hints.rst
+   execution-paths.rst
+   text-output.rst
+   sarif.rst
+   ux.rst
diff --git a/gcc/doc/libdiagnostics/topics/logical-locations.rst b/gcc/doc/libdiagnostics/topics/logical-locations.rst
new file mode 100644 (file)
index 0000000..85900b6
--- /dev/null
@@ -0,0 +1,109 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Logical locations
+=================
+
+A "logical" location is a location expressed in terms of
+construct in a programming language, such as ``within function 'foo'``
+(as opposed to a :doc:`"physical" location <physical-locations>`, which
+refers to a specific file, and line(s) and/or column(s))
+
+Creating location information
+*****************************
+
+.. type:: diagnostic_logical_location
+
+A :type:`diagnostic_logical_location` is an opaque type describing a "logical"
+source location
+
+.. function:: const diagnostic_logical_location * diagnostic_manager_new_logical_location (diagnostic_manager *diag_mgr, \
+                                                                                           enum diagnostic_logical_location_kind_t kind, \
+                                                                                           const diagnostic_logical_location *parent, \
+                                                                                           const char *short_name, \
+                                                                                           const char *fully_qualified_name, \
+                                                                                           const char *decorated_name)
+
+   Create a :type:`diagnostic_logical_location`.
+
+   ``diag_mgr`` must be non-NULL.
+
+   ``kind`` describes the kind of logical location:
+
+   .. enum:: diagnostic_logical_location_kind_t
+
+      This roughly corresponds to the ``kind`` property in SARIF v2.1.0
+      (`§3.33.7 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790976>`_).
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_MEMBER
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_MODULE
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_NAMESPACE
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_TYPE
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_RETURN_TYPE
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_PARAMETER
+
+      .. macro:: DIAGNOSTIC_LOGICAL_LOCATION_KIND_VARIABLE
+
+   ``parent`` can be NULL; if non-NULL it can be used to express tree-like
+   nesting of logical locations, such as in::
+
+     namespace foo { namespace bar { class baz { baz (); }; } }
+
+   where a diagnostic within ``baz``'s constructor could be reported
+   as being within ``foo::bar::baz::baz`` where the logical locations
+   are two namespaces, a type, and a member, respectively.
+
+   ``short_name`` can be NULL, or else a string suitable for use by
+   the SARIF logicalLocation ``name`` property
+   (SARIF v2.1.0 `§3.33.4 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790973>`_).
+
+   ``fully_qualified_name`` can be NULL or else a string  suitable for use by
+   the SARIF logicalLocation ``fullyQualifiedName`` property
+   (SARIF v2.1.0 `§3.33.5 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790974>`_).
+
+   ``decorated_name`` can be NULL or else a string suitable for use by
+   the SARIF logicalLocation ``decoratedName`` property
+   (SARIF v2.1.0 `§3.33.6 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790975>`_).
+
+.. function:: void diagnostic_manager_debug_dump_logical_location (const diagnostic_manager *diag_mgr, \
+                                                                   const diagnostic_logical_location *loc, \
+                                                                   FILE *out)
+
+   Write a representation of ``file`` to ``out``, for debugging.
+   Both ``diag_mgr`` and ``out`` must be non-NULL.
+   ``file`` may be NULL.
+
+   TODO: example of output
+
+Associating diagnostics with locations
+**************************************
+
+.. function:: void diagnostic_set_logical_location (diagnostic *diag, \
+                                                    const diagnostic_logical_location *logical_loc)
+
+    Set the logical location of ``diag``.
+
+    ``diag`` must be non-NULL; ``logical_loc`` can be NULL.
diff --git a/gcc/doc/libdiagnostics/topics/message-formatting.rst b/gcc/doc/libdiagnostics/topics/message-formatting.rst
new file mode 100644 (file)
index 0000000..9d42f89
--- /dev/null
@@ -0,0 +1,224 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Message formatting
+==================
+
+Various libdiagnostics entrypoints take a format string and
+variadic arguments.
+
+The format strings take codes prefixed by ``%``, or ``%q`` to put
+the result in quotes.  For example::
+
+   "hello %s", "world"
+
+would print::
+
+   hello world
+
+whereas::
+
+   "hello %qs", "world"
+
+would print::
+
+   hello `world'
+
+where ```world'`` would be displayed in bold if colorization were enabled
+in the terminal.
+
+The following format specifiers are accepted:
+
+
+Numbers
+*******
+
+``d`` and ``i`` (``signed int``), ``u`` (``unsigned int``)
+   ``%d``, ``%i``, and ``%u`` print integers in base ten.  For example::
+
+     "the answer is %i", 42
+
+   would print::
+
+     the answer is 42
+
+``o`` (``unsigned int``)
+   Print the integer in base eight
+
+``x`` (``unsigned int``)
+   Print the integer in base sixteen
+
+The above can be prefixed with ``l`` and ``ll`` prefixes to take
+``long`` and ``long long`` values of the appropriate signedness.
+
+For example::
+
+   "address: %lx", (unsigned long)0x108b516
+
+would print::
+
+   address: 108b516
+
+Similarly, the prefix ``z`` can be used for ``size_t``::
+
+  "size: %zd", sizeof(struct foo)
+  size: 32
+
+and ``t`` for ptrdiff_t.
+  
+``f`` (``double``)
+   ``%f`` prints a floating-point value.  For example::
+
+     "value: %f", 1.0
+
+   might print::
+
+     value: 1.000000
+
+
+Strings
+*******
+
+``c`` (``char``)
+   ``%c`` prints a single character.
+
+``s`` (``const char *``)
+   ``%s`` prints a string.
+
+   Note that if the string refers to something that might
+   appear in the input file (such as the name of a function), it's better
+   to quote the value; for example::
+
+     "unrecognized identifier: %qs", "foo"
+
+   might print::
+
+     unrecognized identifier: `foo'
+
+``m`` (no argument)
+   Prints ``strerror(errno)``, for example::
+
+     "can't open %qs: %m"
+
+   might print::
+
+     can't open `foo.txt': No such file or directory
+
+``%`` (no argument)
+   ``%%`` prints a `%` character, for example::
+
+     "8%% of 75 is 75%% of 8, and is thus 6"
+
+   prints::
+
+     8% of 75 is 75% of 8, and is thus 6
+
+``'`` (no argument)
+  ``%'`` prints an apostrophe.  This should only be used in untranslated messages;
+  translations should use appropriate punctuation directly.
+
+
+Other format specifiers
+***********************
+
+``p`` (pointer)
+   ``%p`` prints a pointer, although the precise format is
+   implementation-defined.
+
+``r`` (``const char *``)
+   ``%r`` starts colorization on suitable text sinks, where the argument
+   specifies the name of the kind of entity to be colored, such as ``error``.
+
+``R`` (no argument)
+   ``%R`` stops colorization
+
+``<`` and ``>`` (no arguments)
+   ``%<`` adds an opening quote and ``%>`` a closing quote, such as::
+
+     "missing element %<%s:%s%>", ns, name
+
+   which might be printed as::
+
+     missing element `xhtml:head'
+
+   If the thing to be quoted can be handled with another format specifier,
+   then it's simpler to use ``q`` with it.  For example, it's much
+   simpler to print a ``const char *`` in quotes via::
+
+      "%qs", str
+
+   rather than the error-prone::
+     
+      "%<%s%>", str
+
+``{`` (``const char *``)
+   ``%{`` starts a link; the argument is the URL.  This will be displayed
+   in a suitably-capable terminal if a text sink is directly connected to
+   a tty, and will be captured in SARIF output.
+
+``}`` (no argument)
+   ``%}`` stops a link started with ``%{``.
+
+   For example::
+
+      "for more information see %{the documentation%}", "https://example.com"
+
+   would be printed as::
+
+      for more information see the documentation
+
+   with the URL emitted in suitable output sinks.
+
+``@`` (``diagnostic_event_id *``)
+   ``%@`` prints a reference to an event in a
+   :type:`diagnostic_execution_path`, where the :type:`diagnostic_event_id`
+   is passed by pointer.
+
+   For example, if ``event_id`` refers to the first event in a path, then::
+
+      "double-%qs of %qs; first %qs was at %@",
+      function, ptr, function, &event_id
+
+   might print::
+
+     double-`free' of `p'; first `free` was at (1)
+
+.. :
+
+   TODO:
+
+   %.*s: a substring the length of which is specified by an argument
+        integer.
+   %Ns: likewise, but length specified as constant in the format string.
+   %Z: Requires two arguments - array of int, and len. Prints elements
+   of the array.
+
+   %e: Consumes a pp_element * argument.
+
+   Arguments can be used sequentially, or through %N$ resp. *N$
+   notation Nth argument after the format string.  If %N$ / *N$
+   notation is used, it must be used for all arguments, except %m, %%,
+   %<, %>, %} and %', which may not have a number, as they do not consume
+   an argument.  When %M$.*N$s is used, M must be N + 1.  (This may
+   also be written %M$.*s, provided N is not otherwise used.)  The
+   format string must have conversion specifiers with argument numbers
+   1 up to highest argument; each argument may only be used once.
+   A format string can have at most 30 arguments.  */
+
+
diff --git a/gcc/doc/libdiagnostics/topics/metadata.rst b/gcc/doc/libdiagnostics/topics/metadata.rst
new file mode 100644 (file)
index 0000000..c62792a
--- /dev/null
@@ -0,0 +1,149 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Adding metadata
+===============
+
+Tool metadata
+*************
+
+It's possible to set up various metadata on the :type:`diagnostic_manager`
+as a whole, describing the program creating the diagnostics.
+
+.. note::
+
+   It's not required to set up any of this up on a
+   :type:`diagnostic_manager`.  However, if you are doing
+   :doc:`SARIF output <sarif>`, then you need to at least call
+   :func:`diagnostic_manager_set_tool_name` or the generated ``.sarif``
+   file will not validate against the schema.
+
+.. function:: void diagnostic_manager_set_tool_name (diagnostic_manager *diag_mgr, \
+                                                     const char *value)
+
+   Set a string for the name of the tool emitting the diagnostics.
+
+   Both parameters must be non-NULL.
+
+   If set, this string will be used
+
+   * by :doc:`text output sinks <text-output>` as a prefix for output
+     when no physical location is available, replacing ``progname``
+     in the following:
+
+     .. code-block:: console
+
+       $ ./tut01-hello-world
+       progname: error: I'm sorry Dave, I'm afraid I can't do that
+
+   * by :doc:`SARIF output sinks <sarif>` as the value for the
+     ``name`` property of the ``driver``
+     (`SARIF v2.1.0 Â§3.19.8 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790791>`_).
+
+.. function:: void diagnostic_manager_set_full_name (diagnostic_manager *diag_mgr, \
+                                                      const char *value)
+
+   Set a string giving the name of the tool along with the its version and
+   other useful information::
+
+     diagnostic_manager_set_full_name (diag_mgr, "FooChecker 0.1 (en_US)");
+
+   If set, this string will be used by :doc:`SARIF output sinks <sarif>` as
+   the value for the ``fullName`` property of the ``driver``
+   (`SARIF v2.1.0 Â§3.19.9 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790792>`_).
+
+   Both parameters must be non-NULL.
+
+.. function:: void diagnostic_manager_set_version_string (diagnostic_manager *diag_mgr, \
+                                                          const char *value)
+
+   Set a string suitable for use as the value of the SARIF ``version`` property
+   of the ``driver``.
+   (`SARIF v2.1.0 Â§3.19.13 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790796>`_)::
+
+     diagnostic_manager_set_version_string (diag_mgr, "0.1");
+
+   Both parameters must be non-NULL.
+
+.. function:: void diagnostic_manager_set_version_url (diagnostic_manager *diag_mgr, \
+                                                       const char *value)
+
+   Set a string suitable for use as the value of the SARIF ``informationUri``
+   property of the ``driver``.
+   (`SARIF v2.1.0 Â§3.19.17 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790800>`_)::
+
+     diagnostic_manager_set_version_url (diag_mgr,
+                                         "https://www.example.com/foo-checker/releases/0.1/");
+
+  Both parameters must be non-NULL.
+
+Adding metadata to a diagnostic
+*******************************
+
+.. function:: void diagnostic_set_cwe (diagnostic *diag, \
+                                       unsigned cwe_id)
+
+   Associate ``diag`` with the given ID within
+   the `Common Weakness Enumeration <https://cwe.mitre.org/>`_::
+
+     /* CWE-242: Use of Inherently Dangerous Function.  */
+     diagnostic_set_cwe (d, 242);
+
+   ``diag`` must be non-NULL.
+
+   The CWE value will be printed by text sinks after the message::
+
+     test-metadata.c:21:3: warning: never use 'gets' [CWE-242]
+
+   and in a sufficiently-capable terminal will be a link to
+   documentation about the CWE.
+
+.. function:: void diagnostic_add_rule (diagnostic *diag, \
+                                        const char *title, \
+                                        const char *url)
+
+   Associate this :type:`diagnostic` with a particular rule that has been
+   violated (such as in a coding standard, or within a specification).
+
+   A diagnostic can be associated with zero or more rules.
+
+   ``diag`` must be non-NULL.  The rule must have at least one of a
+   title and a URL, but these can be NULL.
+
+   For example, given::
+
+     diagnostic_add_rule (d,
+                          "MSC24-C",
+                          "https://wiki.sei.cmu.edu/confluence/display/c/MSC24-C.+Do+not+use+deprecated+or+obsolescent+functions");
+
+   the rule name will be printed by text sinks after the message::
+
+     test-metadata.c:21:3: warning: never use 'gets' [MSC24-C]
+      21 |   gets (buf);
+         |   ^~~~~~~~~~
+
+   and if so, the URL will be available in a sufficiently capable
+   terminal.
+
+   This can be used in conjunction with :func:`diagnostic_set_cwe`,
+   giving output like this::
+
+     test-metadata.c:21:3: warning: never use 'gets' [CWE-242] [MSC24-C]
+      21 |   gets (buf);
+         |   ^~~~~~~~~~
diff --git a/gcc/doc/libdiagnostics/topics/physical-locations.rst b/gcc/doc/libdiagnostics/topics/physical-locations.rst
new file mode 100644 (file)
index 0000000..bad2b8d
--- /dev/null
@@ -0,0 +1,281 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Physical locations
+==================
+
+A "physical" source location is a location expressed in terms of
+a specific file, and line(s) and column(s) (as opposed to a
+:doc:`"logical" location <logical-locations>`,
+which refers to semantic constructs in a programming language).
+
+Creating location information
+*****************************
+
+The :type:`diagnostic_manager` manages various objects relating to
+locations.
+
+.. type:: diagnostic_file
+
+   A :type:`diagnostic_file` is an opaque type describing a particular input file.
+
+.. function:: const diagnostic_file * diagnostic_manager_new_file (diagnostic_manager *diag_mgr, \
+                                                                   const char *name, \
+                                                                   const char *sarif_source_language)
+
+   Create a new :type:`diagnostic_file` for file ``name``. Repeated calls
+   with strings that match ``name`` will return the same object.
+
+   Both ``diag_mgr`` and ``name`` must be non-NULL.
+
+   If ``sarif_source_language`` is non-NULL, it specifies a
+   ``sourceLanguage`` value for the file for use when writing
+   :doc:`SARIF <sarif>`
+   (`SARIF v2.1.0 Â§3.24.10 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790871>`_).
+   See
+   `SARIF v2.1.0 Appendix J <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141791197>`_
+   for suggested values for various programmming languages.
+
+   For example, this creates a :type:`diagnostic_file` for ``foo.c``
+   and identifies it as C source code::
+
+     foo_c = diagnostic_manager_new_file (diag_mgr,
+                                          "foo.c",
+                                          "c" /* source_language */);
+
+.. function::  void diagnostic_manager_debug_dump_file (diagnostic_manager *diag_mgr, \
+                                                        const diagnostic_file *file, \
+                                                        FILE *out)
+
+   Write a representation of ``file`` to ``out``, for debugging.
+   Both ``diag_mgr`` and ``out`` must be non-NULL.
+   `file`` may be NULL.
+
+   For example::
+
+     diagnostic_manager_debug_dump_file (diag_mgr, foo_c, stderr);
+
+   might lead to this output::
+
+      file(name="foo.c", sarif_source_language="c")
+
+.. type:: diagnostic_line_num_t
+
+A :type:`diagnostic_line_num_t` is used for representing line numbers
+within text files.  libdiagnostics treats the first line of a text file
+as line 1.
+
+.. type:: diagnostic_column_num_t
+
+A :type:`diagnostic_column_num_t` is used for representing column numbers
+within text files.  libdiagnostics treats the first column of a text line
+as column 1, **not** column 0.
+
+.. note::
+
+   Both libdiagnostics and Emacs number source *lines* starting at 1, but
+   they have differing conventions for *columns*.
+
+   libdiagnostics uses a 1-based convention for source columns,
+   whereas Emacs's ``M-x column-number-mode`` uses a 0-based convention.
+
+   For example, an error in the initial, left-hand
+   column of source line 3 is reported by libdiagnostics as::
+
+     some-file.c:3:1: error: ...etc...
+
+   On navigating to the location of that error in Emacs
+   (e.g. via ``next-error``),
+   the locus is reported in the Mode Line
+   (assuming ``M-x column-number-mode``) as::
+
+     some-file.c   10%   (3, 0)
+
+   i.e. ``3:1:`` in libdiagnostics corresponds to ``(3, 0)`` in Emacs.
+
+.. type:: diagnostic_physical_location
+
+A :type:`diagnostic_physical_location` is an opaque type representing a
+key into a database of source locations within a :type:`diagnostic_manager`.
+
+:type:`diagnostic_physical_location` instances are created by various API
+calls into the :type:`diagnostic_manager` expressing source code points
+and ranges.
+
+They persist until the :type:`diagnostic_manager` is released, which
+cleans them up.
+
+A ``NULL`` value means "unknown", and can be returned by the
+:type:`diagnostic_manager` as a fallback when a problem occurs
+(e.g. too many locations).
+
+A :type:`diagnostic_physical_location` can be a single point within the
+source code, such as here (at the the '"' at the start of the string literal)::
+
+  int i = "foo";
+          ^
+
+or be a range with a start and finish, and a "caret" location::
+
+   a = (foo && bar)
+       ~~~~~^~~~~~~
+
+where the caret here is at the first "&", and the start and finish
+are at the parentheses.
+
+.. function::  const diagnostic_physical_location *diagnostic_manager_new_location_from_file_and_line (diagnostic_manager *diag_mgr, \
+                                                                                                       const diagnostic_file *file,  \
+                                                                                                       diagnostic_line_num_t line_num)
+
+   Attempt to create a :type:`diagnostic_physical_location` representing
+   ``FILENAME:LINE_NUM``, with no column information (thus representing
+   the whole of the given line.
+
+   Both ``diag_mgr`` and ``file`` must be non-NULL.
+
+.. function::  const diagnostic_physical_location * diagnostic_manager_new_location_from_file_line_column (diagnostic_manager *diag_mgr, \
+                                                                                                           const diagnostic_file *file, \
+                                                                                                           diagnostic_line_num_t line_num, \
+                                                                                                           diagnostic_column_num_t column_num)
+
+   Attempt to create a :type:`diagnostic_physical_location` for
+   ``FILENAME:LINE_NUM:COLUMN_NUM`` representing a particular point
+   in the source file.
+
+   Both ``diag_mgr`` and ``file`` must be non-NULL.
+
+.. function::  const diagnostic_physical_location *diagnostic_manager_new_location_from_range (diagnostic_manager *diag_mgr,\
+                                                                                               const diagnostic_physical_location *loc_caret,\
+                                                                                               const diagnostic_physical_location *loc_start,\
+                                                                                               const diagnostic_physical_location *loc_end)
+
+   Attempt to create a diagnostic_physical_location representing a
+   range within a source file, with a highlighted "caret" location.
+
+   All must be within the same file, but they can be on different lines.
+
+   For example, consider the location of the binary expression below::
+
+     ...|__________1111111112222222
+     ...|12345678901234567890123456
+     ...|
+     521|int sum (int foo, int bar)
+     522|{
+     523|   return foo + bar;
+     ...|          ~~~~^~~~~
+     524|}
+
+   The location's caret is at the "+", line 523 column 15, but starts
+   earlier, at the "f" of "foo" at column 11.  The finish is at the "r"
+   of "bar" at column 19.
+
+   ``diag_mgr`` must be non-NULL.
+
+.. function::  void diagnostic_manager_debug_dump_location (const diagnostic_manager *diag_mgr,\
+                                                            const diagnostic_physical_location *loc, \
+                                                            FILE *out)
+
+   Write a representation of ``loc`` to ``out``, for debugging.
+
+   Both ``diag_mgr`` and ``out`` must be non-NULL.
+   `loc`` may be NULL.
+
+   TODO: example of output
+
+Associating diagnostics with locations
+**************************************
+
+A :type:`diagnostic` has an optional primary physical location
+and zero or more secondary physical locations.  For example::
+
+   a = (foo && bar)
+       ~~~~~^~~~~~~
+
+This diagnostic has a single :type:`diagnostic_physical_location`,
+with the caret at the first "&", and the start/finish at the parentheses.
+
+Contrast with::
+
+   a = (foo && bar)
+        ~~~ ^~ ~~~
+
+This diagnostic has three locations
+
+* The primary location (at "&&") has its caret and start location at
+  the first "&" and end at the second "&.
+
+* The secondary location for "foo" has its start and finish at the "f"
+  and "o" of "foo"; the caret is not displayed, but is perhaps at
+  the "f" of "foo".
+
+* Similarly, the other secondary location (for "bar") has its start and
+  finish at the "b" and "r" of "bar"; the caret is not displayed, but
+  is perhaps at the"b" of "bar".
+
+.. function::  void diagnostic_set_location (diagnostic *diag, \
+                                             const diagnostic_physical_location * loc)
+
+   Set the primary location of ``diag``.
+
+   ``diag`` must be non-NULL; ``loc`` can be NULL.
+
+.. function:: void diagnostic_set_location_with_label (diagnostic *diag, \
+                                                       const diagnostic_physical_location *loc, \
+                                                       const char *fmt, ...)
+
+   Set the primary location of ``diag``, with a label.  The label is
+   formatted as per the rules FIXME
+
+   ``diag`` and ``fmt`` must be non-NULL; ``loc`` can be NULL.
+
+   See :doc:`message-formatting` for details of how to use ``fmt``.
+
+   TODO: example of use
+
+.. function:: void diagnostic_add_location (diagnostic *diag, \
+                                            const diagnostic_physical_location * loc)
+
+   Add a secondary location to ``diag``.
+
+   ``diag`` must be non-NULL; ``loc`` can be NULL.
+
+
+.. function:: void diagnostic_add_location_with_label (diagnostic *diag, \
+                                                       const diagnostic_physical_location *loc, \
+                                                       const char *text)
+
+   Add a secondary location to ``diag``, with a label.  The label is
+   formatted as per the rules FIXME
+
+   ``diag`` and ``fmt`` must be non-NULL; ``loc`` can be NULL.
+
+   For example,
+
+      .. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-labelled-ranges.c
+        :language: c
+        :start-after: /* begin quoted source */
+        :end-before:  /* end quoted source */
+
+   might give this text output::
+
+      test-labelled-ranges.c:9:6: error: mismatching types: 'int' and 'const char *'
+         19 |   42 + "foo"
+            |   ~~ ^ ~~~~~
+            |   |    |
+            |   int  const char *
diff --git a/gcc/doc/libdiagnostics/topics/retrofitting.rst b/gcc/doc/libdiagnostics/topics/retrofitting.rst
new file mode 100644 (file)
index 0000000..d034057
--- /dev/null
@@ -0,0 +1,23 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Adding libdiagnostics to an existing project
+============================================
+
+TODO
diff --git a/gcc/doc/libdiagnostics/topics/sarif.rst b/gcc/doc/libdiagnostics/topics/sarif.rst
new file mode 100644 (file)
index 0000000..3fd75ed
--- /dev/null
@@ -0,0 +1,51 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+SARIF support
+=============
+
+`SARIF <https://www.sarif.info/>`_ is a machine-readable format, originally
+designed for the output of static analysis tools, but which can be used
+for diagnostics in general.
+
+.. function:: void diagnostic_manager_add_sarif_sink (diagnostic_manager *diag_mgr, \
+                                                      FILE *dst_stream, \
+                                                      const diagnostic_file *main_input_file, \
+                                                      enum diagnostic_sarif_version version)
+
+   Add a new output sink to ``diag_mgr``, which writes SARIF of the given
+   version to ``dst_stream``.
+
+   The output is not written until ``diag_mgr`` is released.
+
+   ``dst_stream`` is borrowed, and must outlive ``diag_mgr``.
+
+   For the result to be a valid SARIF file according to the schema,
+   ``diag_mgr`` must have had :func:`diagnostic_manager_set_tool_name`
+   called on it.
+
+   ``diag_mgr``, ``dst_stream``, and ``main_input_file`` must all be non-NULL.
+
+  .. enum:: diagnostic_sarif_version
+
+     An enum for choosing the SARIF version for a SARIF output sink.
+
+     .. macro:: DIAGNOSTIC_SARIF_VERSION_2_1_0
+
+     .. macro:: DIAGNOSTIC_SARIF_VERSION_2_2_PRERELEASE
diff --git a/gcc/doc/libdiagnostics/topics/text-output.rst b/gcc/doc/libdiagnostics/topics/text-output.rst
new file mode 100644 (file)
index 0000000..32b2a54
--- /dev/null
@@ -0,0 +1,87 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Text output
+===========
+
+.. type:: diagnostic_text_sink
+
+.. function:: diagnostic_text_sink * diagnostic_manager_add_text_sink (diagnostic_manager *diag_mgr,\
+                                                                       FILE *dst_stream, \
+                                                                       enum diagnostic_colorize colorize)
+
+   Add a new output sink to ``diag_mgr``, which writes GCC-style diagnostics
+   to ``dst_stream``.
+   Return a borrowed pointer to the sink, which is cleaned up when ``diag_mgr``
+   is released.
+
+   ``diag_mgr`` must be non-NULL.
+
+   ``dst_stream`` must be non-NULL.  It is borrowed and must outlive ``DIAG_MGR``.
+
+   The output for each diagnostic is written and flushed as each
+   :type:`diagnostic` is finished.
+
+   .. enum:: diagnostic_colorize
+
+      An enum for determining if we should colorize a text output sink.
+
+      .. macro:: DIAGNOSTIC_COLORIZE_IF_TTY
+
+        Diagnostics should be colorized if the destination stream is
+        directly connected to a tty.
+
+      .. macro:: DIAGNOSTIC_COLORIZE_NO
+
+        Diagnostics should not be colorized.
+
+      .. macro:: DIAGNOSTIC_COLORIZE_YES
+
+        Diagnostics should be colorized.
+
+.. function:: void diagnostic_text_sink_set_source_printing_enabled (diagnostic_text_sink *text_sink, \
+                                                                     int value)
+
+   Enable or disable printing of source text in the text sink.
+
+   ``text_sink`` must be non-NULL.
+
+   Default: enabled.
+
+.. function:: void diagnostic_text_sink_set_colorize (diagnostic_text_sink *text_sink, \
+                                                      enum diagnostic_colorize colorize)
+
+   Update colorization of text sink.
+
+   ``text_sink`` must be non-NULL.
+
+.. function:: void diagnostic_text_sink_set_labelled_source_colorization_enabled (diagnostic_text_sink *text_sink, \
+                                                                                  int value)
+
+   ``text_sink`` must be non-NULL.
+
+   Enable or disable colorization of the characters of source text
+   that are underlined.
+
+   This should be true for clients that generate range information
+   (so that the ranges of code are colorized), and false for clients that
+   merely specify points within the source code (to avoid e.g. colorizing
+   just the first character in a token, which would look strange).
+
+   Default: enabled.
diff --git a/gcc/doc/libdiagnostics/topics/ux.rst b/gcc/doc/libdiagnostics/topics/ux.rst
new file mode 100644 (file)
index 0000000..fc96e17
--- /dev/null
@@ -0,0 +1,26 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+User Experience
+===============
+
+Refer to
+`GCC's user experience guidelines <https://gcc.gnu.org/onlinedocs/gccint/User-Experience-Guidelines.html>`_
+for notes on
+`what makes a good diagnostic <https://gcc.gnu.org/onlinedocs/gccint/Guidelines-for-Diagnostics.html>`_.
diff --git a/gcc/doc/libdiagnostics/tutorial/01-hello-world.rst b/gcc/doc/libdiagnostics/tutorial/01-hello-world.rst
new file mode 100644 (file)
index 0000000..4635687
--- /dev/null
@@ -0,0 +1,173 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 1: "Hello world"
+==============================
+
+Before we look at the details of the API, let's look at building and
+running programs that use the library.
+
+Here's a toy program that uses libdiagnostics to emit an error message
+to stderr.
+
+  .. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-example-1.c
+     :language: c
+     :start-after: /* begin quoted source */
+     :end-before:  /* end quoted source */
+
+Copy the above to `tut01-hello-world.c`.
+
+Assuming you have libdiagnostics installed, build the test program
+using:
+
+.. code-block:: console
+
+  $ gcc \
+      tut01-hello-world.c \
+      -o tut01-hello-world \
+      -ldiagnostics
+
+You should then be able to run the built program:
+
+.. code-block:: console
+
+  $ ./tut01-hello-world
+  progname: error: I'm sorry Dave, I'm afraid I can't do that
+
+If stderr is connected to a terminal, you should get colorized output
+(using `SGR control codes <https://en.wikipedia.org/wiki/ANSI_escape_code>`_).
+
+.. image:: example-1.png
+
+Otherwise, the output will be plain text.
+
+Obviously a trivial example like the above could be done using ``fprintf``
+on stderr, and it's fairly easy to colorize text at the terminal.
+
+In :doc:`the next part of the tutorial <02-physical-locations>` we'll add
+file/location information to our error messages, and libdiagnostics will
+quote the pertinent parts of the file, underlining them, which is less trivial
+to reimplement.  libdiagnostics gives us many other such abilities, such as
+fix-it hints and execution paths, which we'll cover in the following
+tutorials.  Also, once a program's diagnostics are using libdiagnostics,
+it is trivial to add support for outputting them in
+machine-readable form as :doc:`SARIF <../topics/sarif>`.
+
+
+Structure
+*********
+
+The above example shows the typical structure of a program using
+libdiagnostics:
+
+* **initialization**: create a :type:`diagnostic_manager` instance,
+  and create an output sink for it, and other one-time initialization
+
+* **emission**: create various :type:`diagnostic` instances, populating
+  them with data, and calling "finish" once they're ready to be emitted.
+  :doc:`Text sinks <../topics/text-output>` emit their diagnostics as soon
+  as "finish" is called on them.
+
+* **cleanup**: call :func:`diagnostic_manager_release` on the
+  :type:`diagnostic_manager` to finish and free up resources.
+  :doc:`SARIF sinks <../topics/sarif>` write their output when
+  :func:`diagnostic_manager_release` is called on the manager.
+
+For non-trivial examples we'll also want to create location information,
+which could happen during initialization, or during a parsing phase of
+the program using libdiagnostics.  See :doc:`02-physical-locations` for
+more information.
+
+
+Formatted messages
+******************
+
+The above example uses :func:`diagnostic_finish`, which takes a format
+string and arguments.  libdiagnostics has its own style of format
+string arguments used for :func:`diagnostic_finish` and some other
+entrypoints.
+
+.. note:: The format syntax is *not* the same as ``printf``; see
+   :doc:`supported formatting options <../topics/message-formatting>`.
+
+You can use the ``q`` modifier on arguments
+to quote them, so, for example ``%qs`` is a quoted string, consuming a
+``const char *`` argument::
+
+   diagnostic_finish (d, "can't find %qs", "foo");
+
+This gives output like this:
+
+.. code-block:: console
+
+  progname: error: can't find â€˜foo’
+
+where the quoted string will appear in bold in a suitably-capable
+terminal, and the quotes will be internationalized, so that e.g. with
+``LANG=fr_FR.UTF8`` we might get:
+
+.. code-block:: console
+
+  progname: erreur: can't find Â«Â free »
+
+Note that:
+
+* the string ``error`` has been localized by libdiagnostics to
+  ``erreur``,
+
+* locale-specific quoting has been used (``«`` and ``»`` rather than
+  ``‘`` and ``’``),
+
+* ``foo`` hasn't been localized - you would typically use quoted strings
+  for referring to identifiers in the input language (such as function names
+  in code, property names in JSON, etc),
+
+* the message itself hasn't been localized: you are responsible for
+  passing a translated format string to :func:`diagnostic_finish` if you
+  want to internationalize the output.
+
+There are many :doc:`supported formatting options <../topics/message-formatting>`.
+
+
+Naming the program
+******************
+
+In the above output the message was preceded with ``progname``.  This
+appears for diagnostics that don't have any location information associated
+with them.  We'll look at setting up location information in the
+:doc:`next tutorial <02-physical-locations>`, but we can override this
+default name via :func:`diagnostic_manager_set_tool_name`::
+
+   diagnostic_manager_set_tool_name (diag_mgr, "my-awesome-checker");
+
+leading to output like this::
+
+   my-awesome-checker: error: can't find â€˜foo’
+
+There are various other functions for
+:doc:`supplying metadata to libdiagnostics <../../topics/metadata>`.
+
+
+Moving beyond trivial examples
+******************************
+
+Obviously it's not very useful if we can't refer to specific files and
+specific locations in those files in our diagnostics, so read
+:doc:`part 2 of the tutorial <02-physical-locations>` for information on
+how to do this.
diff --git a/gcc/doc/libdiagnostics/tutorial/02-physical-locations.rst b/gcc/doc/libdiagnostics/tutorial/02-physical-locations.rst
new file mode 100644 (file)
index 0000000..2e429de
--- /dev/null
@@ -0,0 +1,260 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 2: physical locations
+===================================
+
+libdiagnostics has two kinds of location:
+
+* *physical locations* expressed in terms of a specific file, and line(s)
+  and perhaps column(s), such as ``some-file.c:3:1``, or a range of
+  columns, such as in::
+
+    test-typo.c:19:13: error: unknown field 'colour'
+       19 |   return p->colour;
+          |             ^~~~~~
+
+  or even a range spanning multiple lines of a file.
+
+  All of these are instances of :type:`diagnostic_physical_location`.
+
+* *logical locations* which refers to semantic constructs
+  in the input, such as ``within function 'foo'``, or within
+  namespace ``foo``'s class ``bar``'s member function ``get_color``.
+
+  These are instances of :type:`diagnostic_logical_location`,
+
+A :type:`diagnostic` can have zero or more physical locations,
+and optionally have a logical location.
+
+Let's extend the previous example to add a physical location to the
+:type:`diagnostic`; we'll cover logical locations in the
+:doc:`next section <03-logical-locations>`.
+
+
+Source files
+************
+
+Given these declarations::
+
+  static diagnostic_manager *diag_mgr;
+  static const diagnostic_file *main_file;
+
+we can create a :type:`diagnostic_file` describing an input file ``foo.c``
+via :func:`diagnostic_manager_new_file`::
+
+     foo_c = diagnostic_manager_new_file (diag_mgr,
+                                          "foo.c",
+                                          "c" /* source_language */);
+
+You can use :func:`diagnostic_manager_debug_dump_file` to print a
+representation of a :type:`diagnostic_file` for debugging.
+For example::
+
+   diagnostic_manager_debug_dump_file (diag_mgr, foo_c, stderr);
+
+might lead to this output on ``stderr``::
+
+   file(name="foo.c", sarif_source_language="c")
+
+Once we have a :type:`diagnostic_file` we can use it to create instances
+of :type:`diagnostic_physical_location` within the :type:`diagnostic_manager`.
+These are owned by the :type:`diagnostic_manager` and cleaned up
+automatically when :func:`diagnostic_manager_release` is called.
+
+Instances of :type:`diagnostic_physical_location` can refer to
+
+* a source line as a whole, created via
+  :func:`diagnostic_manager_new_location_from_file_and_line`.
+
+* a particular point within a source file (line/column), created via
+  :func:`diagnostic_manager_new_location_from_file_line_column`.
+
+* a range of text within of source file, created via
+  :func:`diagnostic_manager_new_location_from_range`.
+
+
+Diagnostics affecting a whole source line
+*****************************************
+
+If we want a diagnostic to refer to an entire source line,
+we can use :func:`diagnostic_manager_new_location_from_file_and_line`.
+
+For example, given this example input where the tool can't find the header::
+
+   #include <foo.h>
+
+we could complain about it via libdiagnostics via:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-no-column.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+leading to output like this::
+
+   foo.c:17: error: can't find 'foo.h'"
+      17 | #include <foo.h>
+
+where libdiagnostics will attempt to load the source file and
+quote the pertinent line.
+
+If libdiagnostics cannot open the file, it will merely print::
+
+   foo.c:17: error: can't find 'foo.h'
+
+You can use :func:`diagnostic_manager_debug_dump_location` to dump a
+:type:`diagnostic_physical_location`.  For the above example::
+
+   diagnostic_manager_debug_dump_location (diag_mgr, loc, stderr);
+
+might print::
+
+   foo.c:17
+
+to stderr.
+
+
+Columns and ranges
+******************
+
+If we want to generate output like this::
+
+   foo.c:17:11: error: can't find 'foo'"
+      17 | #include <foo.h>
+         |           ^~~~~
+
+where the diagnostic is marked as relating to the above range of
+characters in line 17, we need to express the range of characters
+within the line of interest.
+
+We can do this by creating a :type:`diagnostic_physical_location` for the
+start of the range, another one for the end of the range, and then using
+these two to create a :type:`diagnostic_physical_location` for the
+range as a whole:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-error.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+On compiling and running the program, we should get this output::
+
+   foo.c:17:11: error: can't find 'foo.h'
+      17 | #include <foo.h>
+         |           ^~~~~
+
+where libdiagnostics will attempt to load the source file and
+underling the pertinent part of the given line.
+
+If libdiagnostics cannot open the file, it will merely print::
+
+   foo.c:17:8: error: can't find 'foo'
+
+A range can span multiple lines within the same file.
+
+As before, you can use :func:`diagnostic_manager_debug_dump_location` to
+dump the locations.  For the above example::
+
+   diagnostic_manager_debug_dump_location (diag_mgr, loc_start, stderr);
+
+and::
+
+   diagnostic_manager_debug_dump_location (diag_mgr, loc_range, stderr);
+
+might print::
+
+   foo.c:17:11
+
+to stderr, whereas::
+
+   diagnostic_manager_debug_dump_location (diag_mgr, loc_end, stderr);
+
+might print::
+
+   foo.c:17:15
+
+
+Multiple locations
+******************
+
+As well as the primary physical location seen above, a :type:`diagnostic`
+can have additional physical locations.  You can add these secondary
+locations via :func:`diagnostic_add_location`.
+
+For example, for this valid but suspicious-looking C code::
+
+   const char *strs[3] = {"foo",
+                          "bar"
+                          "baz"};
+
+the following :type:`diagnostic` has its primary location where the missing
+comma should be, and secondary locations for each of the string literals
+``"foo"``, ``"bar"``, and ``"baz"``, added via :func:`diagnostic_add_location`:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-multiple-lines.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+where the text output might be::
+
+   test-multiple-lines.c:23:29: warning: missing comma
+      22 | const char *strs[3] = {"foo",
+         |                        ~~~~~
+      23 |                        "bar"
+         |                        ~~~~~^
+      24 |                        "baz"};
+         |                        ~~~~~
+
+
+Labelling locations
+*******************
+
+You can give the locations labels using
+:func:`diagnostic_set_location_with_label` and
+:func:`diagnostic_add_location_with_label`.
+
+Consider emitting a "type mismatch" diagnostic for::
+
+  42 + "foo"
+
+where the primary location is on the ``+``, with secondary locations on the``42``
+and the ``"foo"``:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-labelled-ranges.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+giving this text output::
+
+   test-labelled-ranges.c:9:6: error: mismatching types: 'int' and 'const char *'
+      19 |   42 + "foo"
+         |   ~~ ^ ~~~~~
+         |   |    |
+         |   int  const char *
+
+
+More on locations
+*****************
+
+For more details on the above, see :doc:`../topics/physical-locations`.
+Otherwise the :doc:`next part of the tutorial <03-logical-locations>`
+covers logical locations.
diff --git a/gcc/doc/libdiagnostics/tutorial/03-logical-locations.rst b/gcc/doc/libdiagnostics/tutorial/03-logical-locations.rst
new file mode 100644 (file)
index 0000000..d36ac09
--- /dev/null
@@ -0,0 +1,60 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 3: logical locations
+==================================
+
+Let's extend the previous example to add a
+:doc:`logical location <../topics/logical-locations>` to the
+:type:`diagnostic`.
+
+First we create a :type:`diagnostic_logical_location` representing a
+particular function::
+
+  const diagnostic_logical_location *logical_loc
+    = diagnostic_manager_new_logical_location (diag_mgr,
+                                              DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+                                              NULL, /* parent */
+                                              "foo",
+                                              NULL,
+                                              NULL);
+
+In this simple example we specify that it is a function, and just give
+it a name (``foo``).  For more complicated cases we can set up tree-like
+hierarchies of logical locations, set qualified names, "mangled" names,
+and so on; see :func:`diagnostic_manager_new_logical_location` for details.
+    
+Once we have :type:`diagnostic_logical_location` we can associate it with
+a :type:`diagnostic` with :func:`diagnostic_set_logical_location`::
+      
+  diagnostic_set_logical_location (d, logical_loc);
+
+The logical location will be printed by text output sinks like this::
+
+  In function 'foo':
+
+and will be captured in :doc:`SARIF <../topics/sarif>` output.
+
+
+Find out more
+*************
+
+For more details on the above, see :doc:`../topics/logical-locations`.
+Otherwise the :doc:`next part of the tutorial <04-notes>` covers adding
+supplementary "notes" to a :type:`diagnostic`.
diff --git a/gcc/doc/libdiagnostics/tutorial/04-notes.rst b/gcc/doc/libdiagnostics/tutorial/04-notes.rst
new file mode 100644 (file)
index 0000000..117eb6f
--- /dev/null
@@ -0,0 +1,66 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 4: adding notes
+=============================
+
+Let's further extend the previous example to add a "note" to it.
+
+We want to generate output like this::
+
+   test-with-note.c:17:11: error: can't find 'foo'
+   17 | #include <foo.h>
+      |           ^~~~~
+   test-with-note.c:17:11: note: have you looked behind the couch?
+
+The "error" and "note" are both instances of :type:`diagnostic`.
+We want to let libdiagnostics know that they are grouped together.
+The way to do this is to use :func:`diagnostic_manager_begin_group`
+and :func:`diagnostic_manager_end_group` around the "finish" calls
+to the diagnostics.
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-error-with-note.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+On compiling and running the program, we should get the desired output::
+
+   test-with-note.c:17:11: error: can't find 'foo'
+   17 | #include <foo.h>
+      |           ^~~~~
+   test-with-note.c:17:11: note: have you looked behind the couch?
+
+The grouping doesn't affect text output sinks, but a
+:doc:`SARIF sink <../topics/sarif>` will group the note within the error
+(via the ``relatedLocations`` property of ``result`` objects; see SARIF v2.1.0
+`§3.27.22 <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790910>`_).
+
+In the above, the note had the same physical location as the error
+(``loc_range``).  This can be useful for splitting up a message into two
+parts to make localization easier, but they could have different locations, such
+as in::
+
+  test.xml:10:2: error: 'foo' is not valid here
+  test.xml:5:1: note: within element 'bar'
+
+where each :type:`diagnostic` had its own :type:`diagnostic_physical_location`.
+
+In :doc:`the next tutorial <05-warnings>` we'll look at issuing warnings,
+rather than errors.
diff --git a/gcc/doc/libdiagnostics/tutorial/05-warnings.rst b/gcc/doc/libdiagnostics/tutorial/05-warnings.rst
new file mode 100644 (file)
index 0000000..1512ae7
--- /dev/null
@@ -0,0 +1,44 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 5: warnings
+=========================
+
+So far we've only emitted errors, but other kinds of diagnostic are possible,
+such as warnings.
+
+We can select different kinds of diagnostic via :enum:`diagnostic_level`
+when calling :func:`diagnostic_begin`:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-warning.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+On compiling and running the program, we should get output similar to::
+
+   test-warning.c:17:11: warning: this is a warning
+   17 | #include <foo.h>
+      |           ^~~~~
+
+Various severities are possible, see  :enum:`diagnostic_level` for more
+information.
+
+In :doc:`the next section of the tutorial <06-fix-it-hints>` we'll look
+at adding fix-it hints to diagnostics.
diff --git a/gcc/doc/libdiagnostics/tutorial/06-fix-it-hints.rst b/gcc/doc/libdiagnostics/tutorial/06-fix-it-hints.rst
new file mode 100644 (file)
index 0000000..9486ab7
--- /dev/null
@@ -0,0 +1,61 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 6: fix-it hints
+=============================
+
+libdiagnostics supports adding "fix-it hints" to a :type:`diagnostic`:
+suggestions for the user on how to edit their code to fix a problem.  These
+can be expressed as insertions, replacements, and removals of text.
+
+For example, here we add a replacement fix-it hint to a diagnostic:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-fix-it-hint.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+On compiling and running the program, we should get output similar to::
+
+   test-fix-it-hint.c:19:13: error: unknown field 'colour'; did you mean 'color'
+      19 |   return p->colour;
+         |             ^~~~~~
+         |             color
+
+We can also add a call to :func:`diagnostic_manager_write_patch` to the
+program cleanup code::
+
+  diagnostic_manager_write_patch (diag_mgr, stderr);
+
+This will write a patch to the stream (here ``stderr``) giving the effect
+of all fix-it hints on all diagnostics emitted by the
+:type:`diagnostic_manager`, giving something like::
+
+   @@ -16,7 +16,7 @@
+    struct rgb
+    get_color (struct object *p)
+    {
+   -  return p->colour;
+   +  return p->color;
+    }
+    
+
+See the :doc:`guide to fix-it hints <../topics/fix-it-hints>`
+for more information, or go on to
+:doc:`the next section of the tutorial <07-execution-paths>`.
diff --git a/gcc/doc/libdiagnostics/tutorial/07-execution-paths.rst b/gcc/doc/libdiagnostics/tutorial/07-execution-paths.rst
new file mode 100644 (file)
index 0000000..0fbbed2
--- /dev/null
@@ -0,0 +1,141 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 7: execution paths
+================================
+
+A :type:`diagnostic` can optionally have a :type:`diagnostic_execution_path`
+describing a path of execution through code.
+
+For example, let's pretend we're writing a static analyis tool for finding
+bugs in `CPython extension code <https://docs.python.org/3/c-api/index.html>`_.
+
+Let's say we're analyzing this code:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-warning-with-path.c
+   :language: c
+   :start-after: begin fake source
+   :end-before:  end fake source
+
+This code attempts to take an Python integer parameter and then build a
+list of that length, containing random integers.  However, there are
+**numerous bugs** in this code: a type mismatch, mistakes in
+reference-counting, and an almost total lack of error-handling.
+
+For example, ``PyList_Append`` requires a non-NULL first parameter (``list``),
+but ``PyList_New`` can fail, returning NULL, and this isn't checked for,
+which would lead to a segfault if ``PyList_New`` fails.
+
+We can add a :type:`diagnostic_execution_path` to the :type:`diagnostic`
+via :func:`diagnostic_add_execution_path`, and then add events to it
+using :func:`diagnostic_execution_path_add_event`.
+
+For example, with::
+
+  diagnostic_event_id alloc_event_id
+    = diagnostic_execution_path_add_event (path,
+                                          loc_call_to_PyList_New,
+                                          logical_loc, 0,
+                                          "when %qs fails, returning NULL",
+                                          "PyList_New");
+
+we create an event that will be worded as::
+
+  (1) when `PyList_New' fails, returning NULL
+
+Note that :func:`diagnostic_execution_path_add_event` returns a
+:type:`diagnostic_event_id`.  We can use this to refer to this event
+in another event using the ``%@`` format code in its message, which
+takes the address of a :type:`diagnostic_event_id`::
+
+  diagnostic_execution_path_add_event (path,
+                                      loc_call_to_PyList_Append,
+                                      logical_loc, 0,
+                                      "when calling %qs, passing NULL from %@ as argument %i",
+                                      "PyList_Append", &alloc_event_id, 1);
+
+where the latter event will be worded as::
+
+  (2) when calling `PyList_Append', passing NULL from (1) as argument 1
+
+where the ``%@`` reference to the other event has been printed as ``(1)``.
+In SARIF output the text "(1)" will have a embedded link referring within the sarif
+log to the ``threadFlowLocation`` object for the other event, via JSON
+pointer (see `§3.10.3 "URIs that use the sarif scheme" <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790707>`_).
+
+Let's add an event between these describing control flow, creating three
+events in all:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-warning-with-path.c
+   :language: c
+   :start-after: begin path creation
+   :end-before:  end path creation
+
+Assuming we also gave it :type:`diagnostic_logical_location` with:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-warning-with-path.c
+   :language: c
+   :start-after: begin create logical locs
+   :end-before:  end create logical locs
+
+and finish the :type:`diagnostic` with :func:`diagnostic_finish` like this::
+
+  diagnostic_finish (d,
+                    "passing NULL as argument %i to %qs"
+                    " which requires a non-NULL parameter",
+                    1, "PyList_Append");
+
+then we should get output to text sinks similar to the following::
+
+   In function 'make_a_list_of_random_ints_badly':
+   test-warning-with-path.c:30:5: warning: passing NULL as argument 1 to 'PyList_Append' which requires a non-NULL parameter"
+      30 |     PyList_Append(list, item);
+         |     ^~~~~~~~~~~~~~~~~~~~~~~~~
+   make_a_list_of_random_ints_badly': events 1-3
+      26 |   list = PyList_New(0);
+         |          ^~~~~~~~~~~~~
+         |          |
+         |          (1) when 'PyList_New' fails, returning NULL
+      27 | 
+      28 |   for (i = 0; i < count; i++) {
+         |               ~~~~~~~~~
+         |               |
+         |               (2) when 'i < count'
+      29 |     item = PyLong_FromLong(random());
+      30 |     PyList_Append(list, item);
+         |     ~~~~~~~~~~~~~~~~~~~~~~~~~
+         |     |
+         |     (3) when calling 'PyList_Append', passing NULL from (1) as argument 1
+
+and for SARIF sinks the path will be added as a ``codeFlow`` object
+(see SARIF 2.1.0 `3.36 codeFlow object <https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790990>`_).
+
+Here's the above example in full:
+
+.. literalinclude:: ../../../testsuite/libdiagnostics.dg/test-warning-with-path.c
+   :language: c
+   :start-after: begin full example
+   :end-before:  end full example
+
+
+Moving on
+*********
+
+That's the end of the tutorial.  For more information on libdiagnostics, see
+the :doc:`topic guide <../topics/index>`.
diff --git a/gcc/doc/libdiagnostics/tutorial/example-1.png b/gcc/doc/libdiagnostics/tutorial/example-1.png
new file mode 100644 (file)
index 0000000..d637103
Binary files /dev/null and b/gcc/doc/libdiagnostics/tutorial/example-1.png differ
diff --git a/gcc/doc/libdiagnostics/tutorial/index.rst b/gcc/doc/libdiagnostics/tutorial/index.rst
new file mode 100644 (file)
index 0000000..6ea6866
--- /dev/null
@@ -0,0 +1,32 @@
+.. Copyright (C) 2024 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalcolm@redhat.com>
+
+   This 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 of the License, 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, see
+   <https://www.gnu.org/licenses/>.
+
+Tutorial
+========
+
+The following tutorial gives an overview of how to use libdiagnostics.
+
+.. toctree::
+   :maxdepth: 2
+
+   01-hello-world.rst
+   02-physical-locations.rst
+   03-logical-locations.rst
+   04-notes.rst
+   05-warnings.rst
+   06-fix-it-hints.rst
+   07-execution-paths.rst
diff --git a/gcc/libdiagnostics++.h b/gcc/libdiagnostics++.h
new file mode 100644 (file)
index 0000000..14c8493
--- /dev/null
@@ -0,0 +1,595 @@
+/* A C++ wrapper API around libdiagnostics.h for emitting diagnostics.
+   Copyright (C) 2023-2024 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 LIBDIAGNOSTICSPP_H
+#define LIBDIAGNOSTICSPP_H
+
+#include "libdiagnostics.h"
+
+namespace libdiagnostics {
+
+typedef diagnostic_line_num_t line_num_t;
+typedef diagnostic_column_num_t column_num_t;
+
+class file;
+class physical_location;
+class logical_location;
+class execution_path;
+class group;
+class manager;
+class diagnostic;
+
+/* Wrapper around a borrowed diagnostic_text_sink *.  */
+
+class text_sink
+{
+public:
+  text_sink (diagnostic_text_sink *inner)
+  : m_inner (inner)
+  {
+  }
+
+  void
+  set_source_printing_enabled (int value)
+  {
+    diagnostic_text_sink_set_source_printing_enabled (m_inner, value);
+  }
+
+  void
+  set_colorize (enum diagnostic_colorize colorize)
+  {
+    diagnostic_text_sink_set_colorize (m_inner, colorize);
+  }
+
+  void
+  set_labelled_source_colorization_enabled (int value)
+  {
+    diagnostic_text_sink_set_labelled_source_colorization_enabled (m_inner,
+                                                                  value);
+  }
+
+  diagnostic_text_sink *m_inner;
+};
+
+/* Wrapper around a const diagnostic_file *.  */
+
+class file
+{
+public:
+  file () : m_inner (nullptr) {}
+  file (const diagnostic_file *file) : m_inner (file) {}
+  file (const file &other) : m_inner (other.m_inner) {}
+  file &operator= (const file &other) { m_inner = other.m_inner; return *this; }
+
+  const diagnostic_file * m_inner;
+};
+
+/* Wrapper around a const diagnostic_physical_location *.  */
+
+class physical_location
+{
+public:
+  physical_location () : m_inner (nullptr) {}
+
+  physical_location (const diagnostic_physical_location *location)
+  : m_inner (location)
+  {}
+
+  const diagnostic_physical_location *m_inner;
+};
+
+/* Wrapper around a const diagnostic_logical_location *.  */
+
+class logical_location
+{
+public:
+  logical_location () : m_inner (nullptr) {}
+
+  logical_location (const diagnostic_logical_location *logical_loc)
+  : m_inner (logical_loc)
+  {}
+
+  const diagnostic_logical_location *m_inner;
+};
+
+/* RAII class around a diagnostic_execution_path *.  */
+
+class execution_path
+{
+public:
+  execution_path () : m_inner (nullptr), m_owned (false) {}
+
+  execution_path (diagnostic_execution_path *path)
+  : m_inner (path), m_owned (true)
+  {}
+
+  execution_path (const diagnostic_execution_path *path)
+  : m_inner (const_cast<diagnostic_execution_path *> (path)),
+    m_owned (false)
+  {}
+
+  execution_path (const execution_path &other) = delete;
+  execution_path &operator= (const execution_path &other) = delete;
+
+  execution_path (execution_path &&other)
+  : m_inner (other.m_inner),
+    m_owned (other.m_owned)
+  {
+    other.m_inner = nullptr;
+    other.m_owned = false;
+  }
+
+  execution_path &operator= (execution_path &&other)
+  {
+    m_inner = other.m_inner;
+    m_owned = other.m_owned;
+    other.m_inner = nullptr;
+    other.m_owned = false;
+    return *this;
+  }
+
+  ~execution_path ()
+  {
+    if (m_owned)
+      diagnostic_execution_path_release (m_inner);
+  }
+
+  diagnostic_event_id
+  add_event (physical_location physical_loc,
+            logical_location logical_loc,
+            unsigned stack_depth,
+            const char *fmt, ...)
+    LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (5, 6);
+
+  diagnostic_event_id
+  add_event_va (physical_location physical_loc,
+               logical_location logical_loc,
+               unsigned stack_depth,
+               const char *fmt,
+               va_list *args)
+    LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (5, 0);
+
+  diagnostic_execution_path *m_inner;
+  bool m_owned;
+};
+
+/* RAII class for starting/ending a group within a diagnostic_manager.  */
+
+class group
+{
+public:
+  group (manager &mgr);
+  ~group ();
+
+private:
+  manager &m_mgr;
+};
+
+/* Wrapper around a diagnostic *.  */
+
+class diagnostic
+{
+public:
+  diagnostic (::diagnostic *d) : m_inner (d) {}
+
+  void
+  set_cwe (unsigned cwe_id);
+
+  void
+  add_rule (const char *title, const char *url);
+
+  void
+  set_location (physical_location loc);
+
+  void
+  add_location_with_label (physical_location loc,
+                          const char *text);
+
+  void
+  set_logical_location (logical_location loc);
+
+  void
+  add_fix_it_hint_insert_before (physical_location loc,
+                                const char *addition);
+  void
+  add_fix_it_hint_insert_after (physical_location loc,
+                               const char *addition);
+  void
+  add_fix_it_hint_replace (physical_location loc,
+                          const char *replacement);
+  void
+  add_fix_it_hint_delete (physical_location loc);
+
+  void
+  take_execution_path (execution_path path);
+
+  void
+  finish (const char *fmt, ...)
+    LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+    LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (2, 3);
+
+  void
+  finish_va (const char *fmt, va_list *args)
+    LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+    LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (2, 0);
+
+  ::diagnostic * const m_inner;
+};
+
+/* Wrapper around a diagnostic_manager *, possibly with ownership.  */
+
+class manager
+{
+public:
+  manager ()
+  : m_inner (diagnostic_manager_new ()),
+    m_owned (true)
+  {
+  }
+  manager (diagnostic_manager *inner, bool owned)
+  : m_inner (inner),
+    m_owned (owned)
+  {
+  }
+  ~manager ()
+  {
+    if (m_owned)
+      diagnostic_manager_release (m_inner);
+  }
+
+  manager (const manager &other) = delete;
+  manager (manager &&other)
+  : m_inner (other.m_inner),
+    m_owned (other.m_owned)
+  {
+    other.m_inner = nullptr;
+  }
+
+  void
+  set_tool_name (const char *value)
+  {
+    diagnostic_manager_set_tool_name (m_inner, value);
+  }
+
+  void
+  set_full_name (const char *value)
+  {
+    diagnostic_manager_set_full_name (m_inner, value);
+  }
+
+  void
+  set_version_string (const char *value)
+  {
+    diagnostic_manager_set_version_string (m_inner, value);
+  }
+
+  void
+  set_version_url (const char *value)
+  {
+    diagnostic_manager_set_version_url (m_inner, value);
+  }
+
+  text_sink
+  add_text_sink (FILE *dst_stream,
+                enum diagnostic_colorize colorize)
+  {
+    return text_sink
+      (diagnostic_manager_add_text_sink (m_inner, dst_stream, colorize));
+  }
+
+  void
+  add_sarif_sink (FILE *dst_stream,
+                 file main_input_file,
+                 enum diagnostic_sarif_version version)
+  {
+    diagnostic_manager_add_sarif_sink (m_inner, dst_stream,
+                                      main_input_file.m_inner,
+                                      version);
+  }
+
+  void
+  write_patch (FILE *dst_stream)
+  {
+    diagnostic_manager_write_patch (m_inner, dst_stream);
+  }
+
+  /* Location management.  */
+
+  file
+  new_file (const char *name,
+           const char *sarif_source_language)
+    LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+    LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3);
+
+  void
+  debug_dump (file f,
+             FILE *out);
+
+  physical_location
+  new_location_from_file_and_line (file f, diagnostic_line_num_t line_num);
+
+  physical_location
+  new_location_from_file_line_column (file f,
+                                     line_num_t line_num,
+                                     column_num_t column_num);
+
+  physical_location
+  new_location_from_range (physical_location loc_caret,
+                          physical_location loc_start,
+                          physical_location loc_end);
+
+  void
+  debug_dump (physical_location loc,
+             FILE *out);
+
+  logical_location
+  new_logical_location (enum diagnostic_logical_location_kind_t kind,
+                       logical_location parent,
+                       const char *short_name,
+                       const char *fully_qualified_name,
+                       const char *decorated_name);
+
+  void
+  debug_dump (logical_location loc,
+             FILE *out);
+
+  execution_path
+  new_execution_path ();
+
+  diagnostic
+  begin_diagnostic (enum diagnostic_level level);
+
+
+  diagnostic_manager *m_inner;
+  bool m_owned;
+};
+
+// Implementation
+
+// class execution_path
+
+inline diagnostic_event_id
+execution_path::add_event (physical_location physical_loc,
+                          logical_location logical_loc,
+                          unsigned stack_depth,
+                          const char *fmt, ...)
+{
+  va_list args;
+  va_start (args, fmt);
+  diagnostic_event_id result = add_event_va (physical_loc,
+                                            logical_loc,
+                                            stack_depth,
+                                            fmt, &args);
+  va_end (args);
+
+  return result;
+}
+
+inline diagnostic_event_id
+execution_path::add_event_va (physical_location physical_loc,
+                             logical_location logical_loc,
+                             unsigned stack_depth,
+                             const char *fmt,
+                             va_list *args)
+{
+  return diagnostic_execution_path_add_event_va (m_inner,
+                                                physical_loc.m_inner,
+                                                logical_loc.m_inner,
+                                                stack_depth,
+                                                fmt,
+                                                args);
+}
+
+// class group
+
+inline
+group::group (manager &mgr)
+: m_mgr (mgr)
+{
+  diagnostic_manager_begin_group (m_mgr.m_inner);
+}
+
+inline
+group::~group ()
+{
+  diagnostic_manager_end_group (m_mgr.m_inner);
+}
+
+// class diagnostic
+
+inline void
+diagnostic::set_cwe (unsigned cwe_id)
+{
+  diagnostic_set_cwe (m_inner, cwe_id);
+}
+
+inline void
+diagnostic::add_rule (const char *title, const char *url)
+{
+  diagnostic_add_rule (m_inner, title, url);
+}
+
+inline void
+diagnostic::set_location (physical_location loc)
+{
+  diagnostic_set_location (m_inner, loc.m_inner);
+}
+
+inline void
+diagnostic::add_location_with_label (physical_location loc,
+                                    const char *text)
+{
+  diagnostic_add_location_with_label (m_inner, loc.m_inner, text);
+}
+
+inline void
+diagnostic::set_logical_location (logical_location loc)
+{
+  diagnostic_set_logical_location (m_inner, loc.m_inner);
+}
+
+inline void
+diagnostic::add_fix_it_hint_insert_before (physical_location loc,
+                                          const char *addition)
+{
+  diagnostic_add_fix_it_hint_insert_before (m_inner,
+                                           loc.m_inner,
+                                           addition);
+}
+
+inline void
+diagnostic::add_fix_it_hint_insert_after (physical_location loc,
+                               const char *addition)
+{
+  diagnostic_add_fix_it_hint_insert_after (m_inner,
+                                          loc.m_inner,
+                                          addition);
+}
+
+inline void
+diagnostic::add_fix_it_hint_replace (physical_location loc,
+                                    const char *replacement)
+{
+  diagnostic_add_fix_it_hint_replace (m_inner,
+                                     loc.m_inner,
+                                     replacement);
+}
+
+inline void
+diagnostic::add_fix_it_hint_delete (physical_location loc)
+{
+  diagnostic_add_fix_it_hint_delete (m_inner,
+                                    loc.m_inner);
+}
+
+inline void
+diagnostic::take_execution_path (execution_path path)
+{
+  diagnostic_take_execution_path (m_inner,
+                                 path.m_inner);
+  path.m_owned = false;
+}
+
+inline void
+diagnostic::finish (const char *fmt, ...)
+{
+  va_list ap;
+  va_start (ap, fmt);
+  diagnostic_finish_va (m_inner, fmt, &ap);
+  va_end (ap);
+}
+
+inline void
+diagnostic::finish_va (const char *fmt, va_list *args)
+{
+  diagnostic_finish_va (m_inner, fmt, args);
+}
+
+// class manager
+
+inline file
+manager::new_file (const char *name,
+                  const char *sarif_source_language)
+{
+  return file
+    (diagnostic_manager_new_file (m_inner, name, sarif_source_language));
+}
+
+inline physical_location
+manager::new_location_from_file_and_line (file f,
+                                         diagnostic_line_num_t line_num)
+{
+  return physical_location
+    (diagnostic_manager_new_location_from_file_and_line (m_inner,
+                                                        f.m_inner,
+                                                        line_num));
+}
+
+inline physical_location
+manager::new_location_from_file_line_column (file f,
+                                            line_num_t line_num,
+                                            column_num_t column_num)
+{
+  return physical_location
+    (diagnostic_manager_new_location_from_file_line_column (m_inner,
+                                                           f.m_inner,
+                                                           line_num,
+                                                           column_num));
+}
+
+inline physical_location
+manager::new_location_from_range (physical_location loc_caret,
+                                 physical_location loc_start,
+                                 physical_location loc_end)
+{
+  return physical_location
+    (diagnostic_manager_new_location_from_range (m_inner,
+                                                loc_caret.m_inner,
+                                                loc_start.m_inner,
+                                                loc_end.m_inner));
+}
+
+inline void
+manager::debug_dump (physical_location loc,
+                    FILE *out)
+{
+  diagnostic_manager_debug_dump_location (m_inner,
+                                         loc.m_inner,
+                                         out);
+}
+inline logical_location
+manager::new_logical_location (enum diagnostic_logical_location_kind_t kind,
+                              logical_location parent,
+                              const char *short_name,
+                              const char *fully_qualified_name,
+                              const char *decorated_name)
+{
+  return logical_location
+    (diagnostic_manager_new_logical_location (m_inner,
+                                             kind,
+                                             parent.m_inner,
+                                             short_name,
+                                             fully_qualified_name,
+                                             decorated_name));
+}
+
+inline void
+manager::debug_dump (logical_location loc,
+                    FILE *out)
+{
+  diagnostic_manager_debug_dump_logical_location (m_inner,
+                                                 loc.m_inner,
+                                                 out);
+}
+
+inline execution_path
+manager::new_execution_path ()
+{
+  return execution_path (diagnostic_manager_new_execution_path (m_inner));
+}
+
+inline diagnostic
+manager::begin_diagnostic (enum diagnostic_level level)
+{
+  return diagnostic (diagnostic_begin (m_inner, level));
+}
+
+} // namespace libdiagnostics
+
+#endif // #ifndef LIBDIAGNOSTICSPP_H
diff --git a/gcc/libdiagnostics.cc b/gcc/libdiagnostics.cc
new file mode 100644 (file)
index 0000000..c06d3bc
--- /dev/null
@@ -0,0 +1,1683 @@
+/* C++ implementation of a pure C API for emitting diagnostics.
+   Copyright (C) 2023-2024 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_MEMORY
+#define INCLUDE_VECTOR
+#include "system.h"
+#include "coretypes.h"
+#include "intl.h"
+#include "diagnostic.h"
+#include "diagnostic-color.h"
+#include "diagnostic-url.h"
+#include "diagnostic-metadata.h"
+#include "diagnostic-path.h"
+#include "diagnostic-client-data-hooks.h"
+#include "diagnostic-format-sarif.h"
+#include "diagnostic-format-text.h"
+#include "logical-location.h"
+#include "edit-context.h"
+#include "make-unique.h"
+#include "libdiagnostics.h"
+
+class owned_nullable_string
+{
+public:
+  owned_nullable_string () : m_str (nullptr) {}
+  owned_nullable_string (const char *str)
+  : m_str (str ? ::xstrdup (str) : nullptr)
+  {
+  }
+
+  ~owned_nullable_string ()
+  {
+    free (m_str);
+  }
+
+  void set (const char *str)
+  {
+    free (m_str);
+    m_str = str ? ::xstrdup (str) : nullptr;
+  }
+
+  const char *get_str () const { return m_str; }
+
+  char *xstrdup () const
+  {
+    return m_str ? ::xstrdup (m_str) : nullptr;
+  }
+
+private:
+  char *m_str;
+};
+
+/* This has to be a "struct" as it is exposed in the C API.  */
+
+struct diagnostic_file
+{
+  diagnostic_file (const char *name, const char *sarif_source_language)
+  : m_name (name), m_sarif_source_language (sarif_source_language)
+  {
+  }
+
+  const char *get_name () const { return m_name.get_str (); }
+  const char *get_sarif_source_language () const
+  {
+    return m_sarif_source_language.get_str ();
+  }
+
+private:
+  owned_nullable_string m_name;
+  owned_nullable_string m_sarif_source_language;
+};
+
+/* This has to be a "struct" as it is exposed in the C API.  */
+
+struct diagnostic_physical_location
+{
+  diagnostic_physical_location (diagnostic_manager *mgr,
+                               location_t inner)
+  : m_mgr (mgr),
+    m_inner (inner)
+  {}
+
+  diagnostic_manager *m_mgr;
+  location_t m_inner;
+};
+
+static location_t
+as_location_t (const diagnostic_physical_location *loc)
+{
+  if (!loc)
+    return UNKNOWN_LOCATION;
+  return loc->m_inner;
+}
+
+/* This has to be a "struct" as it is exposed in the C API.  */
+
+struct diagnostic_logical_location : public logical_location
+{
+  diagnostic_logical_location (enum diagnostic_logical_location_kind_t kind,
+                              const diagnostic_logical_location *parent,
+                              const char *short_name,
+                              const char *fully_qualified_name,
+                              const char *decorated_name)
+  : m_kind (kind),
+    m_parent (parent),
+    m_short_name (short_name),
+    m_fully_qualified_name (fully_qualified_name),
+    m_decorated_name (decorated_name)
+  {
+  }
+
+  const char *get_short_name () const final override
+  {
+    return m_short_name.get_str ();
+  }
+  const char *get_name_with_scope () const final override
+  {
+    return m_fully_qualified_name.get_str ();
+  }
+  const char *get_internal_name () const final override
+  {
+    return m_decorated_name.get_str ();
+  }
+  enum logical_location_kind get_kind () const final override
+  {
+    switch (m_kind)
+      {
+      default:
+       gcc_unreachable ();
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION:
+       return LOGICAL_LOCATION_KIND_FUNCTION;
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_MEMBER:
+       return LOGICAL_LOCATION_KIND_MEMBER;
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_MODULE:
+       return LOGICAL_LOCATION_KIND_MODULE;
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_NAMESPACE:
+       return LOGICAL_LOCATION_KIND_NAMESPACE;
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_TYPE:
+       return LOGICAL_LOCATION_KIND_TYPE;
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_RETURN_TYPE:
+       return LOGICAL_LOCATION_KIND_RETURN_TYPE;
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_PARAMETER:
+       return LOGICAL_LOCATION_KIND_PARAMETER;
+      case DIAGNOSTIC_LOGICAL_LOCATION_KIND_VARIABLE:
+       return LOGICAL_LOCATION_KIND_VARIABLE;
+      }
+  }
+
+  enum diagnostic_logical_location_kind_t get_external_kind () const
+  {
+    return m_kind;
+  }
+
+  const diagnostic_logical_location *get_parent () const { return m_parent; }
+
+  label_text get_name_for_path_output () const
+  {
+    return label_text::borrow (m_short_name.get_str ());
+  }
+
+private:
+  enum diagnostic_logical_location_kind_t m_kind;
+  const diagnostic_logical_location *m_parent;
+  owned_nullable_string m_short_name;
+  owned_nullable_string m_fully_qualified_name;
+  owned_nullable_string m_decorated_name;
+};
+
+static diagnostic_event_id
+as_diagnostic_event_id (diagnostic_event_id_t id)
+{
+  return id.zero_based ();
+}
+
+class sink
+{
+public:
+  virtual ~sink ();
+
+  void begin_group ()
+  {
+    m_dc.begin_group ();
+  }
+  void end_group ()
+  {
+    m_dc.end_group ();
+  }
+
+  void emit (diagnostic &diag, const char *msgid, va_list *args)
+    LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (3, 0);
+
+protected:
+  sink (diagnostic_manager &mgr);
+
+  diagnostic_manager &m_mgr;
+
+  /* One context per sink.  */
+  diagnostic_context m_dc;
+};
+
+/* This has to be a "struct" as it is exposed in the C API.  */
+
+struct diagnostic_text_sink : public sink
+{
+public:
+  diagnostic_text_sink (diagnostic_manager &mgr,
+                       FILE *dst_stream,
+                       enum diagnostic_colorize colorize);
+
+  void
+  on_begin_text_diagnostic (diagnostic_text_output_format &text_format,
+                           const diagnostic_info *info);
+
+  diagnostic_source_printing_options &get_source_printing_options ()
+  {
+    return m_dc.m_source_printing;
+  }
+
+  void
+  set_colorize (enum diagnostic_colorize colorize);
+
+private:
+  const diagnostic_logical_location *m_current_logical_loc;
+};
+
+class sarif_sink : public sink
+{
+public:
+  sarif_sink (diagnostic_manager &mgr,
+             FILE *dst_stream,
+             const diagnostic_file *main_input_file,
+             enum sarif_version version);
+};
+
+/* Helper for the linemap code.  */
+
+static size_t
+round_alloc_size (size_t s)
+{
+  return s;
+}
+
+class impl_diagnostic_client_data_hooks : public diagnostic_client_data_hooks
+{
+public:
+  impl_diagnostic_client_data_hooks (diagnostic_manager &mgr)
+  : m_mgr (mgr)
+  {}
+
+  const client_version_info *get_any_version_info () const final override;
+  const logical_location *get_current_logical_location () const final override;
+  const char * maybe_get_sarif_source_language (const char *filename)
+    const final override;
+  void add_sarif_invocation_properties (sarif_object &invocation_obj)
+    const final override;
+
+private:
+  diagnostic_manager &m_mgr;
+};
+
+class impl_client_version_info : public client_version_info
+{
+public:
+  const char *get_tool_name () const final override
+  {
+    return m_name.get_str ();
+  }
+
+  char *maybe_make_full_name () const final override
+  {
+    return m_full_name.xstrdup ();
+  }
+
+  const char *get_version_string () const final override
+  {
+    return m_version.get_str ();
+  }
+
+  char *maybe_make_version_url () const final override
+  {
+    return m_version_url.xstrdup ();
+  }
+
+  void for_each_plugin (plugin_visitor &) const final override
+  {
+    // No-op.
+  }
+
+  owned_nullable_string m_name;
+  owned_nullable_string m_full_name;
+  owned_nullable_string m_version;
+  owned_nullable_string m_version_url;
+};
+
+/* This has to be a "struct" as it is exposed in the C API.  */
+
+struct diagnostic_manager
+{
+public:
+  diagnostic_manager ()
+    : m_current_diag (nullptr),
+      m_edit_context (m_file_cache)
+  {
+    linemap_init (&m_line_table, BUILTINS_LOCATION);
+    m_line_table.m_reallocator = xrealloc;
+    m_line_table.m_round_alloc_size = round_alloc_size;
+    m_line_table.default_range_bits = 5;
+  }
+  ~diagnostic_manager ()
+  {
+    /* Clean up sinks first, as they can use other fields. */
+    for (size_t i = 0; i < m_sinks.size (); i++)
+      m_sinks[i] = nullptr;
+
+    for (auto iter : m_str_to_file_map)
+      delete iter.second;
+
+    for (auto iter :m_location_t_map)
+      delete iter.second;
+
+    free (m_line_table.m_location_adhoc_data_map.data);
+    free (m_line_table.info_ordinary.maps);
+  }
+
+  line_maps *get_line_table () { return &m_line_table; }
+  file_cache *get_file_cache () { return &m_file_cache; }
+
+  void write_patch (FILE *dst_stream);
+
+  void add_sink (std::unique_ptr<sink> sink)
+  {
+    m_sinks.push_back (std::move (sink));
+  }
+
+  void emit (diagnostic &diag, const char *msgid, va_list *args)
+    LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING(3, 0);
+
+  const diagnostic_file *
+  new_file (const char *name,
+           const char *sarif_source_language)
+  {
+    if (diagnostic_file **slot = m_str_to_file_map.get (name))
+      return *slot;
+    diagnostic_file *file = new diagnostic_file (name, sarif_source_language);
+    m_str_to_file_map.put (file->get_name (), file);
+    return file;
+  }
+
+  const diagnostic_physical_location *
+  new_location_from_file_and_line (const diagnostic_file *file,
+                                  diagnostic_line_num_t line_num)
+  {
+    ensure_linemap_for_file_and_line (file, line_num);
+    location_t loc = linemap_position_for_column (&m_line_table, 0);
+    return new_location (loc);
+  }
+
+  const diagnostic_physical_location *
+  new_location_from_file_line_column (const diagnostic_file *file,
+                                     diagnostic_line_num_t line_num,
+                                     diagnostic_column_num_t column_num)
+  {
+    ensure_linemap_for_file_and_line (file, line_num);
+    location_t loc = linemap_position_for_column (&m_line_table, column_num);
+    return new_location (loc);
+  }
+
+  const diagnostic_physical_location *
+  new_location_from_range (const diagnostic_physical_location *loc_caret,
+                          const diagnostic_physical_location *loc_start,
+                          const diagnostic_physical_location *loc_end)
+  {
+    return new_location
+      (m_line_table.make_location (as_location_t (loc_caret),
+                                  as_location_t (loc_start),
+                                  as_location_t (loc_end)));
+  }
+
+  const diagnostic_logical_location *
+  new_logical_location (enum diagnostic_logical_location_kind_t kind,
+                       const diagnostic_logical_location *parent,
+                       const char *short_name,
+                       const char *fully_qualified_name,
+                       const char *decorated_name)
+  {
+    std::unique_ptr<diagnostic_logical_location> logical_loc
+      = ::make_unique<diagnostic_logical_location> (kind,
+                                                   parent,
+                                                   short_name,
+                                                   fully_qualified_name,
+                                                   decorated_name);
+    const diagnostic_logical_location *result = logical_loc.get ();
+    m_logical_locs.push_back (std::move (logical_loc));
+    return result;
+  }
+
+  diagnostic_execution_path *
+  new_execution_path ();
+
+  void begin_group ()
+  {
+    for (auto &sink : m_sinks)
+      sink->begin_group ();
+  }
+
+  void end_group ()
+  {
+    for (auto &sink : m_sinks)
+      sink->end_group ();
+  }
+
+  const char *
+  maybe_get_sarif_source_language (const char *filename)
+  {
+    if (diagnostic_file **slot = m_str_to_file_map.get (filename))
+      {
+       gcc_assert (*slot);
+       return (*slot)->get_sarif_source_language ();
+      }
+    return nullptr;
+  }
+
+  const diagnostic *get_current_diag () { return m_current_diag; }
+
+  const client_version_info *get_client_version_info () const
+  {
+    return &m_client_version_info;
+  }
+  impl_client_version_info *get_client_version_info ()
+  {
+    return &m_client_version_info;
+  }
+
+  void
+  assert_valid_diagnostic_physical_location (const diagnostic_physical_location *loc) const
+  {
+    if (!loc)
+      return;
+    gcc_assert (loc->m_mgr == this);
+  }
+
+  /* TODO: Various things still use the "line_table" global variable.
+     Set it to be this diagnostic_manager's m_line_table.
+     Ideally we should eliminate this global (and this function).  */
+  void set_line_table_global () const
+  {
+    line_table = const_cast<line_maps *> (&m_line_table);
+  }
+
+private:
+  void
+  ensure_linemap_for_file_and_line (const diagnostic_file *file,
+                                   diagnostic_line_num_t linenum)
+  {
+    /* Build a simple linemap describing some locations. */
+    if (LINEMAPS_ORDINARY_USED (&m_line_table) == 0)
+      linemap_add (&m_line_table, LC_ENTER, false, file->get_name (), 0);
+    else
+      {
+       line_map *map
+         = const_cast<line_map *>
+           (linemap_add (&m_line_table, LC_RENAME_VERBATIM, false,
+                         file->get_name (), 0));
+       ((line_map_ordinary *)map)->included_from = UNKNOWN_LOCATION;
+      }
+    linemap_line_start (&m_line_table, linenum, 100);
+  }
+
+  const diagnostic_physical_location *
+  new_location (location_t loc)
+  {
+    if (loc == UNKNOWN_LOCATION)
+      return nullptr;
+    if (diagnostic_physical_location **slot = m_location_t_map.get (loc))
+      return *slot;
+    diagnostic_physical_location *phys_loc
+      = new diagnostic_physical_location (this, loc);
+    m_location_t_map.put (loc, phys_loc);
+    return phys_loc;
+  }
+
+  line_maps m_line_table;
+  file_cache m_file_cache;
+  impl_client_version_info m_client_version_info;
+  std::vector<std::unique_ptr<sink>> m_sinks;
+  hash_map<nofree_string_hash, diagnostic_file *> m_str_to_file_map;
+  hash_map<int_hash<location_t, UNKNOWN_LOCATION, UINT_MAX>,
+          diagnostic_physical_location *> m_location_t_map;
+  std::vector<std::unique_ptr<diagnostic_logical_location>> m_logical_locs;
+  const diagnostic *m_current_diag;
+  edit_context m_edit_context;
+};
+
+class impl_rich_location : public rich_location
+{
+public:
+  impl_rich_location (line_maps *set)
+  : rich_location (set, UNKNOWN_LOCATION)
+  {}
+};
+
+class impl_range_label : public range_label
+{
+public:
+  impl_range_label (const char *text)
+  : m_text (xstrdup (text))
+  {}
+
+  ~impl_range_label () { free (m_text); }
+
+  label_text get_text (unsigned) const final override
+  {
+    return label_text::borrow (m_text);
+  }
+
+private:
+  char *m_text;
+};
+
+class impl_rule : public diagnostic_metadata::rule
+{
+public:
+  impl_rule (const char *title, const char *url)
+  :  m_title (title),
+     m_url (url)
+  {
+  }
+
+  virtual ~impl_rule () {}
+
+  char *make_description () const final override
+  {
+    return m_title.xstrdup ();
+  }
+
+  char *make_url () const final override
+  {
+    return m_url.xstrdup ();
+  }
+
+private:
+  owned_nullable_string m_title;
+  owned_nullable_string m_url;
+};
+
+class libdiagnostics_path_event : public diagnostic_event
+{
+public:
+  libdiagnostics_path_event (const diagnostic_physical_location *physical_loc,
+                            const diagnostic_logical_location *logical_loc,
+                            unsigned stack_depth,
+                            const char *gmsgid,
+                            va_list *args)
+  : m_physical_loc (physical_loc),
+    m_logical_loc (logical_loc),
+    m_stack_depth (stack_depth)
+  {
+    m_desc_uncolored = make_desc (gmsgid, args, false);
+    m_desc_colored = make_desc (gmsgid, args, true);
+  }
+
+  /* diagnostic_event vfunc implementations.  */
+
+  location_t get_location () const final override
+  {
+    return as_location_t (m_physical_loc);
+  }
+
+  int get_stack_depth () const final override
+  {
+    return m_stack_depth;
+  }
+
+  void print_desc (pretty_printer &pp) const final override
+  {
+    if (pp_show_color (&pp))
+      pp_string (&pp, m_desc_colored.get ());
+    else
+      pp_string (&pp, m_desc_uncolored.get ());
+  }
+
+  const logical_location *get_logical_location () const
+  {
+    return m_logical_loc;
+  }
+
+  meaning get_meaning () const final override
+  {
+    return meaning ();
+  }
+
+  bool connect_to_next_event_p () const final override
+  {
+    return false; // TODO
+  }
+
+  diagnostic_thread_id_t get_thread_id () const final override
+  {
+    return 0;
+  }
+
+private:
+  static label_text make_desc (const char *gmsgid,
+                              va_list *args,
+                              bool colorize)
+  {
+    va_list copy_of_args;
+    va_copy (copy_of_args, *args);
+
+    // TODO: when should localization happen?
+    text_info text (gmsgid, &copy_of_args, errno);
+    pretty_printer pp;
+    pp_show_color (&pp) = colorize;
+    pp.set_output_stream (nullptr);
+    pp_format (&pp, &text);
+    pp_output_formatted_text (&pp, nullptr);
+    label_text result = label_text::take (xstrdup (pp_formatted_text (&pp)));
+
+    va_end (copy_of_args);
+
+    return result;
+  }
+
+  const diagnostic_physical_location *m_physical_loc;
+  const diagnostic_logical_location *m_logical_loc;
+  unsigned m_stack_depth;
+  label_text m_desc_uncolored;
+  label_text m_desc_colored;
+};
+
+class libdiagnostics_path_thread : public diagnostic_thread
+{
+public:
+  libdiagnostics_path_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
+};
+
+/* This has to be a "struct" as it is exposed in the C API.  */
+
+struct diagnostic_execution_path : public diagnostic_path
+{
+  diagnostic_execution_path ()
+  : m_thread ("")
+  {
+  }
+
+  diagnostic_event_id_t
+  add_event_va (const diagnostic_physical_location *physical_loc,
+               const diagnostic_logical_location *logical_loc,
+               unsigned stack_depth,
+               const char *gmsgid,
+               va_list *args)
+  {
+    m_events.push_back (::make_unique<libdiagnostics_path_event> (physical_loc,
+                                                                 logical_loc,
+                                                                 stack_depth,
+                                                                 gmsgid,
+                                                                 args));
+    return m_events.size () - 1;
+  }
+
+  /* diagnostic_path vfunc implementations.  */
+
+  unsigned num_events () const final override
+  {
+    return m_events.size ();
+  }
+  const diagnostic_event & get_event (int idx) const final override
+  {
+    return *m_events[idx];
+  }
+  unsigned num_threads () const final override { return 1; }
+  const diagnostic_thread &
+  get_thread (diagnostic_thread_id_t) const final override
+  {
+    return m_thread;
+  }
+
+  bool
+  same_function_p (int event_idx_a,
+                  int event_idx_b) const final override
+  {
+    const logical_location *logical_loc_a
+      = m_events[event_idx_a]->get_logical_location ();
+    const logical_location *logical_loc_b
+      = m_events[event_idx_b]->get_logical_location ();
+
+    // TODO:
+    /* Pointer equality, so we may want to uniqify logical loc ptrs.  */
+    return logical_loc_a == logical_loc_b;
+  }
+
+private:
+  libdiagnostics_path_thread m_thread;
+  std::vector<std::unique_ptr<libdiagnostics_path_event>> m_events;
+};
+
+/* This has to be a "struct" as it is exposed in the C API.  */
+
+struct diagnostic
+{
+public:
+  diagnostic (diagnostic_manager &diag_mgr,
+             enum diagnostic_level level)
+  : m_diag_mgr (diag_mgr),
+    m_level (level),
+    m_rich_loc (diag_mgr.get_line_table ()),
+    m_logical_loc (nullptr),
+    m_path (nullptr)
+  {}
+
+  diagnostic_manager &get_manager () const
+  {
+    return m_diag_mgr;
+  }
+
+  enum diagnostic_level get_level () const { return m_level; }
+
+  rich_location *get_rich_location () { return &m_rich_loc; }
+  const diagnostic_metadata *get_metadata () { return &m_metadata; }
+
+  void set_cwe (unsigned cwe_id)
+  {
+    m_metadata.add_cwe (cwe_id);
+  }
+
+  void add_rule (const char *title,
+                const char *url)
+  {
+    std::unique_ptr<impl_rule> rule = ::make_unique<impl_rule> (title, url);
+    m_metadata.add_rule (*rule.get ());
+    m_rules.push_back (std::move (rule));
+  }
+
+  void set_location (const diagnostic_physical_location *loc)
+  {
+    m_rich_loc.set_range (0, as_location_t (loc), SHOW_RANGE_WITH_CARET);
+  }
+
+  void
+  add_location (const diagnostic_physical_location *loc)
+  {
+    m_rich_loc.add_range (as_location_t (loc),
+                         SHOW_RANGE_WITHOUT_CARET);
+  }
+
+  void
+  add_location_with_label (const diagnostic_physical_location *loc,
+                          const char *text)
+  {
+    std::unique_ptr<range_label> label
+      = ::make_unique <impl_range_label> (text);
+    m_rich_loc.add_range (as_location_t (loc),
+                         SHOW_RANGE_WITHOUT_CARET,
+                         label.get ());
+    m_labels.push_back (std::move (label));
+  }
+
+  void
+  set_logical_location (const diagnostic_logical_location *logical_loc)
+  {
+    m_logical_loc = logical_loc;
+  }
+  const diagnostic_logical_location *get_logical_location () const
+  {
+    return m_logical_loc;
+  }
+
+  diagnostic_execution_path *
+  add_execution_path ()
+  {
+    m_path = ::make_unique<diagnostic_execution_path> ();
+    m_rich_loc.set_path (m_path.get ());
+    return m_path.get ();
+  }
+
+  void
+  take_execution_path (diagnostic_execution_path *path)
+  {
+    m_path = std::unique_ptr<diagnostic_execution_path> (path);
+    m_rich_loc.set_path (path);
+  }
+
+private:
+  diagnostic_manager &m_diag_mgr;
+  enum diagnostic_level m_level;
+  impl_rich_location m_rich_loc;
+  const diagnostic_logical_location *m_logical_loc;
+  diagnostic_metadata m_metadata;
+  std::vector<std::unique_ptr<range_label>> m_labels;
+  std::vector<std::unique_ptr<impl_rule>> m_rules;
+  std::unique_ptr<diagnostic_execution_path> m_path;
+};
+
+static diagnostic_t
+diagnostic_t_from_diagnostic_level (enum diagnostic_level level)
+{
+  switch (level)
+    {
+    default:
+      gcc_unreachable ();
+    case DIAGNOSTIC_LEVEL_ERROR:
+      return DK_ERROR;
+    case DIAGNOSTIC_LEVEL_WARNING:
+      return DK_WARNING;
+    case DIAGNOSTIC_LEVEL_NOTE:
+      return DK_NOTE;
+    case DIAGNOSTIC_LEVEL_SORRY:
+      return DK_SORRY;
+    }
+}
+
+/* class impl_diagnostic_client_data_hooks.  */
+
+const client_version_info *
+impl_diagnostic_client_data_hooks::get_any_version_info () const
+{
+  return m_mgr.get_client_version_info ();
+}
+
+const logical_location *
+impl_diagnostic_client_data_hooks::get_current_logical_location () const
+{
+  gcc_assert (m_mgr.get_current_diag ());
+
+  return m_mgr.get_current_diag ()->get_logical_location ();
+}
+
+const char *
+impl_diagnostic_client_data_hooks::
+maybe_get_sarif_source_language (const char *filename) const
+{
+  return m_mgr.maybe_get_sarif_source_language (filename);
+}
+
+void
+impl_diagnostic_client_data_hooks::
+add_sarif_invocation_properties (sarif_object &) const
+{
+  // No-op.
+}
+
+/* class sink.  */
+
+void
+sink::emit (diagnostic &diag, const char *msgid, va_list *args)
+{
+  diagnostic_info info;
+GCC_DIAGNOSTIC_PUSH_IGNORED(-Wsuggest-attribute=format)
+  diagnostic_set_info (&info, msgid, args, diag.get_rich_location (),
+                      diagnostic_t_from_diagnostic_level (diag.get_level ()));
+GCC_DIAGNOSTIC_POP
+  info.metadata = diag.get_metadata ();
+  diagnostic_report_diagnostic (&m_dc, &info);
+}
+
+sink::sink (diagnostic_manager &mgr)
+: m_mgr (mgr)
+{
+  diagnostic_initialize (&m_dc, 0);
+  m_dc.m_client_aux_data = this;
+  m_dc.set_client_data_hooks
+    (::make_unique<impl_diagnostic_client_data_hooks> (mgr));
+}
+
+sink::~sink ()
+{
+  diagnostic_finish (&m_dc);
+}
+
+/* struct diagnostic_text_sink : public sink.  */
+
+static diagnostic_color_rule_t
+get_color_rule (enum diagnostic_colorize colorize)
+{
+  switch (colorize)
+    {
+    default:
+      gcc_unreachable ();
+    case DIAGNOSTIC_COLORIZE_IF_TTY:
+      return DIAGNOSTICS_COLOR_AUTO;
+      break;
+    case DIAGNOSTIC_COLORIZE_NO:
+      return DIAGNOSTICS_COLOR_NO;
+      break;
+    case DIAGNOSTIC_COLORIZE_YES:
+      return DIAGNOSTICS_COLOR_YES;
+      break;
+    }
+}
+
+diagnostic_text_sink::diagnostic_text_sink (diagnostic_manager &mgr,
+                                           FILE *dst_stream,
+                                           enum diagnostic_colorize colorize)
+: sink (mgr),
+  m_current_logical_loc (nullptr)
+{
+  m_dc.set_show_cwe (true);
+  m_dc.set_show_rules (true);
+
+  diagnostic_color_init (&m_dc, get_color_rule (colorize));
+  diagnostic_urls_init (&m_dc);
+
+  auto text_format = ::make_unique<diagnostic_text_output_format> (m_dc, true);
+  text_format->get_printer ()->set_output_stream (dst_stream);
+  m_dc.set_output_format (std::move (text_format));
+  diagnostic_text_starter (&m_dc)
+    = [] (diagnostic_text_output_format &text_format,
+         const diagnostic_info *info)
+      {
+       diagnostic_context &dc = text_format.get_context ();
+       diagnostic_text_sink *sink
+         = static_cast<diagnostic_text_sink *> (dc.m_client_aux_data);
+       sink->on_begin_text_diagnostic (text_format, info);
+      };
+  m_dc.set_show_cwe (true);
+  m_dc.set_show_rules (true);
+  m_dc.m_show_column = true;
+  m_dc.m_source_printing.enabled = true;
+  m_dc.m_source_printing.colorize_source_p = true;
+
+  /* We don't currently expose a way for clients to manipulate the
+     following.  */
+  m_dc.m_source_printing.show_labels_p = true;
+  m_dc.m_source_printing.show_line_numbers_p = true;
+  m_dc.m_source_printing.min_margin_width = 6;
+  m_dc.set_path_format (DPF_INLINE_EVENTS);
+}
+
+void
+diagnostic_text_sink::set_colorize (enum diagnostic_colorize colorize)
+{
+  diagnostic_color_init (&m_dc, get_color_rule (colorize));
+}
+
+void
+diagnostic_text_sink::
+on_begin_text_diagnostic (diagnostic_text_output_format &text_format,
+                         const diagnostic_info *info)
+{
+  const diagnostic *diag = m_mgr.get_current_diag ();
+  gcc_assert (diag);
+  pretty_printer *pp = text_format.get_printer ();
+  const diagnostic_logical_location *diag_logical_loc
+    = diag->get_logical_location ();
+  if (m_current_logical_loc != diag_logical_loc)
+    {
+      m_current_logical_loc = diag_logical_loc;
+      if (m_current_logical_loc)
+       {
+         pp_set_prefix (pp, nullptr);
+         switch (m_current_logical_loc->get_kind ())
+           {
+           default:
+             break;
+           case LOGICAL_LOCATION_KIND_FUNCTION:
+             if (const char *name
+                 = m_current_logical_loc->get_name_with_scope ())
+               {
+                 pp_printf (pp, _("In function %qs"), name);
+                 pp_character (pp, ':');
+                 pp_newline (pp);
+               }
+             break;
+             // TODO: handle other cases
+           }
+       }
+    }
+  pp_set_prefix (pp,
+                text_format.build_prefix (*info));
+}
+
+/* class sarif_sink : public sink.  */
+
+sarif_sink::sarif_sink (diagnostic_manager &mgr,
+                       FILE *dst_stream,
+                       const diagnostic_file *main_input_file,
+                       enum sarif_version version)
+: sink (mgr)
+{
+  const char *main_input_filename = main_input_file->get_name ();
+  diagnostic_output_format_init_sarif_stream (m_dc,
+                                             mgr.get_line_table (),
+                                             main_input_filename,
+                                             true,
+                                             version,
+                                             dst_stream);
+}
+
+/* struct diagnostic_manager.  */
+
+void
+diagnostic_manager::write_patch (FILE *dst_stream)
+{
+  pretty_printer pp;
+  pp.set_output_stream (dst_stream);
+  m_edit_context.print_diff (&pp, true);
+  pp_flush (&pp);
+}
+
+void
+diagnostic_manager::emit (diagnostic &diag, const char *msgid, va_list *args)
+{
+  set_line_table_global ();
+
+  m_current_diag = &diag;
+  for (auto &sink : m_sinks)
+    {
+      va_list arg_copy;
+      va_copy (arg_copy, *args);
+      sink->emit (diag, msgid, &arg_copy);
+    }
+
+  rich_location *rich_loc = diag.get_rich_location ();
+  if (rich_loc->fixits_can_be_auto_applied_p ())
+    m_edit_context.add_fixits (rich_loc);
+
+  m_current_diag = nullptr;
+}
+
+diagnostic_execution_path *
+diagnostic_manager::new_execution_path ()
+{
+  return new diagnostic_execution_path ();
+}
+
+/* Error-checking at the API boundary.  */
+
+#define FAIL_IF_NULL(PTR_ARG) \
+  do {                                             \
+    volatile const void *p = (PTR_ARG);                    \
+    if (!p) {                                      \
+      fprintf (stderr, "%s: %s must be non-NULL\n",   \
+              __func__, #PTR_ARG);                   \
+      abort ();                                              \
+    }                                              \
+  } while (0)
+
+/* Public entrypoints.  */
+
+/* Public entrypoint for clients to acquire a diagnostic_manager.  */
+
+diagnostic_manager *
+diagnostic_manager_new (void)
+{
+  return new diagnostic_manager ();
+}
+
+/* Public entrypoint for clients to release a diagnostic_manager.  */
+
+void
+diagnostic_manager_release (diagnostic_manager *diag_mgr)
+{
+  delete diag_mgr;
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_set_tool_name (diagnostic_manager *diag_mgr,
+                                 const char *value)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (value);
+
+  diag_mgr->get_client_version_info ()->m_name.set (value);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_set_full_name (diagnostic_manager *diag_mgr,
+                                 const char *value)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (value);
+
+  diag_mgr->get_client_version_info ()->m_full_name.set (value);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_set_version_string (diagnostic_manager *diag_mgr,
+                                      const char *value)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (value);
+
+  diag_mgr->get_client_version_info ()->m_version.set (value);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_set_version_url (diagnostic_manager *diag_mgr,
+                                   const char *value)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (value);
+
+  diag_mgr->get_client_version_info ()->m_version_url.set (value);
+}
+
+/* Public entrypoint.  */
+
+diagnostic_text_sink *
+diagnostic_manager_add_text_sink (diagnostic_manager *diag_mgr,
+                                 FILE *dst_stream,
+                                 enum diagnostic_colorize colorize)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (dst_stream);
+
+  diagnostic_text_sink *result
+    = new diagnostic_text_sink (*diag_mgr, dst_stream, colorize);
+  diag_mgr->add_sink (std::unique_ptr<sink> (result));
+  return result;
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_text_sink_set_source_printing_enabled (diagnostic_text_sink *text_sink,
+                                                 int value)
+{
+  FAIL_IF_NULL (text_sink);
+
+  text_sink->get_source_printing_options ().enabled = value;
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_text_sink_set_colorize (diagnostic_text_sink *text_sink,
+                                  enum diagnostic_colorize colorize)
+{
+  FAIL_IF_NULL (text_sink);
+
+  text_sink->set_colorize (colorize);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_text_sink_set_labelled_source_colorization_enabled (diagnostic_text_sink *text_sink,
+                                                              int value)
+{
+  FAIL_IF_NULL (text_sink);
+
+  text_sink->get_source_printing_options ().colorize_source_p = value;
+}
+
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_add_sarif_sink (diagnostic_manager *diag_mgr,
+                                  FILE *dst_stream,
+                                  const diagnostic_file *main_input_file,
+                                  enum diagnostic_sarif_version version)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (dst_stream);
+  FAIL_IF_NULL (main_input_file);
+
+  enum sarif_version internal_version;
+  switch (version)
+    {
+    default:
+      fprintf (stderr, "%s: unrecognized value for version: %i\n",
+              __func__, (int)version);
+      abort ();
+    case DIAGNOSTIC_SARIF_VERSION_2_1_0:
+      internal_version = sarif_version::v2_1_0;
+      break;
+    case DIAGNOSTIC_SARIF_VERSION_2_2_PRERELEASE:
+      internal_version = sarif_version::v2_2_prerelease_2024_08_08;
+      break;
+    }
+
+  diag_mgr->add_sink (make_unique<sarif_sink> (*diag_mgr,
+                                              dst_stream,
+                                              main_input_file,
+                                              internal_version));
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_write_patch (diagnostic_manager *diag_mgr,
+                               FILE *dst_stream)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (dst_stream);
+
+  diag_mgr->write_patch (dst_stream);
+}
+
+/* Public entrypoint.  */
+
+const diagnostic_file *
+diagnostic_manager_new_file (diagnostic_manager *diag_mgr,
+                            const char *name,
+                            const char *sarif_source_language)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (name);
+
+  return diag_mgr->new_file (name, sarif_source_language);
+}
+
+void
+diagnostic_manager_debug_dump_file (diagnostic_manager *,
+                                   const diagnostic_file *file,
+                                   FILE *out)
+{
+  FAIL_IF_NULL (out);
+  if (file)
+    {
+      if (file->get_sarif_source_language ())
+       {
+         fprintf (out, "file(name=\"%s\", sarif_source_language=\"%s\")",
+                  file->get_name (),
+                  file->get_sarif_source_language ());
+       }
+      else
+       {
+         fprintf (out, "file(name=\"%s\")",
+                  file->get_name ());
+       }
+    }
+  else
+    fprintf (out, "(null)");
+}
+
+
+/* Public entrypoint.  */
+
+const diagnostic_physical_location *
+diagnostic_manager_new_location_from_file_and_line (diagnostic_manager *diag_mgr,
+                                                   const diagnostic_file *file,
+                                                   diagnostic_line_num_t linenum)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (file);
+
+  return diag_mgr->new_location_from_file_and_line (file, linenum);
+}
+
+/* Public entrypoint.  */
+
+const diagnostic_physical_location *
+diagnostic_manager_new_location_from_file_line_column (diagnostic_manager *diag_mgr,
+                                                      const diagnostic_file *file,
+                                                      diagnostic_line_num_t line_num,
+                                              diagnostic_column_num_t column_num)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (file);
+
+  return diag_mgr->new_location_from_file_line_column (file,
+                                                      line_num,
+                                                      column_num);
+}
+
+/* Public entrypoint.  */
+
+const diagnostic_physical_location *
+diagnostic_manager_new_location_from_range (diagnostic_manager *diag_mgr,
+                                           const diagnostic_physical_location *loc_caret,
+                                           const diagnostic_physical_location *loc_start,
+                                           const diagnostic_physical_location *loc_end)
+{
+  FAIL_IF_NULL (diag_mgr);
+
+  return diag_mgr->new_location_from_range (loc_caret,
+                                           loc_start,
+                                           loc_end);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_debug_dump_location (const diagnostic_manager *diag_mgr,
+                                       const diagnostic_physical_location *loc,
+                                       FILE *out)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (out);
+
+  if (loc)
+    {
+      const location_t cpplib_loc = as_location_t (loc);
+      diag_mgr->set_line_table_global ();
+      const expanded_location exp_loc (expand_location (cpplib_loc));
+
+      diagnostic_context dc;
+      diagnostic_initialize (&dc, 0);
+      dc.m_show_column = true;
+
+      diagnostic_text_output_format text_format (dc);
+      label_text loc_text = text_format.get_location_text (exp_loc);
+      fprintf (out, "%s", loc_text.get ());
+
+      diagnostic_finish (&dc);
+    }
+  else
+    fprintf (out, "(null)");
+}
+
+/* Public entrypoint.  */
+
+const diagnostic_logical_location *
+diagnostic_manager_new_logical_location (diagnostic_manager *diag_mgr,
+                                        enum diagnostic_logical_location_kind_t kind,
+                                        const diagnostic_logical_location *parent,
+                                        const char *short_name,
+                                        const char *fully_qualified_name,
+                                        const char *decorated_name)
+{
+  FAIL_IF_NULL (diag_mgr);
+
+  return diag_mgr->new_logical_location (kind,
+                                        parent,
+                                        short_name,
+                                        fully_qualified_name,
+                                        decorated_name);
+}
+
+void
+diagnostic_manager_debug_dump_logical_location (const diagnostic_manager *diag_mgr,
+                                               const diagnostic_logical_location *loc,
+                                               FILE *out)
+{
+  FAIL_IF_NULL (diag_mgr);
+  FAIL_IF_NULL (out);
+
+  if (loc)
+    {
+      fprintf (out, "logical_location(kind=");
+      switch (loc->get_external_kind ())
+       {
+       default:
+         gcc_unreachable ();
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION:
+         fprintf (out, "function");
+         break;
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_MEMBER:
+         fprintf (out, "member");
+         break;
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_MODULE:
+         fprintf (out, "module");
+         break;
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_NAMESPACE:
+         fprintf (out, "namespace");
+         break;
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_TYPE:
+         fprintf (out, "file");
+         break;
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_RETURN_TYPE:
+         fprintf (out, "return_type");
+         break;
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_PARAMETER:
+         fprintf (out, "parameter");
+         break;
+       case DIAGNOSTIC_LOGICAL_LOCATION_KIND_VARIABLE:
+         fprintf (out, "variable");
+         break;
+       }
+      if (const diagnostic_logical_location *parent = loc->get_parent ())
+       diagnostic_manager_debug_dump_logical_location (diag_mgr,
+                                                       parent,
+                                                       out);
+      if (const char *val = loc->get_short_name ())
+       fprintf (out, ", short_name=\"%s\"", val);
+      if (const char *val = loc->get_name_with_scope ())
+       fprintf (out, ", fully_qualified_name=\"%s\"", val);
+      if (const char *val = loc->get_internal_name ())
+       fprintf (out, ", decorated_name=\"%s\"", val);
+      fprintf (out, ")");
+    }
+  else
+    fprintf (out, "(null)");
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_manager_begin_group (diagnostic_manager *diag_mgr)
+{
+  FAIL_IF_NULL (diag_mgr);
+  diag_mgr->begin_group ();
+}
+
+/* Public entrypoint.  */
+
+extern void
+diagnostic_manager_end_group (diagnostic_manager *diag_mgr)
+{
+  FAIL_IF_NULL (diag_mgr);
+  diag_mgr->end_group ();
+}
+
+/* Public entrypoint.  */
+
+diagnostic *
+diagnostic_begin (diagnostic_manager *diag_mgr,
+                 enum diagnostic_level level)
+{
+  FAIL_IF_NULL (diag_mgr);
+
+  return new diagnostic (*diag_mgr, level);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_set_cwe (diagnostic *diag,
+                   unsigned cwe_id)
+{
+  FAIL_IF_NULL (diag);
+
+  diag->set_cwe (cwe_id);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_rule (diagnostic *diag,
+                    const char *title,
+                    const char *url)
+{
+  FAIL_IF_NULL (diag);
+
+  diag->add_rule (title, url);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_set_location (diagnostic *diag,
+                        const diagnostic_physical_location *loc)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+
+  diag->set_location (loc);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_location (diagnostic *diag,
+                        const diagnostic_physical_location *loc)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+
+  diag->add_location (loc);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_location_with_label (diagnostic *diag,
+                                   const diagnostic_physical_location *loc,
+                                   const char *text)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+  FAIL_IF_NULL (text);
+
+  diag->add_location_with_label (loc, text);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_set_logical_location (diagnostic *diag,
+                                const diagnostic_logical_location *logical_loc)
+{
+  FAIL_IF_NULL (diag);
+
+  diag->set_logical_location (logical_loc);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_fix_it_hint_insert_before (diagnostic *diag,
+                                         const diagnostic_physical_location *loc,
+                                         const char *addition)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+  FAIL_IF_NULL (addition);
+
+  diag->get_manager ().set_line_table_global ();
+  diag->get_rich_location ()->add_fixit_insert_before (as_location_t (loc),
+                                                      addition);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_fix_it_hint_insert_after (diagnostic *diag,
+                                        const diagnostic_physical_location *loc,
+                                        const char *addition)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+  FAIL_IF_NULL (addition);
+
+  diag->get_manager ().set_line_table_global ();
+  diag->get_rich_location ()->add_fixit_insert_after (as_location_t (loc),
+                                                     addition);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_fix_it_hint_replace (diagnostic *diag,
+                                   const diagnostic_physical_location *loc,
+                                   const char *replacement)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+  FAIL_IF_NULL (replacement);
+
+  diag->get_manager ().set_line_table_global ();
+  diag->get_rich_location ()->add_fixit_replace (as_location_t (loc),
+                                                replacement);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_fix_it_hint_delete (diagnostic *diag,
+                                  const diagnostic_physical_location *loc)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+
+  diag->get_manager ().set_line_table_global ();
+  diag->get_rich_location ()->add_fixit_remove (as_location_t (loc));
+}
+
+/* Public entrypoint.  */
+
+diagnostic_execution_path *
+diagnostic_add_execution_path (diagnostic *diag)
+{
+  FAIL_IF_NULL (diag);
+
+  return diag->add_execution_path ();
+}
+
+/* Public entrypoint.  */
+
+diagnostic_execution_path *
+diagnostic_manager_new_execution_path (diagnostic_manager *manager)
+{
+  FAIL_IF_NULL (manager);
+
+  return manager->new_execution_path ();
+}
+
+/* Public entrypoint.  */
+
+extern void
+diagnostic_take_execution_path (diagnostic *diag,
+                               diagnostic_execution_path *path)
+{
+  FAIL_IF_NULL (diag);
+  FAIL_IF_NULL (path);
+
+  return diag->take_execution_path (path);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_execution_path_release (diagnostic_execution_path *path)
+{
+  delete path;
+}
+
+/* Public entrypoint.  */
+
+diagnostic_event_id
+diagnostic_execution_path_add_event (diagnostic_execution_path *path,
+                                    const diagnostic_physical_location *physical_loc,
+                                    const diagnostic_logical_location *logical_loc,
+                                    unsigned stack_depth,
+                                    const char *gmsgid, ...)
+{
+  FAIL_IF_NULL (path);
+  FAIL_IF_NULL (gmsgid);
+
+  va_list args;
+  va_start (args, gmsgid);
+  diagnostic_event_id_t result = path->add_event_va (physical_loc,
+                                                    logical_loc,
+                                                    stack_depth,
+                                                    gmsgid, &args);
+  va_end (args);
+
+  return as_diagnostic_event_id (result);
+}
+
+/* Public entrypoint.  */
+
+diagnostic_event_id
+diagnostic_execution_path_add_event_va (diagnostic_execution_path *path,
+                                       const diagnostic_physical_location *physical_loc,
+                                       const diagnostic_logical_location *logical_loc,
+                                       unsigned stack_depth,
+                                       const char *gmsgid,
+                                       va_list *args)
+{
+  FAIL_IF_NULL (path);
+  FAIL_IF_NULL (gmsgid);
+
+  diagnostic_event_id_t result = path->add_event_va (physical_loc,
+                                                    logical_loc,
+                                                    stack_depth,
+                                                    gmsgid, args);
+  return as_diagnostic_event_id (result);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_finish (diagnostic *diag, const char *gmsgid, ...)
+{
+  FAIL_IF_NULL (diag);
+
+  va_list args;
+  va_start (args, gmsgid);
+  diagnostic_finish_va (diag, gmsgid, &args);
+  va_end (args);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_finish_va (diagnostic *diag, const char *gmsgid, va_list *args)
+{
+  FAIL_IF_NULL (diag);
+
+  if (const char *tool_name
+      = diag->get_manager ().get_client_version_info ()->m_name.get_str ())
+    progname = tool_name;
+  else
+    progname = "progname";
+  auto_diagnostic_group d;
+  diag->get_manager ().emit (*diag, gmsgid, args);
+  delete diag;
+}
diff --git a/gcc/libdiagnostics.h b/gcc/libdiagnostics.h
new file mode 100644 (file)
index 0000000..96086bc
--- /dev/null
@@ -0,0 +1,691 @@
+/* A pure C API for emitting diagnostics.
+   Copyright (C) 2023-2024 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 LIBDIAGNOSTICS_H
+#define LIBDIAGNOSTICS_H
+
+#include <stdarg.h>
+#include <stdio.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+/**********************************************************************
+ Compatibility macros.
+ **********************************************************************/
+
+/* This macro simplifies testing whether we are using gcc, and if it
+   is of a particular minimum version. (Both major & minor numbers are
+   significant.)  This macro will evaluate to 0 if we are not using
+   gcc at all.  */
+#define LIBDIAGNOSTICS_GCC_VERSION (__GNUC__ * 1000 + __GNUC_MINOR__)
+
+/**********************************************************************
+ Macros for attributes.
+ **********************************************************************/
+
+# if (LIBDIAGNOSTICS_GCC_VERSION >= 3003)
+#  define LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL(ARG_NUM) __attribute__ ((__nonnull__ (ARG_NUM)))
+# else
+#  define LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL(ARG_NUM)
+# endif /* GNUC >= 3.3 */
+
+#define LIBDIAGNOSTICS_PARAM_CAN_BE_NULL(ARG_NUM)
+  /* empty; for the human reader */
+
+#define LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING(FMT_ARG_NUM, ARGS_ARG_NUM) \
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (FMT_ARG_NUM)
+  /* In theory we'd also add
+       __attribute__ ((__format__ (__gcc_diag__, FMT_ARG_NUM, ARGS_ARG_NUM)))
+     if LIBDIAGNOSTICS_GCC_VERSION >= 4001
+     However, doing so leads to warnings from -Wformat-diag, which is part
+     of -Wall but undocumented, and much fussier than I'd want to inflict
+     on users of libdiagnostics.  */
+
+/**********************************************************************
+ Data structures and types.
+ All structs within the API are opaque.
+ **********************************************************************/
+
+/* An opaque bundle of state for a client of the library.
+   Has zero of more "sinks" to which diagnostics are emitted.
+   Responsibilities:
+   - location-management
+   - caching of source file content
+   - patch generation.  */
+typedef struct diagnostic_manager diagnostic_manager;
+
+/* Types relating to diagnostic output sinks.  */
+
+typedef struct diagnostic_text_sink diagnostic_text_sink;
+
+/* An enum for determining if we should colorize a text output sink.  */
+enum diagnostic_colorize
+{
+  DIAGNOSTIC_COLORIZE_IF_TTY,
+  DIAGNOSTIC_COLORIZE_NO,
+  DIAGNOSTIC_COLORIZE_YES
+};
+
+/* An enum for choosing the SARIF version for a SARIF output sink.  */
+
+enum diagnostic_sarif_version
+{
+  DIAGNOSTIC_SARIF_VERSION_2_1_0,
+  DIAGNOSTIC_SARIF_VERSION_2_2_PRERELEASE
+};
+
+/* Types relating to "physical" source locations i.e. locations within
+   specific files expressed via line/column.  */
+
+/* Opaque type describing a particular input file.  */
+typedef struct diagnostic_file diagnostic_file;
+
+/* Opaque type representing a key into a database of source locations within
+   a diagnostic_manager.  Locations are created by various API calls into
+   the diagnostic_manager expressing source code points and ranges.  They
+   persist until the diagnostic_manager is released, which cleans them
+   up.
+
+   NULL means "UNKNOWN", and can be returned by the manager as a
+   fallback when a problem occurs (e.g. too many locations).
+
+   A diagnostic_location can be a single point within the source code,
+   such as here (at the the '"' at the start of the string literal):
+
+   |  int i = "foo";
+   |          ^
+
+   or be a range with a start and finish, and a "caret" location.
+
+   |   a = (foo && bar)
+   |       ~~~~~^~~~~~~
+
+   where the caret here is at the first "&", and the start and finish
+   are at the parentheses.  */
+
+typedef struct diagnostic_physical_location diagnostic_physical_location;
+
+/* Types for storing line and column information in text files.
+
+   Both libdiagnostics and emacs number source *lines* starting at 1, but
+   they have differing conventions for *columns*.
+
+   libdiagnostics uses a 1-based convention for source columns,
+   whereas Emacs's M-x column-number-mode uses a 0-based convention.
+
+   For example, an error in the initial, left-hand
+   column of source line 3 is reported by libdiagnostics as:
+
+      some-file.c:3:1: error: ...etc...
+
+   On navigating to the location of that error in Emacs
+   (e.g. via "next-error"),
+   the locus is reported in the Mode Line
+   (assuming M-x column-number-mode) as:
+
+     some-file.c   10%   (3, 0)
+
+   i.e. "3:1:" in libdiagnostics corresponds to "(3, 0)" in Emacs.  */
+
+typedef unsigned int diagnostic_line_num_t;
+typedef unsigned int diagnostic_column_num_t;
+
+/* An opaque type describing a "logical" source location
+   e.g. "within function 'foo'".  */
+
+typedef struct diagnostic_logical_location diagnostic_logical_location;
+
+/* 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 diagnostic_logical_location_kind_t
+{
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_MEMBER,
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_MODULE,
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_NAMESPACE,
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_TYPE,
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_RETURN_TYPE,
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_PARAMETER,
+  DIAGNOSTIC_LOGICAL_LOCATION_KIND_VARIABLE
+};
+
+/* A "diagnostic" is an opaque bundle of state for a particular
+   diagnostic that is being constructed in memory.
+
+   A diagnostic has a primary location and zero or more secondary
+   locations.  For example:
+
+   |   a = (foo && bar)
+   |       ~~~~~^~~~~~~
+
+   This diagnostic has a single diagnostic_location, with the caret
+   at the first "&", and the start/finish at the parentheses.
+
+   Contrast with:
+
+   |   a = (foo && bar)
+   |        ~~~ ^~ ~~~
+
+   This diagnostic has three locations
+   - The primary location (at "&&") has its caret and start location at
+   the first "&" and end at the second "&.
+   - The secondary location for "foo" has its start and finish at the "f"
+   and "o" of "foo"; the caret is not flagged for display, but is perhaps at
+   the "f" of "foo".
+   - Similarly, the other secondary location (for "bar") has its start and
+   finish at the "b" and "r" of "bar"; the caret is not flagged for
+   display, but is perhaps at the"b" of "bar".  */
+typedef struct diagnostic diagnostic;
+
+enum diagnostic_level
+{
+  DIAGNOSTIC_LEVEL_ERROR,
+  DIAGNOSTIC_LEVEL_WARNING,
+  DIAGNOSTIC_LEVEL_NOTE,
+
+  /* A problem where the input is valid, but the tool isn't
+     able to handle it.  */
+  DIAGNOSTIC_LEVEL_SORRY
+};
+
+/* Types for working with execution paths.  */
+typedef struct diagnostic_execution_path diagnostic_execution_path;
+typedef int diagnostic_event_id;
+
+/**********************************************************************
+ API entrypoints.
+ **********************************************************************/
+
+/* Create a new diagnostic_manager.
+   The client needs to call diagnostic_release_manager on it at some
+   point.
+   Note that no output sinks are created by default.  */
+
+extern diagnostic_manager *
+diagnostic_manager_new (void);
+
+/* Release a diagnostic_manager.
+   This will flush output to all of the output sinks, and clean up. */
+
+extern void
+diagnostic_manager_release (diagnostic_manager *)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Optional metadata about the manager.  */
+
+/* Set a string suitable for use as the value of the SARIF "name" property
+   (SARIF v2.1.0 section 3.19.8).  */
+
+extern void
+diagnostic_manager_set_tool_name (diagnostic_manager *diag_mgr,
+                                 const char *value)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Set a string suitable for use as the value of the SARIF "fullName" property
+   (SARIF v2.1.0 section 3.19.9).  */
+
+extern void
+diagnostic_manager_set_full_name (diagnostic_manager *diag_mgr,
+                                 const char *value)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Set a string suitable for use as the value of the SARIF "version" property
+   (SARIF v2.1.0 section 3.19.13).  */
+
+extern void
+diagnostic_manager_set_version_string (diagnostic_manager *diag_mgr,
+                                      const char *value)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Set a string suitable for use as the value of the SARIF "informationUri"
+   property (SARIF v2.1.0 section 3.19.17).  */
+
+extern void
+diagnostic_manager_set_version_url (diagnostic_manager *diag_mgr,
+                                   const char *value)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Destinations for diagnostics.  */
+
+/* Add a new output sink to DIAG_MGR, which writes GCC-style diagnostics
+   to DST_STREAM.
+   Return a borrowed pointer to the sink, which is cleaned up when DIAG_MGR
+   is released.
+   DST_STREAM is borrowed, and must outlive DIAG_MGR.
+   The output for each diagnostic is written and flushed as each
+   diagnostic is finished.  */
+
+extern diagnostic_text_sink *
+diagnostic_manager_add_text_sink (diagnostic_manager *diag_mgr,
+                                 FILE *dst_stream,
+                                 enum diagnostic_colorize colorize)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Functions to manipulate text sinks.  */
+
+/* Enable/disable printing of source text in the text sink.
+   Default: enabled.  */
+
+extern void
+diagnostic_text_sink_set_source_printing_enabled (diagnostic_text_sink *text_sink,
+                                                 int value)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Update colorization of text sink.  */
+
+extern void
+diagnostic_text_sink_set_colorize (diagnostic_text_sink *text_sink,
+                                  enum diagnostic_colorize colorize)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Enable/disable colorization of the characters of source text
+   that are underlined.
+   This should be true for clients that generate range information
+   (so that the ranges of code are colorized),
+   and false for clients that merely specify points within the
+   source code (to avoid e.g. colorizing just the first character in
+   a token, which would look strange).
+   Default: enabled.  */
+
+extern void
+diagnostic_text_sink_set_labelled_source_colorization_enabled (diagnostic_text_sink *text_sink,
+                                                              int value)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Add a new output sink to DIAG_MGR, which writes SARIF of the given
+   version to DST_STREAM.
+
+   The output is not written until DIAG_MGR is released.
+
+   DST_STREAM is borrowed, and must outlive DIAG_MGR.
+
+   For the result to be a valid SARIF file according to the schema,
+   DIAG_MGR must have had diagnostic_manager_set_tool_name called on it.  */
+
+extern void
+diagnostic_manager_add_sarif_sink (diagnostic_manager *diag_mgr,
+                                  FILE *dst_stream,
+                                  const diagnostic_file *main_input_file,
+                                  enum diagnostic_sarif_version version)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+/* Write a patch to DST_STREAM consisting of all fix-it hints
+   on all diagnostics that have been finished on DIAG_MGR.  */
+
+extern void
+diagnostic_manager_write_patch (diagnostic_manager *diag_mgr,
+                               FILE *dst_stream)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Location management.  */
+
+/* Create a new diagnostic_file * for file NAME.
+
+   Repeated calls with matching NAMEs will return the
+   same object.
+
+   If SARIF_SOURCE_LANGUAGE is non-NULL, it specifies a "sourceLanguage"
+   value for the file when use when writing SARIF.
+   See SARIF v2.1.0 Appendix J for suggested values for various
+   programmming languages.  */
+
+extern const diagnostic_file *
+diagnostic_manager_new_file (diagnostic_manager *diag_mgr,
+                            const char *name,
+                            const char *sarif_source_language)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3);
+
+/* Write a representation of FILE to OUT, for debugging.  */
+
+extern void
+diagnostic_manager_debug_dump_file (diagnostic_manager *diag_mgr,
+                                   const diagnostic_file *file,
+                                   FILE *out)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+/* Attempt to create a diagnostic_location representing
+   FILENAME:LINE_NUM, with no column information
+   (thus "the whole line").  */
+
+extern const diagnostic_physical_location *
+diagnostic_manager_new_location_from_file_and_line (diagnostic_manager *diag_mgr,
+                                                   const diagnostic_file *file,
+                                                   diagnostic_line_num_t line_num)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Attempt to create a diagnostic_physical_location representing
+   FILENAME:LINE_NUM:COLUMN_NUM.  */
+
+extern const diagnostic_physical_location *
+diagnostic_manager_new_location_from_file_line_column (diagnostic_manager *diag_mgr,
+                                                      const diagnostic_file *file,
+                                                      diagnostic_line_num_t line_num,
+                                                      diagnostic_column_num_t column_num)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Attempt to create a diagnostic_physical_location representing a
+   range within a source file, with a highlighted "caret" location.
+
+   All must be within the same file, but they can be on different lines.
+
+   For example, consider the location of the binary expression below:
+
+     ...|__________1111111112222222
+     ...|12345678901234567890123456
+     ...|
+     521|int sum (int foo, int bar)
+     522|{
+     523|   return foo + bar;
+     ...|          ~~~~^~~~~
+     524|}
+
+   The location's caret is at the "+", line 523 column 15, but starts
+   earlier, at the "f" of "foo" at column 11.  The finish is at the "r"
+   of "bar" at column 19.  */
+
+extern const diagnostic_physical_location *
+diagnostic_manager_new_location_from_range (diagnostic_manager *diag_mgr,
+                                           const diagnostic_physical_location *loc_caret,
+                                           const diagnostic_physical_location *loc_start,
+                                           const diagnostic_physical_location *loc_end)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (4);
+
+/* Write a representation of LOC to OUT, for debugging.  */
+
+extern void
+diagnostic_manager_debug_dump_location (const diagnostic_manager *diag_mgr,
+                                       const diagnostic_physical_location *loc,
+                                       FILE *out)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+/* A bundle of state describing a logical location in the user's source,
+   such as "in function 'foo'".
+
+   SHORT_NAME can be NULL, or else a string suitable for use by
+   the SARIF logicalLocation "name" property (SARIF v2.1.0 section 3.33.4).
+
+   FULLY_QUALIFIED_NAME can be NULL or else a string  suitable for use by
+   the SARIF logicalLocation "fullyQualifiedName" property
+   (SARIF v2.1.0 section 3.33.5).
+
+   DECORATED_NAME can be NULL or else a string  suitable for use by
+   the SARIF logicalLocation "decoratedName" property
+   (SARIF v2.1.0 section 3.33.6).  */
+
+extern const diagnostic_logical_location *
+diagnostic_manager_new_logical_location (diagnostic_manager *diag_mgr,
+                                        enum diagnostic_logical_location_kind_t kind,
+                                        const diagnostic_logical_location *parent,
+                                        const char *short_name,
+                                        const char *fully_qualified_name,
+                                        const char *decorated_name)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (4)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (5)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (6);
+
+/* Write a representation of LOC to OUT, for debugging.  */
+
+extern void
+diagnostic_manager_debug_dump_logical_location (const diagnostic_manager *diag_mgr,
+                                               const diagnostic_logical_location *loc,
+                                               FILE *out)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+/* Diagnostic groups.  */
+
+/* Begin a diagnostic group.  All diagnostics emitted within
+   DIAG_MGR after the first one will be treated as notes about
+   the initial diagnostic.  */
+
+extern void
+diagnostic_manager_begin_group (diagnostic_manager *diag_mgr)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Finish a diagnostic group.  */
+
+extern void
+diagnostic_manager_end_group (diagnostic_manager *diag_mgr)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Step-by-step creation of a diagnostic.  */
+
+extern diagnostic *
+diagnostic_begin (diagnostic_manager *diag_mgr,
+                 enum diagnostic_level level)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Associate this diagnostic with the given ID within
+   the Common Weakness Enumeration.  */
+
+extern void
+diagnostic_set_cwe (diagnostic *diag,
+                   unsigned cwe_id)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Associate this diagnostic with a particular rule that has been violated
+   (such as in a coding standard, or within a specification).
+   The rule must have at least one of a title and a URL, but these
+   can be NULL.
+   A diagnostic can be associated with zero or more rules.  */
+
+extern void
+diagnostic_add_rule (diagnostic *diag,
+                    const char *title,
+                    const char *url)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3);
+
+/* Set the primary location of DIAG.  */
+
+extern void
+diagnostic_set_location (diagnostic *diag,
+                        const diagnostic_physical_location * loc)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2);
+
+/* Set the primary location of DIAG, with a label.  */
+
+extern void
+diagnostic_set_location_with_label (diagnostic *diag,
+                                   const diagnostic_physical_location *loc,
+                                   const char *fmt, ...)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+/* Add a secondary location to DIAG.  */
+
+extern void
+diagnostic_add_location (diagnostic *diag,
+                        const diagnostic_physical_location * loc)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Add a secondary location to DIAG, with a label.  */
+
+extern void
+diagnostic_add_location_with_label (diagnostic *diag,
+                                   const diagnostic_physical_location *loc,
+                                   const char *text)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+/* Set the logical location of DIAG.  */
+
+extern void
+diagnostic_set_logical_location (diagnostic *diag,
+                                const diagnostic_logical_location *logical_loc)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2);
+
+/* Fix-it hints.  */
+
+extern void
+diagnostic_add_fix_it_hint_insert_before (diagnostic *diag,
+                                         const diagnostic_physical_location *loc,
+                                         const char *addition)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+extern void
+diagnostic_add_fix_it_hint_insert_after (diagnostic *diag,
+                                        const diagnostic_physical_location *loc,
+                                        const char *addition)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+extern void
+diagnostic_add_fix_it_hint_replace (diagnostic *diag,
+                                   const diagnostic_physical_location *loc,
+                                   const char *replacement)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+extern void
+diagnostic_add_fix_it_hint_delete (diagnostic *diag,
+                                  const diagnostic_physical_location *loc)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2);
+
+/* Create and borrow a pointer to an execution path for DIAG.
+   The path is automatically cleaned up when DIAG is finished.  */
+
+extern diagnostic_execution_path *
+diagnostic_add_execution_path (diagnostic *diag)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Create a new execution path.
+   This is owned by the called and must have either
+   diagnostic_take_execution_path or diagnostic_execution_path_release
+   called on it.  */
+
+extern diagnostic_execution_path *
+diagnostic_manager_new_execution_path (diagnostic_manager *manager)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Set DIAG to use PATH as its execution path, taking ownership of PATH.  */
+
+extern void
+diagnostic_take_execution_path (diagnostic *diag,
+                               diagnostic_execution_path *path)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Release ownership of PATH, which must not have been taken
+   by a diagnostic.  */
+
+extern void
+diagnostic_execution_path_release (diagnostic_execution_path *path)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (1);
+
+/* Append an event to the end of PATH.  */
+
+extern diagnostic_event_id
+diagnostic_execution_path_add_event (diagnostic_execution_path *path,
+                                    const diagnostic_physical_location *physical_loc,
+                                    const diagnostic_logical_location *logical_loc,
+                                    unsigned stack_depth,
+                                    const char *fmt, ...)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (5)
+  LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (5, 6);
+
+/* Append an event to the end of PATH.  */
+
+extern diagnostic_event_id
+diagnostic_execution_path_add_event_va (diagnostic_execution_path *path,
+                                       const diagnostic_physical_location *physical_loc,
+                                       const diagnostic_logical_location *logical_loc,
+                                       unsigned stack_depth,
+                                       const char *fmt,
+                                       va_list *args)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (5)
+  LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (5, 0);
+
+/* Emit DIAG to all sinks of its manager, and release DIAG.
+   Use FMT for the message.
+   Note that this uses gcc's pretty-print format, which is *not* printf.
+   TODO: who is responsible for putting FMT through gettext?  */
+
+extern void
+diagnostic_finish (diagnostic *diag, const char *fmt, ...)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+  LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (2, 3);
+
+/* As diagnostic_finish, but with a va_list.  */
+
+extern void
+diagnostic_finish_va (diagnostic *diag, const char *fmt, va_list *args)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+  LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (2, 0);
+
+/* DEFERRED:
+   - thread-safety
+   - plural forms
+   - enum about what a "column number" means (bytes, unichars, etc)
+   - locations within binary files
+   - options and URLs for warnings
+   - enable/disable of warnings by kind
+   - plugin metadata.  */
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif  /* LIBDIAGNOSTICS_H  */
diff --git a/gcc/libdiagnostics.map b/gcc/libdiagnostics.map
new file mode 100644 (file)
index 0000000..cc32b76
--- /dev/null
@@ -0,0 +1,72 @@
+# Linker script for libdiagnostics.so
+#   Copyright (C) 2023-2024 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/>.  */
+
+# The initial release of the library.
+LIBDIAGNOSTICS_ABI_0
+{
+  global:
+    # Keep this list in order of decls in header file.
+    diagnostic_manager_new;
+    diagnostic_manager_release;
+    diagnostic_manager_set_tool_name;
+    diagnostic_manager_set_full_name;
+    diagnostic_manager_set_version_string;
+    diagnostic_manager_set_version_url;
+    diagnostic_manager_add_text_sink;
+    diagnostic_text_sink_set_source_printing_enabled;
+    diagnostic_text_sink_set_colorize;
+    diagnostic_text_sink_set_labelled_source_colorization_enabled;
+    diagnostic_manager_add_sarif_sink;
+    diagnostic_manager_write_patch;
+    diagnostic_manager_new_file;
+    diagnostic_manager_debug_dump_file;
+    diagnostic_manager_new_location_from_file_and_line;
+    diagnostic_manager_new_location_from_file_line_column;
+    diagnostic_manager_new_location_from_range;
+    diagnostic_manager_debug_dump_location;
+    diagnostic_manager_new_logical_location;
+    diagnostic_manager_debug_dump_logical_location;
+    diagnostic_manager_begin_group;
+    diagnostic_manager_end_group;
+    diagnostic_begin;
+    diagnostic_set_cwe;
+    diagnostic_add_rule;
+    diagnostic_set_location;
+    diagnostic_set_location_with_label;
+    diagnostic_add_location;
+    diagnostic_add_location_with_label;
+    diagnostic_set_logical_location;
+    diagnostic_add_fix_it_hint_insert_before;
+    diagnostic_add_fix_it_hint_insert_after;
+    diagnostic_add_fix_it_hint_replace;
+    diagnostic_add_fix_it_hint_delete;
+
+    diagnostic_add_execution_path;
+    diagnostic_manager_new_execution_path;
+    diagnostic_take_execution_path;
+    diagnostic_execution_path_release;
+    diagnostic_execution_path_add_event;
+    diagnostic_execution_path_add_event_va;
+
+    diagnostic_finish;
+    diagnostic_finish_va;
+
+  local: *;
+};
diff --git a/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp
new file mode 100644 (file)
index 0000000..d29a469
--- /dev/null
@@ -0,0 +1,296 @@
+# Test code for libdiagnostics.so
+#
+# We will compile each of libdiagnostics.dg/test-*.{c,cc} into an executable
+# dynamically linked against libdiagnostics.so, and then run each
+# such executable.
+#
+# These executables call into the libdiagnostics.so API to emit diagnostics,
+# sometimes in text form, and other times in SARIF form.
+
+# Kludge alert:
+# We need g++_init so that it can find the stdlib include path.
+#
+# g++_init (in lib/g++.exp) uses g++_maybe_build_wrapper,
+# which normally comes from the definition of
+# ${tool}_maybe_build_wrapper within lib/wrapper.exp.
+#
+# However, for us, ${tool} is "libdiagnostics".
+# Hence we load wrapper.exp with tool == "g++", so that
+# g++_maybe_build_wrapper is defined.
+set tool g++
+load_lib wrapper.exp
+set tool libdiagnostics
+
+load_lib dg.exp
+load_lib prune.exp
+load_lib target-supports.exp
+load_lib gcc-defs.exp
+load_lib timeout.exp
+load_lib target-libpath.exp
+load_lib gcc.exp
+load_lib g++.exp
+load_lib dejagnu.exp
+load_lib target-supports-dg.exp
+load_lib valgrind.exp
+load_lib scansarif.exp
+load_lib dg-test-cleanup.exp
+
+# The default do-what keyword.
+set dg-do-what-default compile
+
+# Adapted from jit.exp.
+#
+# Execute the executable file.
+#    Returns:
+#      A "" (empty) string if everything worked, or an error message
+#      if there was a problem.
+#
+proc fixed_host_execute {args} {
+    global env
+    global text
+    global spawn_id
+
+    verbose "fixed_host_execute: $args"
+
+    set timeoutmsg "Timed out: Never got started, "
+    set timeout 100
+    set file all
+    set timetol 0
+    set arguments ""
+
+    if { [llength $args] == 0} {
+       set executable $args
+    } else {
+       set executable [lindex $args 0]
+       set params [lindex $args 1]
+    }
+
+    verbose "The executable is $executable" 2
+    if {![file exists ${executable}]} {
+       perror "The executable, \"$executable\" is missing" 0
+       return "No source file found"
+    } elseif {![file executable ${executable}]} {
+       perror "The executable, \"$executable\" is not usable" 0
+       return "Bad executable found"
+    }
+
+    verbose "params: $params" 2
+
+    # spawn the executable and look for the DejaGnu output messages from the
+    # test case.
+    # spawn -noecho -open [open "|./${executable}" "r"]
+
+    # Run under valgrind if RUN_UNDER_VALGRIND is present in the environment.
+    # Note that it's best to configure gcc with --enable-valgrind-annotations
+    # when testing under valgrind.
+    set run_under_valgrind [info exists env(RUN_UNDER_VALGRIND)]
+    if $run_under_valgrind {
+       set valgrind_logfile "${executable}.valgrind.txt"
+       set valgrind_params {"valgrind"}
+       lappend valgrind_params "--leak-check=full"
+       lappend valgrind_params "--log-file=${valgrind_logfile}"
+    } else {
+       set valgrind_params {}
+    }
+    verbose "valgrind_params: $valgrind_params" 2
+
+    set args ${valgrind_params}
+    lappend args "./${executable}"
+    set args [concat $args ${params}]
+    verbose "args: $args" 2
+
+    set status [catch "exec -keepnewline $args" exe_output]
+    verbose "Test program returned $exe_output" 2
+    if $run_under_valgrind {
+       upvar 2 name name
+       parse_valgrind_logfile $name $valgrind_logfile fail
+    }
+
+    # We don't do prune_gcc_output here, as we want
+    # to check *exactly* what we get from libdiagnostics
+
+    return $exe_output
+}
+
+# (end of code from dejagnu.exp)
+
+# GCC_UNDER_TEST is needed by gcc_target_compile
+global GCC_UNDER_TEST
+if ![info exists GCC_UNDER_TEST] {
+    set GCC_UNDER_TEST "[find_gcc]"
+}
+
+g++_init
+
+# Initialize dg.
+dg-init
+
+# Gather a list of all tests.
+
+# C and C++ tests within the testsuite: gcc/testsuite/libdiagnostics.dg/test-*.{c,c++}
+set c_tests [find $srcdir/$subdir test-*.c]
+set cxx_tests [find $srcdir/$subdir test-*.cc]
+set tests [concat $c_tests $cxx_tests]
+
+verbose "tests: $tests"
+
+# Expand "SRCDIR" within ARG to the location of the top-level
+# src directory
+
+proc diagnostics-expand-vars {arg} {
+    verbose "diagnostics-expand-vars: $arg"
+    global srcdir
+    verbose " srcdir: $srcdir"
+    # "srcdir" is that of the gcc/testsuite directory, so
+    # we need to go up two levels.
+    set arg [string map [list "SRCDIR" $srcdir/../..] $arg]
+    verbose " new arg: $arg"
+    return $arg
+}
+
+# Parameters used when invoking the executables built from the test cases.
+
+global diagnostics-exe-params
+set diagnostics-exe-params {}
+
+# Set "diagnostics-exe-params", expanding "SRCDIR" in each arg to the location of
+# the top-level srcdir.
+
+proc dg-diagnostics-set-exe-params { args } {
+    verbose "dg-diagnostics-set-exe-params: $args"
+
+    global diagnostics-exe-params
+    set diagnostics-exe-params {}
+    # Skip initial arg (line number)
+    foreach arg [lrange $args 1 [llength $args] ] {
+       lappend diagnostics-exe-params [diagnostics-expand-vars $arg]
+    }
+}
+
+proc libdiagnostics-dg-test { prog do_what extra_tool_flags } {
+    verbose "within libdiagnostics-dg-test..."
+    verbose "  prog: $prog"
+    verbose "  do_what: $do_what"
+    verbose "  extra_tool_flags: $extra_tool_flags"
+
+    global dg-do-what-default
+    set dg-do-what [list ${dg-do-what-default} "" P]
+
+    # If we're not supposed to try this test on this target, we're done.
+    if { [lindex ${dg-do-what} 1] == "N" } {
+       unsupported "$name"
+       verbose "$name not supported on this target, skipping it" 3
+       return
+    }
+
+    # Determine what to name the built executable.
+    #
+    # We simply append .exe to the filename, e.g.
+    #  "test-foo.c.exe"
+    # since some testcases exist in both
+    #  "test-foo.c" and
+    #  "test-foo.cc"
+    # variants, and we don't want them to clobber each other's
+    # executables.
+    #
+    # This also ensures that the source name makes it into the
+    # pass/fail output, so that we can distinguish e.g. which test-foo
+    # is failing.
+    set output_file "[file tail $prog].exe"
+    verbose "output_file: $output_file"
+
+    # Create the test executable:
+    set extension [file extension $prog]
+    if {$extension == ".cc"} {
+       set compilation_function "g++_target_compile"
+    } else {
+       set compilation_function "gcc_target_compile"
+    }
+    set options "{additional_flags=$extra_tool_flags}"
+    verbose "compilation_function=$compilation_function"
+    verbose "options=$options"
+
+    set comp_output [$compilation_function $prog $output_file \
+                        "executable" $options]
+    upvar 1 name name
+    if ![libdiagnostics_check_compile "$name" "initial compilation" \
+           $output_file $comp_output] then {
+      return
+    }
+
+    # Run the test executable.
+
+    # We need to set LD_LIBRARY_PATH so that the test files can find
+    # libdiagnostics.so
+    # Do this using set_ld_library_path_env_vars from target-libpath.exp
+    # We will restore the old value later using
+    # restore_ld_library_path_env_vars.
+
+    # Unfortunately this API only supports a single saved value, rather
+    # than a stack, and g++_init has already called into this API,
+    # injecting the appropriate value for LD_LIBRARY_PATH for finding
+    # the built copy of libstdc++.
+    # Hence the call to restore_ld_library_path_env_vars would restore
+    # the *initial* value of LD_LIBRARY_PATH, and attempts to run
+    # a C++ testcase after running any prior testcases would thus look
+    # in the wrong place for libstdc++.  This led to failures at startup
+    # of the form:
+    #   ./tut01-hello-world.cc.exe: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by ./tut01-hello-world.cc.exe)
+    # when the built libstdc++ is more recent that the system libstdc++.
+    #
+    # As a workaround, reset the variable "orig_environment_saved" within
+    # target-libpath.exp, so that the {set|restore}_ld_library_path_env_vars
+    # API saves/restores the current value of LD_LIBRARY_PATH (as set up
+    # by g++_init).
+    global orig_environment_saved
+    set orig_environment_saved 0
+
+    global ld_library_path
+    global base_dir
+    set ld_library_path "$base_dir/../../"
+    set_ld_library_path_env_vars
+
+    global diagnostics-exe-params
+    set args ${diagnostics-exe-params}
+    set diagnostics-exe-params {}
+
+    set exe_output [fixed_host_execute $output_file $args ]
+    verbose "exe_output: $exe_output"
+
+    restore_ld_library_path_env_vars
+
+    # Analyze the output from the executable.  To some what extent this
+    # is duplicating prune_gcc_output, but we're looking for *precise*
+    # output, so we can't reuse prune_gcc_output.
+
+    global testname_with_flags
+    set testname_with_flags $name
+
+    # Handle any freeform regexps.
+    set exe_output [handle-dg-regexps $exe_output]
+
+    # Call into multiline.exp to handle any multiline output directives.
+    set exe_output [handle-multiline-outputs $exe_output]
+
+    # Normally we would return $exe_output and $output_file to the
+    # caller, which would delete $output_file, the generated executable.
+    # If we need to debug, it's handy to be able to suppress this behavior,
+    # keeping the executable around.
+    
+    global env
+    set preserve_executables [info exists env(PRESERVE_EXECUTABLES)]
+    if $preserve_executables {
+       set output_file ""
+    }
+
+    return [list $exe_output $output_file]
+}
+
+set DEFAULT_CFLAGS "-I$srcdir/.. -ldiagnostics -g -Wall -Werror"
+
+# Main loop.  This will invoke jig-dg-test on each test-*.c file.
+dg-runtest $tests "" $DEFAULT_CFLAGS
+
+# All done.
+dg-finish
diff --git a/gcc/testsuite/libdiagnostics.dg/sarif.py b/gcc/testsuite/libdiagnostics.dg/sarif.py
new file mode 100644 (file)
index 0000000..7daf35b
--- /dev/null
@@ -0,0 +1,23 @@
+import json
+import os
+
+def sarif_from_env():
+    # return parsed JSON content a SARIF_PATH file
+    json_filename = os.environ['SARIF_PATH']
+    json_filename += '.sarif'
+    print('json_filename: %r' % json_filename)
+    with open(json_filename) as f:
+        json_data = f.read()
+    return json.loads(json_data)
+
+def get_location_artifact_uri(location):
+    return location['physicalLocation']['artifactLocation']['uri']
+
+def get_location_physical_region(location):
+    return location['physicalLocation']['region']
+
+def get_location_snippet_text(location):
+    return location['physicalLocation']['contextRegion']['snippet']['text']
+
+def get_location_relationships(location):
+    return location['relationships']
diff --git a/gcc/testsuite/libdiagnostics.dg/test-dump.c b/gcc/testsuite/libdiagnostics.dg/test-dump.c
new file mode 100644 (file)
index 0000000..9f0576d
--- /dev/null
@@ -0,0 +1,69 @@
+/* Usage example of dump API.  */
+
+#include "libdiagnostics.h"
+
+const int line_num = 42;
+
+int
+main ()
+{
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, "foo.c", "c");
+
+  fprintf (stderr, "file: ");
+  diagnostic_manager_debug_dump_file (diag_mgr, file, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+file: file(name="foo.c", sarif_source_language="c")
+     { dg-end-multiline-output "" } */
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                         loc_start,
+                                         loc_start,
+                                         loc_end);
+
+  fprintf (stderr, "loc_start: ");
+  diagnostic_manager_debug_dump_location (diag_mgr, loc_start, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+loc_start: foo.c:42:8:
+     { dg-end-multiline-output "" } */
+
+  fprintf (stderr, "loc_end: ");
+  diagnostic_manager_debug_dump_location (diag_mgr, loc_end, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+loc_end: foo.c:42:19:
+     { dg-end-multiline-output "" } */
+
+  fprintf (stderr, "loc_range: ");
+  diagnostic_manager_debug_dump_location (diag_mgr, loc_range, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+loc_range: foo.c:42:8:
+     { dg-end-multiline-output "" } */
+
+  const diagnostic_logical_location *logical_loc
+    = diagnostic_manager_new_logical_location (diag_mgr,
+                                              DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+                                              NULL, /* parent */
+                                              "test_short_name",
+                                              "test_qualified_name",
+                                              "test_decorated_name");
+
+  fprintf (stderr, "logical_loc: ");
+  diagnostic_manager_debug_dump_logical_location (diag_mgr, logical_loc, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+logical_loc: logical_location(kind=function, short_name="test_short_name", fully_qualified_name="test_qualified_name", decorated_name="test_decorated_name")
+     { dg-end-multiline-output "" } */
+
+  diagnostic_manager_release (diag_mgr);
+  return 0;
+};
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-c.py b/gcc/testsuite/libdiagnostics.dg/test-error-c.py
new file mode 100644 (file)
index 0000000..1206be8
--- /dev/null
@@ -0,0 +1,54 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 17
+expected_file_name = 'test-error.c'
+
+def test_sarif_output(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == False
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'].endswith(expected_file_name)
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    assert '#include <foo.h>' in artifacts[0]['contents']['text']
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "can't find 'foo.h'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith(expected_file_name)
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 11
+    assert phys_loc['region']['endColumn'] == 16
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '#include <foo.h>\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py b/gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py
new file mode 100644 (file)
index 0000000..bed21ca
--- /dev/null
@@ -0,0 +1,50 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 18
+
+def test_sarif_output_for_note(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-error-with-note.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "can't find 'foo.h'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-error-with-note.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 11
+    assert phys_loc['region']['endColumn'] == 16
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '#include <foo.h>\n'
+
+    assert len(results[0]['relatedLocations']) == 1
+    note = results[0]['relatedLocations'][0]
+    phys_loc = note['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-error-with-note.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 11
+    assert phys_loc['region']['endColumn'] == 16
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '#include <foo.h>\n'
+    assert note['message']['text'] == 'have you looked behind the couch?'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note.c b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.c
new file mode 100644 (file)
index 0000000..f406743
--- /dev/null
@@ -0,0 +1,76 @@
+/* Example of emitting an error with an associated note.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:18:11: error: can't find 'foo.h'
+    6 | #include <foo.h>
+      |           ^~~~~
+PATH/test-error-with-note.c:18:11: note: have you looked behind the couch?
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122
+123456789012345678901
+#include <foo.h>
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-error-with-note.c.exe",
+             "test-error-with-note.c.sarif",
+             __FILE__, "c");
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            line_num,
+                                                            11);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            line_num,
+                                                            15);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                                 loc_start,
+                                                 loc_start,
+                                                 loc_end);
+
+  /* begin quoted source */
+  diagnostic_manager_begin_group (diag_mgr);
+  
+  diagnostic *err = diagnostic_begin (diag_mgr,
+                                     DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (err, loc_range);
+  diagnostic_finish (err, "can't find %qs", "foo.h");
+
+  diagnostic *note = diagnostic_begin (diag_mgr, DIAGNOSTIC_LEVEL_NOTE);
+  diagnostic_set_location (note, loc_range);
+  diagnostic_finish (note, "have you looked behind the couch?");
+
+  diagnostic_manager_end_group (diag_mgr);
+  /* end quoted source */
+
+  return end_test ();
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error-with-note.c:18:11: error: can't find 'foo.h'" }
+   { dg-begin-multiline-output "" }
+   18 | #include <foo.h>
+      |           ^~~~~
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-error-with-note.c:18:11: note: have you looked behind the couch." } */
+
+/* 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 test-error-with-note.c "test-error-with-note-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc
new file mode 100644 (file)
index 0000000..e211297
--- /dev/null
@@ -0,0 +1,55 @@
+/* C++ example of emitting an error with an associated note.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:17:8: error: can't find 'foo'
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+PATH/test-error-with-note.c:17:8: note: have you looked behind the couch?
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics++.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  libdiagnostics::manager mgr;
+
+  auto file = mgr.new_file (__FILE__, "c");
+
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+
+  auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8);
+  auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19);
+  auto loc_range = mgr.new_location_from_range (loc_start,
+                                               loc_start,
+                                               loc_end);
+
+  libdiagnostics::group g (mgr);
+  
+  auto err (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+  err.set_location (loc_range);
+  err.finish ("can't find %qs", "foo");
+
+  auto note = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_NOTE);
+  note.set_location (loc_range);
+  note.finish ("have you looked behind the couch?");
+
+  return 0;
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error-with-note.cc:17:8: error: can't find 'foo'" }
+   { dg-begin-multiline-output "" }
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-error-with-note.cc:17:8: note: have you looked behind the couch\\\?" } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error.c b/gcc/testsuite/libdiagnostics.dg/test-error.c
new file mode 100644 (file)
index 0000000..6f17ce2
--- /dev/null
@@ -0,0 +1,61 @@
+/* Example of emitting an error.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:6:11: error: can't find 'foo.h'
+    6 | #include <foo.h>
+      |           ^~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________1111111
+1234567890123456
+#include <foo.h>
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-error.c.exe",
+             "test-error.c.sarif",
+             __FILE__, "c");
+
+  /* begin quoted source */
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, main_file, line_num, 11);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, main_file, line_num, 15);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                                 loc_start,
+                                                 loc_start,
+                                                 loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+  
+  diagnostic_finish (d, "can't find %qs", "foo.h");
+  /* end quoted source */
+
+  return end_test ();
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error.c:17:11: error: can't find 'foo.h'" }
+   { dg-begin-multiline-output "" }
+   17 | #include <foo.h>
+      |           ^~~~~
+   { dg-end-multiline-output "" } */
+
+/* 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 test-error.c "test-error-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error.cc b/gcc/testsuite/libdiagnostics.dg/test-error.cc
new file mode 100644 (file)
index 0000000..6f919e4
--- /dev/null
@@ -0,0 +1,47 @@
+/* C++ example of emitting an error.
+
+   Intended output is similar to:
+
+PATH/test-error.cc:16:8: error: can't find 'foo'
+   16 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics++.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  libdiagnostics::manager mgr;
+
+  auto file = mgr.new_file (__FILE__, "c");
+
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+
+  auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8);
+  auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19);
+  auto loc_range = mgr.new_location_from_range (loc_start,
+                                               loc_start,
+                                               loc_end);
+
+  libdiagnostics::diagnostic d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+  d.set_location (loc_range);
+  d.finish ("can't find %qs", "foo");
+
+  return 0;
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error.cc:16:8: error: can't find 'foo'" }
+   { dg-begin-multiline-output "" }
+   16 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-example-1.c b/gcc/testsuite/libdiagnostics.dg/test-example-1.c
new file mode 100644 (file)
index 0000000..58a146b
--- /dev/null
@@ -0,0 +1,43 @@
+/* begin quoted source */
+/* Minimal usage example.  */
+#include "libdiagnostics.h"
+
+static diagnostic_manager *diag_mgr;
+
+static void
+init_diagnostics (void)
+{
+  diag_mgr = diagnostic_manager_new ();
+  diagnostic_manager_add_text_sink (diag_mgr, stderr,
+                                   DIAGNOSTIC_COLORIZE_IF_TTY);
+}
+
+static void
+finish_diagnostics (void)
+{
+  diagnostic_manager_release (diag_mgr);
+}
+
+static void
+do_stuff (void)
+{
+  const char *username = "Dave";
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_finish (d,
+                    "I'm sorry %s, I'm afraid I can't do that",
+                    username);
+}
+
+int
+main ()
+{
+  init_diagnostics ();
+
+  do_stuff ();
+
+  finish_diagnostics ();
+};
+/* end quoted source */
+
+/* { dg-regexp "progname: error: I'm sorry Dave, I'm afraid I can't do that" } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py
new file mode 100644 (file)
index 0000000..f3dc71c
--- /dev/null
@@ -0,0 +1,46 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output_with_fixes(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-fix-it-hint.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "unknown field 'colour'; did you mean 'color'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-fix-it-hint.c')
+    assert phys_loc['region']['startLine'] == 19
+    assert phys_loc['region']['startColumn'] == 13
+    assert phys_loc['region']['endColumn'] == 19
+    assert phys_loc['contextRegion']['startLine'] == 19
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  return p->colour;\n'
+
+    assert len(results[0]['fixes']) == 1
+    fix = results[0]['fixes'][0]
+    assert len(fix['artifactChanges']) == 1
+    change = fix['artifactChanges'][0]
+    assert change['artifactLocation']['uri'].endswith('test-fix-it-hint.c')
+    assert len(change['replacements']) == 1
+    replacement = change['replacements'][0]
+    assert replacement['deletedRegion'] == phys_loc['region']
+    assert replacement['insertedContent']['text'] == 'color'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c
new file mode 100644 (file)
index 0000000..0d8ee46
--- /dev/null
@@ -0,0 +1,83 @@
+/* Example of a fix-it hint, including patch generation.
+
+   Intended output is similar to:
+
+PATH/test-fix-it-hint.c:19:13: error: unknown field 'colour'; did you mean 'color'
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+
+   along with the equivalent in SARIF, and a generated patch (on stderr) to
+   make the change.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________11111111112
+12345678901234567890
+  return p->colour;
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-fix-it-hint.c.exe",
+             "test-fix-it-hint.c.sarif",
+             __FILE__, "c");
+
+  const diagnostic_physical_location *loc_token
+    = make_range (diag_mgr, main_file, line_num, 13, 18);
+
+  /* begin quoted source */
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_token);
+
+  diagnostic_add_fix_it_hint_replace (d, loc_token, "color");
+  
+  diagnostic_finish (d, "unknown field %qs; did you mean %qs", "colour", "color");
+  /* end quoted source */
+
+  diagnostic_manager_write_patch (diag_mgr, stderr);
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-fix-it-hint.c:19:13: error: unknown field 'colour'; did you mean 'color'" }
+   { dg-begin-multiline-output "" }
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+   { dg-end-multiline-output "" } */
+
+/* Verify the output from diagnostic_manager_write_patch.
+   We expect the patch to begin with a header, containing this
+   source filename, via an absolute path.
+   Given the path, we can only capture it via regexps.  */
+/* { dg-regexp "\\-\\-\\- .*" } */
+/* { dg-regexp "\\+\\+\\+ .*" } */
+/* Use #if 0/#endif rather than comments, to allow the text to contain
+   a comment.  */
+#if 0
+{ dg-begin-multiline-output "" }
+@@ -16,7 +16,7 @@
+ /*
+ _________11111111112
+ 12345678901234567890
+-  return p->colour;
++  return p->color;
+ */
+ const int line_num = __LINE__ - 2;
+{ dg-end-multiline-output "" }
+#endif
+
+/* 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 test-fix-it-hint.c "test-fix-it-hint-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc
new file mode 100644 (file)
index 0000000..92c7f07
--- /dev/null
@@ -0,0 +1,74 @@
+/* C++ example of a fix-it hint, including patch generation.
+
+   Intended output is similar to:
+
+PATH/test-fix-it-hint.cc:19:13: error: unknown field 'colour'; did you mean 'color'
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+
+   along with the equivalent in SARIF, and a generated patch (on stderr) to
+   make the change.  */
+
+#include "libdiagnostics++.h"
+#include "test-helpers++.h"
+
+/*
+_________11111111112
+12345678901234567890
+  return p->colour;
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  libdiagnostics::manager mgr;
+
+  auto file = mgr.new_file (__FILE__, "c");
+
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+
+  auto loc_token = make_range (mgr, file, line_num, 13, 18);
+
+  auto d = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR);
+  d.set_location (loc_token);
+
+  d.add_fix_it_hint_replace (loc_token, "color");
+  
+  d.finish ("unknown field %qs; did you mean %qs", "colour", "color");
+
+  mgr.write_patch (stderr);
+
+  return 0;
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-fix-it-hint.cc:19:13: error: unknown field 'colour'; did you mean 'color'" }
+   { dg-begin-multiline-output "" }
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+   { dg-end-multiline-output "" } */
+
+/* Verify the output from diagnostic_manager_write_patch.
+   We expect the patch to begin with a header, containing this
+   source filename, via an absolute path.
+   Given the path, we can only capture it via regexps.  */
+/* { dg-regexp "\\-\\-\\- .*" } */
+/* { dg-regexp "\\+\\+\\+ .*" } */
+/* Use #if 0/#endif rather than comments, to allow the text to contain
+   a comment.  */
+#if 0
+{ dg-begin-multiline-output "" }
+@@ -16,7 +16,7 @@
+ /*
+ _________11111111112
+ 12345678901234567890
+-  return p->colour;
++  return p->color;
+ */
+ const int line_num = __LINE__ - 2;
+{ dg-end-multiline-output "" }
+#endif
diff --git a/gcc/testsuite/libdiagnostics.dg/test-helpers++.h b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h
new file mode 100644 (file)
index 0000000..c8ff2de
--- /dev/null
@@ -0,0 +1,28 @@
+/* Common utility code shared between test cases.  */
+
+#ifndef TEST_HELPERSPP_H
+#define TEST_HELPERSPP_H
+
+namespace libdiagnostics {
+
+inline physical_location
+make_range (manager &mgr,
+           file f,
+           line_num_t line_num,
+           column_num_t start_column,
+           column_num_t end_column)
+{
+  auto loc_start = mgr.new_location_from_file_line_column (f,
+                                                          line_num,
+                                                          start_column);
+  auto loc_end = mgr.new_location_from_file_line_column (f,
+                                                        line_num,
+                                                        end_column);
+  return mgr.new_location_from_range (loc_start,
+                                     loc_start,
+                                     loc_end);
+}
+
+} // namespace libdiagnostics
+
+#endif /* #ifndef TEST_HELPERSPP_H */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-helpers.h b/gcc/testsuite/libdiagnostics.dg/test-helpers.h
new file mode 100644 (file)
index 0000000..a578850
--- /dev/null
@@ -0,0 +1,72 @@
+/* Common utility code shared between test cases.  */
+
+#ifndef TEST_HELPERS_H
+#define TEST_HELPERS_H
+
+const diagnostic_physical_location *
+make_range (diagnostic_manager *diag_mgr,
+           const diagnostic_file *file,
+           diagnostic_line_num_t line_num,
+           diagnostic_column_num_t start_column,
+           diagnostic_column_num_t end_column)
+{
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            file,
+                                                            line_num,
+                                                            start_column);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            file,
+                                                            line_num,
+                                                            end_column);
+  return diagnostic_manager_new_location_from_range (diag_mgr,
+                                                    loc_start,
+                                                    loc_start,
+                                                    loc_end);
+}
+
+/* A begin_test/end_test pair to consolidate the code shared by tests:
+   create a diagnostic_manager, a main file, a text sink, and a SARIF sink,
+   and clean these up after emitting zero or more diagnostics.  */
+
+static diagnostic_manager *diag_mgr;
+static const diagnostic_file *main_file;
+static FILE *sarif_outfile;
+
+static void
+begin_test (const char *tool_name,
+           const char *sarif_output_name,
+           const char *main_file_name,
+           const char *source_language)
+{
+  diag_mgr = diagnostic_manager_new ();
+
+  /* We need to set this for generated .sarif files to validate
+     against the schema.  */
+  diagnostic_manager_set_tool_name (diag_mgr, tool_name);
+
+  main_file = diagnostic_manager_new_file (diag_mgr,
+                                          main_file_name,
+                                          source_language);
+
+  diagnostic_manager_add_text_sink (diag_mgr, stderr,
+                                   DIAGNOSTIC_COLORIZE_IF_TTY);
+  sarif_outfile = fopen (sarif_output_name, "w");
+  if (sarif_outfile)
+    diagnostic_manager_add_sarif_sink (diag_mgr,
+                                      sarif_outfile,
+                                      main_file,
+                                      DIAGNOSTIC_SARIF_VERSION_2_1_0);
+}
+
+static int
+end_test (void)
+{
+  diagnostic_manager_release (diag_mgr);
+  if (sarif_outfile)
+    fclose (sarif_outfile);
+  return 0;
+}
+
+#endif /* #ifndef TEST_HELPERS_H */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c
new file mode 100644 (file)
index 0000000..39978b2
--- /dev/null
@@ -0,0 +1,71 @@
+/* Example of multiple locations, with labelling of ranges.
+
+   Intended output is similar to:
+
+PATH/test-labelled-ranges.c:9:6: error: mismatching types: 'int' and 'const char *'
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________11111111112
+12345678901234567890
+  42 + "foo"
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-labelled-ranges.c.exe",
+             "test-labelled-ranges.c.sarif",
+             __FILE__, "c");
+
+  const diagnostic_physical_location *loc_operator
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            line_num,
+                                                            6);
+
+  /* begin quoted source */
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_operator);
+  diagnostic_add_location_with_label (d,
+                                     make_range (diag_mgr,
+                                                 main_file,
+                                                 line_num, 3, 4),
+                                     "int");
+  diagnostic_add_location_with_label (d,
+                                     make_range (diag_mgr,
+                                                 main_file,
+                                                 line_num, 8, 12),
+                                     "const char *");
+  
+  diagnostic_finish (d, "mismatching types: %qs and %qs", "int", "const char *");
+  /* end quoted source */
+  
+  return end_test ();
+}
+
+/* Check the output from the text sink.  */
+/* { dg-regexp "\[^\n\r\]+test-labelled-ranges.c:19:6: error: mismatching types: 'int' and 'const char \\*'" } */
+/* { dg-begin-multiline-output "" }
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+   { dg-end-multiline-output "" } */
+
+/* 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 test-labelled-ranges.c "test-labelled-ranges.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc
new file mode 100644 (file)
index 0000000..1c1c050
--- /dev/null
@@ -0,0 +1,64 @@
+/* C++ example of multiple locations, with labelling of ranges.
+
+   Intended output is similar to:
+
+PATH/test-labelled-ranges.cc:19:6: error: mismatching types: 'int' and 'const char *'
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics++.h"
+#include "test-helpers++.h"
+
+/*
+_________11111111112
+12345678901234567890
+  42 + "foo"
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  FILE *sarif_outfile;
+  libdiagnostics::manager mgr;
+  mgr.set_tool_name ("test-labelled-ranges.cc.exe");
+
+  libdiagnostics::file file = mgr.new_file (__FILE__, "c");
+  
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+  sarif_outfile = fopen ("test-labelled-ranges.cc.sarif", "w");
+  if (sarif_outfile)
+    mgr.add_sarif_sink (sarif_outfile, file, DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+  auto loc_operator = mgr.new_location_from_file_line_column (file, line_num, 6);
+
+  auto d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+  d.set_location (loc_operator);
+  d.add_location_with_label (make_range (mgr, file, line_num, 3, 4),
+                            "int");
+  d.add_location_with_label (make_range (mgr, file, line_num, 8, 12),
+                            "const char *");
+  d.finish ("mismatching types: %qs and %qs", "int", "const char *");
+  
+  return 0;
+}
+
+/* Check the output from the text sink.  */
+/* { dg-regexp "\[^\n\r\]+test-labelled-ranges.cc:19:6: error: mismatching types: 'int' and 'const char \\*'" } */
+/* { dg-begin-multiline-output "" }
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+   { dg-end-multiline-output "" } */
+
+/* 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 test-labelled-ranges.cc "test-labelled-ranges.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py
new file mode 100644 (file)
index 0000000..dce404f
--- /dev/null
@@ -0,0 +1,48 @@
+# Verify the SARIF output of test-labelled-ranges.{c,cc}
+
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] \
+        == "mismatching types: 'int' and 'const char *'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['region']['startLine'] == 19
+    assert phys_loc['region']['startColumn'] == 6
+    assert phys_loc['region']['endColumn'] == 7
+    assert phys_loc['contextRegion']['startLine'] == 19
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  42 + "foo"\n'
+
+    annotations = location['annotations']
+    assert len(annotations) == 2
+
+    assert annotations[0]['startLine'] == 19
+    assert annotations[0]['startColumn'] == 3
+    assert annotations[0]['endColumn'] == 5
+    assert annotations[0]['message']['text'] == 'int'
+
+    assert annotations[1]['startLine'] == 19
+    assert annotations[1]['startColumn'] == 8
+    assert annotations[1]['endColumn'] == 13
+    assert annotations[1]['message']['text'] == 'const char *'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py b/gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py
new file mode 100644 (file)
index 0000000..7448a1e
--- /dev/null
@@ -0,0 +1,37 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output_with_logical_location(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-logical-location.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+
+    result = results[0]
+    assert result['ruleId'] == 'error'
+    assert result['level'] == 'error'
+    assert result['message']['text'] == "can't find 'foo'"
+    assert len(result['locations']) == 1
+    location = result['locations'][0]
+
+    assert len(location['logicalLocations']) == 1
+    logical_loc = location['logicalLocations'][0]
+    assert logical_loc['name'] == 'test_short_name'
+    assert logical_loc['fullyQualifiedName'] == 'test_qualified_name'
+    assert logical_loc['decoratedName'] == 'test_decorated_name'
+    assert logical_loc['kind'] == 'function'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-logical-location.c b/gcc/testsuite/libdiagnostics.dg/test-logical-location.c
new file mode 100644 (file)
index 0000000..7d0bed9
--- /dev/null
@@ -0,0 +1,81 @@
+/* Example of using a logical location.
+
+   Intended output is similar to:
+
+In function 'test_qualified_name':
+PATH/test-error-with-note.c:18:8: error: can't find 'foo'
+   18 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source:
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-logical-location.c.exe",
+             "test-logical-location.c.sarif",
+             __FILE__, "c");
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            line_num,
+                                                            8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            line_num,
+                                                            19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                         loc_start,
+                                         loc_start,
+                                         loc_end);
+
+  /* begin quoted source */
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+
+  const diagnostic_logical_location *logical_loc
+    = diagnostic_manager_new_logical_location (diag_mgr,
+                                              DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+                                              NULL, /* parent */
+                                              "test_short_name",
+                                              "test_qualified_name",
+                                              "test_decorated_name");
+
+  diagnostic_set_logical_location (d, logical_loc);
+
+  diagnostic_finish (d, "can't find %qs", "foo");
+  /* end quoted source */
+
+  return end_test ();
+}
+
+/* Check the output from the text sink.  */
+/* { dg-begin-multiline-output "" }
+In function 'test_qualified_name':
+   { dg-end-multiline-output "" } */
+/* { dg-regexp "\[^\n\r\]+test-logical-location.c:18:8: error: can't find 'foo'" } */
+/* { dg-begin-multiline-output "" }
+   18 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
+
+/* 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 test-logical-location.c "test-logical-location-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-metadata-c.py b/gcc/testsuite/libdiagnostics.dg/test-metadata-c.py
new file mode 100644 (file)
index 0000000..fc83658
--- /dev/null
@@ -0,0 +1,45 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output_metadata(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'FooChecker'
+    assert tool['driver']['fullName'] == 'FooChecker 0.1 (en_US)'
+    assert tool['driver']['version'] == '0.1'
+    assert tool['driver']['informationUri'] == 'https://www.example.com/0.1/'
+
+    taxonomies = run["taxonomies"]
+    assert len(taxonomies) == 1
+
+    cwe = taxonomies[0]
+    assert cwe['name'] == 'CWE'
+    assert cwe['version'] == '4.7'
+    assert cwe['organization'] == 'MITRE'
+    assert cwe['shortDescription']['text'] \
+        == 'The MITRE Common Weakness Enumeration'
+    assert len(cwe['taxa']) == 1
+    assert cwe['taxa'][0]['id'] == '242'
+    assert cwe['taxa'][0]['helpUri'] \
+        == 'https://cwe.mitre.org/data/definitions/242.html'
+    
+    results = run['results']
+    assert len(results) == 1
+
+    result = results[0]
+    assert result['ruleId'] == 'warning'
+    assert result['level'] == 'warning'
+    assert result['message']['text'] == "never use 'gets'"
diff --git a/gcc/testsuite/libdiagnostics.dg/test-metadata.c b/gcc/testsuite/libdiagnostics.dg/test-metadata.c
new file mode 100644 (file)
index 0000000..7881c9e
--- /dev/null
@@ -0,0 +1,61 @@
+/* Example of setting a CWE and adding extra metadata.
+
+   Intended output is similar to:
+
+PATH/test-metadata.c:21:3: warning: never use 'gets' [CWE-242] [STR34-C]
+   21 |   gets (buf);
+      |   ^~~~~~~~~~
+
+   where the metadata tags are linkified in a sufficiently capable terminal,
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source:
+_________11111111112
+12345678901234567890
+void test_cwe (void)
+{
+  char buf[1024];
+  gets (buf);
+}
+*/
+const int line_num = __LINE__ - 3;
+
+int
+main ()
+{
+  begin_test ("FooChecker",
+             "test-metadata.c.sarif",
+             __FILE__, "c");
+
+  diagnostic_manager_set_full_name (diag_mgr, "FooChecker 0.1 (en_US)");
+  diagnostic_manager_set_version_string (diag_mgr, "0.1");
+  diagnostic_manager_set_version_url (diag_mgr, "https://www.example.com/0.1/");
+
+  const diagnostic_physical_location *loc_token
+    = make_range (diag_mgr, main_file, line_num, 3, 12);
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_token);
+  diagnostic_set_cwe (d, 242); /* CWE-242: Use of Inherently Dangerous Function.  */
+  diagnostic_add_rule (d, "STR34-C", "https://example.com/");
+  
+  diagnostic_finish (d, "never use %qs", "gets");
+
+  return end_test ();
+}
+
+/* { dg-regexp "\[^\n\r\]+test-metadata.c:21:3: warning: never use 'gets' \\\[CWE-242\\\] \\\[STR34-C\\\]" } */
+/* { dg-begin-multiline-output "" }
+   21 |   gets (buf);
+      |   ^~~~~~~~~~
+   { dg-end-multiline-output "" } */
+
+/* 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 test-metadata.c "test-metadata-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py
new file mode 100644 (file)
index 0000000..3189fcf
--- /dev/null
@@ -0,0 +1,83 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-multiple-lines.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    result = results[0]
+    assert result['ruleId'] == 'warning'
+    assert result['level'] == 'warning'
+    assert result['message']['text'] == "missing comma"
+    assert len(result['locations']) == 1
+
+    # The primary location should be that of the missing comma
+    location = result['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-multiple-lines.c')
+    assert phys_loc['region']['startLine'] == 23
+    assert phys_loc['region']['startColumn'] == 29
+    assert phys_loc['region']['endColumn'] == 30
+    assert phys_loc['contextRegion']['startLine'] == 23
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '                       "bar"\n'
+
+    assert len(location['relationships']) == 3
+    location['relationships'][0]['target'] == 0
+    location['relationships'][0]['kinds'] == ['relevant']
+    location['relationships'][1]['target'] == 1
+    location['relationships'][1]['kinds'] == ['relevant']
+    location['relationships'][2]['target'] == 2
+    location['relationships'][2]['kinds'] == ['relevant']
+
+    # We should be capturing the secondary locations in relatedLocations
+    assert len(result['relatedLocations']) == 3
+
+    rel_loc_0 = result['relatedLocations'][0]
+    assert get_location_artifact_uri(rel_loc_0) \
+        .endswith('test-multiple-lines.c')
+    assert get_location_snippet_text(rel_loc_0) \
+        == 'const char *strs[3] = {"foo",\n'
+    assert get_location_physical_region(rel_loc_0)['startLine'] == 22
+    assert get_location_physical_region(rel_loc_0)['startColumn'] == 24
+    assert get_location_physical_region(rel_loc_0)['endColumn'] == 29
+    assert rel_loc_0['id'] == 0
+    assert 'relationships' not in rel_loc_0
+
+    rel_loc_1 = result['relatedLocations'][1]
+    assert get_location_artifact_uri(rel_loc_1) \
+        .endswith('test-multiple-lines.c')
+    assert get_location_snippet_text(rel_loc_1) \
+        == '                       "bar"\n'
+    assert get_location_physical_region(rel_loc_1)['startLine'] == 23
+    assert get_location_physical_region(rel_loc_1)['startColumn'] == 24
+    assert get_location_physical_region(rel_loc_1)['endColumn'] == 29
+    assert rel_loc_1['id'] == 1
+    assert 'relationships' not in rel_loc_1
+
+    rel_loc_2 = result['relatedLocations'][2]
+    assert get_location_artifact_uri(rel_loc_2) \
+        .endswith('test-multiple-lines.c')
+    assert get_location_snippet_text(rel_loc_2) \
+        == '                       "baz"};\n'
+    assert get_location_physical_region(rel_loc_2)['startLine'] == 24
+    assert get_location_physical_region(rel_loc_2)['startColumn'] == 24
+    assert get_location_physical_region(rel_loc_2)['endColumn'] == 29
+    assert rel_loc_2['id'] == 2
+    assert 'relationships' not in rel_loc_2
diff --git a/gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c
new file mode 100644 (file)
index 0000000..888897a
--- /dev/null
@@ -0,0 +1,78 @@
+/* Example of a warning with multiple locations in various source lines,
+   with an insertion fix-it hint.
+
+   Intended output is similar to:
+   
+/PATH/test-multiple-lines.c:23:29: warning: missing comma
+   22 | const char *strs[3] = {"foo",
+      |                        ~~~~~ 
+   23 |                        "bar"
+      |                        ~~~~~^
+   24 |                        "baz"};
+      |                        ~~~~~ 
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source (missing comma after "bar"):
+_________11111111112222222222
+12345678901234567890123456789
+const char *strs[3] = {"foo",
+                       "bar"
+                       "baz"};
+*/
+const int foo_line_num = __LINE__ - 4;
+
+int
+main ()
+{
+  begin_test ("test-multiple-lines.c.exe",
+             "test-multiple-lines.c.sarif",
+             __FILE__, "c");
+  
+  /* begin quoted source */
+  const diagnostic_physical_location *loc_comma
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            foo_line_num + 1,
+                                                            29);
+  const diagnostic_physical_location *loc_foo
+    = make_range (diag_mgr, main_file, foo_line_num, 24, 28);
+  const diagnostic_physical_location *loc_bar
+    = make_range (diag_mgr, main_file, foo_line_num + 1, 24, 28);
+  const diagnostic_physical_location *loc_baz
+    = make_range (diag_mgr, main_file, foo_line_num + 2, 24, 28);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_comma);
+  diagnostic_add_location (d, loc_foo);
+  diagnostic_add_location (d, loc_bar);
+  diagnostic_add_location (d, loc_baz);
+
+  diagnostic_add_fix_it_hint_insert_after (d, loc_bar, ",");
+
+  diagnostic_finish (d, "missing comma");
+  /* end quoted source */
+
+  return end_test ();
+};
+
+/* { dg-regexp "\[^\n\r\]+test-multiple-lines.c:23:29: warning: missing comma" } */
+/* { dg-begin-multiline-output "" }
+   22 | const char *strs[3] = {"foo",
+      |                        ~~~~~ 
+   23 |                        "bar"
+      |                        ~~~~~^
+   24 |                        "baz"};
+      |                        ~~~~~ 
+   { dg-end-multiline-output "" } */
+
+/* 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 test-multiple-lines.c "test-multiple-lines-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-column-c.py b/gcc/testsuite/libdiagnostics.dg/test-no-column-c.py
new file mode 100644 (file)
index 0000000..afef984
--- /dev/null
@@ -0,0 +1,35 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 16
+
+def test_sarif_output(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-no-column.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-no-column.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    # We should have no column properties:
+    assert 'startColumn' not in phys_loc['region']
+    assert 'endColumn' not in phys_loc['region']
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '#include <foo.h>\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-column.c b/gcc/testsuite/libdiagnostics.dg/test-no-column.c
new file mode 100644 (file)
index 0000000..5354f75
--- /dev/null
@@ -0,0 +1,54 @@
+/* Example of emitting an error without a column number.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:6: error: can't find 'foo'
+    6 | #include <foo.h>
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122
+123456789012345678901
+#include <foo.h>
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-no-column.c.exe",
+             "test-no-column.c.sarif",
+             __FILE__, "c");
+
+  /* begin quoted source */
+  const diagnostic_physical_location *loc
+    = diagnostic_manager_new_location_from_file_and_line (diag_mgr,
+                                                         main_file,
+                                                         line_num);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc);
+
+  diagnostic_finish (d, "can't find %qs", "foo.h");
+  /* end quoted source */
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-no-column.c:16: error: can't find 'foo.h'" }
+   { dg-begin-multiline-output "" }
+   16 | #include <foo.h>
+   { dg-end-multiline-output "" } */
+
+/* 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 test-no-column.c "test-no-column-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py
new file mode 100644 (file)
index 0000000..9ce1c2a
--- /dev/null
@@ -0,0 +1,42 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_file_name = 'test-no-diagnostics.c'
+
+def test_sarif_output(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == True
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'].endswith(expected_file_name)
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    # We don't bother capturing the contents if there are
+    # no diagnostics to display
+    assert 'contents' not in artifacts[0]
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 0
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c
new file mode 100644 (file)
index 0000000..78e186a
--- /dev/null
@@ -0,0 +1,25 @@
+/* Test of the "no diagnostics are emitted" case.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+int
+main ()
+{
+  begin_test ("test-no-diagnostics.c.exe",
+             "test-no-diagnostics.c.sarif",
+             __FILE__, "c");
+
+  /* No-op.  */
+
+  return end_test ();
+};
+
+/* There should be no output from the text sink.  */
+
+/* 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 test-no-diagnostics.c "test-no-diagnostics-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py
new file mode 100644 (file)
index 0000000..cd4e6e2
--- /dev/null
@@ -0,0 +1,54 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 21
+
+def test_sarif_output_with_fixes(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-note-with-fix-it-hint.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    result = results[0]
+    assert result['ruleId'] == 'error'
+    assert result['level'] == 'error'
+    assert result['message']['text'] == "unknown field 'colour'"
+    assert len(result['locations']) == 1
+    location = result['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-note-with-fix-it-hint.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 13
+    assert phys_loc['region']['endColumn'] == 19
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  return p->colour;\n'
+
+    assert len(result['relatedLocations']) == 1
+    note = result['relatedLocations'][0]
+    phys_loc = note['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-note-with-fix-it-hint.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 13
+    assert phys_loc['region']['endColumn'] == 19
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  return p->colour;\n'
+    assert note['message']['text'] == "did you mean 'color'"
+
+    # TODO: we don't yet capture fix-it hints on notes (PR other/116164)
+    assert 'fixes' not in result
diff --git a/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c
new file mode 100644 (file)
index 0000000..19fa7c1
--- /dev/null
@@ -0,0 +1,69 @@
+/* Example of a grouped error and note, with a fix-it hint on the note.
+
+   Intended output is similar to:
+   
+/PATH/test-note-with-fix-it-hint.c:21:13: error: unknown field 'colour'
+   21 |   return p->colour;
+      |             ^~~~~~
+/PATH/test-note-with-fix-it-hint.c:21:13: note: did you mean 'color'
+   21 |   return p->colour;
+      |             ^~~~~~
+      |             color
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source:
+_________11111111112
+12345678901234567890
+  return p->colour;
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-note-with-fix-it-hint.c.exe",
+             "test-note-with-fix-it-hint.c.sarif",
+             __FILE__, "c");
+
+  const diagnostic_physical_location *loc_token
+    = make_range (diag_mgr, main_file, line_num, 13, 18);
+
+  diagnostic_manager_begin_group (diag_mgr);
+
+  diagnostic *err = diagnostic_begin (diag_mgr, DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (err, loc_token);
+  diagnostic_finish (err, "unknown field %qs", "colour");
+
+  diagnostic *n = diagnostic_begin (diag_mgr, DIAGNOSTIC_LEVEL_NOTE);
+  diagnostic_set_location (n, loc_token);
+  diagnostic_add_fix_it_hint_replace (n, loc_token, "color");
+  diagnostic_finish (n, "did you mean %qs", "color");
+
+  diagnostic_manager_end_group (diag_mgr);
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-note-with-fix-it-hint.c:21:13: error: unknown field 'colour'" }
+   { dg-begin-multiline-output "" }
+   21 |   return p->colour;
+      |             ^~~~~~
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-note-with-fix-it-hint.c:21:13: note: did you mean 'color'" }
+   { dg-begin-multiline-output "" }
+   21 |   return p->colour;
+      |             ^~~~~~
+      |             color
+   { dg-end-multiline-output "" } */
+
+/* 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 test-note-with-fix-it-hint.c "test-note-with-fix-it-hint-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c b/gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c
new file mode 100644 (file)
index 0000000..c146855
--- /dev/null
@@ -0,0 +1,59 @@
+/* Example of controlling options for text sinks,
+   with multiple text sinks,
+   and color output.  */
+
+#include "libdiagnostics.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+
+  diagnostic_text_sink *sink_1
+    = diagnostic_manager_add_text_sink (diag_mgr, stderr,
+                                       DIAGNOSTIC_COLORIZE_NO);
+  diagnostic_text_sink_set_source_printing_enabled (sink_1, 0);
+
+  diagnostic_text_sink *sink_2
+    = diagnostic_manager_add_text_sink (diag_mgr, stderr,
+                                       DIAGNOSTIC_COLORIZE_YES);
+  diagnostic_text_sink_set_labelled_source_colorization_enabled (sink_2, 0);
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, __FILE__, "c");
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                                 loc_start,
+                                                 loc_start,
+                                                 loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+  
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  diagnostic_manager_release (diag_mgr);
+  return 0;
+};
+
+/* Verify the output from text sink 1.  */
+/* { dg-regexp "\[^\n\r\]+test-text-sink-options.c:10:8: error: can't find 'foo'" } */
+
+/* Verify the output from text sink 2.
+   { dg-regexp "\[^\n\r\]+test-text-sink-options.c:10:8:" }
+   { dg-begin-multiline-output "" }
+\e[m\e[K \e[01;31m\e[Kerror: \e[m\e[Kcan't find '\e[01m\e[Kfoo\e[m\e[K'
+   10 | PRINT "hello world!";
+      |        \e[01;31m\e[K^~~~~~~~~~~~\e[m\e[K
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning-c.py b/gcc/testsuite/libdiagnostics.dg/test-warning-c.py
new file mode 100644 (file)
index 0000000..c6e3752
--- /dev/null
@@ -0,0 +1,54 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 17
+expected_file_name = 'test-warning.c'
+
+def test_sarif_output(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == True
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'].endswith(expected_file_name)
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    assert 'PRINT' in artifacts[0]['contents']['text']
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'warning'
+    assert results[0]['level'] == 'warning'
+    assert results[0]['message']['text'] == "this is a warning"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith(expected_file_name)
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 8
+    assert phys_loc['region']['endColumn'] == 20
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py
new file mode 100644 (file)
index 0000000..5d3bbc4
--- /dev/null
@@ -0,0 +1,108 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+final_line_num = 34
+
+line_num_call_to_PyList_New = final_line_num - 7;
+line_num_for_loop = final_line_num - 5;
+line_num_call_to_PyList_Append = final_line_num - 3;
+
+expected_file_name = 'test-warning-with-path.c'
+
+def test_sarif_output_for_warning_with_path(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    results = run['results']
+    assert len(results) == 1
+
+    result = results[0]
+    assert result['ruleId'] == 'warning'
+    assert result['level'] == 'warning'
+    assert result['message']['text'] \
+        == "passing NULL as argument 1 to 'PyList_Append' which requires a non-NULL parameter"
+    assert len(result['locations']) == 1
+    location = result['locations'][0]
+
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith(expected_file_name)
+    assert phys_loc['region']['startLine'] \
+        == line_num_call_to_PyList_Append
+    assert phys_loc['region']['startColumn'] == 5
+    assert phys_loc['region']['endColumn'] == 30
+    assert phys_loc['contextRegion']['startLine'] \
+        == line_num_call_to_PyList_Append
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '    PyList_Append(list, item);\n'
+
+    assert len(location['logicalLocations']) == 1
+    logical_loc = location['logicalLocations'][0]
+    assert logical_loc['name'] == 'make_a_list_of_random_ints_badly'
+    assert logical_loc['fullyQualifiedName'] == 'make_a_list_of_random_ints_badly'
+    assert logical_loc['decoratedName'] == 'make_a_list_of_random_ints_badly'
+    assert logical_loc['kind'] == 'function'
+
+    assert len(result['codeFlows']) == 1
+    assert len(result['codeFlows'][0]['threadFlows']) == 1
+    thread_flow = result['codeFlows'][0]['threadFlows'][0]
+
+    assert len(thread_flow['locations']) == 3
+
+    tfl_0 = thread_flow['locations'][0]
+    tfl_0_loc = tfl_0['location']
+    assert get_location_artifact_uri(tfl_0_loc).endswith(expected_file_name)
+    assert get_location_physical_region(tfl_0_loc)['startLine'] \
+        == line_num_call_to_PyList_New
+    assert get_location_physical_region(tfl_0_loc)['startColumn'] == 10
+    assert get_location_physical_region(tfl_0_loc)['endColumn'] == 23
+    assert get_location_snippet_text(tfl_0_loc) \
+        == '  list = PyList_New(0);\n'
+    assert tfl_0_loc['logicalLocations'] == location['logicalLocations']
+    assert tfl_0_loc['message']['text'] \
+        == "when 'PyList_New' fails, returning NULL"
+    assert tfl_0['nestingLevel'] == 0
+    assert tfl_0['executionOrder'] == 1
+
+    tfl_1 = thread_flow['locations'][1]
+    tfl_1_loc = tfl_1['location']
+    assert get_location_artifact_uri(tfl_1_loc).endswith(expected_file_name)
+    assert get_location_physical_region(tfl_1_loc)['startLine'] \
+        == line_num_for_loop
+    assert get_location_physical_region(tfl_1_loc)['startColumn'] == 15
+    assert get_location_physical_region(tfl_1_loc)['endColumn'] == 24
+    assert get_location_snippet_text(tfl_1_loc) \
+        == '  for (i = 0; i < count; i++) {\n'
+    assert tfl_1_loc['logicalLocations'] == location['logicalLocations']
+    assert tfl_1_loc['message']['text'] \
+        == "when 'i < count'"
+    assert tfl_1['nestingLevel'] == 0
+    assert tfl_1['executionOrder'] == 2
+
+    tfl_2 = thread_flow['locations'][2]
+    tfl_2_loc = tfl_2['location']
+    assert get_location_artifact_uri(tfl_2_loc).endswith(expected_file_name)
+    assert get_location_physical_region(tfl_2_loc)['startLine'] \
+        == line_num_call_to_PyList_Append
+    assert get_location_physical_region(tfl_2_loc)['startColumn'] == 5
+    assert get_location_physical_region(tfl_2_loc)['endColumn'] == 30
+    assert get_location_snippet_text(tfl_2_loc) \
+        == '    PyList_Append(list, item);\n'
+    assert tfl_2_loc['logicalLocations'] == location['logicalLocations']
+    assert tfl_2_loc['message']['text'] \
+        == "when calling 'PyList_Append', passing NULL from (1) as argument 1"
+    assert tfl_2['nestingLevel'] == 0
+    assert tfl_2['executionOrder'] == 3
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c
new file mode 100644 (file)
index 0000000..4ff5548
--- /dev/null
@@ -0,0 +1,138 @@
+/* Example of emitting a warning with an execution path.
+
+TODO:
+
+   Intended output is similar to:
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122222222223333333333444444444455555555556
+123456789012345678901234567890123456789012345678901234567890
+begin fake source
+PyObject *
+make_a_list_of_random_ints_badly(PyObject *self,
+                                PyObject *args)
+{
+  PyObject *list, *item;
+  long count, i;
+
+  if (!PyArg_ParseTuple(args, "i", &count)) {
+    return NULL;
+  }
+
+  list = PyList_New(0);
+       
+  for (i = 0; i < count; i++) {
+    item = PyLong_FromLong(random());
+    PyList_Append(list, item);
+  }
+  
+  return list;
+}
+end fake source
+*/
+const int final_line_num = __LINE__ - 4; /* line of "return list;" */
+
+/* begin line consts */
+const int line_num_call_to_PyList_New = final_line_num - 7;
+const int line_num_for_loop = final_line_num - 5;
+const int line_num_call_to_PyList_Append = final_line_num - 3;
+/* end line consts */
+
+int
+main ()
+{
+  begin_test ("test-warning-with-path.c.exe",
+             "test-warning-with-path.c.sarif",
+             __FILE__, "c");
+
+  /* begin full example */
+  /* begin create phys locs */
+  const diagnostic_physical_location *loc_call_to_PyList_New
+    = make_range (diag_mgr, main_file, line_num_call_to_PyList_New, 10, 22);
+  const diagnostic_physical_location *loc_for_cond
+    = make_range (diag_mgr, main_file, line_num_for_loop, 15, 23);
+  const diagnostic_physical_location *loc_call_to_PyList_Append
+    = make_range (diag_mgr, main_file, line_num_call_to_PyList_Append, 5, 29);
+  /* end create phys locs */
+
+  /* begin create logical locs */
+  const char *funcname = "make_a_list_of_random_ints_badly";
+  const diagnostic_logical_location *logical_loc
+    = diagnostic_manager_new_logical_location (diag_mgr,
+                                              DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+                                              NULL, /* parent */
+                                              funcname,
+                                              funcname,
+                                              funcname);
+  /* end create logical locs */
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_call_to_PyList_Append);
+  diagnostic_set_logical_location (d, logical_loc);
+
+  /* begin path creation */
+  diagnostic_execution_path *path = diagnostic_add_execution_path (d);
+  
+  diagnostic_event_id alloc_event_id
+    = diagnostic_execution_path_add_event (path,
+                                          loc_call_to_PyList_New,
+                                          logical_loc, 0,
+                                          "when %qs fails, returning NULL",
+                                          "PyList_New");
+  diagnostic_execution_path_add_event (path,
+                                      loc_for_cond,
+                                      logical_loc, 0,
+                                      "when %qs", "i < count");
+  diagnostic_execution_path_add_event (path,
+                                      loc_call_to_PyList_Append,
+                                      logical_loc, 0,
+                                      "when calling %qs, passing NULL from %@ as argument %i",
+                                      "PyList_Append", &alloc_event_id, 1);
+  /* end path creation */
+
+  diagnostic_finish (d,
+                    "passing NULL as argument %i to %qs"
+                    " which requires a non-NULL parameter",
+                    1, "PyList_Append");
+  /* end full example */
+  
+  return end_test ();
+};
+
+/* Check the output from the text sink.
+   { dg-begin-multiline-output "" }
+In function 'make_a_list_of_random_ints_badly':
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-warning-with-path.c:31:5: warning: passing NULL as argument 1 to 'PyList_Append' which requires a non-NULL parameter" }
+   { dg-begin-multiline-output "" }
+   31 |     PyList_Append(list, item);
+      |     ^~~~~~~~~~~~~~~~~~~~~~~~~
+  'make_a_list_of_random_ints_badly': events 1-3
+   27 |   list = PyList_New(0);
+      |          ^~~~~~~~~~~~~
+      |          |
+      |          (1) when 'PyList_New' fails, returning NULL
+   28 | 
+   29 |   for (i = 0; i < count; i++) {
+      |               ~~~~~~~~~
+      |               |
+      |               (2) when 'i < count'
+   30 |     item = PyLong_FromLong(random());
+   31 |     PyList_Append(list, item);
+      |     ~~~~~~~~~~~~~~~~~~~~~~~~~
+      |     |
+      |     (3) when calling 'PyList_Append', passing NULL from (1) as argument 1
+   { dg-end-multiline-output "" } */
+
+/* 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 test-warning-with-path.c "test-warning-with-path-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning.c b/gcc/testsuite/libdiagnostics.dg/test-warning.c
new file mode 100644 (file)
index 0000000..252f646
--- /dev/null
@@ -0,0 +1,67 @@
+/* Example of emitting a warning.
+
+   Intended output is similar to:
+
+/PATH/test-warning.c:17:8: warning: this is a warning
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-warning.c.exe",
+             "test-warning.c.sarif",
+             __FILE__, "c");
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            line_num,
+                                                            8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+                                                            main_file,
+                                                            line_num,
+                                                            19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                                 loc_start,
+                                                 loc_start,
+                                                 loc_end);
+
+  /* begin quoted source */
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_range);
+
+  diagnostic_finish (d, "this is a warning");
+  /* end quoted source */
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-warning.c:17:8: warning: this is a warning" }
+   { dg-begin-multiline-output "" }
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
+
+/* 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 test-warning.c "test-warning-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py
new file mode 100644 (file)
index 0000000..2d5ebe9
--- /dev/null
@@ -0,0 +1,55 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 8
+
+def test_sarif_output(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'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-write-sarif-to-file.c.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == False
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'] \
+        .endswith('test-write-sarif-to-file.c')
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    assert 'PRINT' in artifacts[0]['contents']['text']
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "can't find 'foo'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'] \
+        .endswith('test-write-sarif-to-file.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 8
+    assert phys_loc['region']['endColumn'] == 20
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c
new file mode 100644 (file)
index 0000000..637935e
--- /dev/null
@@ -0,0 +1,55 @@
+/* Example of writing diagnostics as SARIF to a file.  */
+
+#include "libdiagnostics.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  FILE *sarif_outfile = fopen ("test-write-sarif-to-file.c.sarif", "w");
+  if (!sarif_outfile)
+    return -1;
+
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+  diagnostic_manager_set_tool_name (diag_mgr, "test-write-sarif-to-file.c.exe");
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, __FILE__, "c");
+
+  diagnostic_manager_add_sarif_sink (diag_mgr, sarif_outfile, file,
+                                    DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                                 loc_start,
+                                                 loc_start,
+                                                 loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  diagnostic_manager_release (diag_mgr);
+
+  fclose (sarif_outfile);
+
+  return 0;
+};
+
+/* 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 test-write-sarif-to-file.c "test-write-sarif-to-file-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c b/gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c
new file mode 100644 (file)
index 0000000..8ad448c
--- /dev/null
@@ -0,0 +1,47 @@
+/* Example of writing diagnostics in text form, but to a file, 
+   rather than stderr.  */
+
+#include "libdiagnostics.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  FILE *outfile = fopen ("test.txt", "w");
+  if (!outfile)
+    return -1;
+
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+
+  diagnostic_manager_add_text_sink (diag_mgr, outfile,
+                                   DIAGNOSTIC_COLORIZE_NO);
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, __FILE__, "c");
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+                                                 loc_start,
+                                                 loc_start,
+                                                 loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+  
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  diagnostic_manager_release (diag_mgr);
+
+  fclose (outfile);
+
+  return 0;
+};