lint: Add .clang-tidy and analyzer scripts

Signed-off-by: John Studnicka <contact@zentec.dev>
This commit is contained in:
John Studnicka 2026-05-17 01:23:48 -07:00 • committed by Enrico Weigelt
commit ecede3875b
6 changed files with 509 additions and 42 deletions

18
.clang-tidy Normal file
View file

@ -0,0 +1,18 @@
Checks: >
clang-analyzer-*,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-reserved-identifier,
hicpp-no-assembler,
misc-include-cleaner,
performance-*,
portability-*,
readability-redundant-control-flow
HeaderFilterRegex: '.*'
WarningsAsErrors: ''
CheckOptions:
misc-include-cleaner.UnusedIncludes: 'true'
misc-include-cleaner.MissingIncludes: 'false'

321
.github/scripts/clang-tidy-analyze.sh vendored Executable file
View file

@ -0,0 +1,321 @@
#!/bin/sh
set -eu
default_build_dir="build"
build_dir=""
jobs=1
show_total=0
target_only=0
total_only=0
warnings_only=0
errors_only=0
target=""
target_kind="all"
target_file_abs=""
usage() {
echo "Usage: $0 [-p BUILD_DIR] [-j N] [--warnings-only|--errors-only] [-t|--total] [--target-only] [--total-only] [target]" >&2
echo " target: optional .c/.h file path or directory path to check; omit for whole project." >&2
echo " -p DIR: compile database directory. Defaults to ./compile_commands.json, then $default_build_dir/compile_commands.json." >&2
echo " -j N: number of parallel clang-tidy jobs (positive integer, default: 1)." >&2
echo " -t, --total: append TOTAL counts after per-file counts." >&2
echo " --warnings-only: print warning counts only." >&2
echo " --errors-only: print error counts only." >&2
echo " --target-only: for file targets, count diagnostics only in that file (exclude headers)." >&2
echo " --total-only: print only TOTAL counts (no per-file breakdown)." >&2
echo "" >&2
echo "Examples:" >&2
echo " $0 -j 8 --total" >&2
echo " $0 -j 8 hw/xfree86" >&2
echo " $0 --target-only hw/xfree86/drivers/video/modesetting/drmmode_display.c" >&2
echo "" >&2
echo "Requires: clang-tidy, jq, and a Meson compile_commands.json database." >&2
}
while [ "$#" -gt 0 ]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-t | --total)
show_total=1
shift
;;
--warnings-only)
warnings_only=1
shift
;;
--errors-only)
errors_only=1
shift
;;
-p)
if [ "$#" -lt 2 ]; then
echo "Missing value for -p." >&2
usage
exit 2
fi
build_dir="$2"
shift 2
;;
--target-only)
target_only=1
shift
;;
--total-only)
total_only=1
show_total=1
shift
;;
-j)
if [ "$#" -lt 2 ]; then
echo "Missing value for -j." >&2
usage
exit 2
fi
jobs="$2"
case "$jobs" in
'' | *[!0-9]* | 0)
echo "Invalid -j value: $jobs (expected positive integer)." >&2
usage
exit 2
;;
esac
shift 2
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage
exit 2
;;
*) break ;;
esac
done
if [ "$#" -gt 1 ]; then
echo "Expected at most one target argument." >&2
usage
exit 2
elif [ "$#" -eq 1 ]; then
target="$1"
fi
if [ "$warnings_only" -eq 1 ] && [ "$errors_only" -eq 1 ]; then
echo "--warnings-only and --errors-only are incompatible." >&2
usage
exit 2
fi
if ! command -v clang-tidy >/dev/null 2>&1; then
echo "clang-tidy not found in PATH." >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found in PATH." >&2
exit 1
fi
if [ -n "$build_dir" ]; then
compdb_dir="$build_dir"
elif [ -f compile_commands.json ]; then
compdb_dir="."
else
compdb_dir="$default_build_dir"
fi
if [ ! -d "$compdb_dir" ]; then
echo "Compile database directory '$compdb_dir' not found. Run meson setup first." >&2
exit 1
fi
compdb="$compdb_dir/compile_commands.json"
if [ ! -f "$compdb" ]; then
echo "Compilation database '$compdb' not found. Run meson setup first." >&2
exit 1
fi
compdb_dir_abs="$(cd "$compdb_dir" && pwd -P)"
tmp_outdir="$(mktemp -d)"
tmp_files="$tmp_outdir/files.txt"
tmp_files_nul="$tmp_outdir/files.nul"
tmp_parsed="$tmp_outdir/parsed.txt"
trap 'rm -rf "$tmp_outdir"' EXIT INT TERM HUP
first_dir="$(jq -r '.[0].directory // empty' "$compdb")"
case "$first_dir" in
/*) diag_base="$(cd "$first_dir" && pwd -P)" ;;
"") diag_base="$compdb_dir_abs" ;;
*) diag_base="$(cd "$compdb_dir/$first_dir" && pwd -P)" ;;
esac
canonical_path() {
if command -v realpath >/dev/null 2>&1; then
realpath "$1"
else
dir=$(dirname "$1")
base=$(basename "$1")
printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base"
fi
}
emit_compdb_files() {
jq -r '.[] | [.directory, .file] | @tsv' "$compdb" |
while IFS="$(printf '\t')" read -r dir file; do
case "$file" in
/*) canonical_path "$file" ;;
*) canonical_path "$dir/$file" ;;
esac
done |
awk '!seen[$0]++'
}
if [ -n "$target" ] && [ -f "$target" ]; then
target_kind="file"
target_file_abs="$(canonical_path "$target")"
case "$target" in
*.c | *.h) printf '%s\n' "$target_file_abs" >"$tmp_files" ;;
*)
echo "Not a .c or .h file: $target" >&2
exit 2
;;
esac
elif [ -n "$target" ] && [ -d "$target" ]; then
target_kind="dir"
target_dir_abs="$(canonical_path "$target")"
emit_compdb_files |
while IFS= read -r file; do
case "$file" in
"$target_dir_abs" | "$target_dir_abs"/*) printf '%s\n' "$file" ;;
esac
done >"$tmp_files"
elif [ -n "$target" ]; then
echo "Path not found: $target" >&2
exit 2
else
emit_compdb_files >"$tmp_files"
fi
if [ "$target_only" -eq 1 ] && [ "$target_kind" != "file" ]; then
echo "--target-only requires a file target." >&2
exit 2
fi
if [ ! -s "$tmp_files" ]; then
if [ "$show_total" -eq 1 ]; then
if [ "$warnings_only" -eq 1 ] || [ "$errors_only" -eq 1 ]; then
printf "0\tTOTAL\n"
else
printf "0\t0\tTOTAL\n"
fi
fi
exit 0
fi
while IFS= read -r file; do
printf '%s\0' "$file"
done <"$tmp_files" >"$tmp_files_nul"
emit_clang_output() {
if command -v xargs >/dev/null 2>&1; then
xargs -0 -n 1 -P "$jobs" clang-tidy -p "$compdb_dir_abs" <"$tmp_files_nul" 2>&1 || true
else
while IFS= read -r file; do
clang-tidy -p "$compdb_dir_abs" "$file" 2>&1 || true
done <"$tmp_files"
fi
}
emit_clang_output |
awk -F: \
-v base="$diag_base" \
-v target_only="$target_only" \
-v target_file="$target_file_abs" '
function canon(p, abs, n, i, part, top, stack, out) {
if (p ~ /^\//) {
abs = p
} else {
abs = base "/" p
}
n = split(abs, stack, "/")
top = 0
for (i = 1; i <= n; i++) {
part = stack[i]
if (part == "" || part == ".") {
continue
}
if (part == "..") {
if (top > 0) {
top--
}
continue
}
top++
stack[top] = part
}
out = ""
for (i = 1; i <= top; i++) {
out = out "/" stack[i]
}
return (out == "" ? "/" : out)
}
{
if ($0 !~ /^([^:[:space:]]+):([1-9][0-9]*):([1-9][0-9]*): (warning|error): .+ \[[[:alnum:].,-]+\]$/) {
next
}
file = canon($1)
if (target_only == 1 && file != target_file) {
next
}
if ($0 ~ /: warning: /) {
warnings[file]++
total_warnings++
} else {
errors[file]++
total_errors++
}
seen[file] = 1
}
END {
for (file in seen) {
printf "%d\t%d\t%s\n", warnings[file] + 0, errors[file] + 0, file
}
printf "__TOTAL__\t%d\t%d\n", total_warnings + 0, total_errors + 0
}
' >"$tmp_parsed"
print_counts() {
while IFS="$(printf '\t')" read -r warnings errors file; do
if [ "$warnings_only" -eq 1 ]; then
printf '%s\t%s\n' "$warnings" "$file"
elif [ "$errors_only" -eq 1 ]; then
printf '%s\t%s\n' "$errors" "$file"
else
printf '%s\t%s\t%s\n' "$warnings" "$errors" "$file"
fi
done
}
if [ "$total_only" -eq 0 ]; then
grep -v '^__TOTAL__' "$tmp_parsed" | sort -nr | print_counts
fi
if [ "$show_total" -eq 1 ]; then
total_line="$(grep '^__TOTAL__' "$tmp_parsed")"
total_warnings="$(printf '%s\n' "$total_line" | cut -f2)"
total_errors="$(printf '%s\n' "$total_line" | cut -f3)"
if [ "$warnings_only" -eq 1 ]; then
printf '%s\tTOTAL\n' "$total_warnings"
elif [ "$errors_only" -eq 1 ]; then
printf '%s\tTOTAL\n' "$total_errors"
else
printf '%s\t%s\tTOTAL\n' "$total_warnings" "$total_errors"
fi
fi

27
.github/scripts/link-compile-commands.sh vendored Executable file
View file

@ -0,0 +1,27 @@
#!/bin/sh
set -eu
build_dir="${1:-build}"
root_link="compile_commands.json"
case "$(uname -s 2>/dev/null || echo unknown)" in
MINGW* | MSYS* | CYGWIN*)
echo "Skipping compile_commands.json symlink on non-Unix platform." >&2
exit 0
;;
esac
if [ ! -f "$build_dir/compile_commands.json" ]; then
echo "Compilation database '$build_dir/compile_commands.json' not found. Run meson setup first." >&2
exit 1
fi
if [ -e "$root_link" ] && [ ! -L "$root_link" ]; then
echo "Refusing to overwrite non-symlink '$root_link'." >&2
exit 1
fi
tmp_link="$root_link.tmp.$$"
rm -f "$tmp_link"
ln -s "$build_dir/compile_commands.json" "$tmp_link"
mv -f "$tmp_link" "$root_link"

View file

@ -5,6 +5,8 @@
#
# SPDX-License-Identifier: MIT
#
# shellcheck disable=SC1091,SC2086
#
# Usage:
# meson-build.sh
# [-C directory] ... change to directory before doing anything
@ -39,47 +41,47 @@ MESON_RUN_DIST="$MESON_DIST_ARGS"
while [[ $# -gt 0 ]]; do
case $1 in
-C)
directory=$2
shift 2
pushd "$directory" || exit 1
;;
--skip-setup)
shift
MESON_SKIP_SETUP="1"
;;
--skip-build)
shift
MESON_SKIP_BUILD="1"
;;
--skip-test)
shift
MESON_RUN_TEST=""
;;
--run-test)
shift
MESON_RUN_TEST="1"
;;
--skip-dist)
shift
MESON_RUN_DIST=""
;;
--run-dist)
shift
MESON_RUN_DIST="1"
;;
--skip-install)
shift
MESON_RUN_INSTALL=""
;;
--run-install)
shift
MESON_RUN_INSTALL="1"
;;
*)
echo "Unknow commandline argument $1"
exit 1
;;
-C)
directory=$2
shift 2
pushd "$directory" || exit 1
;;
--skip-setup)
shift
MESON_SKIP_SETUP="1"
;;
--skip-build)
shift
MESON_SKIP_BUILD="1"
;;
--skip-test)
shift
MESON_RUN_TEST=""
;;
--run-test)
shift
MESON_RUN_TEST="1"
;;
--skip-dist)
shift
MESON_RUN_DIST=""
;;
--run-dist)
shift
MESON_RUN_DIST="1"
;;
--skip-install)
shift
MESON_RUN_INSTALL=""
;;
--run-install)
shift
MESON_RUN_INSTALL="1"
;;
*)
echo "Unknow commandline argument $1"
exit 1
;;
esac
done
@ -124,6 +126,7 @@ if [[ -z "$MESON_SKIP_SETUP" ]]; then
meson setup "$MESON_BUILDDIR" $MESON_ARGS
fi
meson configure "$MESON_BUILDDIR"
.github/scripts/link-compile-commands.sh "$MESON_BUILDDIR"
if [[ -z "$MESON_SKIP_BUILD" ]]; then
if [[ -n "$NINJA_ARGS" ]]; then
@ -137,7 +140,7 @@ if [[ -n "$MESON_RUN_TEST" ]]; then
fi
if [[ -n "$MESON_RUN_INSTALL" ]]; then
meson install --no-rebuild -C "$MESON_BUILDDIR" $MESON_INSTALL_ARGS
meson install --no-rebuild -C "$MESON_BUILDDIR" $MESON_INSTALL_ARGS
fi
if [[ -n "$MESON_RUN_DIST" ]]; then

3
.gitignore vendored
View file

@ -2,3 +2,6 @@
*.swp
*.patch
tags
compile_commands.json
.vscode/*
!.vscode/tasks.json

95
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,95 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "clang-tidy (current file)",
"type": "shell",
"command": "clang-tidy",
"args": [
"${file}"
],
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": false
}
},
{
"label": "meson setup (clang)",
"type": "shell",
"command": "sh",
"args": [
"-c",
"if [ -d build ]; then meson setup build --reconfigure; else meson setup build; fi && .github/scripts/link-compile-commands.sh build"
],
"options": {
"env": {
"CC": "clang",
"CXX": "clang++"
}
},
"problemMatcher": []
},
{
"label": "ninja (build, clang)",
"type": "shell",
"command": "ninja",
"args": [
"-C",
"build"
],
"dependsOn": [
"meson setup (clang)"
],
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": false
}
},
{
"label": "meson setup (gcc)",
"type": "shell",
"command": "sh",
"args": [
"-c",
"if [ -d build ]; then meson setup build --reconfigure; else meson setup build; fi && .github/scripts/link-compile-commands.sh build"
],
"options": {
"env": {
"CC": "gcc",
"CXX": "g++"
}
},
"problemMatcher": []
},
{
"label": "ninja (build, gcc)",
"type": "shell",
"command": "ninja",
"args": [
"-C",
"build"
],
"dependsOn": [
"meson setup (gcc)"
],
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "clang-tidy (all files)",
"type": "shell",
"command": ".github/scripts/clang-tidy-analyze.sh",
"args": [
"-j",
"4",
"--total"
],
"problemMatcher": []
}
]
}