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
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Static Analysis

GitHub action for CMake based C++ project, that runs [cppcheck](http://cppcheck.sourceforge.net/) and [clang-tidy](https://clang.llvm.org/extra/clang-tidy/). This action works both no push and on pull requests.
GitHub action for CMake based C++ project, that runs [cppcheck](http://cppcheck.sourceforge.net/) and [clang-tidy](https://clang.llvm.org/extra/clang-tidy/). This action works on both push and pull requests.

In order for this action to work properly, your project has to be CMake based and also include ```.clang-tidy``` file in your root directory. If your project requires some additional packages to be installed, you can use `apt_pckgs` and/or `init_script` input variables to install them (see the **Workflow example** or **Inputs** sections below)
In order for this action to work properly, your project has to be CMake based and it's also recomennded to use ```.clang-tidy``` file, which should be located in your root directory. If your project requires some additional packages to be installed, you can use `apt_pckgs` and/or `init_script` input variables to install them (see the **Workflow example** or **Inputs** sections below). Also, if your repository should allow contribiutions from forks, then it's required to use this Action with `pull_request_target` trigger event, otherwise the GitHub API won't allow to create PR comments.

- **Cppcheck** will run with the following default flags: </br>
- **cppcheck** will run with the following default flags: </br>
```--enable=all --suppress=missingInclude --inline-suppr --inconclusive```
You can use `cppcheck_args` input to set your flags.

- **clang-tidy** will look for the ```.clang-tidy``` file in your repository.
- **clang-tidy** will look for the ```.clang-tidy``` file in your repository, or you can set checks via `clang_tidy_args` input.

## Pull Request comment

Expand All @@ -31,8 +31,19 @@ For non Pull Requests, the output will be printed to GitHub's output console. Th
```yml
name: Static analysis

# This example runs on per PR basis. This can be changed to also work on push
on: [pull_request]
on:
# Will run on push when merging to 'branches'. The output will be shown in 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)
pull_request_target:
branches:
- "*"

jobs:
static_analysis:
Expand Down Expand Up @@ -61,6 +72,12 @@ jobs:

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

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

# (Optional) cppcheck args
cppcheck_args: --enable=all --suppress=missingInclude
```

## Inputs
Expand All @@ -74,7 +91,7 @@ jobs:
| `apt_pckgs` | FALSE | Additional (space separated) packages that need to be installed in order for project to compile | `<empty>` |
| `init_script` | FALSE | Optional shell script that will be run before running CMake command. This should be used, when the project requires some environmental set-up beforehand. | `<empty>` |
| `cppcheck_args` | FALSE | Cppcheck (space separated) arguments that will be used |`--enable=all --suppress=missingInclude --inline-suppr --inconclusive`|
| `clang_tidy_args` | FALSE | clang-tidy (space separated) arguments that will be used |`<empty>`|
| `clang_tidy_args` | FALSE | clang-tidy arguments that will be used (example: `-checks='*,fuchsia-*,google-*,zircon-*'` |`<empty>`|
| `report_pr_changes_only`| FALSE | Only post the issues found within the changes introduced in this Pull Request. This means that only the issues found within the changed lines will po posted. Any other issues caused by these changes in the repository, won't be reported, so in general you should run static analysis on entire code base |`false`|
| `cmake_args` | FALSE | Additional CMake arguments |`<empty>`|
| `force_console_print` | FALSE | Output the action result to console, instead of creating the comment |`false`|
Expand Down
11 changes: 8 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ inputs:
repo:
description: 'Repository name'
default: ${{ github.repository }}
pr_repo:
description: 'Head repository (This is useful when using Action with [pull_request_target])'
default: ${{ github.event.pull_request.head.repo.full_name }}
pr_head:
description: 'Head (branch) for PR (same as `pr_repo` input, useful with [pull_request_target]'
default: ${{ github.event.pull_request.head.ref }}
comment_title:
description: 'Title for comment with the raport. This should be an unique name'
default: Static analysis result
Expand All @@ -24,11 +30,10 @@ inputs:
'This should be used, when the project requires some environmental set-up beforehand'
'Note. `apt_pckgs` will run before this script, just in case you need some packages installed'
cppcheck_args:
description: 'Cppcheck (space separated) arguments that will be used'
description: 'cppcheck (space separated) arguments that will be used'
default: --enable=all --suppress=missingInclude --inline-suppr --inconclusive
clang_tidy_args:
description: 'clang-tidy (space separated) arguments that will be used'
default: ""
description: 'clang-tidy arguments that will be used (example: -checks="*,fuchsia-*,google-*,zircon-*"'
report_pr_changes_only:
description: 'Only post the issues found within the changes introduced in this Pull Request'
default: false
Expand Down
45 changes: 42 additions & 3 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
#!/bin/bash
# shellcheck disable=SC2155

set -e

export TERM=xterm-color

debug_print() {
if [ "$INPUT_VERBOSE" = "true" ]; then
IFS=$'\n' read -ra ADDR <<< "$1"
for i in "${ADDR[@]}"; do
echo -e "\u001b[32m $i"
done
fi
}

print_to_console=${INPUT_FORCE_CONSOLE_PRINT}

if [ $print_to_console = true ]; then
echo "The 'force_console_print' option is enabled. Printing output to console."
elif [ -z "$INPUT_PR_NUM" ]; then
echo "Pull request number input is not present. Printing output to console."
echo "Pull request number input (pr_num) is not present. Printing output to console."
print_to_console=true
else
echo "Pull request numer is ${INPUT_PR_NUM}"
debug_print "Pull request number: ${INPUT_PR_NUM}"
fi

if [ -n "$INPUT_APT_PCKGS" ]; then
Expand All @@ -23,18 +35,45 @@ if [ -n "$INPUT_INIT_SCRIPT" ]; then
source "$INPUT_INIT_SCRIPT"
fi

debug_print "Repo = ${INPUT_PR_REPO} SHA = ${INPUT_PR_HEAD} event name = ${GITHUB_EVENT_NAME}"

use_extra_directorty=false

# This is useful when running this Action from fork (together with [pull_request_target])
if [ "$GITHUB_EVENT_NAME" = "pull_request_target" ] && [ -n "$INPUT_PR_REPO" ]; then
debug_print "Running in [pull_request_target] event! Cloning the Head repo ..."
git clone "https://www.github.com/$INPUT_PR_REPO" pr_tree
cd pr_tree || exit
git checkout "$INPUT_PR_HEAD"
use_extra_directorty=true

# Override commit SHA, in order to get the correct code snippet
NEW_GITHUB_SHA=$(git rev-parse HEAD)
export GITHUB_SHA=$NEW_GITHUB_SHA

export GITHUB_WORKSPACE=$(pwd)
fi

mkdir build && cd build || exit

cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON "$INPUT_CMAKE_ARGS" ..

files_to_check=$(python3 /get_files_to_check.py -exclude="$INPUT_EXCLUDE_DIR" -json="compile_commands.json")

debug_print "Files to check = $files_to_check"
debug_print "INPUT_CPPCHECK_ARGS = $INPUT_CPPCHECK_ARGS"
debug_print "INPUT_CLANG_TIDY_ARGS = $INPUT_CLANG_TIDY_ARGS"

if [ -z "$INPUT_EXCLUDE_DIR" ]; then
debug_print "Running cppcheck --project=compile_commands.json $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt ..."
eval cppcheck --project=compile_commands.json "$INPUT_CPPCHECK_ARGS" --output-file=cppcheck.txt
else
debug_print "Running cppcheck --project=compile_commands.json $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt -i$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR ..."
eval cppcheck --project=compile_commands.json "$INPUT_CPPCHECK_ARGS" --output-file=cppcheck.txt -i"$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR"
fi

# Excludes for clang-tidy are handled in python script
debug_print "Running clang-tidy-12 $INPUT_CLANG_TIDY_ARGS -p $(pwd) $files_to_check -- > clang_tidy.txt"
eval clang-tidy-12 "$INPUT_CLANG_TIDY_ARGS" -p "$(pwd)" "$files_to_check" -- >"clang_tidy.txt"

python3 /run_static_analysis.py -cc cppcheck.txt -ct clang_tidy.txt -o $print_to_console
python3 /run_static_analysis.py -cc cppcheck.txt -ct clang_tidy.txt -o $print_to_console -fk $use_extra_directorty
85 changes: 68 additions & 17 deletions run_static_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
# Input variables from Github action
GITHUB_TOKEN = os.getenv("INPUT_GITHUB_TOKEN")
PR_NUM = os.getenv("INPUT_PR_NUM")
WORK_DIR = os.getenv("GITHUB_WORKSPACE")
WORK_DIR = f'{os.getenv("GITHUB_WORKSPACE")}'
REPO_NAME = os.getenv("INPUT_REPO")
TARGET_REPO_NAME = os.getenv("INPUT_REPO")
SHA = os.getenv("GITHUB_SHA")
COMMENT_TITLE = os.getenv("INPUT_COMMENT_TITLE")
ONLY_PR_CHANGES = os.getenv("INPUT_REPORT_PR_CHANGES_ONLY")
VERBOSE = os.getenv("INPUT_VERBOSE", "False").lower() == "true"
FILES_WITH_ISSUES = dict()

# Max characters per comment - 65536
# Make some room for HTML tags and error message
Expand All @@ -23,14 +25,21 @@
CURRENT_COMMENT_LENGTH = 0


def debug_print(message):
if VERBOSE:
lines = message.split("\n")
for line in lines:
print(f"\033[96m {line}")


def is_part_of_pr_changes(file_path, issue_file_line, files_changed_in_pr):
if ONLY_PR_CHANGES == "false":
return True

file_name = file_path[file_path.rfind("/") + 1 :]
print(f"Looking for issue found in file={file_name} ...")
debug_print(f"Looking for issue found in file={file_name} ...")
for file, (status, lines_changed_for_file) in files_changed_in_pr.items():
print(
debug_print(
f"Changed file by this PR {file} with status {status} and changed lines {lines_changed_for_file}"
)
if file == file_name:
Expand Down Expand Up @@ -82,10 +91,10 @@ def setup_changed_files():
files_changed = dict()

github = Github(GITHUB_TOKEN)
repo = github.get_repo(REPO_NAME)
repo = github.get_repo(TARGET_REPO_NAME)
pull_request = repo.get_pull(int(PR_NUM))
num_changed_files = pull_request.changed_files
print(f"Changed files {num_changed_files}")
debug_print(f"Changed files {num_changed_files}")
files = pull_request.get_files()
for file in files:
if file.patch is not None:
Expand All @@ -108,7 +117,9 @@ def is_excluded_dir(line):

excluded_dir = f"{WORK_DIR}/{exclude_dir}"
if VERBOSE:
print(f"{line} and {excluded_dir} with result {line.startswith(excluded_dir)}")
debug_print(
f"{line} and {excluded_dir} with result {line.startswith(excluded_dir)}"
)

return line.startswith(excluded_dir)

Expand All @@ -123,6 +134,7 @@ def create_comment_for_output(
):
issues_found = 0
global CURRENT_COMMENT_LENGTH
global FILES_WITH_ISSUES
output_string = ""
for line in tool_output:
if line.startswith(prefix) and not is_excluded_dir(line):
Expand All @@ -140,10 +152,34 @@ def create_comment_for_output(
file_line_end = get_file_line_end(file_path, file_line_start)
description = f"\n```diff\n!Line: {file_line_start} - {line[line.index(' ')+1:]}``` \n"

new_line = (
f"\n\nhttps://github.com/{REPO_NAME}/blob/{SHA}{file_path}"
f"#L{file_line_start}-L{file_line_end} {description} <br>\n"
)
if TARGET_REPO_NAME != REPO_NAME:

if file_path not in FILES_WITH_ISSUES:
with open(f"../{file_path}") as file:
lines = file.readlines()
FILES_WITH_ISSUES[file_path] = lines

modified_content = FILES_WITH_ISSUES[file_path][
file_line_start - 1 : file_line_end - 1
]
modified_content[0] = modified_content[0][:-1] + " <---- HERE\n"
file_content = "".join(modified_content)

file_url = f"https://github.com/{REPO_NAME}/blob/{SHA}{file_path}#L{file_line_start}"
new_line = (
"\n\n------"
f"\n\n <b><i>Issue found in file</b></i> [{REPO_NAME + file_path}]({file_url})\n"
f"```cpp\n"
f"{file_content}"
f"\n``` \n"
f"{description} <br>\n"
)

else:
new_line = (
f"\n\nhttps://github.com/{REPO_NAME}/blob/{SHA}{file_path}"
f"#L{file_line_start}-L{file_line_end} {description} <br>\n"
)

if is_part_of_pr_changes(file_path, file_line_start, files_changed_in_pr):
if check_for_char_limit(new_line):
Expand Down Expand Up @@ -172,6 +208,19 @@ def read_files_and_parse_results():
help="Whether to output the result to console",
required=True,
)
parser.add_argument(
"-fk",
"--fork_repository",
help="Whether the actual code is in 'pr_tree' directory",
required=True,
)

if parser.parse_args().fork_repository == "true":
global REPO_NAME

# Make sure to use Head repository
REPO_NAME = os.getenv("INPUT_PR_REPO")

cppcheck_file_name = parser.parse_args().cppcheck
clangtidy_file_name = parser.parse_args().clangtidy
output_to_console = parser.parse_args().output_to_console == "true"
Expand All @@ -185,10 +234,12 @@ def read_files_and_parse_results():
clang_tidy_content = file.readlines()

line_prefix = f"{WORK_DIR}"
if VERBOSE:
print(f"Cppcheck result: \n {cppcheck_content} \n")
print(f"clang-tidy result: \n {clang_tidy_content} \n")
print(f"line_prefix: {line_prefix} \n")

debug_print(
f"cppcheck result: \n {cppcheck_content} \n"
f"clang-tidy result: \n {clang_tidy_content} \n"
f"line_prefix: {line_prefix} \n"
)

files_changed_in_pr = dict()
if not output_to_console:
Expand Down Expand Up @@ -236,7 +287,7 @@ def prepare_comment_body(

if len(cppcheck_comment) > 0:
full_comment_body += (
f"<details> <summary> <b> :red_circle: Cppcheck found "
f"<details> <summary> <b> :red_circle: cppcheck found "
f"{cppcheck_issues_found} {'issues' if cppcheck_issues_found > 1 else 'issue'}!"
" Click here to see details. </b> </summary> <br>"
f"{cppcheck_comment} </details>"
Expand All @@ -255,14 +306,14 @@ def prepare_comment_body(
if CURRENT_COMMENT_LENGTH == COMMENT_MAX_SIZE:
full_comment_body += f"\n```diff\n{MAX_CHAR_COUNT_REACHED}\n```"

print(f"Repo={REPO_NAME} pr_num={PR_NUM} comment_title={COMMENT_TITLE}")
debug_print(f"Repo={REPO_NAME} pr_num={PR_NUM} comment_title={COMMENT_TITLE}")

return full_comment_body


def create_or_edit_comment(comment_body):
github = Github(GITHUB_TOKEN)
repo = github.get_repo(REPO_NAME)
repo = github.get_repo(TARGET_REPO_NAME)
pull_request = repo.get_pull(int(PR_NUM))

comments = pull_request.get_issue_comments()
Expand Down