feat(tools): add support for clang-tidy

This patch integrates clang-tidy into the project. Custom targets for
clang-tidy are added, which run clang-tidy on either the codebase or
pending commits.

A selection of checks has been enabled, some have their warnings
promoted to errors. The enabled checks, and the checks which have
warnings promoted to errors, are configured in the .clang-tidy file.

Signed-off-by: Chuyue Luo <Chuyue.Luo@arm.com>
Change-Id: Idba6d6b3e4438f524ac68307b1e463b9aec958e3
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000..e0d8eae
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,36 @@
+#
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
+#
+
+#
+# Configuration file for clang-tidy.
+#
+# Checks are specified as a comma-separated list. A '-' before the name of the
+# check will disable that check.
+#
+Checks: '-*,
+    bugprone-*,
+    -bugprone-reserved-identifier,
+    -bugprone-easily-swappable-parameters,
+    -bugprone-branch-clone,
+    misc-redundant-expression,
+    misc-unused-parameters,
+    google-readability-casting'
+
+#
+# The WarningsAsErrors field specifies which checks will have their warnings
+# promoted to errors. These checks are specified in the same way as above.
+#
+WarningsAsErrors: '-*,
+    bugprone-narrowing-conversions,
+    bugprone-implicit-widening-of-multiplication-result,
+    bugprone-infinite-loop,
+    bugprone-too-small-loop-variable'
+
+#
+# The HeaderFilterRegex field specifies which header files clang-tidy will
+# output warnings from. Note that this does NOT affect which *.c files are
+# checked.
+#
+HeaderFilterRegex: 'drivers/|plat/|runtime/|lib/'
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 40e887a..905db56 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -191,3 +191,28 @@
   WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
   COMMAND ${CMAKE_COMMAND} -DCHECKINCLUDES_PATCH=1 -P ${CMAKE_SOURCE_DIR}/tools/checkincludes/CheckIncludes.cmake
   )
+
+#
+# Rules for running clang-tidy checks
+#
+# Pass through the value of RMM_TOOLCHAIN as this must be verified before
+# clang-tidy can be run.
+#
+# Also pass through the build directory as this cannot be accessed when the
+# clang-tidy target is built.
+#
+add_custom_target(clang-tidy-codebase
+  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+  COMMAND ${CMAKE_COMMAND} -DCLANG-TIDY_CODEBASE=1
+    -DRMM_TOOLCHAIN=${RMM_TOOLCHAIN}
+    -DBUILD_DIR=${CMAKE_BINARY_DIR}
+    -P ${CMAKE_SOURCE_DIR}/tools/clang-tidy/clang-tidy.cmake
+  )
+
+add_custom_target(clang-tidy-patch
+  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+  COMMAND ${CMAKE_COMMAND} -DCLANG-TIDY_PATCH=1
+    -DRMM_TOOLCHAIN=${RMM_TOOLCHAIN}
+    -DBUILD_DIR=${CMAKE_BINARY_DIR}
+    -P ${CMAKE_SOURCE_DIR}/tools/clang-tidy/clang-tidy.cmake
+  )
diff --git a/docs/getting_started/build-options.rst b/docs/getting_started/build-options.rst
index d4fe338..a14200c 100644
--- a/docs/getting_started/build-options.rst
+++ b/docs/getting_started/build-options.rst
@@ -162,7 +162,27 @@
     cmake -DRMM_CONFIG=fvp_defcfg -S ${RMM_SOURCE_DIR} -B ${RMM_BUILD_DIR}
     cmake --build ${RMM_BUILD_DIR} -- checkincludes-codebase
 
-14. Perform unit tests on development host:
+14. Perform a clang-tidy analysis:
+
+Run clang-tidy on commits in the current branch against BASE_COMMIT (default
+origin/master):
+
+.. code-block:: bash
+
+    cmake -DRMM_CONFIG=fvp_defcfg -DRMM_TOOLCHAIN=llvm -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -S ${RMM_SOURCE_DIR} -B ${RMM_BUILD_DIR}
+    cmake --build ${RMM_BUILD_DIR} -- clang-tidy-patch
+
+Run clang-tidy on entire codebase:
+
+.. code-block:: bash
+
+    cmake -DRMM_CONFIG=fvp_defcfg -DRMM_TOOLCHAIN=llvm -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -S ${RMM_SOURCE_DIR} -B ${RMM_BUILD_DIR}
+    cmake --build ${RMM_BUILD_DIR} -- clang-tidy-codebase
+
+Note that clang-tidy will work with all configurations. It will only check the
+source files that are used for the specified configuration.
+
+15. Perform unit tests on development host:
 
 Build and run unit tests on host platform. It is recommended to enable the
 Debug build of RMM.
@@ -180,7 +200,7 @@
     cmake --build ${RMM_BUILD_DIR} -- build -j
     ${RMM_BUILD_DIR}/Debug/rmm.elf -gxlat -v -r${NUMBER_OF_TEST_ITERATIONS}
 
-15. Generate Coverage Report.
+16. Generate Coverage Report.
 
 It is possible to generate a coverage report for a last execution of the host
 platform (whichever the variant) by using the `run-coverage` build target.
diff --git a/tools/clang-tidy/clang-tidy.cmake b/tools/clang-tidy/clang-tidy.cmake
new file mode 100644
index 0000000..63bd1a8
--- /dev/null
+++ b/tools/clang-tidy/clang-tidy.cmake
@@ -0,0 +1,260 @@
+#
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
+#
+
+#
+# This script is called from main CMakeLists.txt to run clang-tidy checks. The
+# checks are run on either the codebase (if variable CLANG-TIDY_CODEBASE is
+# defined) or just new commits (if variable CLANG-TIDY_PATCH is defined).
+#
+# Enabled checks can be configured in the .clang-tidy file.
+#
+find_package(Git REQUIRED)
+find_package(Python REQUIRED)
+find_package(Python3 REQUIRED)
+
+find_program(CLANGTIDY_EXECUTABLE "run-clang-tidy"
+  DOC "Path to run-clang-tidy executable."
+  )
+
+if(NOT CLANGTIDY_EXECUTABLE)
+  message(FATAL_ERROR "Could not find run-clang-tidy executable.")
+endif()
+
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/tools/common")
+include(GitUtils)
+
+#
+# List of directories and files to exclude from checking for target
+#
+list(APPEND glob_excludes "^.git")
+list(APPEND glob_excludes "^out")
+list(APPEND glob_excludes "^build")
+list(APPEND glob_excludes "^ext")
+list(APPEND glob_excludes "^tools")
+list(APPEND glob_excludes "^configs")
+list(APPEND glob_excludes "^cmake")
+list(APPEND glob_excludes "^docs")
+
+#
+# clang-tidy_get_stats: Parse output and return number of errors and warnings
+#
+function(clangtidy_get_stats stats_arg warnings_ret errors_ret)
+  set(output_lines)
+  string(REPLACE "\n" ";" output_lines "${stats_arg}")
+
+  set(warnings 0)
+  set(errors 0)
+
+  #
+  # Ideally we would match against the substring ": warning: ", and similarly
+  # for errors.
+  #
+  # Unfortunately, the run-clang-tidy included with Clang 14.0.0 enables
+  # colours in the output by default, and there is no way to change this in
+  # the configuration. The use of colours in the output means the exact
+  # substring ": warning: " is never present, due to the presence of escape
+  # characters.
+  #
+  # In addition, the presence of escape characters presents difficulties in
+  # splitting the output into lines. This is why REGEX MATCHALL is used
+  # instead of FIND.
+  #
+  foreach(output_line IN LISTS output_lines)
+    set(warnings_list)
+
+    string(REGEX MATCHALL "warning: " warnings_list "${output_line}")
+    list(LENGTH warnings_list warnings_count)
+    MATH(EXPR warnings "${warnings} + ${warnings_count}")
+
+    set(errors_list)
+
+    string(REGEX MATCHALL "error: " errors_list "${output_line}")
+    list(LENGTH errors_list errors_count)
+    MATH(EXPR errors "${errors} + ${errors_count}")
+  endforeach()
+
+  set(${warnings_ret} ${warnings} PARENT_SCOPE)
+  set(${errors_ret} ${errors} PARENT_SCOPE)
+endfunction()
+
+#
+# print_stats_and_exit: Print summary of all errors and warnings.
+# If there are errors call message(FATAL_ERROR)
+#
+function(print_stats_and_exit check_type total_warnings total_errors)
+  message(STATUS "${check_type}: total warnings: ${total_warnings}, total errors: ${total_errors}")
+
+  if(${total_errors} GREATER 0)
+    message(FATAL_ERROR "${check_type}: FAILED")
+  endif()
+
+  message(STATUS "${check_type}: PASSED")
+endfunction()
+
+#
+# filter_source_files: Filter all files except *.c/*.h files
+#
+function(filter_source_files all_files source_files_ret)
+  foreach(exclude IN LISTS glob_excludes)
+    list(FILTER all_files EXCLUDE REGEX "${exclude}")
+  endforeach()
+
+  foreach(source_file ${all_files})
+    if(NOT source_file MATCHES ".c$" AND
+        NOT source_file MATCHES ".h$")
+      list(REMOVE_ITEM all_files ${source_file})
+    endif()
+  endforeach()
+
+  set(${source_files_ret} ${all_files} PARENT_SCOPE)
+endfunction()
+
+#
+# run_clangtidy: Run clang-tidy on the list of files.
+#
+function(run_clangtidy source_files warnings_ret errors_ret)
+  set(total_warnings 0)
+  set(total_errors 0)
+
+  string(REPLACE ";" " " source_files "${source_files}")
+  separate_arguments(source_files NATIVE_COMMAND "${source_files}")
+
+  foreach(source_file ${source_files})
+    message("Checking file ${source_file}")
+
+    #
+    # Run clang-tidy on each file, one at a time. Note that although this loop
+    # iterates through all files in the codebase, only files used for the
+    # specified configuration will actually be checked.
+    #
+    # We pass the compile commands database (in the build folder), which
+    # records the compile options used in the build, to clang-tidy.
+    #
+    # -quiet flag is required to prevent clang-tidy from printing the enabled
+    # checks for every file!
+    #
+    # We discard the standard error output, which is simply a line stating "X
+    # warnings generated". This number X includes warnings from system headers,
+    # which are not displayed. Instead, we manually count the number of
+    # warnings and errors generated from the relevant files.
+    #
+    execute_process(
+      WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+      COMMAND ${CLANGTIDY_EXECUTABLE}
+        -p ${BUILD_DIR}
+        -quiet
+        ${source_file}
+        OUTPUT_VARIABLE clang-tidy_output
+        ERROR_QUIET
+        )
+
+    if(clang-tidy_output)
+      message(${clang-tidy_output})
+    endif()
+
+    # Update total counts for warnings and errors
+    clangtidy_get_stats("${clang-tidy_output}" warnings errors)
+    MATH(EXPR total_warnings "${total_warnings} + ${warnings}")
+    MATH(EXPR total_errors "${total_errors} + ${errors}")
+  endforeach()
+
+  set(${warnings_ret} ${total_warnings} PARENT_SCOPE)
+  set(${errors_ret} ${total_errors} PARENT_SCOPE)
+endfunction()
+
+#
+# Run clang-tidy on entire codebase. This verifies all files in this
+# repository in "GLOB_INCLUDES".
+#
+# Exits with FATAL_ERROR upon errors.
+#
+if(CLANG-TIDY_CODEBASE)
+  set(compile_commands
+    "${BUILD_DIR}/compile_commands.json")
+
+  if(NOT EXISTS "${compile_commands}")
+    message(FATAL_ERROR
+      "clang-tidy requires a compile command database. Use flag "
+      "`-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` during configuration.")
+  endif()
+
+  if(NOT "${RMM_TOOLCHAIN}" STREQUAL "llvm")
+    message(FATAL_ERROR
+      "clang-tidy can be used only if the project is built using the LLVM "
+      "toolchain. Use flag `DRMM_TOOLCHAIN=llvm` during configuration.")
+  endif()
+
+  set(source_files "")
+
+  if (GIT_FOUND AND IS_DIRECTORY .git)
+    Git_Get_All_Files(all_files)
+  else()
+    file(GLOB_RECURSE all_files RELATIVE ${CMAKE_SOURCE_DIR} "*")
+  endif()
+
+  filter_source_files("${all_files}" source_files)
+
+  if(NOT source_files)
+    message(STATUS "clang-tidy-codebase: No files to check")
+    return()
+  endif()
+
+  run_clangtidy("${source_files}" total_warnings total_errors)
+  print_stats_and_exit("clang-tidy-codebase" ${total_warnings} ${total_errors})
+endif()
+
+#
+# Run clang-tidy on pending commits.
+#
+# Exits with FATAL_ERROR upon errors.
+#
+if(CLANG-TIDY_PATCH)
+  set(compile_commands
+    "${BUILD_DIR}/compile_commands.json")
+
+  if(NOT EXISTS "${compile_commands}")
+    message(FATAL_ERROR
+      "clang-tidy requires a compile command database. Use flag "
+      "`-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` during configuration.")
+  endif()
+
+  if(NOT "${RMM_TOOLCHAIN}" STREQUAL "llvm")
+    message(FATAL_ERROR
+      "clang-tidy can be used only if the project is built using the LLVM "
+      "toolchain. Use flag `DRMM_TOOLCHAIN=llvm` during configuration.")
+  endif()
+
+  if(GIT_NOT_FOUND OR NOT IS_DIRECTORY .git)
+    message(FATAL_ERROR "Required dependencies Git not found")
+  endif()
+
+  #
+  # Get list of commits to check
+  #
+  Git_Get_Pending_Commits(pending_commits)
+
+  #
+  # Iterate through list of commit ids
+  #
+  set(total_warnings 0)
+  set(total_errors 0)
+
+  foreach(commit IN LISTS pending_commits)
+    message(STATUS "Checking commit: ${commit}")
+
+    Git_Get_Files_In_Commit("${commit}" files_in_commit)
+
+    set(source_files "")
+    filter_source_files("${files_in_commit}" source_files)
+
+    if(source_files)
+      run_clangtidy("${source_files}" warnings errors)
+      MATH(EXPR total_warnings "${total_warnings} + ${warnings}")
+      MATH(EXPR total_errors "${total_errors} + ${errors}")
+    endif()
+  endforeach()
+
+  print_stats_and_exit("clang-tidy-patch" ${total_warnings} ${total_errors})
+endif()