Skip to content

Commit

Permalink
Merge pull request #8 from JacobDomagala/7-add-option-to-run-only-on-…
Browse files Browse the repository at this point in the history
…pr-changes

7: Add option to run static analysis only on files changed in PR
  • Loading branch information
JacobDomagala authored May 1, 2021
2 parents 3b84271 + 641eb4c commit f58aca5
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 75 deletions.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ You can use `cppcheck_args` input to set your flags.

- **clang-tidy** will look for the ```.clang-tidy``` file in your repository.

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

## Workflow example

```yml
Expand All @@ -23,7 +26,7 @@ name: Static analysis
on: [pull_request]

jobs:
static analysis:
static_analysis:
runs-on: ubuntu-latest

steps:
Expand All @@ -41,8 +44,13 @@ jobs:
- name: Run static analysis
uses: JacobDomagala/StaticAnalysis@master
with:
# Exclude any issues found in ${Project_root_dir}/lib
exclude_dir: lib
apt_pckgs: software-properties-common

# Additional apt packages that need to be installed 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
init_script: init_script.sh
```
Expand All @@ -54,9 +62,10 @@ jobs:
| `pr_num` | TRUE | Pull request number for which the comment will be created |`${{github.event.pull_request.number}}`|
| `comment_title` | TRUE | Title for comment with the raport. This should be an unique name | `Static analysis result` |
| `exclude_dir` | FALSE | Directory which should be excluded from the raport | `<empty>` |
| `apt_pckgs` | FALSE | Additional (comma separated) packages that need to be installed in order for project to compile | `<empty>` |
| `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` | TRUE | Cppcheck (space separated) arguments that will be used |`--enable=all --suppress=missingInclude --inline-suppr --inconclusive`|
| `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`|



Expand Down
5 changes: 4 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ inputs:
exclude_dir:
description: 'Directory which should be excluded from the raport'
apt_pckgs:
description: 'Additional (comma separated) packages that need to be installed in order for project to compile'
description: 'Additional (space separated) packages that need to be installed in order for project to compile'
init_script:
description: |
'Optional shell script that will be run before running CMake command.'
Expand All @@ -30,6 +30,9 @@ inputs:
cppcheck_args:
description: 'CPPCHECK (space separated) arguments that will be used'
default: --enable=all --suppress=missingInclude --inline-suppr --inconclusive
report_pr_changes_only:
description: 'Only post the issues found within the changes introduced in this Pull Request'
default: false

runs:
using: "docker"
Expand Down
34 changes: 29 additions & 5 deletions docker/static_analysis.dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
FROM ubuntu:20.04
FROM ubuntu:20.04 as base

ENV CXX=clang++
ARG DEBIAN_FRONTEND=noninteractive
ENV CC=clang

RUN apt-get update && apt-get install -y python3 python3-pip git xorg-dev\
build-essential clang-11 lldb-11 lld-11 libc++-11-dev cppcheck llvm-dev clang-tidy
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y python3 python3-pip git \
build-essential clang-11 clang-tidy-11 wget libssl-dev ninja-build && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN pip3 install PyGithub

RUN git clone https://github.com/Kitware/CMake.git && cd CMake && ./bootstrap && make && make install
RUN ln -s \
"$(which clang++-11)" \
/usr/bin/clang++

RUN ln -s \
"$(which clang-11)" \
/usr/bin/clang

RUN ln -s \
/usr/bin/python3 \
/usr/bin/python

RUN git clone https://github.com/Kitware/CMake.git && \
cd CMake && ./bootstrap && \
make -j4 && make install

RUN wget 'https://sourceforge.net/projects/cppcheck/files/cppcheck/2.4/cppcheck-2.4.tar.gz/download' && \
tar xf download && \
cd cppcheck-2.4 && mkdir build && cd build && \
cmake -G "Ninja" .. && ninja install

15 changes: 8 additions & 7 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

set -x

if [ "$INPUT_PR_NUM" == "null" ]; then
echo "Pull request number input is not present! This action can only run on Pull Requests!"
exit 0
fi

if [ -n "$INPUT_APT_PCKGS" ]; then
for i in ${INPUT_APT_PCKGS//,/ }
do
apt-get update
apt-get install -y "$i"
done
apt-get update && apt-get install -y "$INPUT_APT_PCKGS"
fi

if [ -n "$INPUT_INIT_SCRIPT" ]; then
Expand All @@ -20,10 +21,10 @@ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..

if [ -z "$INPUT_EXCLUDE_DIR" ]; then
cppcheck --project=compile_commands.json $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt
run-clang-tidy >(tee "clang_tidy.txt")
run-clang-tidy-11 >(tee "clang_tidy.txt")
else
cppcheck --project=compile_commands.json $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt -i"$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR"
run-clang-tidy "^((?!$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR).)*$" > clang_tidy.txt
run-clang-tidy-11 "^((?!$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR).)*$" > clang_tidy.txt
fi

python3 /run_static_analysis.py -cc cppcheck.txt -ct clang_tidy.txt
214 changes: 155 additions & 59 deletions run_static_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,86 @@
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')

# Max characters per comment - 65536
# Make some room for HTML tags and error message
MAX_CHAR_COUNT_REACHED = '!Maximum character count per GitHub comment has been reached! Not all warnings/errors has been parsed!'
COMMENT_MAX_SIZE = 65000
current_comment_length = 0

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} ...")
for file, (status, lines_changed_for_file) in files_changed_in_pr.items():
print(f"Changed file by this PR {file} with status {status} and changed lines {lines_changed_for_file}")
if file == file_name:
if status == "added":
return True

for (start, end) in lines_changed_for_file:
if issue_file_line >= start and issue_file_line <= end:
return True

return False

def get_lines_changed_from_patch(patch):
lines_changed = []
lines = patch.split('\n')

for line in lines:
# Example line @@ -43,6 +48,8 @@
# ------------ ^
if line.startswith("@@"):
# Example line @@ -43,6 +48,8 @@
# ----------------------^
idx_beg = line.index("+")

# Example line @@ -43,6 +48,8 @@
# ^--^
idx_end = line[idx_beg:].index(",")
line_begin = int(line[idx_beg + 1 : idx_beg + idx_end])

idx_beg = idx_beg + idx_end
idx_end = line[idx_beg + 1 : ].index("@@")

num_lines = int(line[idx_beg + 1 : idx_beg + idx_end])

lines_changed.append((line_begin, line_begin + num_lines))

return lines_changed

def setup_changed_files():
files_changed = dict()

g = Github(GITHUB_TOKEN)
repo = g.get_repo(REPO_NAME)
pull_request = repo.get_pull(PR_NUM)
num_changed_files = pull_request.changed_files
print(f"Changed files {num_changed_files}")
files = pull_request.get_files()
for file in files:
# additions # blob_url # changes # contents_url # deletions # filename
# patch # previous_filename # raw_url # sha # status
# print(f"File: additions={file.additions} blob_url={file.blob_url} changes={file.changes} contents_url={file.contents_url}"\
# f"deletions={file.deletions} filename={file.filename} patch={file.patch} previous_filename={file.previous_filename}"\
# f"raw_url={file.raw_url} sha={file.sha} status={file.status} ")

if file.patch is not None:
lines_changed_for_file = get_lines_changed_from_patch(file.patch)
files_changed[file.filename] = (file.status, lines_changed_for_file)

return files_changed

def check_for_char_limit(incoming_line):
global current_comment_length
return (current_comment_length + len(incoming_line)) <= COMMENT_MAX_SIZE

def create_comment_for_output(tool_output, prefix):
def create_comment_for_output(tool_output, prefix, files_changed_in_pr):
issues_found = 0
global current_comment_length
output_string = ''
for line in tool_output:
Expand All @@ -33,62 +101,90 @@ def create_comment_for_output(tool_output, prefix):
file_line_end = file_line_start + 5
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}#L{file_line_start}-L{file_line_end} {description} <br>\n'
if check_for_char_limit(new_line):
output_string += new_line
current_comment_length += len(new_line)
else:
current_comment_length = COMMENT_MAX_SIZE
return output_string

return output_string

# Get cppcheck and clang-tidy files
parser = argparse.ArgumentParser()
parser.add_argument('-cc', '--cppcheck', help='Output file name for cppcheck', required=True)
parser.add_argument('-ct', '--clangtidy', help='Output file name for clang-tidy', required=True)
cppcheck_file_name = parser.parse_args().cppcheck
clangtidy_file_name = parser.parse_args().clangtidy

cppcheck_content = ''
with open(cppcheck_file_name, 'r') as file:
cppcheck_content = file.readlines()

clang_tidy_content = ''
with open(clangtidy_file_name, 'r') as file:
clang_tidy_content = file.readlines()

line_prefix = f'{WORK_DIR}'

cppcheck_comment = create_comment_for_output(cppcheck_content, line_prefix)
clang_tidy_comment = create_comment_for_output(clang_tidy_content, line_prefix)

full_comment_body = f'<b><h2> {COMMENT_TITLE} </h2></b> <br>'\
f'<details> <summary> <b>CPPCHECK</b> </summary> <br>'\
f'{cppcheck_comment} </details><br>'\
f'<details> <summary> <b>CLANG-TIDY</b> </summary> <br>'\
f'{clang_tidy_comment} </details><br>\n'

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}')

g = Github(GITHUB_TOKEN)
repo = g.get_repo(REPO_NAME)
pr = repo.get_pull(PR_NUM)

comments = pr.get_issue_comments()
found_id = -1
comment_to_edit = None
for comment in comments:
if (comment.user.login == 'github-actions[bot]') and (COMMENT_TITLE in comment.body):
found_id = comment.id
comment_to_edit = comment
break

if found_id != -1:
comment_to_edit.edit(body = full_comment_body)
else:
pr.create_issue_comment(body = full_comment_body)
new_line = f'\n\nhttps://github.com/{REPO_NAME}/blob/{SHA}{file_path}#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):
output_string += new_line
current_comment_length += len(new_line)
issues_found += 1
else:
current_comment_length = COMMENT_MAX_SIZE
return output_string, issues_found

return output_string, issues_found

def read_files_and_parse_results(files_changed_in_pr):
# Get cppcheck and clang-tidy files
parser = argparse.ArgumentParser()
parser.add_argument('-cc', '--cppcheck', help='Output file name for cppcheck', required=True)
parser.add_argument('-ct', '--clangtidy', help='Output file name for clang-tidy', required=True)
cppcheck_file_name = parser.parse_args().cppcheck
clangtidy_file_name = parser.parse_args().clangtidy

cppcheck_content = ''
with open(cppcheck_file_name, 'r') as file:
cppcheck_content = file.readlines()

clang_tidy_content = ''
with open(clangtidy_file_name, 'r') as file:
clang_tidy_content = file.readlines()

line_prefix = f'{WORK_DIR}'

cppcheck_comment, cppcheck_issues_found = create_comment_for_output(cppcheck_content, line_prefix, files_changed_in_pr)
clang_tidy_comment, clang_tidy_issues_found = create_comment_for_output(clang_tidy_content, line_prefix, files_changed_in_pr)

return cppcheck_comment, clang_tidy_comment, cppcheck_issues_found, clang_tidy_issues_found

def prepare_comment_body(cppcheck_comment, clang_tidy_comment, cppcheck_issues_found, clang_tidy_issues_found):

if cppcheck_issues_found == 0 and clang_tidy_issues_found == 0:
full_comment_body = f'## <p align="center"><b> :white_check_mark: {COMMENT_TITLE} - no issues found! :white_check_mark: </b></p>'
else:
full_comment_body = f'## <p align="center"><b> :zap: {COMMENT_TITLE} :zap: </b></p> \n\n'

if len(cppcheck_comment) > 0:
full_comment_body +=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>'

full_comment_body += "\n\n *** \n"

if len(clang_tidy_comment) > 0:
full_comment_body += f'<details> <summary> <b> :red_circle: clang-tidy found'\
f' {clang_tidy_issues_found} {"issues" if cppcheck_issues_found > 1 else "issue"}! Click here to see details. </b> </summary> <br>'\
f'{clang_tidy_comment} </details><br>\n'

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}')

return full_comment_body

def create_or_edit_comment(comment_body):
g = Github(GITHUB_TOKEN)
repo = g.get_repo(REPO_NAME)
pr = repo.get_pull(PR_NUM)

comments = pr.get_issue_comments()
found_id = -1
comment_to_edit = None
for comment in comments:
if (comment.user.login == 'github-actions[bot]') and (COMMENT_TITLE in comment.body):
found_id = comment.id
comment_to_edit = comment
break

if found_id != -1:
comment_to_edit.edit(body = comment_body)
else:
pr.create_issue_comment(body = comment_body)


if __name__ == "__main__":
files_changed_in_pr = setup_changed_files()
cppcheck_comment, clang_tidy_comment, cppcheck_issues_found, clang_tidy_issues_found = read_files_and_parse_results(files_changed_in_pr)
comment_body = prepare_comment_body(cppcheck_comment, clang_tidy_comment, cppcheck_issues_found, clang_tidy_issues_found)
create_or_edit_comment(comment_body)

0 comments on commit f58aca5

Please sign in to comment.