2025-11-01 16:38:38 +01:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
# Lint all C code in C/ and its subfolders with aggressive rules
|
|
|
|
|
|
# - Installs required tools if missing (clang-tidy, clang-format, cppcheck, flawfinder)
|
|
|
|
|
|
# - Uses compile_commands.json if present for clang-tidy; otherwise uses sane defaults
|
|
|
|
|
|
# - Checks formatting with clang-format --dry-run --Werror
|
|
|
|
|
|
# - Runs cppcheck with exhaustive rules
|
|
|
|
|
|
# - Runs flawfinder for security issues
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
set -u
|
|
|
|
|
|
|
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
|
YELLOW='\033[1;33m'
|
|
|
|
|
|
BLUE='\033[0;34m'
|
|
|
|
|
|
NC='\033[0m'
|
|
|
|
|
|
|
|
|
|
|
|
info() { echo -e "${BLUE}==>${NC} $*"; }
|
|
|
|
|
|
ok() { echo -e "${GREEN}✓${NC} $*"; }
|
|
|
|
|
|
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
|
|
|
|
|
err() { echo -e "${RED}✗${NC} $*"; }
|
|
|
|
|
|
|
|
|
|
|
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
|
|
|
|
C_DIR="${ROOT_DIR}/C"
|
2025-11-01 20:11:45 +01:00
|
|
|
|
AUTOFIX=${LINT_AUTOFIX:-1}
|
2025-11-01 19:18:53 +01:00
|
|
|
|
|
|
|
|
|
|
if [[ ! -d "${C_DIR}" ]]; then
|
|
|
|
|
|
err "C directory not found at ${C_DIR}"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
ISSUES=0
|
|
|
|
|
|
MISSING=()
|
2025-11-01 20:11:45 +01:00
|
|
|
|
C_FILES=()
|
|
|
|
|
|
C_SOURCES=()
|
2025-11-01 19:18:53 +01:00
|
|
|
|
|
|
|
|
|
|
need_cmd() {
|
|
|
|
|
|
command -v "$1" >/dev/null 2>&1 || MISSING+=("$1")
|
2025-11-01 16:38:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
detect_pkg_manager() {
|
|
|
|
|
|
if command -v pacman >/dev/null 2>&1; then echo pacman; return; fi
|
|
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then echo apt; return; fi
|
|
|
|
|
|
if command -v apt >/dev/null 2>&1; then echo apt; return; fi
|
|
|
|
|
|
if command -v dnf >/dev/null 2>&1; then echo dnf; return; fi
|
|
|
|
|
|
if command -v zypper >/dev/null 2>&1; then echo zypper; return; fi
|
|
|
|
|
|
if command -v apk >/dev/null 2>&1; then echo apk; return; fi
|
|
|
|
|
|
echo none
|
2025-11-01 16:38:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
install_tools() {
|
|
|
|
|
|
info "Checking required tools..."
|
|
|
|
|
|
need_cmd clang-tidy
|
|
|
|
|
|
need_cmd clang-format
|
|
|
|
|
|
need_cmd cppcheck
|
|
|
|
|
|
need_cmd flawfinder
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
if [[ ${#MISSING[@]} -eq 0 ]]; then
|
|
|
|
|
|
ok "All tools present: clang-tidy, clang-format, cppcheck, flawfinder"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
warn "Missing tools: ${MISSING[*]} — attempting to install with sudo"
|
|
|
|
|
|
local pm
|
|
|
|
|
|
pm=$(detect_pkg_manager)
|
|
|
|
|
|
case "$pm" in
|
|
|
|
|
|
pacman)
|
|
|
|
|
|
sudo pacman -S --needed --noconfirm clang clang-tools-extra clang-format cppcheck flawfinder || true
|
|
|
|
|
|
;;
|
|
|
|
|
|
apt|apt-get)
|
|
|
|
|
|
sudo "$pm" update -y || true
|
|
|
|
|
|
# clang-tidy and clang-format may be versioned; prefer unversioned meta pkgs
|
|
|
|
|
|
sudo "$pm" install -y clang-tidy clang-format cppcheck flawfinder || true
|
|
|
|
|
|
;;
|
|
|
|
|
|
dnf)
|
|
|
|
|
|
sudo dnf install -y clang-tools-extra clang cppcheck flawfinder || true
|
|
|
|
|
|
;;
|
|
|
|
|
|
zypper)
|
|
|
|
|
|
sudo zypper --non-interactive install clang-tools clang-tools-extra cppcheck flawfinder || true
|
|
|
|
|
|
;;
|
|
|
|
|
|
apk)
|
|
|
|
|
|
sudo apk add clang-extra-tools clang cppcheck flawfinder || true
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
warn "Unsupported package manager. Please install: clang-tidy clang-format cppcheck flawfinder"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
# Re-check after attempted install
|
|
|
|
|
|
MISSING=()
|
|
|
|
|
|
need_cmd clang-tidy
|
|
|
|
|
|
need_cmd clang-format
|
|
|
|
|
|
need_cmd cppcheck
|
|
|
|
|
|
need_cmd flawfinder
|
|
|
|
|
|
if [[ ${#MISSING[@]} -ne 0 ]]; then
|
|
|
|
|
|
warn "Still missing: ${MISSING[*]} — continuing, but related steps may be skipped"
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "Tools installed"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
ensure_configs() {
|
|
|
|
|
|
# Provide default aggressive configs if missing
|
|
|
|
|
|
if [[ ! -f "${C_DIR}/.clang-tidy" ]]; then
|
|
|
|
|
|
warn ".clang-tidy not found in C/. Creating a default aggressive config."
|
|
|
|
|
|
cat >"${C_DIR}/.clang-tidy" <<'YAML'
|
|
|
|
|
|
Checks: >
|
|
|
|
|
|
clang-analyzer-*,bugprone-*,cert-*,concurrency-*,hicpp-*,misc-*,performance-*,
|
|
|
|
|
|
portability-*,readability-*,clang-diagnostic-*,cppcoreguidelines-*
|
|
|
|
|
|
WarningsAsErrors: '*'
|
|
|
|
|
|
HeaderFilterRegex: '.*'
|
|
|
|
|
|
AnalyzeTemporaryDtors: true
|
|
|
|
|
|
FormatStyle: none
|
|
|
|
|
|
YAML
|
|
|
|
|
|
fi
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
if [[ ! -f "${C_DIR}/.clang-format" ]]; then
|
|
|
|
|
|
warn ".clang-format not found in C/. Creating a default style."
|
|
|
|
|
|
cat >"${C_DIR}/.clang-format" <<'YAML'
|
|
|
|
|
|
BasedOnStyle: LLVM
|
|
|
|
|
|
IndentWidth: 4
|
|
|
|
|
|
TabWidth: 4
|
|
|
|
|
|
UseTab: Never
|
|
|
|
|
|
ColumnLimit: 100
|
|
|
|
|
|
SortIncludes: true
|
|
|
|
|
|
AlignConsecutiveAssignments: true
|
|
|
|
|
|
AlignConsecutiveDeclarations: true
|
|
|
|
|
|
AllowShortIfStatementsOnASingleLine: false
|
|
|
|
|
|
BreakBeforeBraces: Allman
|
|
|
|
|
|
Standard: C23
|
|
|
|
|
|
YAML
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
collect_files() {
|
|
|
|
|
|
# shellcheck disable=SC2207
|
|
|
|
|
|
C_FILES=($(find "${C_DIR}" -type f \( -name '*.c' -o -name '*.h' -o -name '*.inc' \) \
|
|
|
|
|
|
-not -path '*/.*' -not -path '*/build/*' -not -path '*/dist/*' -not -path '*/out/*' \
|
|
|
|
|
|
-not -path '*/bin/*' -not -path '*/obj/*'))
|
|
|
|
|
|
if [[ ${#C_FILES[@]} -eq 0 ]]; then
|
|
|
|
|
|
warn "No C files found under ${C_DIR}"
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "Found ${#C_FILES[@]} C-related files to check"
|
|
|
|
|
|
fi
|
2025-11-01 20:11:45 +01:00
|
|
|
|
mapfile -t C_SOURCES < <(find "${C_DIR}" -type f -name '*.c' \
|
|
|
|
|
|
-not -path '*/.*' -not -path '*/build/*' -not -path '*/dist/*' -not -path '*/out/*' \
|
|
|
|
|
|
-not -path '*/bin/*' -not -path '*/obj/*')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
apply_clang_format_fix() {
|
|
|
|
|
|
if ! command -v clang-format >/dev/null 2>&1; then
|
|
|
|
|
|
warn "clang-format unavailable; skipping auto-format"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [[ ${#C_FILES[@]} -eq 0 ]]; then
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
info "Applying clang-format -i to source files"
|
|
|
|
|
|
local formatted=0
|
|
|
|
|
|
for f in "${C_FILES[@]}"; do
|
|
|
|
|
|
if clang-format -i "$f" 2>/dev/null; then
|
|
|
|
|
|
formatted=$((formatted+1))
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
ok "clang-format applied to ${formatted} file(s)"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
apply_clang_tidy_fix() {
|
|
|
|
|
|
if ! command -v clang-tidy >/dev/null 2>&1; then
|
|
|
|
|
|
warn "clang-tidy unavailable; skipping auto-fix"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [[ ${#C_SOURCES[@]} -eq 0 ]]; then
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
local db="${C_DIR}/compile_commands.json"
|
|
|
|
|
|
local used_db="no"
|
|
|
|
|
|
if [[ -f "$db" ]] && head -n 1 "$db" | grep -q '\['; then
|
|
|
|
|
|
used_db="yes"
|
|
|
|
|
|
fi
|
|
|
|
|
|
info "Applying clang-tidy --fix to C sources"
|
|
|
|
|
|
local failures=0
|
|
|
|
|
|
for f in "${C_SOURCES[@]}"; do
|
|
|
|
|
|
local rel
|
|
|
|
|
|
rel=$(realpath --relative-to="${ROOT_DIR}" "$f" 2>/dev/null || echo "$f")
|
|
|
|
|
|
printf ' • %s\n' "$rel"
|
|
|
|
|
|
if [[ "$used_db" == "yes" ]]; then
|
|
|
|
|
|
if ! clang-tidy "$f" -p "${C_DIR}" --fix --format-style=file --quiet >/dev/null 2>&1; then
|
|
|
|
|
|
failures=$((failures+1))
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
if ! clang-tidy "$f" --fix --format-style=file --quiet -- -std=c2x -I"$(dirname "$f")" -I"${C_DIR}" >/dev/null 2>&1; then
|
|
|
|
|
|
failures=$((failures+1))
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
if [[ $failures -gt 0 ]]; then
|
|
|
|
|
|
warn "clang-tidy auto-fix encountered $failures issue(s); manual review may be required"
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "clang-tidy auto-fix pass completed"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
apply_autofix() {
|
|
|
|
|
|
if [[ "$AUTOFIX" == "0" ]]; then
|
|
|
|
|
|
info "Automatic fixes disabled (LINT_AUTOFIX=0)"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
info "Automatic fixes enabled (LINT_AUTOFIX=${AUTOFIX})"
|
|
|
|
|
|
apply_clang_format_fix
|
|
|
|
|
|
apply_clang_tidy_fix
|
|
|
|
|
|
# Refresh file lists in case new files were introduced by fixes
|
|
|
|
|
|
collect_files
|
2025-11-01 19:18:53 +01:00
|
|
|
|
}
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
run_clang_format() {
|
|
|
|
|
|
if ! command -v clang-format >/dev/null 2>&1; then
|
|
|
|
|
|
warn "clang-format unavailable; skipping format check"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
info "Checking formatting with clang-format (--dry-run --Werror)"
|
|
|
|
|
|
local bad=0
|
|
|
|
|
|
for f in "${C_FILES[@]}"; do
|
|
|
|
|
|
if ! clang-format --dry-run --Werror "$f" >/dev/null 2>&1; then
|
|
|
|
|
|
echo "format issue: $f"
|
|
|
|
|
|
bad=$((bad+1))
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
if [[ $bad -gt 0 ]]; then
|
|
|
|
|
|
warn "clang-format found $bad files needing formatting"
|
|
|
|
|
|
ISSUES=$((ISSUES+bad))
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "Formatting OK"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
run_cppcheck() {
|
|
|
|
|
|
if ! command -v cppcheck >/dev/null 2>&1; then
|
|
|
|
|
|
warn "cppcheck unavailable; skipping"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
info "Running cppcheck (aggressive, recursive)"
|
|
|
|
|
|
# Use a temp report file to avoid noisy exit codes stopping script
|
|
|
|
|
|
local report
|
|
|
|
|
|
report=$(mktemp)
|
|
|
|
|
|
local opts=(--enable=all --inconclusive --std=c23 --check-level=exhaustive --force \
|
|
|
|
|
|
--quiet --error-exitcode=2 --inline-suppr --suppress=missingIncludeSystem \
|
|
|
|
|
|
--library=posix)
|
|
|
|
|
|
# Exclude common non-source dirs
|
|
|
|
|
|
opts+=(--exclude=build --exclude=dist --exclude=out --exclude=.git --exclude=bin --exclude=obj)
|
|
|
|
|
|
if ! cppcheck "${opts[@]}" "${C_DIR}" 2>"$report"; then
|
|
|
|
|
|
warn "cppcheck reported issues (see summary below)"
|
|
|
|
|
|
ISSUES=$((ISSUES+1))
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "cppcheck passed"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [[ -s "$report" ]]; then
|
|
|
|
|
|
echo
|
|
|
|
|
|
echo "cppcheck output:" && sed -e 's/^/ /' "$report"
|
|
|
|
|
|
fi
|
|
|
|
|
|
rm -f "$report"
|
|
|
|
|
|
}
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
run_clang_tidy() {
|
|
|
|
|
|
if ! command -v clang-tidy >/dev/null 2>&1; then
|
|
|
|
|
|
warn "clang-tidy unavailable; skipping"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
info "Running clang-tidy on .c files"
|
|
|
|
|
|
local db="${C_DIR}/compile_commands.json"
|
|
|
|
|
|
local used_db="no"
|
2025-11-01 20:11:45 +01:00
|
|
|
|
if [[ ${#C_SOURCES[@]} -eq 0 ]]; then
|
2025-11-01 19:18:53 +01:00
|
|
|
|
warn "No .c files for clang-tidy"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [[ -f "$db" ]]; then
|
|
|
|
|
|
# Basic validation: ensure JSON array starts with [ and includes "directory"
|
|
|
|
|
|
if head -n 1 "$db" | grep -q '\['; then
|
|
|
|
|
|
used_db="yes"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "compile_commands.json seems malformed; ignoring"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
local failures=0
|
2025-11-01 20:11:45 +01:00
|
|
|
|
for f in "${C_SOURCES[@]}"; do
|
2025-11-01 19:18:53 +01:00
|
|
|
|
if [[ "$used_db" == "yes" ]]; then
|
|
|
|
|
|
clang-tidy "$f" -p "${C_DIR}" --quiet || failures=$((failures+1))
|
|
|
|
|
|
else
|
|
|
|
|
|
# Fallback args: try C23 and include local dir
|
|
|
|
|
|
clang-tidy "$f" --quiet -- -std=c2x -I"$(dirname "$f")" -I"${C_DIR}" || failures=$((failures+1))
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
if [[ $failures -gt 0 ]]; then
|
|
|
|
|
|
warn "clang-tidy found issues in $failures file(s)"
|
|
|
|
|
|
ISSUES=$((ISSUES+failures))
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "clang-tidy passed"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
run_flawfinder() {
|
|
|
|
|
|
if ! command -v flawfinder >/dev/null 2>&1; then
|
|
|
|
|
|
warn "flawfinder unavailable; skipping"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
info "Running flawfinder (security-focused scan)"
|
|
|
|
|
|
local report
|
|
|
|
|
|
report=$(mktemp)
|
|
|
|
|
|
if ! flawfinder --quiet --columns --minlevel=1 --falsepositive "${C_DIR}" >"$report" 2>/dev/null; then
|
|
|
|
|
|
warn "flawfinder reported issues"
|
|
|
|
|
|
ISSUES=$((ISSUES+1))
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "flawfinder completed"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [[ -s "$report" ]]; then
|
|
|
|
|
|
echo
|
|
|
|
|
|
echo "flawfinder notable findings:" && head -n 200 "$report" | sed -e 's/^/ /'
|
|
|
|
|
|
fi
|
|
|
|
|
|
rm -f "$report"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
summary_exit() {
|
|
|
|
|
|
echo
|
|
|
|
|
|
if [[ $ISSUES -gt 0 ]]; then
|
|
|
|
|
|
err "Lint completed with $ISSUES issue(s) detected"
|
|
|
|
|
|
echo "Tip: run 'clang-format -i' to fix formatting; many clang-tidy checks support '--fix'"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "All checks passed with no issues"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main() {
|
|
|
|
|
|
echo -e "${BLUE}C folder – aggressive lint suite${NC}"
|
|
|
|
|
|
echo
|
|
|
|
|
|
install_tools
|
|
|
|
|
|
ensure_configs
|
|
|
|
|
|
collect_files
|
2025-11-01 20:11:45 +01:00
|
|
|
|
apply_autofix
|
2025-11-01 19:18:53 +01:00
|
|
|
|
run_clang_format
|
|
|
|
|
|
run_cppcheck
|
|
|
|
|
|
run_clang_tidy
|
|
|
|
|
|
run_flawfinder
|
|
|
|
|
|
summary_exit
|
|
|
|
|
|
}
|
2025-11-01 16:38:38 +01:00
|
|
|
|
|
2025-11-01 19:18:53 +01:00
|
|
|
|
main "$@"
|