Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Checks: >
-*,
clang-analyzer-*,
bugprone-*,
performance-*,
modernize-use-nullptr,
modernize-use-override
WarningsAsErrors: "*"
HeaderFilterRegex: "^(src|tests|tools)/"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HeaderFilterRegex won't match absolute paths

clang-tidy evaluates HeaderFilterRegex against the full absolute path of each header file. On a typical GitHub-hosted runner the workspace path is something like /home/runner/work/zvec/zvec/, so project headers will have paths like /home/runner/work/zvec/zvec/src/foo.h. The regex ^(src|tests|tools)/ is anchored to the beginning of the string and will therefore never match an absolute path, silently suppressing all diagnostics originating in project headers.

Change the pattern to match the directory name anywhere in the path:

Suggested change
HeaderFilterRegex: "^(src|tests|tools)/"
HeaderFilterRegex: ".*(src|tests|tools)/.*"

This will correctly flag headers under src/, tests/, or tools/ regardless of the absolute prefix.

FormatStyle: none
SystemHeaders: false
146 changes: 146 additions & 0 deletions .github/workflows/clang_tidy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: Clang-Tidy

on:
push:
branches: ["main"]
paths:
- "**/*.c"
- "**/*.cc"
- "**/*.cpp"
- "**/*.cxx"
- "**/*.h"
- "**/*.hpp"
- "CMakeLists.txt"
- "**/CMakeLists.txt"
Comment on lines +13 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant CMakeLists.txt path entry

The pattern "**/CMakeLists.txt" (line 14) already matches all CMakeLists.txt files in the repository, including the root one. The "CMakeLists.txt" entry on line 13 is therefore a no-op. The same redundancy exists in the pull_request trigger block (lines 27–28).

Suggested change
- "CMakeLists.txt"
- "**/CMakeLists.txt"
- "**/CMakeLists.txt"

- "cmake/**"
- ".clang-tidy"
pull_request:
branches: ["main"]
paths:
- "**/*.c"
- "**/*.cc"
- "**/*.cpp"
- "**/*.cxx"
- "**/*.h"
- "**/*.hpp"
- "CMakeLists.txt"
- "**/CMakeLists.txt"
- "cmake/**"
- ".clang-tidy"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
clang_tidy:
name: Clang-Tidy Checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang-tidy cmake ninja-build

- name: Configure CMake and export compile commands
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DENABLE_HASWELL=ON \
-DBUILD_TOOLS=OFF

- name: Collect changed C/C++ files
id: changed_files
uses: tj-actions/changed-files@v46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third-party action not pinned to a commit SHA

tj-actions/changed-files@v46 references a mutable tag. If the tag is force-pushed to a different commit, the workflow silently picks up potentially malicious code without any diff being visible in this repository. GitHub's own security hardening guidelines recommend pinning third-party actions to a full commit SHA.

Example:

Suggested change
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@4edd678ac3f81e2dc578756871e4d00c19191daf # v46

(Replace the SHA with the one that corresponds to the v46 release you intend to use.)

with:
files: |
**/*.c
**/*.cc
**/*.cpp
**/*.cxx
!thirdparty/**
!build/**
separator: "\n"

- name: Filter changed files to compile database entries
id: tidy_files
if: steps.changed_files.outputs.any_changed == 'true'
run: |
python3 - <<'PY'
import json
import os
from pathlib import Path

changed = [line.strip() for line in """${{ steps.changed_files.outputs.all_changed_files }}""".splitlines() if line.strip()]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template injection risk in inline Python

The GitHub Actions expression ${{ steps.changed_files.outputs.all_changed_files }} is interpolated directly into a Python triple-quoted string before the shell sees the script. If any changed file path contains """ (three consecutive double-quotes, a valid sequence in Linux filenames), it will close the Python string literal early and allow arbitrary Python code injection from PR authors.

The safe pattern is to pass the value via an environment variable and read it in Python, which avoids any possibility of code injection regardless of the content:

      - name: Filter changed files to compile database entries
        id: tidy_files
        if: steps.changed_files.outputs.any_changed == 'true'
        env:
          ALL_CHANGED_FILES: ${{ steps.changed_files.outputs.all_changed_files }}
        run: |
          python3 - <<'PY'
          import json
          import os
          from pathlib import Path

          changed = [line.strip() for line in os.environ["ALL_CHANGED_FILES"].splitlines() if line.strip()]
          ...
          PY

By reading the file list from an environment variable inside Python, the content is never evaluated as code.

compile_db_path = Path("build/compile_commands.json")
with compile_db_path.open("r", encoding="utf-8") as f:
compile_db = json.load(f)

compile_entries = set()
for entry in compile_db:
file_path = entry.get("file")
if not file_path:
continue
normalized = os.path.normpath(file_path).replace("\\", "/")
compile_entries.add(normalized)

selected = []
skipped = []
cwd = Path.cwd()
for file_path in changed:
if not Path(file_path).is_file():
skipped.append(f"{file_path} (missing)")
continue

abs_normalized = os.path.normpath(str((cwd / file_path).resolve())).replace("\\", "/")
rel_normalized = os.path.normpath(file_path).replace("\\", "/")

if abs_normalized in compile_entries or rel_normalized in compile_entries:
selected.append(file_path)
else:
skipped.append(f"{file_path} (no compile_commands entry)")

output_path = os.environ["GITHUB_OUTPUT"]
with open(output_path, "a", encoding="utf-8") as out:
out.write(f"any_tidy_files={'true' if selected else 'false'}\n")
out.write("all_tidy_files<<EOF\n")
out.write("\n".join(selected))
out.write("\nEOF\n")
out.write("skipped_files<<EOF\n")
out.write("\n".join(skipped))
out.write("\nEOF\n")
PY

- name: Show skipped files
if: steps.changed_files.outputs.any_changed == 'true' && steps.tidy_files.outputs.skipped_files != ''
run: |
echo "Skipping files:"
printf '%s\n' "${{ steps.tidy_files.outputs.skipped_files }}"

- name: Run clang-tidy
if: steps.tidy_files.outputs.any_tidy_files == 'true'
run: |
mapfile -t changed_files <<'EOF'
${{ steps.tidy_files.outputs.all_tidy_files }}
EOF

for file in "${changed_files[@]}"; do
if [ -f "$file" ]; then
echo "Running clang-tidy on $file"
clang-tidy -p build --warnings-as-errors='*' "$file"
fi
done

- name: No clang-tidy files to analyze
if: steps.changed_files.outputs.any_changed != 'true' || steps.tidy_files.outputs.any_tidy_files != 'true'
run: |
echo "No changed source files with compile_commands entries matched clang-tidy patterns."
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ tests/de_integration/log
tests/de_integration/*.log
!.git*
!.clang-format
!.clang-tidy
!.circleci
!.drone.yml
sdk/python/dist/
Expand Down