From e31084012a41da42cc5859a72af0a60d83345bdd Mon Sep 17 00:00:00 2001 From: Luis Miguens Fernandez Date: Thu, 19 Mar 2026 15:29:57 +0100 Subject: [PATCH 1/2] Add Clang analysis stage to CI and implement analysis script - Introduced a new CI stage for running Clang analysis on Linux. - Added a script for performing Clang static analysis and clang-tidy checks. - Updated CanDeviceConfiguration to maintain consistency in string conversion. --- ci/gitlab/normal.yml | 9 ++ ci/run-clang-analysis-linux.sh | 139 ++++++++++++++++++++++++++++ src/main/CanDeviceConfiguration.cpp | 2 +- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100755 ci/run-clang-analysis-linux.sh diff --git a/ci/gitlab/normal.yml b/ci/gitlab/normal.yml index 420daaff..dcdc0f6d 100644 --- a/ci/gitlab/normal.yml +++ b/ci/gitlab/normal.yml @@ -91,6 +91,15 @@ Run Sanity Checks: - pip install pre-commit - pre-commit run --all-files +Run Clang Analysis on Linux: + stage: unstagged + image: $ALMA_LATEST_IMAGE + needs: [] + script: + - dnf clean all + - dnf -y install clang clang-tools-extra clang-analyzer + - bash ci/run-clang-analysis-linux.sh + Update Documentation Website: stage: unstagged image: $ALMA_LATEST_IMAGE diff --git a/ci/run-clang-analysis-linux.sh b/ci/run-clang-analysis-linux.sh new file mode 100755 index 00000000..652fa7f7 --- /dev/null +++ b/ci/run-clang-analysis-linux.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +BUILD_DIR="build-clang" +REPO_ROOT="$(pwd)" +SCAN_REPORT_DIR="${BUILD_DIR}/scan-build-reports" +SCAN_LOG="${BUILD_DIR}/scan-build.log" +TIDY_LOG="${BUILD_DIR}/clang-tidy.log" + +log() { + echo "[clang-analysis] $*" +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + log "ERROR: required command not found: $cmd" + exit 1 + fi +} + +log "Checking required tooling" +require_cmd cmake +require_cmd clang +require_cmd clang++ +require_cmd scan-build +require_cmd clang-tidy +require_cmd python3 + +log "Configuring CMake in ${BUILD_DIR}" +cmake -S . -B "$BUILD_DIR" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + +log "Building CanModuleMain once without analyzers (prebuild external dependencies)" +cmake --build "$BUILD_DIR" --config Release --target CanModuleMain + +log "Marking project sources as modified for analyzer-only rebuild" +find src/main src/python src/include -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.h' -o -name '*.hpp' -o -name '*.hxx' \) -exec touch {} + + +log "Running Clang Static Analyzer (scan-build)" +rm -rf "$SCAN_REPORT_DIR" +mkdir -p "$SCAN_REPORT_DIR" +set +e +scan-build --keep-empty -plist -o "$SCAN_REPORT_DIR" --use-cc=clang --use-c++=clang++ \ + cmake --build "$BUILD_DIR" --config Release --target CanModuleMain |& tee "$SCAN_LOG" +scan_build_status=${PIPESTATUS[0]} +set -e + +scan_summary=$( +python3 - "$SCAN_REPORT_DIR" "$REPO_ROOT" <<'PY' +import pathlib +import plistlib +import sys + +report_root = pathlib.Path(sys.argv[1]) +repo_root = pathlib.Path(sys.argv[2]).resolve() +hits = [] + +for plist_path in report_root.rglob("*.plist"): + try: + data = plistlib.loads(plist_path.read_bytes()) + except Exception: + continue + + files = data.get("files", []) + diagnostics = data.get("diagnostics", []) + for d in diagnostics: + loc = d.get("location", {}) + file_idx = loc.get("file") + if not isinstance(file_idx, int) or file_idx < 0 or file_idx >= len(files): + continue + + raw_path = pathlib.Path(files[file_idx]) + abs_path = raw_path if raw_path.is_absolute() else (repo_root / raw_path) + abs_path = abs_path.resolve() + try: + rel_path = abs_path.relative_to(repo_root) + except Exception: + continue + + rel = str(rel_path) + if not rel.startswith("src/"): + continue + + line = loc.get("line", 0) + col = loc.get("col", 0) + checker = d.get("check_name", "unknown-checker") + desc = d.get("description", "").replace("\n", " ").strip() + hits.append((rel, line, col, checker, desc)) + +for rel, line, col, checker, desc in hits: + print(f"{rel}:{line}:{col}: [{checker}] {desc}") + +print(f"SCAN_PROJECT_COUNT={len(hits)}") +PY +) +echo "$scan_summary" +scan_project_count="$(echo "$scan_summary" | awk -F= '/^SCAN_PROJECT_COUNT=/{print $2}' | tail -n1)" +scan_project_count="${scan_project_count:-0}" +scan_completed=0 +if grep -q "scan-build: Analysis run complete." "$SCAN_LOG"; then + scan_completed=1 +fi +scan_invocation_failed=0 + +if [[ $scan_build_status -ne 0 && $scan_completed -eq 0 ]]; then + scan_invocation_failed=1 + log "ERROR: scan-build build invocation failed (exit=${scan_build_status})" +fi + +log "Running clang-tidy" +tidy_status=0 +rm -f "$TIDY_LOG" +mapfile -t tidy_files < <(find src/main src/python -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' \) | sort) +if [[ ${#tidy_files[@]} -eq 0 ]]; then + log "ERROR: no project C/C++ source files found for clang-tidy" + exit 1 +fi + +for f in "${tidy_files[@]}"; do + set +e + clang-tidy -p "$BUILD_DIR" --warnings-as-errors='*' "$f" |& tee -a "$TIDY_LOG" + file_status=${PIPESTATUS[0]} + set -e + if [[ $file_status -ne 0 ]]; then + tidy_status=1 + fi +done + +if [[ $tidy_status -ne 0 ]]; then + log "clang-tidy findings (project sources only):" + grep -E '^.*/src/.*:[0-9]+:[0-9]+: (warning|error):' "$TIDY_LOG" || true +fi + +if [[ $scan_invocation_failed -ne 0 || $scan_project_count -ne 0 || $tidy_status -ne 0 ]]; then + log "ERROR: analyzer failures detected (scan-invocation-failed=$scan_invocation_failed, scan-build-exit=$scan_build_status, scan-project-findings=$scan_project_count, clang-tidy=$tidy_status)" + exit 1 +fi + +log "Clang analysis completed without findings" diff --git a/src/main/CanDeviceConfiguration.cpp b/src/main/CanDeviceConfiguration.cpp index c3678bf7..f2f3910f 100644 --- a/src/main/CanDeviceConfiguration.cpp +++ b/src/main/CanDeviceConfiguration.cpp @@ -65,7 +65,7 @@ std::string CanDeviceConfiguration::to_string() const noexcept { if (sent_acknowledgement.has_value()) { if (!first) oss << ", "; oss << "sent_acknowledgement=" << sent_acknowledgement.value(); - first = false; + first = false; // NOLINT: Indeed, this is the last field, but we set first to false for consistency. } return oss.str(); From 0340ff975fd70c96e0d3a15745e169a42d77bd1b Mon Sep 17 00:00:00 2001 From: Luis Miguens Fernandez Date: Thu, 19 Mar 2026 15:49:21 +0100 Subject: [PATCH 2/2] Formatting --- src/main/CanDeviceConfiguration.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/CanDeviceConfiguration.cpp b/src/main/CanDeviceConfiguration.cpp index f2f3910f..207c75f6 100644 --- a/src/main/CanDeviceConfiguration.cpp +++ b/src/main/CanDeviceConfiguration.cpp @@ -65,7 +65,8 @@ std::string CanDeviceConfiguration::to_string() const noexcept { if (sent_acknowledgement.has_value()) { if (!first) oss << ", "; oss << "sent_acknowledgement=" << sent_acknowledgement.value(); - first = false; // NOLINT: Indeed, this is the last field, but we set first to false for consistency. + first = false; // NOLINT: Indeed, this is the last field, but we set first + // to false for consistency. } return oss.str();