yams/scripts/check-quality.sh
Trevon 577adf1e0f
build: standardize the Debug build dir on build/debug (#248)
* build: standardize the Debug build dir on build/debug

`docs/BUILD.md` and `AGENTS.md` document `./setup.sh Debug` producing
`build/debug`, but `setup.sh`/`setup.ps1` created `builddir` and `.clangd`
pointed there too, so the documented layout and the actual tree disagreed.

Align the Debug mapping with the documented layout:
- setup.sh / setup.ps1: Debug -> `build/debug`
- CMakeLists.txt: Debug `_meson_build_dir` -> `build/debug`
- .clangd: CompilationDatabase -> `build/debug`
- plugin test fallbacks, S3 smoke default, quality-gate candidate list, and
  doc examples -> `build/debug`

Legacy `builddir` trees stay readable: the quality gate and local-ci scripts
still probe `builddir` as a fallback, and `plugin_trust` already treats
`/build/` paths as build artifacts.

* build: migrate remaining Debug consumers off builddir

Follow-up to the Debug build-dir standardization: tracked consumers still
hardcoded `builddir` and would break or silently skip against a fresh
`build/debug` tree. Migrate them, keeping tolerant fallbacks where a legacy
tree may still exist.

- CI: `.github/workflows/tests.yml` runs `./setup.sh Debug` and then compiled
  `-C builddir`; switch BUILD_DIR/COMPILE_DB_DIR/compile paths to `build/debug`.
  Same for windows-test-diagnose.yml.
- IDE: `.vscode/tasks.json` build/test/bench tasks target `build/debug`.
- Runtime: `object_storage_plugin_loader` also probes `build/debug/plugins`
  (and the flat `build/debug`), retaining the `builddir` fallbacks.
- Tests/docs: ONNX tuning integration plugin dir, benchmarks, and plugin
  READMEs/examples use `build/debug`.

`./build/*` is already excluded from the quality-gate find and `.dockerignore`,
so no exclusion lists change.
2026-10-01 08:45:20 -06:00

940 lines
27 KiB
Bash
Executable file

#!/usr/bin/env bash
# Comprehensive static analysis using cppcheck and other quality tools
set -euo pipefail
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Default values
PROFILE="normal"
OUTPUT_FORMAT="human"
OUTPUT_DIR=""
GIT_ONLY=false
DIFF_BASE=""
PARALLEL=true
VERBOSE=false
FIX_FORMAT=false
BUILD_DIR=""
COMPILE_COMMANDS=""
EXCLUDE_DIRS=""
INCLUDE_DIRS=""
SUPPRESSIONS_FILE=""
BASELINE_FILE=""
GENERATE_BASELINE=false
CPPCHECK_BUILD_DIR=""
CLANG_TIDY_AVAILABLE=false
# Script directory and project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Function to print colored output
print_color() {
local color=$1
shift
echo -e "${color}$*${NC}"
}
# Function to print section headers
print_section() {
print_color "$CYAN" "\n==== $* ===="
}
# Function to print progress
print_progress() {
print_color "$BLUE" "→ $*"
}
# Function to print success
print_success() {
print_color "$GREEN" "✓ $*"
}
# Function to print warning
print_warning() {
print_color "$YELLOW" "⚠ $*"
}
# Function to print error
print_error() {
print_color "$RED" "✗ $*"
}
# Usage information
usage() {
cat << EOF
Usage: $(basename "$0") [OPTIONS]
Code Quality Analysis Script for YAMS C++ Project
OPTIONS:
Quality Profiles:
-p, --profile PROFILE Quality profile: strict, normal, quick, ci (default: normal)
Output Options:
-f, --format FORMAT Output format: human, xml, json, sarif (default: human)
-o, --output DIR Output directory for reports (default: stdout)
-v, --verbose Show detailed output and progress
Source Selection:
-g, --git Only analyze files changed in git (staged and unstaged)
--diff-base REV Only analyze C/C++ files changed since REV and only fail on
findings that land on changed lines
-d, --build-dir DIR Build directory for compile_commands.json
Analysis Options:
--fix-format Automatically fix formatting issues found
--suppressions FILE Use custom suppressions file
--baseline FILE Compare against baseline results
--generate-baseline Generate baseline file for future comparisons
Performance:
-j, --parallel Enable parallel analysis (default: true)
-s, --serial Force serial processing
Advanced:
--exclude DIRS Additional directories to exclude (comma-separated)
--include DIRS Additional include directories (comma-separated)
--compile-commands FILE Explicit path to compile_commands.json
Help:
-h, --help Show this help message
PROFILES:
strict All checks enabled, warnings as errors, strict coding standards
normal Standard checks with reasonable suppressions (default)
quick Essential checks only for rapid feedback
ci Optimized for continuous integration with machine-readable output
OUTPUT FORMATS:
human Human-readable colored terminal output (default)
xml XML format for IDE integration
json JSON format for tool integration
sarif SARIF format for GitHub integration
EXAMPLES:
# Basic quality check
$(basename "$0")
# Strict analysis with XML output
$(basename "$0") --profile strict --format xml --output reports/
# Quick check on git changes only
$(basename "$0") --profile quick --git
# CI regression check against a base commit
$(basename "$0") --profile ci --diff-base origin/main
# CI mode with baseline comparison
$(basename "$0") --profile ci --format json --baseline quality-baseline.json
# Generate new baseline
$(basename "$0") --generate-baseline --output quality-baseline.json
# Fix formatting issues automatically
$(basename "$0") --fix-format --git
EOF
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-p|--profile)
PROFILE="$2"
shift 2
;;
-f|--format)
OUTPUT_FORMAT="$2"
shift 2
;;
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-g|--git)
GIT_ONLY=true
shift
;;
--diff-base)
DIFF_BASE="$2"
shift 2
;;
-d|--build-dir)
BUILD_DIR="$2"
shift 2
;;
--fix-format)
FIX_FORMAT=true
shift
;;
--suppressions)
SUPPRESSIONS_FILE="$2"
shift 2
;;
--baseline)
BASELINE_FILE="$2"
shift 2
;;
--generate-baseline)
GENERATE_BASELINE=true
shift
;;
-j|--parallel)
PARALLEL=true
shift
;;
-s|--serial)
PARALLEL=false
shift
;;
--exclude)
EXCLUDE_DIRS="$2"
shift 2
;;
--include)
INCLUDE_DIRS="$2"
shift 2
;;
--compile-commands)
COMPILE_COMMANDS="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
print_error "Unknown option: $1"
usage
exit 1
;;
esac
done
# Validate profile
case $PROFILE in
strict|normal|quick|ci)
;;
*)
print_error "Invalid profile: $PROFILE"
print_error "Valid profiles: strict, normal, quick, ci"
exit 1
;;
esac
# Validate output format
case $OUTPUT_FORMAT in
human|xml|json|sarif)
;;
*)
print_error "Invalid output format: $OUTPUT_FORMAT"
print_error "Valid formats: human, xml, json, sarif"
exit 1
;;
esac
if [[ "$GIT_ONLY" == true && -n "$DIFF_BASE" ]]; then
print_error "--git and --diff-base are mutually exclusive"
exit 1
fi
if [[ -n "$DIFF_BASE" ]]; then
if ! git rev-parse --verify "${DIFF_BASE}^{commit}" >/dev/null 2>&1; then
print_error "Invalid --diff-base revision: $DIFF_BASE"
exit 1
fi
fi
# Check for required tools
check_tool() {
local tool=$1
local package=$2
local description=$3
if ! command -v "$tool" &> /dev/null; then
print_error "$tool not found in PATH"
print_error "$description"
print_error "Install with: $package"
return 1
fi
return 0
}
print_section "Checking Required Tools"
MISSING_TOOLS=false
if ! check_tool "cppcheck" "brew install cppcheck" "Static analysis tool"; then
MISSING_TOOLS=true
fi
# Check for optional tools
if ! command -v "clang-format" &> /dev/null; then
print_warning "clang-format not found (needed for --fix-format)"
print_warning "Install with: brew install clang-format"
if [[ "$FIX_FORMAT" == true ]]; then
print_error "--fix-format requires clang-format"
exit 1
fi
fi
if command -v "clang-tidy" &> /dev/null; then
CLANG_TIDY_AVAILABLE=true
else
print_warning "clang-tidy not found (compile database audit only)"
print_warning "Install with: brew install llvm"
fi
if [[ "$MISSING_TOOLS" == true ]]; then
exit 1
fi
print_success "All required tools found"
# Change to project root
cd "$PROJECT_ROOT"
# Set up default paths
if [[ -z "$BUILD_DIR" ]]; then
# Look for common build directories, preferring active Meson trees with compile_commands.json
for candidate in \
"builddir-nosan" \
"build/debug" \
"builddir" \
"builddir-asan" \
"builddir-ubsan" \
"build/asan" \
"build/yams-debug" \
"build/yams-release" \
"build" \
"cmake-build-release" \
"cmake-build-debug"; do
if [[ -d "$candidate" ]]; then
BUILD_DIR="$candidate"
break
fi
done
fi
if [[ -z "$COMPILE_COMMANDS" && -n "$BUILD_DIR" ]]; then
if [[ -f "$BUILD_DIR/compile_commands.json" ]]; then
COMPILE_COMMANDS="$BUILD_DIR/compile_commands.json"
fi
fi
if [[ -z "$SUPPRESSIONS_FILE" ]]; then
SUPPRESSIONS_FILE="$PROJECT_ROOT/.cppcheck-suppressions"
fi
if [[ -z "$CPPCHECK_BUILD_DIR" ]]; then
if [[ -n "$BUILD_DIR" ]]; then
CPPCHECK_BUILD_DIR="$BUILD_DIR/.cppcheck"
else
CPPCHECK_BUILD_DIR="$PROJECT_ROOT/.cppcheck"
fi
fi
mkdir -p "$CPPCHECK_BUILD_DIR"
# Create output directory if specified
if [[ -n "$OUTPUT_DIR" ]]; then
mkdir -p "$OUTPUT_DIR"
fi
print_section "Configuration"
print_progress "Profile: $PROFILE"
print_progress "Output format: $OUTPUT_FORMAT"
[[ -n "$OUTPUT_DIR" ]] && print_progress "Output directory: $OUTPUT_DIR"
[[ -n "$BUILD_DIR" ]] && print_progress "Build directory: $BUILD_DIR"
[[ -n "$COMPILE_COMMANDS" ]] && print_progress "Compile commands: $COMPILE_COMMANDS"
[[ -f "$SUPPRESSIONS_FILE" ]] && print_progress "Suppressions file: $SUPPRESSIONS_FILE"
[[ -n "$CPPCHECK_BUILD_DIR" ]] && print_progress "Cppcheck build dir: $CPPCHECK_BUILD_DIR"
print_progress "Git only: $GIT_ONLY"
[[ -n "$DIFF_BASE" ]] && print_progress "Diff base: $DIFF_BASE"
print_progress "Parallel: $PARALLEL"
print_progress "Fix format: $FIX_FORMAT"
meson_option_value() {
local build_dir=$1
local option_name=$2
local intro="$build_dir/meson-info/intro-buildoptions.json"
if [[ ! -f "$intro" ]]; then
return 1
fi
python3 - "$intro" "$option_name" <<'PY'
import json
import sys
intro_path = sys.argv[1]
option_name = sys.argv[2]
with open(intro_path, 'r', encoding='utf-8') as fh:
data = json.load(fh)
for item in data:
if item.get('name') == option_name:
value = item.get('value')
if isinstance(value, list):
print(','.join(str(v) for v in value))
else:
print(value)
break
PY
}
find_first_build_dir() {
for candidate in "$@"; do
if [[ -d "$candidate" ]]; then
echo "$candidate"
return 0
fi
done
return 1
}
report_sanitizer_build() {
local label=$1
local expected=$2
shift 2
local build_dir
build_dir=$(find_first_build_dir "$@") || {
print_warning "$label build directory not found"
return 0
}
local intro="$build_dir/meson-info/intro-buildoptions.json"
local ccdb="$build_dir/compile_commands.json"
if [[ ! -f "$intro" ]]; then
print_warning "$label build dir found but Meson intro options missing: $build_dir"
return 0
fi
local sanitize
sanitize=$(meson_option_value "$build_dir" "b_sanitize" || true)
local buildtype
buildtype=$(meson_option_value "$build_dir" "buildtype" || true)
local lundef
lundef=$(meson_option_value "$build_dir" "b_lundef" || true)
if [[ "$sanitize" == *"$expected"* ]]; then
print_success "$label build ready: $build_dir (buildtype=$buildtype, b_sanitize=$sanitize)"
else
print_warning "$label build dir found but sanitizer mismatch: $build_dir (b_sanitize=$sanitize)"
fi
if [[ -f "$ccdb" ]]; then
print_progress "$label compile database: $ccdb"
else
print_warning "$label compile database missing: $build_dir/compile_commands.json"
fi
if [[ "$expected" == "address" && "$lundef" == "true" ]]; then
print_warning "$label uses Clang ASAN with b_lundef=true; Meson warns this may fail. Prefer -Db_lundef=false."
fi
}
print_section "Toolchain Readiness"
if [[ -n "$COMPILE_COMMANDS" && -f "$COMPILE_COMMANDS" ]]; then
print_success "Compile commands available: $COMPILE_COMMANDS"
else
print_warning "compile_commands.json not found; clang-tidy and project-aware checks are limited"
fi
if [[ "$CLANG_TIDY_AVAILABLE" == true ]]; then
print_success "clang-tidy available"
else
print_warning "clang-tidy unavailable in PATH"
fi
report_sanitizer_build "TSAN" "thread" "builddir" "build/tsan" "builddir-tsan"
report_sanitizer_build "ASAN" "address" "builddir-asan" "build/asan" "builddir-asan"
report_sanitizer_build "UBSAN" "undefined" "builddir-ubsan" "build/ubsan" "builddir-ubsan"
# Get list of files to analyze
print_section "Collecting Source Files"
if [[ -n "$DIFF_BASE" ]]; then
print_progress "Getting list of files changed since $DIFF_BASE..."
FILES=$(git diff --name-only --diff-filter=ACMR "$DIFF_BASE"...HEAD 2>/dev/null || true)
FILES=$(echo "$FILES" | grep -E '\.(cpp|h|hpp|cc|cxx|c)$' | sort -u || true)
if [[ -z "$FILES" ]]; then
print_success "No changed C++ files found since $DIFF_BASE"
exit 0
fi
FILE_ARRAY=()
while IFS= read -r line; do
[[ -n "$line" ]] && FILE_ARRAY+=("$line")
done <<< "$FILES"
elif [[ "$GIT_ONLY" == true ]]; then
print_progress "Getting list of git-modified files..."
# Get both staged and unstaged files
FILES=$(git diff --name-only --diff-filter=ACMR HEAD 2>/dev/null || true)
FILES+=$'\n'
FILES+=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
# Filter for C++ files and remove duplicates
FILES=$(echo "$FILES" | grep -E '\.(cpp|h|hpp|cc|cxx|c)$' | sort -u || true)
if [[ -z "$FILES" ]]; then
print_success "No modified C++ files found"
exit 0
fi
# Convert to array
FILE_ARRAY=()
while IFS= read -r line; do
[[ -n "$line" ]] && FILE_ARRAY+=("$line")
done <<< "$FILES"
else
# Find all C++ source files, excluding build directories and third-party code
print_progress "Finding all C++ source files..."
FIND_EXCLUDES=(
-not -path "./build/*"
-not -path "./builddir/*"
-not -path "./.cache/*"
-not -path "./cmake-build-*/*"
-not -path "./third_party/*"
-not -path "./external/*"
-not -path "./vendor/*"
-not -path "./.git/*"
-not -path "./examples/plugins/*"
-not -path "*/generated/*"
-not -path "*/_deps/*"
-not -path "*.pb.h"
-not -path "*.pb.cc"
)
# Add custom excludes
if [[ -n "$EXCLUDE_DIRS" ]]; then
IFS=',' read -ra EXCLUDE_ARRAY <<< "$EXCLUDE_DIRS"
for exclude in "${EXCLUDE_ARRAY[@]}"; do
FIND_EXCLUDES+=(-not -path "./${exclude}/*")
done
fi
FILE_ARRAY=()
while IFS= read -r line; do
FILE_ARRAY+=("$line")
done < <(find . \
-type f \
\( -name "*.cpp" -o -name "*.h" -o -name "*.hpp" -o -name "*.cc" -o -name "*.cxx" -o -name "*.c" \) \
"${FIND_EXCLUDES[@]}" \
| sort)
fi
# Check if we found any files
if [[ ${#FILE_ARRAY[@]} -eq 0 ]]; then
print_warning "No C++ files found to analyze"
exit 0
fi
print_success "Found ${#FILE_ARRAY[@]} files to analyze"
# Fix formatting if requested
if [[ "$FIX_FORMAT" == true ]]; then
print_section "Fixing Code Formatting"
if [[ -x "$SCRIPT_DIR/format-code.sh" ]]; then
if [[ "$GIT_ONLY" == true ]]; then
"$SCRIPT_DIR/format-code.sh" --git
else
"$SCRIPT_DIR/format-code.sh"
fi
else
print_warning "format-code.sh not found or not executable"
fi
fi
# Configure cppcheck based on profile
print_section "Configuring Static Analysis"
CPPCHECK_ARGS=()
# Base configuration
CPPCHECK_ARGS+=(
"--std=c++20"
"--language=c++"
"--inline-suppr"
"--quiet"
"--cppcheck-build-dir=$CPPCHECK_BUILD_DIR"
)
# In this repo/environment, system header resolution is noisy and drowns out
# actionable findings. compile_commands.json still provides project include paths.
CPPCHECK_ARGS+=("--suppress=missingIncludeSystem")
CPPCHECK_ARGS+=("--suppress=unmatchedSuppression")
CPPCHECK_ARGS+=("--suppress=checkersReport")
# Add suppressions file if it exists
if [[ -f "$SUPPRESSIONS_FILE" ]]; then
CPPCHECK_ARGS+=("--suppressions-list=$SUPPRESSIONS_FILE")
fi
# Add include directories
CPPCHECK_ARGS+=("-I" ".")
CPPCHECK_ARGS+=("-I" "tests")
CPPCHECK_ARGS+=("-I" "include")
CPPCHECK_ARGS+=("-I" "src")
if [[ -n "$INCLUDE_DIRS" ]]; then
IFS=',' read -ra INCLUDE_ARRAY <<< "$INCLUDE_DIRS"
for include_dir in "${INCLUDE_ARRAY[@]}"; do
CPPCHECK_ARGS+=("-I" "$include_dir")
done
fi
# Add compile commands if available (but only if not using git-scoped modes)
USE_COMPILE_COMMANDS=false
if [[ -f "$COMPILE_COMMANDS" && "$GIT_ONLY" == false && -z "$DIFF_BASE" ]]; then
CPPCHECK_ARGS+=("--project=$COMPILE_COMMANDS")
USE_COMPILE_COMMANDS=true
fi
# Profile-specific configuration
case $PROFILE in
strict)
CPPCHECK_ARGS+=(
"--enable=all"
"--error-exitcode=1"
"--check-config"
"--check-library"
"--inconclusive"
)
;;
normal)
CPPCHECK_ARGS+=(
"--enable=warning,style,performance,portability"
)
;;
quick)
CPPCHECK_ARGS+=(
# cppcheck --enable supports categories like warning/style/performance/etc.
# "error" is a message severity, not a valid --enable category.
"--enable=warning,style,performance"
)
;;
ci)
CPPCHECK_ARGS+=(
"--enable=warning,style,performance,portability"
"--error-exitcode=1"
)
;;
esac
# Output format configuration
case $OUTPUT_FORMAT in
xml)
CPPCHECK_ARGS+=("--xml" "--xml-version=2")
;;
json)
# cppcheck doesn't have native JSON, we'll convert XML
CPPCHECK_ARGS+=("--xml" "--xml-version=2")
;;
sarif)
# We'll need to convert XML to SARIF
CPPCHECK_ARGS+=("--xml" "--xml-version=2")
;;
human)
CPPCHECK_ARGS+=("--template={file}:{line}:{column}: {severity}: {message} [{id}]")
;;
esac
# Add parallel processing
if [[ "$PARALLEL" == true ]]; then
# Use number of CPU cores
if command -v nproc &> /dev/null; then
JOBS=$(nproc)
elif [[ -f /proc/cpuinfo ]]; then
JOBS=$(grep -c ^processor /proc/cpuinfo)
else
JOBS=4 # Default fallback
fi
CPPCHECK_ARGS+=("-j" "$JOBS")
fi
print_success "cppcheck configured with ${#CPPCHECK_ARGS[@]} arguments"
filter_cppcheck_to_changed_lines() {
local diff_base=$1
local input_file=$2
local output_file=$3
python3 - "$diff_base" "$input_file" "$output_file" <<'PY'
import os
import re
import subprocess
import sys
diff_base, input_path, output_path = sys.argv[1:]
diff = subprocess.run(
["git", "diff", "--unified=0", f"{diff_base}...HEAD", "--"],
check=False,
capture_output=True,
text=True,
).stdout.splitlines()
current = None
ranges = {}
path_re = re.compile(r"^\+\+\+ b/(.+)$")
hunk_re = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
for line in diff:
path_match = path_re.match(line)
if path_match:
current = os.path.normpath(path_match.group(1))
continue
hunk_match = hunk_re.match(line)
if hunk_match and current:
start = int(hunk_match.group(1))
count = int(hunk_match.group(2) or "1")
if count == 0:
continue
ranges.setdefault(current, []).append((start, start + count - 1))
finding_re = re.compile(r"^(?P<file>.+?):(?P<line>\d+):(?P<column>\d+):\s+(?P<severity>[^:]+):")
matches = []
with open(input_path, "r", encoding="utf-8", errors="replace") as fh:
for raw in fh:
line = raw.rstrip("\n")
match = finding_re.match(line)
if not match:
continue
path = os.path.normpath(match.group("file").lstrip("./"))
line_no = int(match.group("line"))
if any(lo <= line_no <= hi for lo, hi in ranges.get(path, [])):
matches.append(raw)
with open(output_path, "w", encoding="utf-8") as out:
out.writelines(matches)
print(len(matches))
PY
}
# Run static analysis
print_section "Running Static Analysis"
# Prepare output redirection
OUTPUT_FILE=""
if [[ -n "$OUTPUT_DIR" ]]; then
case $OUTPUT_FORMAT in
xml)
OUTPUT_FILE="$OUTPUT_DIR/cppcheck-results.xml"
;;
json)
OUTPUT_FILE="$OUTPUT_DIR/cppcheck-results.json"
;;
sarif)
OUTPUT_FILE="$OUTPUT_DIR/cppcheck-results.sarif"
;;
human)
OUTPUT_FILE="$OUTPUT_DIR/cppcheck-results.txt"
;;
esac
fi
# Create file list for cppcheck
TEMP_FILE_LIST=$(mktemp)
TEMP_CPPCHECK_OUTPUT=""
TEMP_FILTERED_OUTPUT=""
trap 'rm -f "$TEMP_FILE_LIST" "$TEMP_CPPCHECK_OUTPUT" "$TEMP_FILTERED_OUTPUT"' EXIT
if [[ -n "$DIFF_BASE" && "$OUTPUT_FORMAT" == "human" ]]; then
TEMP_CPPCHECK_OUTPUT=$(mktemp)
TEMP_FILTERED_OUTPUT=$(mktemp)
fi
printf '%s\n' "${FILE_ARRAY[@]}" > "$TEMP_FILE_LIST"
# Run cppcheck
print_progress "Running cppcheck analysis..."
# Choose between project mode and file list mode
if [[ "$USE_COMPILE_COMMANDS" == true ]]; then
# Use project mode - don't specify individual files
if [[ "$VERBOSE" == true ]]; then
print_progress "Command: cppcheck ${CPPCHECK_ARGS[*]} (using compile_commands.json)"
fi
CPPCHECK_EXIT_CODE=0
if [[ -n "$TEMP_CPPCHECK_OUTPUT" ]]; then
if ! cppcheck "${CPPCHECK_ARGS[@]}" > "$TEMP_CPPCHECK_OUTPUT" 2>&1; then
CPPCHECK_EXIT_CODE=$?
fi
elif [[ -n "$OUTPUT_FILE" ]]; then
# Output to file
if [[ "$OUTPUT_FORMAT" == "xml" || "$OUTPUT_FORMAT" == "json" || "$OUTPUT_FORMAT" == "sarif" ]]; then
# XML output goes to stderr
if ! cppcheck "${CPPCHECK_ARGS[@]}" 2> "$OUTPUT_FILE.tmp"; then
CPPCHECK_EXIT_CODE=$?
fi
mv "$OUTPUT_FILE.tmp" "$OUTPUT_FILE"
else
# Human output
if ! cppcheck "${CPPCHECK_ARGS[@]}" > "$OUTPUT_FILE" 2>&1; then
CPPCHECK_EXIT_CODE=$?
fi
fi
else
# Output to stdout/stderr
if ! cppcheck "${CPPCHECK_ARGS[@]}"; then
CPPCHECK_EXIT_CODE=$?
fi
fi
else
# Use file list mode
if [[ "$VERBOSE" == true ]]; then
print_progress "Command: cppcheck ${CPPCHECK_ARGS[*]} --file-list=$TEMP_FILE_LIST"
fi
CPPCHECK_EXIT_CODE=0
if [[ -n "$TEMP_CPPCHECK_OUTPUT" ]]; then
if ! cppcheck "${CPPCHECK_ARGS[@]}" --file-list="$TEMP_FILE_LIST" > "$TEMP_CPPCHECK_OUTPUT" 2>&1; then
CPPCHECK_EXIT_CODE=$?
fi
elif [[ -n "$OUTPUT_FILE" ]]; then
# Output to file
if [[ "$OUTPUT_FORMAT" == "xml" || "$OUTPUT_FORMAT" == "json" || "$OUTPUT_FORMAT" == "sarif" ]]; then
# XML output goes to stderr
if ! cppcheck "${CPPCHECK_ARGS[@]}" --file-list="$TEMP_FILE_LIST" 2> "$OUTPUT_FILE.tmp"; then
CPPCHECK_EXIT_CODE=$?
fi
mv "$OUTPUT_FILE.tmp" "$OUTPUT_FILE"
else
# Human output
if ! cppcheck "${CPPCHECK_ARGS[@]}" --file-list="$TEMP_FILE_LIST" > "$OUTPUT_FILE" 2>&1; then
CPPCHECK_EXIT_CODE=$?
fi
fi
else
# Output to stdout/stderr
if ! cppcheck "${CPPCHECK_ARGS[@]}" --file-list="$TEMP_FILE_LIST"; then
CPPCHECK_EXIT_CODE=$?
fi
fi
fi
if [[ -n "$TEMP_CPPCHECK_OUTPUT" ]]; then
FILTERED_COUNT=$(filter_cppcheck_to_changed_lines "$DIFF_BASE" "$TEMP_CPPCHECK_OUTPUT" "$TEMP_FILTERED_OUTPUT")
RAW_FINDING_COUNT=$(rg -c '^[^:]+:[0-9]+:[0-9]+:\s+[^:]+:' "$TEMP_CPPCHECK_OUTPUT" || true)
if [[ -n "$OUTPUT_FILE" ]]; then
cp "$TEMP_FILTERED_OUTPUT" "$OUTPUT_FILE"
else
cat "$TEMP_FILTERED_OUTPUT"
fi
if [[ "$FILTERED_COUNT" == "0" ]]; then
if [[ "${RAW_FINDING_COUNT:-0}" -gt 0 ]]; then
print_success "No cppcheck findings on changed lines since $DIFF_BASE"
CPPCHECK_EXIT_CODE=0
elif [[ $CPPCHECK_EXIT_CODE -ne 0 ]]; then
cat "$TEMP_CPPCHECK_OUTPUT"
fi
fi
fi
# Post-process output if needed
if [[ -n "$OUTPUT_FILE" && -f "$OUTPUT_FILE" ]]; then
case $OUTPUT_FORMAT in
json)
# Convert XML to JSON (basic conversion)
print_progress "Converting XML output to JSON..."
# This is a placeholder - you might want to use a proper XML to JSON converter
print_warning "JSON conversion not yet implemented, XML file saved as $OUTPUT_FILE"
;;
sarif)
# Convert XML to SARIF
print_progress "Converting XML output to SARIF..."
# This is a placeholder - you might want to use a proper XML to SARIF converter
print_warning "SARIF conversion not yet implemented, XML file saved as $OUTPUT_FILE"
;;
esac
fi
# Handle baseline comparison
if [[ -n "$BASELINE_FILE" && -f "$BASELINE_FILE" ]]; then
print_section "Baseline Comparison"
print_progress "Comparing results against baseline: $BASELINE_FILE"
# This would need to be implemented based on your needs
print_warning "Baseline comparison not yet implemented"
fi
# Generate baseline if requested
if [[ "$GENERATE_BASELINE" == true ]]; then
print_section "Generating Baseline"
BASELINE_OUTPUT="$OUTPUT_DIR/quality-baseline.xml"
if [[ -z "$OUTPUT_DIR" ]]; then
BASELINE_OUTPUT="quality-baseline.xml"
fi
print_progress "Generating baseline file: $BASELINE_OUTPUT"
# Copy current results as baseline
if [[ -n "$OUTPUT_FILE" && -f "$OUTPUT_FILE" ]]; then
cp "$OUTPUT_FILE" "$BASELINE_OUTPUT"
print_success "Baseline generated: $BASELINE_OUTPUT"
fi
fi
# Summary
print_section "Analysis Summary"
if [[ -n "$OUTPUT_FILE" && -f "$OUTPUT_FILE" ]]; then
# Count issues in output file
case $OUTPUT_FORMAT in
xml)
ERROR_COUNT=$(grep -c '<error ' "$OUTPUT_FILE" 2>/dev/null || echo "0")
;;
human)
ERROR_COUNT=$(wc -l < "$OUTPUT_FILE" 2>/dev/null || echo "0")
;;
*)
ERROR_COUNT="unknown"
;;
esac
print_progress "Results saved to: $OUTPUT_FILE"
if [[ "$ERROR_COUNT" != "unknown" ]]; then
print_progress "Issues found: $ERROR_COUNT"
fi
fi
print_progress "Files analyzed: ${#FILE_ARRAY[@]}"
print_progress "Profile: $PROFILE"
print_progress "Format: $OUTPUT_FORMAT"
# Final exit code and summary
if [[ $CPPCHECK_EXIT_CODE -eq 0 ]]; then
print_success "Code quality analysis completed successfully"
print_success "No critical issues found"
else
print_error "Code quality analysis found issues (exit code: $CPPCHECK_EXIT_CODE)"
if [[ "$PROFILE" == "strict" || "$PROFILE" == "ci" ]]; then
print_error "Critical issues detected in $PROFILE mode"
else
print_warning "Issues found, but not treating as errors in $PROFILE mode"
CPPCHECK_EXIT_CODE=0 # Don't fail in normal/quick mode
fi
fi
exit $CPPCHECK_EXIT_CODE