11#! /usr/bin/env bash
22#
3- # tidy.sh -- Run clang-tidy locally using the same file set and config as CI.
4- #
5- # Matches the file filter used by the cpp-linter GitHub Action in
6- # .github/workflows/builds.yml: only src/**/*.{c,cpp,cc,cxx} excluding
7- # src/tests/. Picks up checks from the repo-root .clang-tidy automatically.
3+ # tidy.sh -- Run clang-tidy locally and in CI using the same file set and
4+ # config. Picks up checks from the repo-root .clang-tidy automatically.
85#
96# Usage:
10- # ./tidy.sh # run on full src/ tree
11- # ./tidy.sh -j 4 # override parallelism
12- # ./tidy.sh -fix # auto-apply fixes (forwarded to run-clang-tidy)
7+ # ./tidy.sh # run on full src/ tree
8+ # ./tidy.sh -j 4 # override parallelism
9+ # ./tidy.sh --github-actions # force GitHub Actions annotation mode
10+ # ./tidy.sh -fix # forwarded to run-clang-tidy
11+ #
12+ # In GitHub Actions (auto-detected via $GITHUB_ACTIONS=true, or forced with
13+ # --github-actions), this script additionally:
14+ # - Emits ::warning/::error workflow commands so findings appear as PR file
15+ # annotations (yellow for warnings, red for errors). Severity comes from
16+ # clang-tidy itself -- errors are findings promoted by WarningsAsErrors
17+ # in .clang-tidy.
18+ # - Writes a short markdown summary to $GITHUB_STEP_SUMMARY.
19+ # - Exits non-zero only when run-clang-tidy does (i.e. only on errors);
20+ # warnings annotate but do not fail the build.
1321#
1422# Requires CMake to have generated build-release/compile_commands.json.
1523# Run once: cmake --preset macos-release (or linux-release)
@@ -18,11 +26,29 @@ set -euo pipefail
1826
1927BUILD_DIR=" build-release"
2028# Positive match for top-level src/*.{c,cpp,cc,cxx}; negative lookahead excludes
21- # dep paths (_deps/, build-*/, -src/src/) and other top-level dirs that CI's
22- # cpp-linter `ignore:` list filters out. Python regex (PCRE-ish) supports
23- # lookahead; this regex is evaluated by run-clang-tidy.
29+ # dep paths (_deps/, build-*/, -src/src/) and every other top-level dir. Python
30+ # regex (PCRE-ish) supports lookahead; this regex is evaluated by run-clang-tidy.
2431FILE_REGEX=' ^(?!.*/(_deps|build-[^/]*|bridge|examples|client-sdk-rust|cpp-example-collection|vcpkg_installed|docker|docs|data)/).*/src/(?!tests/).*\.(c|cpp|cc|cxx)$'
2532
33+ CI_MODE=0
34+ if [[ " ${GITHUB_ACTIONS:- } " == " true" ]]; then
35+ CI_MODE=1
36+ fi
37+
38+ forward_args=()
39+ while (( $# )) ; do
40+ case " $1 " in
41+ --github-actions|--gh)
42+ CI_MODE=1
43+ shift
44+ ;;
45+ * )
46+ forward_args+=(" $1 " )
47+ shift
48+ ;;
49+ esac
50+ done
51+
2652if [[ ! -f " ${BUILD_DIR} /compile_commands.json" ]]; then
2753 echo " ERROR: ${BUILD_DIR} /compile_commands.json not found." >&2
2854 echo " Run: cmake --preset macos-release (or linux-release)" >&2
3258if ! command -v run-clang-tidy > /dev/null 2>&1 ; then
3359 echo " ERROR: run-clang-tidy not found in PATH." >&2
3460 echo " Install LLVM: brew install llvm (macOS)" >&2
35- echo " apt install clang-tidy (Linux)" >&2
61+ echo " apt install clang-tools-NN (Linux)" >&2
3662 exit 1
3763fi
3864
5076 jobs=$( sysctl -n hw.ncpu 2> /dev/null || echo 4)
5177fi
5278
79+ # Emit GitHub Actions workflow commands for each clang-tidy diagnostic line
80+ # in the given log. Notes (`path:L:C: note: ...`) are deliberately skipped --
81+ # they belong to the preceding warning/error and would produce noisy extra
82+ # annotations. Severity (::warning vs ::error) mirrors clang-tidy's prefix.
83+ emit_annotations () {
84+ local log=" $1 "
85+ local workspace=" ${GITHUB_WORKSPACE:- ${PWD} } "
86+ local line path lineno col severity message check rel_path
87+
88+ while IFS= read -r line; do
89+ [[ " ${line} " =~ ^(.+):([0-9]+):([0-9]+):[[:space:]]+ (warning| error):[[:space:]]+ (.+)[[:space:]]\[ ([^]]+)\] [[:space:]]* $ ]] || continue
90+ path=" ${BASH_REMATCH[1]} "
91+ lineno=" ${BASH_REMATCH[2]} "
92+ col=" ${BASH_REMATCH[3]} "
93+ severity=" ${BASH_REMATCH[4]} "
94+ message=" ${BASH_REMATCH[5]} "
95+ check=" ${BASH_REMATCH[6]} "
96+
97+ rel_path=" ${path# ${workspace} / } "
98+
99+ message=" ${message// $' %' /% 25} "
100+ message=" ${message// $' \r ' /% 0D} "
101+ message=" ${message// $' \n ' /% 0A} "
102+
103+ printf ' ::%s file=%s,line=%s,col=%s,title=clang-tidy (%s)::%s\n' \
104+ " ${severity} " " ${rel_path} " " ${lineno} " " ${col} " " ${check} " " ${message} "
105+ done < " ${log} "
106+ }
107+
108+ # Append a small markdown summary (counts + top checks) to $GITHUB_STEP_SUMMARY
109+ # so the GitHub job page surfaces totals without needing to scan the log.
110+ write_step_summary () {
111+ local log=" $1 "
112+ local summary_file=" ${GITHUB_STEP_SUMMARY:- } "
113+ [[ -n " ${summary_file} " ]] || return 0
114+
115+ local warnings errors
116+ warnings=$( grep -Ec ' ^.+:[0-9]+:[0-9]+:[[:space:]]+warning:[[:space:]]' " ${log} " || true)
117+ errors=$( grep -Ec ' ^.+:[0-9]+:[0-9]+:[[:space:]]+error:[[:space:]]' " ${log} " || true)
118+
119+ {
120+ echo " ## clang-tidy results"
121+ echo
122+ echo " | Severity | Count |"
123+ echo " |----------|-------|"
124+ echo " | Errors | ${errors} |"
125+ echo " | Warnings | ${warnings} |"
126+ echo
127+
128+ if (( warnings + errors > 0 )) ; then
129+ echo " ### Top checks"
130+ echo
131+ echo ' | Check | Count |'
132+ echo ' |-------|-------|'
133+ grep -Eo ' \[[a-zA-Z0-9._,-]+\]$' " ${log} " \
134+ | sort | uniq -c | sort -rn | head -5 \
135+ | awk ' { n = $1; $1 = ""; sub(/^ /, ""); gsub(/[\[\]]/, "", $0); printf("| `%s` | %d |\n", $0, n) }'
136+ echo
137+ fi
138+ } >> " ${summary_file} "
139+ }
140+
141+ log=" $( mktemp -t tidy-log.XXXXXX) "
142+ trap ' rm -f "${log}"' EXIT
143+
144+ set +e
53145run-clang-tidy \
54146 -p " ${BUILD_DIR} " \
55147 -quiet \
56148 -j " ${jobs} " \
57149 " ${extra_args[@]} " \
58- " $@ " \
59- " ${FILE_REGEX} "
150+ " ${forward_args[@]} " \
151+ " ${FILE_REGEX} " \
152+ 2>&1 | tee " ${log} "
153+ rc=" ${PIPESTATUS[0]} "
154+ set -e
155+
156+ if [[ " ${CI_MODE} " == " 1" ]]; then
157+ emit_annotations " ${log} "
158+ write_step_summary " ${log} "
159+ fi
160+
161+ exit " ${rc} "
0 commit comments