apply_clang.sh 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #!/usr/bin/env bash
  2. # Robust, gentle clang-tidy runner.
  3. # - Resolves absolute/relative paths from compile_commands.json
  4. # - Avoids running directly on headers; analyzes through TUs
  5. # - Throttled (nice/ionice), per-file timeout, soft memory cap (Linux)
  6. # - Skips generated build/autogen files (Qt moc/uic, *_autogen)
  7. # - Verbose discovery + clean summary
  8. set -euo pipefail
  9. BUILD_DIR="build"
  10. CHECKS="modernize-*,readability-*,performance-*,misc-*,-misc-unused-parameters,-readability-identifier-naming"
  11. # ---- options ----
  12. DRY_RUN=false
  13. MEM_CAP_MB=4096 # soft cap per process (Linux only)
  14. FILE_TIMEOUT="300s" # per-TU timeout; "" to disable
  15. SHOW_DISCOVERY=1 # print TU count + a few samples
  16. if [[ "${1:-}" == "--dry-run" ]]; then
  17. DRY_RUN=true
  18. echo "🧪 Dry-run mode: no fixes will be applied."
  19. fi
  20. # ---- sanity ----
  21. if [[ ! -f "$BUILD_DIR/compile_commands.json" ]]; then
  22. echo "❌ Error: $BUILD_DIR/compile_commands.json not found."
  23. echo "Run CMake with: -DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
  24. exit 1
  25. fi
  26. REPO_ABS="$(pwd)"
  27. escape_regex() { sed -e 's/[.[\*^$+?(){}|\\]/\\&/g' <<<"$1"; }
  28. HEADER_FILTER="^$(escape_regex "$REPO_ABS")/.*"
  29. # Accept a wide set of C/C++ TU extensions (case-insensitive)
  30. is_source_ext() {
  31. shopt -s nocasematch
  32. [[ "$1" =~ \.(c|cc|cpp|cxx|c\+\+|C|ixx|ix|mm|m)$ ]]
  33. }
  34. # Fast absolute path helper using python3 -c with stdin/args (no here-doc on stdin!)
  35. abspath_from_dir_file() {
  36. local dir="$1" file="$2"
  37. if [[ "$file" = /* ]]; then
  38. python3 -c 'import os,sys; print(os.path.abspath(sys.stdin.read().strip()))' <<<"$file"
  39. else
  40. python3 - "$dir" "$file" <<'PY'
  41. import os, sys
  42. dir_ = sys.argv[1]
  43. file_ = sys.argv[2]
  44. print(os.path.abspath(os.path.join(dir_, file_)))
  45. PY
  46. fi
  47. }
  48. echo "🔍 Collecting translation units from $BUILD_DIR/compile_commands.json ..."
  49. FILES=()
  50. if command -v jq >/dev/null 2>&1; then
  51. # Primary: use .file (resolving relative to .directory)
  52. while IFS=$'\t' read -r DIR FILE; do
  53. [[ -n "${FILE:-}" ]] || continue
  54. ABS="$(abspath_from_dir_file "$DIR" "$FILE")"
  55. if is_source_ext "$ABS"; then
  56. FILES+=("$ABS")
  57. fi
  58. done < <(jq -r '.[] | [.directory, (.file // "")] | @tsv' "$BUILD_DIR/compile_commands.json")
  59. # Fallback: derive from arguments/command if needed
  60. if [[ ${#FILES[@]} -eq 0 ]]; then
  61. while IFS=$'\t' read -r DIR HAS_ARGS; do
  62. SRC=""
  63. if [[ "$HAS_ARGS" == "args" ]]; then
  64. # flatten tokens from .arguments
  65. mapfile -t TOKS < <(jq -r '.arguments[]' <<<"$(jq -c '.arguments' <<<"$(jq -c ' .' -r)")" 2>/dev/null)
  66. else
  67. # split .command string
  68. CMD="$(jq -r '.command' <<<"$(jq -c ' .' -r)")"
  69. read -r -a TOKS <<<"$CMD"
  70. fi
  71. for t in "${TOKS[@]:-}"; do
  72. if is_source_ext "$t"; then SRC="$t"; fi
  73. done
  74. [[ -n "$SRC" ]] || continue
  75. ABS="$(abspath_from_dir_file "$DIR" "$SRC")"
  76. if is_source_ext "$ABS"; then
  77. FILES+=("$ABS")
  78. fi
  79. done < <(jq -r '.[] | . as $e | if has("arguments") then
  80. [$e.directory, "args"] | @tsv
  81. else
  82. [$e.directory, "cmd"] | @tsv
  83. end' "$BUILD_DIR/compile_commands.json")
  84. fi
  85. else
  86. # Fallback without jq: parse directory/file pairs
  87. mapfile -t FILES < <(
  88. awk -F'"' '
  89. $2=="directory"{dir=$4}
  90. $2=="file"{
  91. f=$4
  92. if (substr(f,1,1)=="/") print f; else print dir "/" f
  93. }' "$BUILD_DIR/compile_commands.json" \
  94. | while IFS= read -r p; do
  95. python3 -c 'import os,sys; print(os.path.abspath(sys.stdin.read().strip()))' <<<"$p"
  96. done \
  97. | awk 'BEGIN{IGNORECASE=1} $0 ~ /\.(c|cc|cpp|cxx|c\+\+|C|ixx|ix|mm|m)$/ {print}' \
  98. | sort -u
  99. )
  100. # Fallback-from-command if still empty
  101. if [[ ${#FILES[@]} -eq 0 ]]; then
  102. mapfile -t FILES < <(
  103. awk -F'"' '
  104. $2=="directory"{dir=$4}
  105. $2=="command"{
  106. cmd=$4
  107. n=split(cmd, a, /[[:space:]]+/)
  108. src=""
  109. for(i=1;i<=n;i++) {
  110. t=a[i]
  111. if (match(t, /\.(c|cc|cpp|cxx|c\+\+|C|ixx|ix|mm|m)$/i)) src=t
  112. }
  113. if (src!="") {
  114. if (substr(src,1,1)=="/") print src;
  115. else print dir "/" src;
  116. }
  117. }' "$BUILD_DIR/compile_commands.json" \
  118. | while IFS= read -r p; do
  119. python3 -c 'import os,sys; print(os.path.abspath(sys.stdin.read().strip()))' <<<"$p"
  120. done \
  121. | sort -u
  122. )
  123. fi
  124. fi
  125. # Deduplicate (preserve order)
  126. declare -A _seen=()
  127. UNIQ=()
  128. for f in "${FILES[@]}"; do
  129. [[ -n "$f" ]] || continue
  130. if [[ -z "${_seen[$f]:-}" ]]; then
  131. _seen["$f"]=1
  132. UNIQ+=("$f")
  133. fi
  134. done
  135. FILES=("${UNIQ[@]}")
  136. # Filter out generated stuff to save RAM/time
  137. FILTERED=()
  138. for f in "${FILES[@]}"; do
  139. case "$f" in
  140. "$REPO_ABS/$BUILD_DIR/"* | *"/_autogen/"* | *"/autogen/"* | */mocs_compilation.cpp | */qrc_*.cpp )
  141. continue
  142. ;;
  143. * )
  144. FILTERED+=("$f")
  145. ;;
  146. esac
  147. done
  148. FILES=("${FILTERED[@]}")
  149. if [[ ${#FILES[@]} -eq 0 ]]; then
  150. echo "⚠️ No translation units found."
  151. echo " Hints:"
  152. echo " • Check $BUILD_DIR/compile_commands.json for \"file\" or usable \"command/arguments\" entries."
  153. echo " • Ensure you built C/C++ targets so the DB contains real TUs."
  154. exit 0
  155. fi
  156. # Discovery report
  157. if [[ ${SHOW_DISCOVERY} -eq 1 ]]; then
  158. echo "📦 Found ${#FILES[@]} translation unit(s) (after filtering autogen/build)."
  159. echo " Sample(s):"
  160. for s in "${FILES[@]:0:5}"; do
  161. if [[ "$s" == "$REPO_ABS"* ]]; then
  162. echo " - ${s#$REPO_ABS/}"
  163. else
  164. echo " - $s"
  165. fi
  166. done
  167. fi
  168. # ---- gentle scheduling ----
  169. NI_CMD=()
  170. command -v nice >/dev/null 2>&1 && NI_CMD+=(nice -n 10)
  171. command -v ionice >/dev/null 2>&1 && NI_CMD+=(ionice -c2 -n7)
  172. # Soft mem cap
  173. if [[ "$(uname -s)" == "Linux" ]]; then
  174. # shellcheck disable=SC3045
  175. ulimit -S -v $((MEM_CAP_MB * 1024)) || true
  176. fi
  177. FIX_FLAG=()
  178. $DRY_RUN || FIX_FLAG=(-fix)
  179. TIMEOUT_CMD=()
  180. if [[ -n "$FILE_TIMEOUT" ]] && command -v timeout >/dev/null 2>&1; then
  181. TIMEOUT_CMD=(timeout "$FILE_TIMEOUT")
  182. fi
  183. echo
  184. echo "🚀 Running clang-tidy (single CPU, throttled)…"
  185. echo " Checks: $CHECKS"
  186. echo " Header filter: $HEADER_FILTER"
  187. [[ -n "$FILE_TIMEOUT" && ${#TIMEOUT_CMD[@]} -gt 0 ]] && echo " Per-file timeout: $FILE_TIMEOUT"
  188. [[ "$(uname -s)" == "Linux" ]] && echo " Per-process soft mem cap: ${MEM_CAP_MB} MB"
  189. echo
  190. FAILED=()
  191. SKIPPED=()
  192. for ABS in "${FILES[@]}"; do
  193. if [[ ! -f "$ABS" ]]; then
  194. SKIPPED+=("$ABS")
  195. continue
  196. fi
  197. # Pretty label
  198. if [[ "$ABS" == "$REPO_ABS"* ]]; then REL="${ABS#$REPO_ABS/}"; else REL="$ABS"; fi
  199. echo "🔧 Processing: $REL"
  200. if ! "${NI_CMD[@]}" "${TIMEOUT_CMD[@]}" \
  201. clang-tidy -p "$BUILD_DIR" "${FIX_FLAG[@]}" \
  202. -checks="$CHECKS" \
  203. -header-filter="$HEADER_FILTER" \
  204. -extra-arg=-fno-color-diagnostics \
  205. -extra-arg=-Wno-unknown-warning-option \
  206. "$ABS"
  207. then
  208. FAILED+=("$REL")
  209. fi
  210. done
  211. echo
  212. if [[ ${#FAILED[@]} -gt 0 ]]; then
  213. echo "⚠️ clang-tidy failed on ${#FAILED[@]} file(s):"
  214. printf ' - %s\n' "${FAILED[@]}"
  215. echo "💡 Tip: re-run with --dry-run to inspect without applying fixes."
  216. else
  217. echo "✅ All files processed successfully!"
  218. fi
  219. if [[ ${#SKIPPED[@]} -gt 0 ]]; then
  220. echo
  221. echo "ℹ️ Skipped (not found on disk):"
  222. printf ' - %s\n' "${SKIPPED[@]}"
  223. fi