From 00cc497d320ba6b0c6e19b556f5f864570ee9980 Mon Sep 17 00:00:00 2001 From: Rasmus Munk Larsen <4643818-rmlarsen1@users.noreply.gitlab.com> Date: Mon, 23 Feb 2026 19:43:45 -0800 Subject: [PATCH] Add clang-tidy, codespell, and sanitizer checks to CI pipeline libeigen/eigen!2178 Co-authored-by: Rasmus Munk Larsen --- .clang-tidy | 37 +++++++++ ci/build.linux.gitlab-ci.yml | 40 ++++++---- ci/checkformat.gitlab-ci.yml | 38 +++++++++- ci/scripts/run-clang-tidy.sh | 130 ++++++++++++++++++++++++++++++++ ci/scripts/test.linux.script.sh | 3 +- ci/test.linux.gitlab-ci.yml | 42 +++++++++++ setup.cfg | 26 +++++++ 7 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 .clang-tidy create mode 100755 ci/scripts/run-clang-tidy.sh create mode 100644 setup.cfg diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000..4f85be89d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,37 @@ +--- +# Conservative clang-tidy configuration for Eigen. +# +# Focuses on bug-finding checks with low false-positive rates. +# Intentionally omits style-enforcement checks (modernize-*, google-*, +# cppcoreguidelines-*) since Eigen has its own conventions and is a +# heavily-templated math library where many "modern C++" idioms don't apply. + +Checks: > + -*, + bugprone-*, + -bugprone-narrowing-conversions, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-exception-escape, + misc-redundant-expression, + misc-unused-using-decls, + misc-misleading-identifier, + performance-for-range-copy, + performance-implicit-conversion-in-loop, + performance-unnecessary-copy-initialization, + performance-unnecessary-value-param, + readability-container-size-empty, + readability-duplicate-include, + readability-misleading-indentation, + readability-redundant-control-flow, + readability-redundant-smartptr-get, + +WarningsAsErrors: '' + +HeaderFilterRegex: 'Eigen/.*|test/.*|blas/.*|lapack/.*|unsupported/Eigen/.*' + +# Eigen uses its own assert macros. +CheckOptions: + - key: bugprone-assert-side-effect.AssertMacros + value: 'eigen_assert,eigen_internal_assert,EIGEN_STATIC_ASSERT,VERIFY,VERIFY_IS_APPROX,VERIFY_IS_EQUAL,VERIFY_IS_MUCH_SMALLER_THAN,VERIFY_IS_NOT_APPROX,VERIFY_IS_NOT_EQUAL,VERIFY_IS_UNITARY,VERIFY_RAISES_ASSERT' +... diff --git a/ci/build.linux.gitlab-ci.yml b/ci/build.linux.gitlab-ci.yml index af22f8a95..e7f068ba5 100644 --- a/ci/build.linux.gitlab-ci.yml +++ b/ci/build.linux.gitlab-ci.yml @@ -125,20 +125,19 @@ build:linux:docs: - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_PROJECT_NAMESPACE == "libeigen" && $CI_MERGE_REQUEST_LABELS =~ "/all-tests/" -# # Sanitizers (Disabled because ASAN hangs and MSAN requires instrumented libc++) -# build:linux:cross:x86-64:clang-12:default:asan: -# extends: build:linux:cross:x86-64:clang-12:default -# variables: -# EIGEN_CI_ADDITIONAL_ARGS: -# -DEIGEN_TEST_CUSTOM_CXX_FLAGS=-fsanitize=address,undefined -# -DEIGEN_TEST_CUSTOM_LINKER_FLAGS=-fsanitize=address,undefined +######## Sanitizers ############################################################ -# build:linux:cross:x86-64:clang-12:default:msan: -# extends: build:linux:cross:x86-64:clang-12:default -# variables: -# EIGEN_CI_ADDITIONAL_ARGS: -# -DEIGEN_TEST_CUSTOM_CXX_FLAGS=-fsanitize=memory -# -DEIGEN_TEST_CUSTOM_LINKER_FLAGS=-fsanitize=memory +build:linux:cross:x86-64:clang-19:default:asan-ubsan: + extends: .build:linux:cross:x86-64 + image: ubuntu:24.04 + variables: + EIGEN_CI_INSTALL: clang-19 + EIGEN_CI_C_COMPILER: clang-19 + EIGEN_CI_CXX_COMPILER: clang++-19 + EIGEN_CI_CROSS_INSTALL: g++-14-x86-64-linux-gnu clang-19 + EIGEN_CI_ADDITIONAL_ARGS: >- + -DEIGEN_TEST_CUSTOM_CXX_FLAGS=-fsanitize=address,undefined;-fno-omit-frame-pointer;-fno-sanitize-recover=undefined + -DEIGEN_TEST_CUSTOM_LINKER_FLAGS=-fsanitize=address,undefined ######## NVHPC ################################################################# @@ -327,3 +326,18 @@ build:linux:cross:x86-64:clang-12:default:smoketest: tags: - saas-linux-medium-amd64 +######## Sanitizer Smoke Tests ################################################# + +build:linux:cross:x86-64:clang-12:sanitizer:smoketest: + extends: build:linux:cross:x86-64:clang-12:default + variables: + EIGEN_CI_BUILD_TARGET: buildsmoketests + EIGEN_CI_ADDITIONAL_ARGS: >- + -DEIGEN_TEST_CUSTOM_CXX_FLAGS=-fsanitize=address,undefined;-fno-sanitize-recover=address;-fno-omit-frame-pointer + -DEIGEN_TEST_CUSTOM_LINKER_FLAGS=-fsanitize=address,undefined + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + tags: + - saas-linux-medium-amd64 + allow_failure: true + timeout: 30m diff --git a/ci/checkformat.gitlab-ci.yml b/ci/checkformat.gitlab-ci.yml index d8547d97f..c3603df72 100644 --- a/ci/checkformat.gitlab-ci.yml +++ b/ci/checkformat.gitlab-ci.yml @@ -1,6 +1,6 @@ checkformat:clangformat: stage: checkformat - image: alpine:3.19 + image: alpine:3.20 only: - merge_requests allow_failure: true @@ -8,3 +8,39 @@ checkformat:clangformat: - apk add --no-cache git clang17-extra-tools python3 script: - git clang-format --diff --commit ${CI_MERGE_REQUEST_DIFF_BASE_SHA} + +checkformat:codespell: + stage: checkformat + image: alpine:3.20 + only: + - merge_requests + allow_failure: true + before_script: + - apk add --no-cache py3-codespell + script: + - codespell --config setup.cfg + +checkformat:clangtidy: + stage: checkformat + image: ubuntu:24.04 + only: + - merge_requests + allow_failure: true + timeout: 15m + tags: + - saas-linux-medium-amd64 + variables: + DEBIAN_FRONTEND: noninteractive + before_script: + - apt-get update -y > /dev/null + - apt-get install -y --no-install-recommends + git cmake ninja-build clang-tidy clang python3 > /dev/null + script: + - mkdir -p .tidy-build + - cmake -G Ninja -B .tidy-build + -DCMAKE_CXX_COMPILER=clang++ + -DCMAKE_C_COMPILER=clang + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + -DEIGEN_BUILD_TESTING=ON + - chmod +x ci/scripts/run-clang-tidy.sh + - ci/scripts/run-clang-tidy.sh ${CI_MERGE_REQUEST_DIFF_BASE_SHA} .tidy-build diff --git a/ci/scripts/run-clang-tidy.sh b/ci/scripts/run-clang-tidy.sh new file mode 100755 index 000000000..6b40deab6 --- /dev/null +++ b/ci/scripts/run-clang-tidy.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# +# Run clang-tidy on files changed in the current MR. +# +# Usage: run-clang-tidy.sh +# +# The merge-base commit to diff against. +# Path to a CMake build directory containing compile_commands.json. +# +# For header files under Eigen/src//, the script generates a minimal +# driver .cpp that includes the parent module header so that +# InternalHeaderCheck.h does not #error out. + +set -euo pipefail + +BASE_SHA="${1:?Usage: run-clang-tidy.sh }" +BUILD_DIR="${2:?Usage: run-clang-tidy.sh }" + +if [ ! -f "${BUILD_DIR}/compile_commands.json" ]; then + echo "ERROR: ${BUILD_DIR}/compile_commands.json not found." + echo "Run cmake with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON first." + exit 1 +fi + +# External-dependency modules that require third-party headers we don't have. +SKIP_MODULES="AccelerateSupport|CholmodSupport|KLUSupport|MetisSupport|PaStiXSupport|PardisoSupport|SPQRSupport|SuperLUSupport|UmfPackSupport" + +# Get changed files (Added, Modified, Renamed). +CHANGED_FILES=$(git diff --name-only --diff-filter=AMR "${BASE_SHA}" HEAD) + +if [ -z "${CHANGED_FILES}" ]; then + echo "No changed files to check." + exit 0 +fi + +TMPDIR=$(mktemp -d) +trap 'rm -rf "${TMPDIR}"' EXIT + +ERRORS=0 + +# Map a header path under Eigen/src// to its module include. +# e.g. Eigen/src/SVD/BDCSVD.h -> Eigen/SVD +module_include_for_header() { + local header="$1" + local module + + # Handle Eigen/src//... -> Eigen/ + if [[ "${header}" =~ ^Eigen/src/([^/]+)/ ]]; then + module="${BASH_REMATCH[1]}" + # Handle unsupported/Eigen/src//... -> unsupported/Eigen/ + elif [[ "${header}" =~ ^unsupported/Eigen/src/([^/]+)/ ]]; then + module="unsupported/Eigen/${BASH_REMATCH[1]}" + else + return 1 + fi + + # Skip external-dependency modules. + if [[ "${module}" =~ ^(${SKIP_MODULES})$ ]]; then + return 1 + fi + + if [[ "${header}" =~ ^unsupported/ ]]; then + echo "${module}" + else + echo "Eigen/${module}" + fi +} + +# Pick a compile command from compile_commands.json to use as a template. +# We just need a valid set of compiler flags. +TEMPLATE_CMD=$(python3 -c " +import json, sys +with open('${BUILD_DIR}/compile_commands.json') as f: + cmds = json.load(f) +# Pick the first .cpp entry. +for c in cmds: + if c['file'].endswith('.cpp'): + print(c['directory']) + break +") + +echo "Checking changed files with clang-tidy..." +echo "Base SHA: ${BASE_SHA}" +echo "" + +for file in ${CHANGED_FILES}; do + # Only check C++ source and header files. + case "${file}" in + *.cpp|*.cc|*.cxx) + # Source file: run clang-tidy directly if it's in the compilation database. + if grep -q "\"${file}\"" "${BUILD_DIR}/compile_commands.json" 2>/dev/null; then + echo "=== ${file} ===" + if ! clang-tidy -p "${BUILD_DIR}" "${file}" 2>&1; then + ERRORS=$((ERRORS + 1)) + fi + fi + ;; + *.h|*.hpp) + # Header file: generate a driver .cpp that includes the right module. + MODULE_INCLUDE=$(module_include_for_header "${file}" || true) + if [ -z "${MODULE_INCLUDE}" ]; then + # Not a recognized module header or in skip list. + continue + fi + + DRIVER="${TMPDIR}/tidy_driver_$(echo "${file}" | tr '/' '_').cpp" + cat > "${DRIVER}" < +EOF + + echo "=== ${file} (via ${MODULE_INCLUDE}) ===" + if ! clang-tidy \ + -p "${BUILD_DIR}" \ + --header-filter="$(echo "${file}" | sed 's/[.[\*^$()+?{|]/\\&/g')" \ + "${DRIVER}" 2>&1; then + ERRORS=$((ERRORS + 1)) + fi + ;; + esac +done + +if [ ${ERRORS} -gt 0 ]; then + echo "" + echo "clang-tidy reported issues in ${ERRORS} file(s)." + exit 1 +else + echo "" + echo "clang-tidy: all clean." + exit 0 +fi diff --git a/ci/scripts/test.linux.script.sh b/ci/scripts/test.linux.script.sh index 6e2e1c58c..e0d2c21e4 100755 --- a/ci/scripts/test.linux.script.sh +++ b/ci/scripts/test.linux.script.sh @@ -15,7 +15,8 @@ fi set +x -ctest_cmd="ctest ${EIGEN_CI_CTEST_ARGS} --parallel ${NPROC} --output-on-failure --no-compress-output --build-noclean ${target}" +EIGEN_CI_CTEST_PARALLEL=${EIGEN_CI_CTEST_PARALLEL:-${NPROC}} +ctest_cmd="ctest ${EIGEN_CI_CTEST_ARGS} --parallel ${EIGEN_CI_CTEST_PARALLEL} --output-on-failure --no-compress-output --build-noclean ${target}" echo "Running initial tests..." if ${ctest_cmd} -T test; then diff --git a/ci/test.linux.gitlab-ci.yml b/ci/test.linux.gitlab-ci.yml index e042d48d2..7df351c3f 100644 --- a/ci/test.linux.gitlab-ci.yml +++ b/ci/test.linux.gitlab-ci.yml @@ -220,6 +220,29 @@ test:linux:x86-64:clang-19:generic:avx512dq:unsupported: variables: EIGEN_CI_CTEST_LABEL: Unsupported +##### Sanitizers ############################################################### + +.test:linux:x86-64:clang-19:default:asan-ubsan: + extends: .test:linux:x86-64 + image: ubuntu:24.04 + needs: [ build:linux:cross:x86-64:clang-19:default:asan-ubsan ] + variables: + EIGEN_CI_INSTALL: clang-19 llvm-19 + ASAN_OPTIONS: detect_leaks=0:print_stacktrace=1 + UBSAN_OPTIONS: print_stacktrace=1 + ASAN_SYMBOLIZER_PATH: /usr/lib/llvm-19/bin/llvm-symbolizer + EIGEN_CI_CTEST_ARGS: --timeout 2000 + +test:linux:x86-64:clang-19:default:asan-ubsan:official: + extends: .test:linux:x86-64:clang-19:default:asan-ubsan + variables: + EIGEN_CI_CTEST_LABEL: Official + +test:linux:x86-64:clang-19:default:asan-ubsan:unsupported: + extends: .test:linux:x86-64:clang-19:default:asan-ubsan + variables: + EIGEN_CI_CTEST_LABEL: Unsupported + ##### NVHPC #################################################################### .test:linux:x86-64:nvhpc-25.1:default: @@ -440,3 +463,22 @@ test:linux:x86-64:clang-12:default:smoketest: tags: - saas-linux-medium-amd64 +##### Sanitizer Smoke Tests #################################################### + +test:linux:x86-64:clang-12:sanitizer:smoketest: + extends: .test:linux:x86-64:clang-12:default + needs: [ build:linux:cross:x86-64:clang-12:sanitizer:smoketest ] + variables: + EIGEN_CI_INSTALL: clang-12 llvm-12 + EIGEN_CI_CTEST_LABEL: smoketest + EIGEN_CI_CTEST_PARALLEL: "2" + EIGEN_CI_CTEST_ARGS: --timeout 120 + ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1:abort_on_error=1:allocator_may_return_null=1:print_stacktrace=1:detect_stack_use_after_return=0" + ASAN_SYMBOLIZER_PATH: "/usr/lib/llvm-12/bin/llvm-symbolizer" + UBSAN_OPTIONS: "halt_on_error=0:print_stacktrace=1" + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + tags: + - saas-linux-medium-amd64 + allow_failure: true + timeout: 30m diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..4c8d5be4b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,26 @@ +[codespell] +# Skip build directories, third-party code, and binary files. +skip = .git,build,.build,build-clang,*.o,*.a,*.so,*.pyc,blas/f2c,blas/testing,lapack/f2c,CHANGELOG.md +# Ignore words that are false positives in this codebase. +# NOTE: codespell 2.2.x requires all entries to be lowercase. +# Variable names: matA/matC/MatA/MatC/matc, ComplexT, bReal, kInf, Indx, +# ans, ges, workd, whch, pres, dum, ot/Ot, bu/Bu, fo/Fo, ue/Ue, +# ALS, FOM, NIN, lowd (AVX), anc (TensorIntDiv), scond (LAPACK API), +# somme (bench/btl variable), copie/frequence (bench/btl French comments) +# Author name: Manuel Yguel (polynomial modules) +# Eigen packet op names: padd/padds (packet add), preverse (packet reverse) +# AltiVec function name: bload (block load) +# Tensor parameter name: vaccum (vector accumulator) +ignore-words-list = + nd,te,ba,ser, + mata,matc, + complext,breal,kinf,indx, + ans,padd,padds,ges,workd,whch,pres,dum, + ot,bu,fo,ue, + als,fom,nin, + lowd,anc,scond, + somme,copie,frequence, + preverse,halfs,compiletime, + ded,numer,inpt,overfl,lastr, + bload,vaccum, + manuel