Add clang-tidy, codespell, and sanitizer checks to CI pipeline

libeigen/eigen!2178

Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
This commit is contained in:
Rasmus Munk Larsen
2026-02-23 19:43:45 -08:00
parent 241af1c0ba
commit 00cc497d32
7 changed files with 301 additions and 15 deletions

37
.clang-tidy Normal file
View File

@@ -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'
...

View File

@@ -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

View File

@@ -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

130
ci/scripts/run-clang-tidy.sh Executable file
View File

@@ -0,0 +1,130 @@
#!/bin/bash
#
# Run clang-tidy on files changed in the current MR.
#
# Usage: run-clang-tidy.sh <base_sha> <build_dir>
#
# <base_sha> The merge-base commit to diff against.
# <build_dir> Path to a CMake build directory containing compile_commands.json.
#
# For header files under Eigen/src/<Module>/, 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 <base_sha> <build_dir>}"
BUILD_DIR="${2:?Usage: run-clang-tidy.sh <base_sha> <build_dir>}"
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/<Module>/ 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/<Module>/... -> Eigen/<Module>
if [[ "${header}" =~ ^Eigen/src/([^/]+)/ ]]; then
module="${BASH_REMATCH[1]}"
# Handle unsupported/Eigen/src/<Module>/... -> unsupported/Eigen/<Module>
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
#include <${MODULE_INCLUDE}>
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

View File

@@ -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

View File

@@ -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

26
setup.cfg Normal file
View File

@@ -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