From 6eadbda9783e4c700c6dbe20acc00f9c0e44031a Mon Sep 17 00:00:00 2001
From: Florian Uhlig <f.uhlig@gsi.de>
Date: Mon, 11 Sep 2023 15:04:57 +0200
Subject: [PATCH] Add new CMake target to run clang-tidy

The target will run clang-tidy on all changed source and header files
The target is only created if the used clang-tidy supports all required
checks. The required checks will be extracted from the clang-tidy configuration
file, so the list should be alwyas correct.

Instead of having a hardcoded list of required checks the list is extracted
from the clang-tidy config file such that the list is always up to date.

Add CMake script to execute the TidyCheck in our CI

Download and install external packages needed when running clang-tidy.
To speed up things execute clang-tidy in parallel on differnt files if more
cores are available.

Add shell script used by the CMake target

Check changed header files only if there is a corresponding source file in the
compile_commands database. Currently there is no way to test header files
without corresponding source file (compilation unit).
Create missing but expected output directories.
Remove clang-tidy command line option
The option was moved to the config file.

The script find_files.sh is now used from two places so pass the required
information as parameters. Remove one unneded parameter
---
 cmake/modules/CbmTargets.cmake    | 46 ++++++++++++++++-
 cmake/modules/FindClangTidy.cmake | 85 +++++++++++++++++++++++++++++++
 cmake/scripts/check-tidy.sh       | 61 ++++++++++++++++++++++
 cmake/scripts/checktidy.cmake     | 34 +++++++++++++
 cmake/scripts/find_files.sh       |  4 +-
 5 files changed, 226 insertions(+), 4 deletions(-)
 create mode 100644 cmake/modules/FindClangTidy.cmake
 create mode 100755 cmake/scripts/check-tidy.sh
 create mode 100644 cmake/scripts/checktidy.cmake

diff --git a/cmake/modules/CbmTargets.cmake b/cmake/modules/CbmTargets.cmake
index ab37a0728e..3aac007772 100644
--- a/cmake/modules/CbmTargets.cmake
+++ b/cmake/modules/CbmTargets.cmake
@@ -26,7 +26,7 @@ macro(define_additional_targets)
 
     # Create a list C, C++ and header files which have been changed in the
     # current commit
-    execute_process(COMMAND ${CMAKE_SOURCE_DIR}/cmake/scripts/find_files.sh
+    execute_process(COMMAND ${CMAKE_SOURCE_DIR}/cmake/scripts/find_files.sh ${BASE_COMMIT}
                     WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
                     OUTPUT_VARIABLE FileList
                     OUTPUT_STRIP_TRAILING_WHITESPACE
@@ -58,6 +58,50 @@ macro(define_additional_targets)
 
   endif()
 
+  find_package2(PRIVATE ClangTidy)
+  if(ClangTidy_FOUND AND EXISTS ${CMAKE_SOURCE_DIR}/.git)
+    if (FAIRROOT_TIDY_BASE)
+      set(BASE_COMMIT ${FAIRROOT_TIDY_BASE})
+    else()
+      set(BASE_COMMIT upstream/master)
+    endif()
+
+    # Create a list C, C++ and header files which have been changed in the
+    # current commit
+    execute_process(COMMAND ${CMAKE_SOURCE_DIR}/cmake/scripts/find_files.sh ${BASE_COMMIT}
+                    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+                    OUTPUT_VARIABLE FileList
+                    OUTPUT_STRIP_TRAILING_WHITESPACE
+                   )
+    string(REGEX REPLACE " " ";" FileList "${FileList}")
+
+    # Loop over the files and create the code whch is executed when running
+    # "make FormatCheck".
+    # The produced code will run clang-format on one of the files. If
+    # clang-format finds code which are not satisfying our code rules a
+    # detailed error message is created. This error message can be checked on
+    # our CDash web page.
+    foreach(file ${FileList})
+
+      set(file1 ${CMAKE_BINARY_DIR}/${file}.ct_out)
+
+      list(APPEND myfilelist1 ${file1})
+      add_custom_command(OUTPUT ${file1}
+                         COMMAND ${CMAKE_SOURCE_DIR}/cmake/scripts/check-tidy.sh ${file} ${file1} ${CMAKE_BINARY_DIR}
+                         WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+                        )
+    endforeach()
+
+    # Create the target FormatCheck which only depends on the files creted in
+    # previous step. When running "make FormatCheck" clang-format is executed
+    # for all C, C++ and header files which have changed in the commit.
+    add_custom_target(TidyCheck
+                      DEPENDS ${myfilelist1}
+                      )
+
+  endif()
+
+
 # TODO: check if still needed
 #  if(RULE_CHECKER_FOUND)
 #    ADD_CUSTOM_TARGET(RULES
diff --git a/cmake/modules/FindClangTidy.cmake b/cmake/modules/FindClangTidy.cmake
new file mode 100644
index 0000000000..885261d864
--- /dev/null
+++ b/cmake/modules/FindClangTidy.cmake
@@ -0,0 +1,85 @@
+# Defines the following variables:
+#
+#   ClangTidy_FOUND - Found clang-tidy
+#   CLANG_TIDY_BIN - clang-tidy executable
+
+find_program(CLANG_TIDY_BIN
+  NAMES clang-tidy
+        clang-tidy-16
+        clang-tidy-15
+        clang-tidy-14
+        clang-tidy-13
+        clang-tidy-12
+        clang-tidy-11
+)
+
+Message("CLANG_TIDY_BIN: ${CLANG_TIDY_BIN}")
+
+#list(APPEND required_tidy_checks
+#  modernize-deprecated-headers
+#  modernize-use-nullptr
+#)
+
+# Extract the list of checks from the clang-tidy configuration
+# The line looks like: "Checks:          '-*,modernize-deprecated-headers'"
+# Remove everything beside the part between the quotes and convert the
+# string to a list
+# The checks must not contain any wildcards
+# IDEA: Check if one can extract the same information from clang-tidy --dump-version
+
+file(STRINGS ${CMAKE_SOURCE_DIR}/.clang-tidy required_tidy_checks_from_file REGEX "Checks.*")
+
+string(LENGTH ${required_tidy_checks_from_file} _length)
+string(FIND ${required_tidy_checks_from_file} "'" _pos_first_quote)
+math(EXPR _pos_first_quote ${_pos_first_quote}+1)
+string(SUBSTRING ${required_tidy_checks_from_file} ${_pos_first_quote} ${_length} required_tidy_checks_from_file)
+
+string(FIND ${required_tidy_checks_from_file} "'" _pos_last_quote)
+string(SUBSTRING ${required_tidy_checks_from_file} 0 ${_pos_last_quote} required_tidy_checks_from_file)
+
+string(REPLACE "," ";" required_tidy_checks ${required_tidy_checks_from_file})
+
+message(VERBOSE "required_tidy_checks: ${required_tidy_checks}")
+if (CLANG_TIDY_BIN)
+  # Loop over list of required checks
+  # Succeed if all required checks are supported by the clang version
+
+  execute_process(COMMAND  ${CLANG_TIDY_BIN} -checks=-*,modernize-* --list-checks
+                  OUTPUT_VARIABLE available_tidy_checks
+                 )
+  message(VERBOSE "Available clang-tidy checks: ${available_tidy_checks}")
+
+  set(CLANG_TIDY_CHECKS_SUPPORTED TRUE)
+  foreach(check IN LISTS required_tidy_checks)
+    # Skip all checks which contain wildcards
+    string(REGEX MATCH "\\*" _wildcard ${check})
+    if(_wildcard)
+      message("The checker doesn't support wildcards in the check")
+      string(REGEX MATCH "^-\\*" _wildcard ${check})
+      if(_wildcard)
+        message("The check ${check} is droped.")
+        continue()
+      else()
+        message("The check ${check} violates this. clang-tidy support is disabled.")
+        set(CLANG_TIDY_CHECKS_SUPPORTED FALSE)
+        break()
+      endif()
+    endif()
+    string(FIND "${available_tidy_checks}" "${check}" check_avail)
+    if(${check_avail} EQUAL -1)
+      message("clang-tidy doesn't support the needed Check ${check}. clang-tidy support is disabled.")
+      set(CLANG_TIDY_CHECKS_SUPPORTED FALSE)
+      break()
+    endif()
+    message("Used check: ${check}")
+  endforeach()
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(ClangTidy
+  REQUIRED_VARS CLANG_TIDY_BIN CLANG_TIDY_CHECKS_SUPPORTED
+)
+
+if(ClangTidy_FOUND)
+  message("The found clang tidy supports all requested checks.")
+endif()
diff --git a/cmake/scripts/check-tidy.sh b/cmake/scripts/check-tidy.sh
new file mode 100755
index 0000000000..30d3109ad1
--- /dev/null
+++ b/cmake/scripts/check-tidy.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+# SPDX-License-Identifier: GPL-3.0-only
+# First commited by Florian Uhlig
+
+infile=$1
+outfile=$2
+builddir=$3
+
+CLANG_TIDY_BIN=${CLANG_TIDY_BIN:-clang-tidy}
+
+# special case when a file was deleted
+# don't run the test in such a case
+if [ ! -e $infile ]; then
+  exit 0
+fi
+
+extension=${infile##*.}
+
+# Only check source or header files
+if [ "$extension" == "h" -o "$extension" == "cxx" ]; then
+  echo "Checking file $infile"
+else
+  exit 0
+fi
+
+# Don't do anythink for LinkDef files
+if [[ $infile =~ "LinkDef" ]]; then
+  exit 0
+fi
+
+# Check if the file is a target in the compilation database or a header file
+# for which the corresponding source file is in the compilation database
+if [[ "$extension" == "h" ]]; then
+  checkfile=${infile%.*}.cxx
+else
+  checkfile=$infile
+fi
+
+file=$(grep '"file"' $builddir/compile_commands.json | grep "$checkfile" | cut -d: -f2 | cut -d\" -f2)
+
+if [[ "$file" != "" ]]; then
+  # create directory if it not yet exist
+  dir="$(dirname $outfile)"
+  mkdir -p $dir
+  OUTPUT=$($CLANG_TIDY_BIN -p $builddir $file 2> $outfile)
+else
+  OUTPUT=""
+fi
+
+if [ "$OUTPUT" == "" ]; then
+  exit 0
+else
+  echo "ERROR: clang-tidy check failed for file $infile. Suggested changes:" > $outfile
+  echo  >> $outfile
+  echo "$OUTPUT" >> $outfile
+  echo "ERROR: clang-tidy check failed for file $infile. Suggested changes:"
+  echo
+  echo "$OUTPUT"
+  exit 1
+fi
diff --git a/cmake/scripts/checktidy.cmake b/cmake/scripts/checktidy.cmake
new file mode 100644
index 0000000000..d9e932863a
--- /dev/null
+++ b/cmake/scripts/checktidy.cmake
@@ -0,0 +1,34 @@
+cmake_host_system_information(RESULT fqdn QUERY FQDN)
+
+Set(CTEST_SOURCE_DIRECTORY $ENV{SOURCEDIR})
+Set(CTEST_BINARY_DIRECTORY $ENV{BUILDDIR})
+Set(CTEST_PROJECT_NAME "CbmRoot")
+set(CTEST_CMAKE_GENERATOR "Unix Makefiles")
+set(CTEST_USE_LAUNCHERS ON)
+
+Set(CTEST_CONFIGURE_COMMAND " \"${CMAKE_EXECUTABLE_NAME}\" \"-G${CTEST_CMAKE_GENERATOR}\" \"-DCTEST_USE_LAUNCHERS=${CTEST_USE_LAUNCHERS}\" \"-DBUILD_FOR_TIDY=ON\" \"${CTEST_SOURCE_DIRECTORY}\" ")
+
+if ("$ENV{CTEST_SITE}" STREQUAL "")
+  set(CTEST_SITE "${fqdn}")
+else()
+  set(CTEST_SITE $ENV{CTEST_SITE})
+endif()
+
+if ("$ENV{LABEL}" STREQUAL "")
+  set(CTEST_BUILD_NAME "tidy-check")
+else()
+  set(CTEST_BUILD_NAME $ENV{LABEL})
+endif()
+
+ctest_start(Experimental)
+
+ctest_configure(BUILD "${CTEST_BINARY_DIRECTORY}")
+
+ctest_build(TARGET KFPARTICLE PARALLEL_LEVEL $ENV{NCPU})
+ctest_build(TARGET ANALYSISTREE PARALLEL_LEVEL $ENV{NCPU})
+ctest_build(TARGET GTEST PARALLEL_LEVEL $ENV{NCPU})
+ctest_build(TARGET NICAFEMTO PARALLEL_LEVEL $ENV{NCPU})
+
+ctest_build(TARGET TidyCheck PARALLEL_LEVEL $ENV{NCPU})
+
+ctest_submit()
diff --git a/cmake/scripts/find_files.sh b/cmake/scripts/find_files.sh
index 4ebd9ea054..55cc481eba 100755
--- a/cmake/scripts/find_files.sh
+++ b/cmake/scripts/find_files.sh
@@ -3,9 +3,7 @@
 # SPDX-License-Identifier: GPL-3.0-only
 # First commited by Florian Uhlig
 
-
-BASE_COMMIT=${FAIRROOT_FORMAT_BASE:-HEAD}
-GIT_CLANG_FORMAT_BIN=${FAIRROOT_GIT_CLANG_FORMAT_BIN:-git-clang-format}
+BASE_COMMIT=${1:-HEAD}
 
 FILES=$(git diff --name-only $BASE_COMMIT | grep -E '.*\.(h|hpp|c|C|cpp|cxx|tpl)$' | grep -viE '.*LinkDef.h$')
 
-- 
GitLab