diff --git a/.github/actions/load-ci-image/action.yml b/.github/actions/load-ci-image/action.yml index 7e1bc73d..596335a8 100644 --- a/.github/actions/load-ci-image/action.yml +++ b/.github/actions/load-ci-image/action.yml @@ -6,7 +6,7 @@ runs: uses: docker/setup-buildx-action@v2 - name: Download image artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: note_c_ci_image path: /tmp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01ac6e9a..daee2f36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 # TODO: This is a 3rd party GitHub action from some dude. Ideally, we'd # use something more "official". @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Login to GitHub Container Registry uses: docker/login-action@v2 @@ -55,7 +55,7 @@ jobs: outputs: type=docker,dest=/tmp/note_c_ci_image.tar - name: Upload image artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: note_c_ci_image path: /tmp/note_c_ci_image.tar @@ -67,7 +67,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -105,9 +105,11 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load CI Docker image + # Only load the Docker image artifact if build_ci_docker_image actually + # ran (e.g. it wasn't skipped and was successful). if: ${{ needs.build_ci_docker_image.result == 'success' }} uses: ./.github/actions/load-ci-image @@ -131,9 +133,11 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load CI Docker image + # Only load the Docker image artifact if build_ci_docker_image actually + # ran (e.g. it wasn't skipped and was successful). if: ${{ needs.build_ci_docker_image.result == 'success' }} uses: ./.github/actions/load-ci-image @@ -147,10 +151,12 @@ jobs: needs: [build_ci_docker_image] steps: - - name: Checkout Code - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v4 - name: Load CI Docker image + # Only load the Docker image artifact if build_ci_docker_image actually + # ran (e.g. it wasn't skipped and was successful). if: ${{ needs.build_ci_docker_image.result == 'success' }} uses: ./.github/actions/load-ci-image @@ -158,6 +164,25 @@ jobs: run: | docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/run_astyle.sh ghcr.io/blues/note_c_ci:latest + run_cppcheck: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [build_ci_docker_image] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Load CI Docker image + # Only load the Docker image artifact if build_ci_docker_image actually + # ran (e.g. it wasn't skipped and was successful). + if: ${{ needs.build_ci_docker_image.result == 'success' }} + uses: ./.github/actions/load-ci-image + + - name: Run cppcheck + run: | + docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/run_cppcheck.sh ghcr.io/blues/note_c_ci:latest + publish_ci_image: runs-on: ubuntu-latest # Make sure unit tests unit tests passed before publishing. diff --git a/.gitignore b/.gitignore index 9552ba03..d6056bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ __pycache__/ *.code-workspace *.orig settings.json + +# Development Artifacts +cppcheck_output.txt diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e42b64fa..b00c48ce 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -104,6 +104,21 @@ "group": { "kind": "none" } + }, + { + "label": "Note-C: Run Static Analysis", + "type": "shell", + "command": "${workspaceFolder}/scripts/run_cppcheck.sh", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "LC_ALL": "C" + } + }, + "problemMatcher": [ + "$gcc" + ], + "group": "test" } ] } diff --git a/Dockerfile b/Dockerfile index 09bd4e9e..334df54f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,6 +65,7 @@ RUN ["dash", "-c", "\ nano \ python3-pip \ python3-sphinx \ + cppcheck \ valgrind \ && pip install --break-system-packages \ breathe \ diff --git a/scripts/run_cppcheck.sh b/scripts/run_cppcheck.sh new file mode 100755 index 00000000..a6761b55 --- /dev/null +++ b/scripts/run_cppcheck.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +SRC_DIR="$SCRIPT_DIR/.." + +# Ensure build directory exists and compile_commands.json is generated +if [ ! -d "$SRC_DIR/build" ] || [ ! -f "$SRC_DIR/build/compile_commands.json" ]; then + cmake -B "$SRC_DIR/build" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +fi + +echo "Running Static Analysis..." +echo + +# Create a function to generate the summary +generate_summary() { + { + # Initialize flag + has_critical_issues=false + + echo + + # Always generate and display summary regardless of exit code + echo "=== Static Analysis Summary ===" + echo + + # Display critical issues + echo "Critical Issues (Errors & Warnings):" + echo "-----------------------------------" + grep -E "error:|warning:" cppcheck_output.txt | grep -v "Checking " | grep -v "nofile:0:" | \ + sort | uniq | awk -F': ' '{printf "%-40s %s\n", $1, $4}' || echo "None found" + echo + + # Display performance issues + echo "Performance & Portability Issues:" + echo "--------------------------------" + grep -E "performance:|portability:" cppcheck_output.txt | grep -v "Checking " | \ + sort | uniq | awk -F': ' '{printf "%-40s %s\n", $1, $4}' || echo "None found" + echo + + # Count issues by severity + echo " Issue Count by Severity: " + echo "--------------------------" + for sev in error warning performance portability style missingInclude information debug; do + count=$(grep -c "${sev}:" cppcheck_output.txt) || true + printf "%-15s %3d issues\n" "${sev^^}:" "$count" + + # Check if 'sev' is 'error' or 'warning' and if 'count' is greater than 0 + if [[ "$sev" == "error" || "$sev" == "warning" ]] && [ "$count" -gt 0 ]; then + has_critical_issues=true + fi + done + echo + + # Display status and details + if [ $has_critical_issues ]; then + echo "Status: FAILED - Critical issues found" + echo + echo "Review and fix critical issues before proceeding" + else + echo "Status: PASSED - No critical issues found" + echo + echo "Note: Review non-critical issues for potential improvements" + fi + } + + # Return 1 if critical issues found, 0 otherwise + if $has_critical_issues; then + return 1 + else + return 0 + fi +} + +# Run cppcheck and capture output and exit code +cppcheck \ + --enable=all \ + --inconclusive \ + --std=c11 \ + --force \ + --inline-suppr \ + --suppress=unusedFunction \ + -i test \ + -i build/_deps \ + --template="{file}:{line}: {severity}: {id}: {message}" \ + --max-configs=32 \ + --check-library \ + --debug-warnings \ + . 2>&1 | tee cppcheck_output.txt + +generate_summary + +# Exit with cppcheck's status code +exit $?