diff --git a/configure b/configure index a2e86731b08..10c1589d473 100755 --- 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 diff --git a/configure.ac b/configure.ac index 25419a1d2ab..fb61550dba7 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/gcc/Makefile.in b/gcc/Makefile.in index e8b2a38c8b3..41b0c2d0248 100644 --- a/gcc/Makefile.in +++ b/gcc/Makefile.in @@ -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. diff --git a/gcc/configure b/gcc/configure index 19cb5d06959..b2eceb967f8 100755 --- a/gcc/configure +++ b/gcc/configure @@ -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 @@ -34276,6 +34283,21 @@ fi +# 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 : diff --git a/gcc/configure.ac b/gcc/configure.ac index bdd8cb0002f..2dda8f2a570 100644 --- a/gcc/configure.ac +++ b/gcc/configure.ac @@ -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], diff --git a/gcc/diagnostic-format-text.h b/gcc/diagnostic-format-text.h index f01d8c118e6..2ca3bb227ef 100644 --- a/gcc/diagnostic-format-text.h +++ b/gcc/diagnostic-format-text.h @@ -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. */ diff --git a/gcc/doc/install.texi b/gcc/doc/install.texi index 03bbfece0ec..d365c07987d 100644 --- a/gcc/doc/install.texi +++ b/gcc/doc/install.texi @@ -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 index 00000000000..d4bb2cbb9ed --- /dev/null +++ b/gcc/doc/libdiagnostics/Makefile @@ -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 index 00000000000..1ff7552411d --- /dev/null +++ b/gcc/doc/libdiagnostics/conf.py @@ -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 index 00000000000..a05eb4ef261 --- /dev/null +++ b/gcc/doc/libdiagnostics/index.rst @@ -0,0 +1,113 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +libdiagnostics +============== + +This document describes `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 `, + 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 `:: + + 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` 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 ` + +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 index 00000000000..954237b9b9f --- /dev/null +++ b/gcc/doc/libdiagnostics/make.bat @@ -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 index 00000000000..7f86f6bba61 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/diagnostic-manager.rst @@ -0,0 +1,58 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..66f0a25143c --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/diagnostics.rst @@ -0,0 +1,127 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..3f4109ce4f3 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/execution-paths.rst @@ -0,0 +1,93 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 `_). diff --git a/gcc/doc/libdiagnostics/topics/fix-it-hints.rst b/gcc/doc/libdiagnostics/topics/fix-it-hints.rst new file mode 100644 index 00000000000..08acb7119a1 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/fix-it-hints.rst @@ -0,0 +1,135 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 `_). + +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 index 00000000000..064340b98ba --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/index.rst @@ -0,0 +1,38 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..85900b6344f --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/logical-locations.rst @@ -0,0 +1,109 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 `, 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 `_). + + .. 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 `_). + + ``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 `_). + + ``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 `_). + +.. 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 index 00000000000..9d42f8937d0 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/message-formatting.rst @@ -0,0 +1,224 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..c62792a5b76 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/metadata.rst @@ -0,0 +1,149 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 `, 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 ` 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 ` as the value for the + ``name`` property of the ``driver`` + (`SARIF v2.1.0 §3.19.8 `_). + +.. 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 ` as + the value for the ``fullName`` property of the ``driver`` + (`SARIF v2.1.0 §3.19.9 `_). + + 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 `_):: + + 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 `_):: + + 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 `_:: + + /* 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 index 00000000000..bad2b8db27f --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/physical-locations.rst @@ -0,0 +1,281 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 `, +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 v2.1.0 §3.24.10 `_). + See + `SARIF v2.1.0 Appendix J `_ + 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 index 00000000000..d0340570733 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/retrofitting.rst @@ -0,0 +1,23 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..3fd75ed21c6 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/sarif.rst @@ -0,0 +1,51 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. default-domain:: c + +SARIF support +============= + +`SARIF `_ 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 index 00000000000..32b2a54f18a --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/text-output.rst @@ -0,0 +1,87 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..fc96e176eb6 --- /dev/null +++ b/gcc/doc/libdiagnostics/topics/ux.rst @@ -0,0 +1,26 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. default-domain:: c + +User Experience +=============== + +Refer to +`GCC's user experience guidelines `_ +for notes on +`what makes a good diagnostic `_. diff --git a/gcc/doc/libdiagnostics/tutorial/01-hello-world.rst b/gcc/doc/libdiagnostics/tutorial/01-hello-world.rst new file mode 100644 index 00000000000..4635687fabd --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/01-hello-world.rst @@ -0,0 +1,173 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 `_). + +.. 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 index 00000000000..2e429de1734 --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/02-physical-locations.rst @@ -0,0 +1,260 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 + +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 + +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 + | ^~~~~ + +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 + | ^~~~~ + +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 index 00000000000..d36ac098a80 --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/03-logical-locations.rst @@ -0,0 +1,60 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..117eb6fa7ad --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/04-notes.rst @@ -0,0 +1,66 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 + | ^~~~~ + 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 + | ^~~~~ + 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 `_). + +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 index 00000000000..1512ae78f98 --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/05-warnings.rst @@ -0,0 +1,44 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 + | ^~~~~ + +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 index 00000000000..9486ab7f91c --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/06-fix-it-hints.rst @@ -0,0 +1,61 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 index 00000000000..0fbbed25d3a --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/07-execution-paths.rst @@ -0,0 +1,141 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +.. 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 `_. + +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" `_). + +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 `_). + +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 index 00000000000..d637103036b 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 index 00000000000..6ea6866e291 --- /dev/null +++ b/gcc/doc/libdiagnostics/tutorial/index.rst @@ -0,0 +1,32 @@ +.. Copyright (C) 2024 Free Software Foundation, Inc. + Originally contributed by David Malcolm + + 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 + . + +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 index 00000000000..14c84934a44 --- /dev/null +++ b/gcc/libdiagnostics++.h @@ -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 +. */ + +#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 (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 index 00000000000..c06d3bc722f --- /dev/null +++ b/gcc/libdiagnostics.cc @@ -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 +. */ + +#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) + { + 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 logical_loc + = ::make_unique (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 (&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 + (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> m_sinks; + hash_map m_str_to_file_map; + hash_map, + diagnostic_physical_location *> m_location_t_map; + std::vector> 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, ©_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 (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> 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 rule = ::make_unique (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 label + = ::make_unique (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 (); + 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 (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> m_labels; + std::vector> m_rules; + std::unique_ptr 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 (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 (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 (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 (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 (*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 index 00000000000..96086bca617 --- /dev/null +++ b/gcc/libdiagnostics.h @@ -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 +. */ + +#ifndef LIBDIAGNOSTICS_H +#define LIBDIAGNOSTICS_H + +#include +#include + +#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 index 00000000000..cc32b76d016 --- /dev/null +++ b/gcc/libdiagnostics.map @@ -0,0 +1,72 @@ +# Linker script for libdiagnostics.so +# Copyright (C) 2023-2024 Free Software Foundation, Inc. +# Contributed by David Malcolm . +# +# 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 +# . */ + +# 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 index 00000000000..d29a469ae4f --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp @@ -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 index 00000000000..7daf35b5819 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/sarif.py @@ -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 index 00000000000..9f0576d5cd3 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-dump.c @@ -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 index 00000000000..1206be80fb4 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error-c.py @@ -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 ' 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 \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 index 00000000000..bed21caec74 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py @@ -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 \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 \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 index 00000000000..f406743f64b --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.c @@ -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 + | ^~~~~ +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 +*/ +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 + | ^~~~~ + { 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 index 00000000000..e211297c552 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc @@ -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 index 00000000000..6f17ce2c098 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error.c @@ -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 + | ^~~~~ + + along with the equivalent in SARIF. */ + +#include "libdiagnostics.h" +#include "test-helpers.h" + +/* +_________1111111 +1234567890123456 +#include +*/ +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 + | ^~~~~ + { 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 index 00000000000..6f919e473f4 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error.cc @@ -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 index 00000000000..58a146b3f18 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-example-1.c @@ -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 index 00000000000..f3dc71c27b5 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py @@ -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 index 00000000000..0d8ee46cb96 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c @@ -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 index 00000000000..92c7f07117b --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc @@ -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 index 00000000000..c8ff2def1ff --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h @@ -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 index 00000000000..a578850e9d5 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-helpers.h @@ -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 index 00000000000..39978b22f23 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c @@ -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 index 00000000000..1c1c050e304 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc @@ -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 index 00000000000..dce404f92fd --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py @@ -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 index 00000000000..7448a1e0dac --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py @@ -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 index 00000000000..7d0bed9600e --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-logical-location.c @@ -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 index 00000000000..fc83658e0c5 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-metadata-c.py @@ -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 index 00000000000..7881c9eca3e --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-metadata.c @@ -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 index 00000000000..3189fcf6d31 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py @@ -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 index 00000000000..888897ae791 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c @@ -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 index 00000000000..afef9849314 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-no-column-c.py @@ -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 \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 index 00000000000..5354f7543f1 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-no-column.c @@ -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 + + along with the equivalent in SARIF. */ + +#include "libdiagnostics.h" +#include "test-helpers.h" + +/* +_________111111111122 +123456789012345678901 +#include +*/ +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 + { 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 index 00000000000..9ce1c2a0e3a --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py @@ -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 index 00000000000..78e186ab95a --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c @@ -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 index 00000000000..cd4e6e21449 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py @@ -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 index 00000000000..19fa7c1b46d --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c @@ -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 index 00000000000..c1468553a12 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c @@ -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 "" } + error: can't find 'foo' + 10 | PRINT "hello world!"; + | ^~~~~~~~~~~~ + { 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 index 00000000000..c6e3752c4f4 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-warning-c.py @@ -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 index 00000000000..5d3bbc47e17 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py @@ -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 index 00000000000..4ff5548b1a3 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c @@ -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 index 00000000000..252f646f2f0 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-warning.c @@ -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 index 00000000000..2d5ebe93b1f --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py @@ -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 index 00000000000..637935e4598 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c @@ -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 index 00000000000..8ad448c8c9c --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c @@ -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; +};