Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ jobs:
use-mypy: true
use-isort: false
extra-mypy-options: "--ignore-missing-imports --show-error-codes"
extra-flake8-options: "--max-line-length=120 --ignore=E203,E402"
extra-pycodestyle-options: "--max-line-length=120 --ignore=E203,E402"
extra-flake8-options: "--max-line-length=120 --ignore=E203,E402,W503"
extra-pycodestyle-options: "--max-line-length=120 --ignore=E203,E402,W503"
80 changes: 54 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,84 @@

# Static Analysis

This GitHub action is designed for C++/Python projects and performs static analysis using:
- [cppcheck](http://cppcheck.sourceforge.net/) and [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) for C++
- [pylint](https://pylint.readthedocs.io/en/latest/index.html) for Python
This GitHub Action is designed for **C++ and Python projects** and performs static analysis using:
* [cppcheck](http://cppcheck.sourceforge.net/) and [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) for C++
* [pylint](https://pylint.readthedocs.io/en/latest/index.html) for Python

It can be triggered by push and pull requests.

For further information and guidance about setup and various inputs, please see sections dedicated to each language ([**C++**](https://github.com/JacobDomagala/StaticAnalysis?tab=readme-ov-file#c) and [**Python**](https://github.com/JacobDomagala/StaticAnalysis?tab=readme-ov-file#python))
For further information and guidance on setup and various inputs, please see the sections dedicated to each language ([**C++**](https://github.com/JacobDomagala/StaticAnalysis?tab=readme-ov-file#c) and [**Python**](https://github.com/JacobDomagala/StaticAnalysis?tab=readme-ov-file#python)).

## Pull Request comment
---

Created comment will contain code snippets with the issue description. When this action is run for the first time, the comment with the initial result will be created for current Pull Request. Consecutive runs will edit this comment with updated status.
## Pull Request Comment

Note that it's possible that the amount of issues detected can make the comment's body to be greater than the GitHub's character limit per PR comment (which is 65536). In that case, the created comment will contain only the issues found up to that point, and the information that the limit of characters was reached.
The created comment will include code snippets and issue descriptions. When this action runs for the first time on a pull request, it creates a comment with the initial analysis results. Subsequent runs will update this same comment with the latest status.

## Output example (C++)
Note that the number of detected issues might cause the comment's body to exceed GitHub's character limit (currently 65,536 characters) per PR comment. If this occurs, the comment will contain issues up to the limit and indicate that the character limit was reached.

---

## Output Example (C++)
![output](https://github.com/JacobDomagala/StaticAnalysis/wiki/output_example.png)

## Non Pull Request
---

For non Pull Requests, the output will be printed to GitHub's output console. This behaviour can also be forced via `force_console_print` input.
## Non-Pull Request Events

## Output example (C++)
![output](https://github.com/JacobDomagala/StaticAnalysis/wiki/console_output_example.png)
For non-pull request events, the output will be printed directly to the GitHub Actions console. This behavior can also be forced using the `force_console_print` input.

---

<br><br>
## Output Example (C++)
![output](https://github.com/JacobDomagala/StaticAnalysis/wiki/console_output_example.png)

---

# C++
While it's recommended that your project is CMake-based, it's not required (see the [**Inputs**](https://github.com/JacobDomagala/StaticAnalysis#inputs) section below). We also recommend using a ```.clang-tidy``` file in your root directory. If your project requires additional packages to be installed, you can use the `apt_pckgs` and/or `init_script` input variables to install them (see the [**Workflow example**](https://github.com/JacobDomagala/StaticAnalysis#workflow-example) or [**Inputs**](https://github.com/JacobDomagala/StaticAnalysis#inputs) sections below). If your repository allows contributions from forks, you must use this Action with the `pull_request_target` trigger event, as the GitHub API won't allow PR comments otherwise.

While it's recommended that your project is CMake-based, it's not strictly required (see the [**Inputs**](https://github.com/JacobDomagala/StaticAnalysis#inputs) section below). We also recommend using a `.clang-tidy` file in your repository's root directory. If your project requires additional packages, you can install them using the `apt_pckgs` and/or `init_script` input variables (see the [**Workflow example**](https://github.com/JacobDomagala/StaticAnalysis#workflow-example) or [**Inputs**](https://github.com/JacobDomagala/StaticAnalysis#inputs) sections below). If your repository allows contributions from forks, you must use this Action with the `pull_request_target` trigger event, as the GitHub API won't allow PR comments otherwise.

By default, **cppcheck** runs with the following flags:
```--enable=all --suppress=missingIncludeSystem --inline-suppr --inconclusive```
You can use the `cppcheck_args` input to set your own flags.

**clang-tidy** looks for the ```.clang-tidy``` file in your repository, but you can also set checks using the `clang_tidy_args` input.
**Clang-Tidy** looks for a `.clang-tidy` file in your repository, but you can also specify checks using the `clang_tidy_args` input.

---

## Workflow example
## Using a Custom `compile_commands.json` File

You can use a pre-generated `compile_commands.json` file with the `compile_commands` input. This is incredibly useful when you need **more control over your compilation database**, whether you're working with a complex build system, have a specific build configuration, or simply want to reuse a file generated elsewhere.

When using a custom `compile_commands.json` with this GitHub Action, you'll encounter a common technical challenge: a **mismatch between the directory where the file was originally generated and the path used by this GitHub Action** (specifically, inside its Docker container). This means the source file paths listed in your `compile_commands.json` might not be valid from the container's perspective.

To resolve this, you have two main options:

* **Manually replace the prefixes** in your `compile_commands.json` file (for example, change `/original/path/to/repo` to `/github/workspace`). This method gives you complete control over the path adjustments.
* **Let the action try to replace the prefixes for you.** For simpler directory structures, you can enable this convenient feature using the `compile_commands_replace_prefix` input.

---

Beyond path adjustments, another important consideration when using a custom `compile_commands.json` file is **dependency resolution** for your static analysis tools. `clang-tidy` performs deep semantic analysis, which means it requires all necessary include files and headers to be found and accessible during its run. If these dependencies are missing or incorrectly referenced, `clang-tidy` may stop analyzing the affected file, leading to incomplete results. In contrast, `cppcheck` is generally more resilient to missing include paths, as it primarily focuses on lexical and syntactic analysis rather than full semantic parsing.

---

## Workflow Example

```yml
name: Static analysis
name: Static Analysis

on:
# Will run on push when merging to 'branches'. The output will be shown in the console
# Runs on 'push' events to specified branches. Output will be printed to the console.
push:
branches:
- develop
- master
- main

# 'pull_request_target' allows this Action to also run on forked repositories
# The output will be shown in PR comments (unless the 'force_console_print' flag is used)
# Uses 'pull_request_target' to allow analysis of forked repositories.
# Output will be shown in PR comments (unless 'force_console_print' is used).
pull_request_target:
branches:
- "*"
Expand All @@ -78,10 +104,10 @@ jobs:
echo \"Hello from the init script! First arg=\${root_dir} second arg=\${build_dir}\"

add-apt-repository ppa:oibaf/graphics-drivers
apt update && apt upgrade
apt update && apt upgrade -y
apt install -y libvulkan1 mesa-vulkan-drivers vulkan-utils" > init_script.sh

- name: Run static analysis
- name: Run Static Analysis
uses: JacobDomagala/StaticAnalysis@master
with:
language: c++
Expand All @@ -91,16 +117,16 @@ jobs:

use_cmake: true

# Additional apt packages that need to be installed before running Cmake
# Additional apt packages required before running CMake
apt_pckgs: software-properties-common libglu1-mesa-dev freeglut3-dev mesa-common-dev

# Additional script that will be run (sourced) AFTER 'apt_pckgs' and before running Cmake
# Optional shell script that runs AFTER 'apt_pckgs' and before CMake
init_script: init_script.sh

# (Optional) clang-tidy args
# Optional Clang-Tidy arguments
clang_tidy_args: -checks='*,fuchsia-*,google-*,zircon-*,abseil-*,modernize-use-trailing-return-type'

# (Optional) cppcheck args
# Optional Cppcheck arguments
cppcheck_args: --enable=all --suppress=missingIncludeSystem
```

Expand All @@ -120,6 +146,8 @@ jobs:
| `use_cmake` | Determines wether CMake should be used to generate compile_commands.json file | `true` |
| `cmake_args` | Additional CMake arguments |`<empty>`|
| `force_console_print` | Output the action result to console, instead of creating the comment |`false`|
| `compile_commands` | User generated compile_commands.json |`<empty>`|
| `compile_commands_replace_prefix` | Whether we should replace the prefix of files inside user generated compile_commands.json file |`false`|

**NOTE: `apt_pckgs` will run before `init_script`, just in case you need some packages installed before running the script**

Expand Down
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ inputs:
description: 'Directories (space separated) which should be excluded from the raport'
apt_pckgs:
description: 'Additional (space separated) packages that need to be installed in order for project to compile'
compile_commands:
description: 'User generated compile_commands.json'
compile_commands_replace_prefix:
description: 'Whether we should replace the prefix of files inside user generated compile_commands.json file'
default: false
init_script:
description: |
'Optional shell script that will be run before configuring project (i.e. running CMake command).'
Expand Down
33 changes: 26 additions & 7 deletions entrypoint_cpp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ common_ancestor=${common_ancestor:-""}
CLANG_TIDY_ARGS="${INPUT_CLANG_TIDY_ARGS//$'\n'/}"
CPPCHECK_ARGS="${INPUT_CPPCHECK_ARGS//$'\n'/}"

if [ -n "$INPUT_COMPILE_COMMANDS" ]; then
debug_print "Using compile_commands.json file ($INPUT_COMPILE_COMMANDS) - use_cmake input is not being used!"
export INPUT_USE_CMAKE=false
if [ "$INPUT_COMPILE_COMMANDS_REPLACE_PREFIX" = true ]; then
debug_print "Replacing prefix inside user generated compile_commands.json file!"
python3 /src/patch_compile_commands.py "/github/workspace/$INPUT_COMPILE_COMMANDS"
fi
fi

cd build

if [ "$INPUT_REPORT_PR_CHANGES_ONLY" = true ]; then
Expand Down Expand Up @@ -50,7 +59,17 @@ num_proc=$(nproc)
if [ -z "$files_to_check" ]; then
echo "No files to check"
else
if [ "$INPUT_USE_CMAKE" = true ]; then
if [ "$INPUT_USE_CMAKE" = true ] || [ -n "$INPUT_COMPILE_COMMANDS" ]; then
# Determine path to compile_commands.json
if [ -n "$INPUT_COMPILE_COMMANDS" ]; then
compile_commands_path="/github/workspace/$INPUT_COMPILE_COMMANDS"
compile_commands_dir=$(dirname "$compile_commands_path")

else
compile_commands_path="compile_commands.json"
compile_commands_dir=$(pwd)
fi

for file in $files_to_check; do
exclude_arg=""
if [ -n "$INPUT_EXCLUDE_DIR" ]; then
Expand All @@ -60,23 +79,23 @@ else
# Replace '/' with '_'
file_name=$(echo "$file" | tr '/' '_')

debug_print "Running cppcheck --project=compile_commands.json $CPPCHECK_ARGS --file-filter=$file --output-file=cppcheck_$file_name.txt $exclude_arg"
eval cppcheck --project=compile_commands.json "$CPPCHECK_ARGS" --file-filter="$file" --output-file="cppcheck_$file_name.txt" "$exclude_arg" || true
debug_print "Running cppcheck --project=$compile_commands_path $CPPCHECK_ARGS --file-filter=$file --output-file=cppcheck_$file_name.txt $exclude_arg"
eval cppcheck --project="$compile_commands_path" "$CPPCHECK_ARGS" --file-filter="$file" --output-file="cppcheck_$file_name.txt" "$exclude_arg" || true
done

cat cppcheck_*.txt > cppcheck.txt

# Excludes for clang-tidy are handled in python script
debug_print "Running run-clang-tidy-20 $CLANG_TIDY_ARGS -p $(pwd) $files_to_check >>clang_tidy.txt 2>&1"
eval run-clang-tidy-20 "$CLANG_TIDY_ARGS" -p "$(pwd)" "$files_to_check" >clang_tidy.txt 2>&1 || true
debug_print "Running run-clang-tidy-20 $CLANG_TIDY_ARGS -p $compile_commands_dir $files_to_check >>clang_tidy.txt 2>&1"
eval run-clang-tidy-20 "$CLANG_TIDY_ARGS" -p "$compile_commands_dir" "$files_to_check" > clang_tidy.txt 2>&1 || true

else
# Excludes for clang-tidy are handled in python script
# Without compile_commands.json
debug_print "Running cppcheck -j $num_proc $files_to_check $CPPCHECK_ARGS --output-file=cppcheck.txt ..."
eval cppcheck -j "$num_proc" "$files_to_check" "$CPPCHECK_ARGS" --output-file=cppcheck.txt || true

debug_print "Running run-clang-tidy-20 $CLANG_TIDY_ARGS $files_to_check >>clang_tidy.txt 2>&1"
eval run-clang-tidy-20 "$CLANG_TIDY_ARGS" "$files_to_check" >clang_tidy.txt 2>&1 || true
eval run-clang-tidy-20 "$CLANG_TIDY_ARGS" "$files_to_check" > clang_tidy.txt 2>&1 || true
fi

cd /
Expand Down
55 changes: 55 additions & 0 deletions src/patch_compile_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
from __future__ import annotations

import json
import os
import sys
from typing import Any, Dict, List, Sequence


NEW_PREFIX = "/github/workspace"


def _collect_paths(entries: Sequence[Dict[str, Any]]) -> List[str]:
"""Return every absolute file/dir path found in *entries*."""
raw_paths = (
value
for entry in entries
for value in (entry.get("directory"), entry.get("file"))
if isinstance(value, str) # guard against None / non-string
)
return [os.path.realpath(path) for path in raw_paths]


def patch_compile_commands(path: str) -> None: # noqa: D401
with open(path, "r", encoding="utf-8") as f:
data: List[Dict[str, Any]] = json.load(f)

paths: List[str] = _collect_paths(data)
if not paths: # nothing to patch
print("[WARN] compile_commands.json contained no absolute paths")
return

old_prefix = os.path.commonpath(paths)
print(f"[INFO] Patching compile_commands.json: '{old_prefix}' → '{NEW_PREFIX}'")

for entry in data:
for key in ("file", "directory", "command"):
val = entry.get(key)
if (
isinstance(val, str)
and os.path.isabs(val)
and val.startswith(old_prefix)
):
entry[key] = val.replace(old_prefix, NEW_PREFIX, 1)

with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)


if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: patch_compile_commands.py <path-to-compile_commands.json>")
sys.exit(1)

patch_compile_commands(sys.argv[1])