Merge pull request #1310 from Yyunozor/fix/1264-hook-diff-aware-clang-tidy

fix(hooks): scope pre-commit clang-tidy to staged changes (#1264)
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-08-18 00:03:46 +02:00
commit 0273250c33
3 files changed
+120 -2

No files matched your search

+7 -1
View File
@@ -729,7 +729,7 @@ PP_OBJ_TSAN = $(BUILD_DIR)/tsan_preprocessor.o
# ── Targets ──────────────────────────────────────────────────────
.PHONY: test test-par test-repro test-foundation test-tsan test-daemon-smoke cbm cbm-with-ui frontend embed clean-c lint lint-tidy lint-cppcheck lint-format security
.PHONY: test test-par test-repro test-foundation test-tsan test-daemon-smoke cbm cbm-with-ui frontend embed clean-c lint lint-tidy lint-tidy-diff lint-cppcheck lint-format security
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
@@ -1103,6 +1103,12 @@ lint-tidy:
@echo "=== clang-tidy ==="
@$(CLANG_TIDY) --quiet $(LINT_SRCS) -- $(CFLAGS_COMMON) $(SYSROOT_FLAG)
# clang-tidy, scoped to this commit's staged changes only (scripts/lint-tidy-diff.sh).
# The pre-commit hook runs this instead of the full lint-tidy sweep above, so a
# clean commit is not blocked by pre-existing findings elsewhere in the tree.
lint-tidy-diff:
@scripts/lint-tidy-diff.sh "$(CLANG_TIDY)" $(LINT_SRCS) -- $(CFLAGS_COMMON) $(SYSROOT_FLAG)
# cppcheck: complementary analysis (config in .cppcheck)
lint-cppcheck:
@echo "=== cppcheck ==="
+5 -1
View File
@@ -7,7 +7,11 @@ set -euo pipefail
# git config core.hooksPath scripts/hooks
echo "pre-commit: running all linters in parallel..."
make -j3 -f Makefile.cbm lint
# clang-tidy runs diff-scoped (lint-tidy-diff, see scripts/lint-tidy-diff.sh):
# the full lint-tidy sweep surfaces thousands of pre-existing findings that
# have nothing to do with this commit and would block every contributor who
# has clang-tidy on PATH. `make lint-tidy` remains available for a full audit.
make -j3 -f Makefile.cbm lint-ci lint-tidy-diff
echo "pre-commit: security audit (source-level)..."
scripts/security-audit.sh
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
# lint-tidy-diff.sh — clang-tidy scoped to the lines this commit changes.
#
# `make lint-tidy` (the full sweep) checks the entire LINT_SRCS tree, which
# currently surfaces ~5,100 pre-existing findings unrelated to any given
# commit (see Makefile.cbm's `lint-tidy` target). That is fine for a manual
# deep audit but makes the pre-commit hook unusable: it blocks every commit,
# including ones that touch files with zero findings on the touched lines.
#
# This script runs the same clang-tidy binary/config against the SAME staged
# .c files, but passes clang-tidy's own `-line-filter` so only lines the
# commit actually added/modified can produce a diagnostic. Pre-existing
# findings on untouched lines — in the same file or elsewhere — are not
# reported.
#
# Invoked by `make lint-tidy-diff` (scripts/hooks/pre-commit), which supplies
# the resolved clang-tidy binary, then LINT_SRCS, then `--`, then the
# compiler flags — the same argv shape as the `lint-tidy` target itself.
#
# Known limitation: only staged *.c files that are members of LINT_SRCS are
# checked (matching the full sweep's own scope: vendored code and tests/ are
# never clang-tidy'd). clang-tidy reports a header's diagnostics under
# whatever path it was #include-d with, which is not reliably the same
# string as its path in the working tree, so a commit that touches only a
# header is not covered here even if a .c file including it also changed;
# run `make lint-tidy` for a full pass in that case.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [ "$#" -lt 1 ]; then
echo "usage: lint-tidy-diff.sh <clang-tidy-bin> [LINT_SRCS...] -- [CFLAGS...]" >&2
exit 2
fi
CLANG_TIDY="$1"
shift
LINT_SRCS=()
while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do
LINT_SRCS+=("$1")
shift
done
[ "$#" -gt 0 ] && shift # drop the `--` separator
CFLAGS=("$@")
# bash 3.2 (macOS default, see scripts/check-lsp-originality.sh) has neither
# `mapfile` nor associative arrays — read loop + linear membership check.
is_lint_src() {
local needle="$1" f
[ "${#LINT_SRCS[@]}" -eq 0 ] && return 1
for f in "${LINT_SRCS[@]}"; do
[ "$f" = "$needle" ] && return 0
done
return 1
}
# Staged .c files this commit actually changed (added/copied/modified/
# renamed — a delete has no new lines to check) that are members of
# LINT_SRCS.
CHANGED=()
while IFS= read -r f; do
[ -n "$f" ] && is_lint_src "$f" && CHANGED+=("$f")
done < <(git diff --cached --name-only --diff-filter=ACMR -- '*.c')
if [ "${#CHANGED[@]}" -eq 0 ]; then
echo "lint-tidy-diff: no staged changes to a linted .c file, skipping clang-tidy"
exit 0
fi
# For each changed file, turn its staged hunks into a clang-tidy line-filter
# range: `@@ -a,b +c,d @@` -> lines [c, c+d-1]. Zero-context (-U0) so a range
# never includes untouched, pre-existing lines. Pure-deletion hunks (d == 0)
# add no new lines and are skipped.
LINE_FILTER='['
FILTER_SEP=''
TARGETS=()
for f in "${CHANGED[@]}"; do
[ -f "$f" ] || continue # renamed away / not present in the working tree
ranges=''
range_sep=''
while IFS= read -r hunk; do
if [[ "$hunk" =~ ^@@\ -[0-9]+(,[0-9]+)?\ \+([0-9]+)(,([0-9]+))?\ @@ ]]; then
new_start="${BASH_REMATCH[2]}"
new_count="${BASH_REMATCH[4]:-1}"
[ "$new_count" -eq 0 ] && continue
new_end=$((new_start + new_count - 1))
ranges+="${range_sep}[${new_start},${new_end}]"
range_sep=','
fi
done < <(git diff --cached -U0 --diff-filter=ACMR -- "$f" | grep -E '^@@ ')
[ -z "$ranges" ] && continue
LINE_FILTER+="${FILTER_SEP}{\"name\":\"${f}\",\"lines\":[${ranges}]}"
FILTER_SEP=','
TARGETS+=("$f")
done
LINE_FILTER+=']'
if [ "${#TARGETS[@]}" -eq 0 ]; then
echo "lint-tidy-diff: staged .c changes are deletion-only, skipping clang-tidy"
exit 0
fi
echo "lint-tidy-diff: ${#TARGETS[@]} changed file(s), diff-scoped clang-tidy"
"$CLANG_TIDY" --quiet "-line-filter=${LINE_FILTER}" "${TARGETS[@]}" -- "${CFLAGS[@]}"