diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..3923495 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,41 @@ +name: StaticAnalysis-MoreCPP Coverage and Test Results Upload + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + check: + name: Run Unit Tests with Coverage, with CodeCov for coverage and test results upload + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.13.0 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest PyGithub pytest-cov sarif-om jsonpickle + - name: Test with coverage + run: | + pytest --cov-report xml:coverage.xml + - name: Test with analysis + run: | + pytest --cov --junitxml=junit.xml -o junit_family=legacy + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5.3.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: eljonny/StaticAnalysis-MoreCPP + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 417ca8e..c66e180 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,25 +1,27 @@ -name: Linter +name: StaticAnalysis-MoreCPP Project Code Linting and Style Checks on: push: branches: - main pull_request: + branches: + - main jobs: check: name: Run Linter - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: CodeQuality - uses: JacobDomagala/StaticAnalysis@main + uses: JacobDomagala/StaticAnalysis@v0.0.8 with: language: "Python" pylint_args: "--rcfile=.pylintrc --recursive=true" python_dirs: "src test" - exclude_dir: "test/utils/" + exclude_dir: "test/utils/dummy_project" - name: PyLint uses: ricardochaves/python-lint@v1.4.0 diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index abb4b72..99096df 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -1,10 +1,12 @@ -name: "Shellcheck" +name: "StaticAnalysis-MoreCPP Shellcheck" on: push: branches: - main pull_request: + branches: + - main jobs: shellcheck: @@ -13,6 +15,6 @@ jobs: steps: - uses: actions/checkout@v3 - name: Run ShellCheck - uses: ludeeus/action-shellcheck@main + uses: ludeeus/action-shellcheck@master with: ignore_names: llvm.sh # External file diff --git a/.github/workflows/test_action.yml b/.github/workflows/test_action.yml index ff9e799..8fae109 100644 --- a/.github/workflows/test_action.yml +++ b/.github/workflows/test_action.yml @@ -1,10 +1,12 @@ -name: Test Action +name: StaticAnalysis-MoreCPP Test GitHub Action on: push: branches: - main pull_request: + branches: + - main jobs: check: @@ -31,8 +33,8 @@ jobs: BRANCH_NAME=${GITHUB_HEAD_REF} fi - git clone "https://${{secrets.TOKEN}}@github.com/JacobDomagala/TestRepo.git" - cd TestRepo + git clone "https://${{secrets.TOKEN}}@github.com/eljonnyTest/StaticAnalysisTestRepo.git" + cd StaticAnalysisTestRepo python ./switch_sa_branch.py -br=$BRANCH_NAME git diff --quiet && git diff --staged --quiet || git commit -am"Update branch name: ($BRANCH_NAME)" git push @@ -48,8 +50,8 @@ jobs: git push -f # test pull_request_target - git clone "https://${{secrets.TOKEN}}@github.com/JacobDTest/TestRepo.git" - cd TestRepo + git clone "https://${{secrets.TOKEN}}@github.com/eljonnyTest/StaticAnalysisTestRepoFork.git" + cd StaticAnalysisTestRepoFork git checkout test-branch-fork git commit -as --amend --no-edit git push -f @@ -61,16 +63,16 @@ jobs: message: | ## Test Action results *** - ### [Result for push](https://github.com/JacobDomagala/TestRepo/actions/workflows/test.yml?query=branch%3Amain) + ### [Result for push](https://github.com/eljonny/StaticAnalysisTestRepo/actions/workflows/test.yml?query=branch%3Amain) *** - ### [Result for pull_request (CMake)](https://github.com/JacobDomagala/TestRepo/pull/3#issuecomment-1404081176) + ### [Result for pull_request (CMake)](https://github.com/eljonny/StaticAnalysisTestRepo/pull/3#issuecomment-1404081176) *** - ### [Result for pull_request (non CMake)](https://github.com/JacobDomagala/TestRepo/pull/3#issuecomment-1404102205) + ### [Result for pull_request (non CMake)](https://github.com/eljonny/StaticAnalysisTestRepo/pull/3#issuecomment-1404102205) *** - ### [Result for pull_request_target (CMake)](https://github.com/JacobDomagala/TestRepo/pull/7#issuecomment-1404081052) + ### [Result for pull_request_target (CMake)](https://github.com/eljonny/StaticAnalysisTestRepo/pull/7#issuecomment-1404081052) *** - ### [Result for pull_request_target (non CMake)](https://github.com/JacobDomagala/TestRepo/pull/7#issuecomment-1404101648) + ### [Result for pull_request_target (non CMake)](https://github.com/eljonny/StaticAnalysisTestRepo/pull/7#issuecomment-1404101648) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 97e9a16..fc89c60 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -1,10 +1,12 @@ -name: Unit Tests +name: StaticAnalysis-MoreCPP Unit Tests on: push: branches: - main pull_request: + branches: + - main jobs: check: @@ -19,7 +21,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest PyGithub + pip install pytest PyGithub sarif-om jsonpickle - name: Test with pytest run: | pytest + diff --git a/Dockerfile b/Dockerfile index ad59e55..65e5b18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM jdomagala/static_analysis:latest +FROM jhyry9docks/static_analysis:morecpp WORKDIR /src @@ -7,5 +7,4 @@ COPY src/*.py ./ COPY *.sh ./ RUN chmod +x *.sh - ENTRYPOINT ["/src/entrypoint.sh"] diff --git a/LICENSE b/LICENSE index efe010e..fbea532 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 GitHub, Inc. and contributors +Copyright (c) 2025 Jonathan Hyry et al Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a5e48c5..792242f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,24 @@ -[![Linter](https://github.com/JacobDomagala/StaticAnalysis/actions/workflows/linter.yml/badge.svg?branch=main)](https://github.com/JacobDomagala/StaticAnalysis/actions/workflows/linter.yml?query=branch%3Amain) -[![Test Action](https://github.com/JacobDomagala/StaticAnalysis/actions/workflows/test_action.yml/badge.svg?branch=main)](https://github.com/JacobDomagala/StaticAnalysis/actions/workflows/test_action.yml?query=branch%3Amain) -[![Unit Tests](https://github.com/JacobDomagala/StaticAnalysis/actions/workflows/unit_tests.yml/badge.svg?branch=main)](https://github.com/JacobDomagala/StaticAnalysis/actions/workflows/unit_tests.yml?query=branch%3Amain) +[![Linter](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/linter.yml/badge.svg)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/linter.yml) +[![Test Action](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/test_action.yml/badge.svg)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/test_action.yml) +[![Unit Tests](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/unit_tests.yml) +[![Shell Script Check](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/shellcheck.yml) +[![Code Coverage](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/coverage.yml/badge.svg)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/coverage.yml) +[![codecov](https://codecov.io/gh/eljonny/StaticAnalysis-MoreCPP/graph/badge.svg?token=QJZvQ3D9aK)](https://codecov.io/gh/eljonny/StaticAnalysis-MoreCPP) # 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 +- For C++: + - [cppcheck](https://github.com/danmar/cppcheck) Main page [here](http://cppcheck.sourceforge.net/) + - [clang-tidy](https://github.com/llvm/llvm-project/tree/main/clang-tools-extra/clang-tidy) Main page [here](https://clang.llvm.org/extra/clang-tidy/) + - [fbinfer](https://github.com/facebook/infer) Main page [here](https://fbinfer.com/) + - [flawfinder](http://sourceforge.net/projects/flawfinder/) Main page [here](https://dwheeler.com/flawfinder/) +- For Python + - [pylint](https://github.com/pylint-dev/pylint) Main page [here](https://pylint.readthedocs.io/en/latest/index.html) 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 about setup and various inputs, please see sections dedicated to each language ([**C++**](https://github.com/eljonny/StaticAnalysis-MoreCPP?tab=readme-ov-file#c) and [**Python**](https://github.com/eljonny/StaticAnalysis-MoreCPP?tab=readme-ov-file#python)) ## Pull Request comment @@ -32,7 +40,7 @@ For non Pull Requests, the output will be printed to GitHub's output console. Th

# 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 required (see the [**Inputs**](https://github.com/eljonny/StaticAnalysis-MoreCPP#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/eljonny/StaticAnalysis-MoreCPP#workflow-example) or [**Inputs**](https://github.com/eljonny/StaticAnalysis-MoreCPP#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``` @@ -52,7 +60,6 @@ on: branches: - develop - main - - 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) @@ -72,7 +79,7 @@ jobs: run: | echo "#!/bin/bash - # Input args provided by StaticAnalysis action + # Input args provided by StaticAnalysis-MoreCPP action root_dir=\${1} build_dir=\${2} echo \"Hello from the init script! First arg=\${root_dir} second arg=\${build_dir}\" @@ -82,7 +89,7 @@ jobs: apt install -y libvulkan1 mesa-vulkan-drivers vulkan-utils" > init_script.sh - name: Run static analysis - uses: JacobDomagala/StaticAnalysis@main + uses: eljonny/StaticAnalysis-MoreCPP@morecpp-latest with: language: c++ @@ -102,24 +109,36 @@ jobs: # (Optional) cppcheck args cppcheck_args: --enable=all --suppress=missingIncludeSystem + + # (Optional) infer args, required if not using CMake + fbinfer_args: -- make -j 4 + + # (Optional) flawfinder args + flawfinder_args: --minlevel=1 --context --dataonly --quiet --columns --error-level=2 + + # (Optional) flawfinder targets, if you use something other than src and include for implementations and headers, respectively, or if there are additional directories with implementation code and/or headers + flawfinder_targets: src include otherSrc otherInclude ``` ## Inputs | Name | Description | Default value | |-------------------------|------------------------------------|---------------| -| `github_token` | Github token used for Github API requests |`${{github.token}}`| -| `pr_num` | Pull request number for which the comment will be created |`${{github.event.pull_request.number}}`| +| `github_token` | Github token used for Github API requests | `${{github.token}}` | +| `pr_num` | Pull request number for which the comment will be created | `${{github.event.pull_request.number}}` | | `comment_title` | Title for comment with the raport. This should be an unique name | `Static analysis result` | | `exclude_dir` | Directory which should be excluded from the raport | `` | | `apt_pckgs` | Additional (space separated) packages that need to be installed in order for project to compile | `` | | `init_script` | Optional shell script that will be run before configuring project (i.e. running CMake command). This should be used, when the project requires some environmental set-up beforehand. Script will be run with 2 arguments: `root_dir`(root directory of user's code) and `build_dir`(build directory created for running SA). Note. `apt_pckgs` will run before this script, just in case you need some packages installed. Also this script will be run in the root of the project (`root_dir`) | `` | -| `cppcheck_args` | Cppcheck (space separated) arguments that will be used |`--enable=all --suppress=missingIncludeSystem --inline-suppr --inconclusive`| -| `clang_tidy_args` | clang-tidy arguments that will be used (example: `-checks='*,fuchsia-*,google-*,zircon-*'` |``| -| `report_pr_changes_only`| 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`| +| `cppcheck_args` | Cppcheck (space separated) arguments that will be used | `--enable=all --suppress=missingIncludeSystem --inline-suppr --inconclusive` | +| `clang_tidy_args` | clang-tidy arguments that will be used (example: `-checks='*,fuchsia-*,google-*,zircon-*'`) | `` | +| `fbinfer_args` | FB Infer arguments that will be used, if you don't use CMake, use this to specify the build system (example: `-- make -j 4`) | `` | +| `flawfinder_args` | flawfinder arguments that will be used (example: `--minlevel=0 --html --html-title="ProjectX Flawfinder Report" --columns`) | `--minlevel=0 --context --dataonly --quiet --columns --error-level=0` | +| `flawfinder_targets` | Directories with implementation and/or header files that will be analyzed with flawfinder | `src include` | +| `report_pr_changes_only`| 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` | | `use_cmake` | Determines wether CMake should be used to generate compile_commands.json file | `true` | -| `cmake_args` | Additional CMake arguments |``| -| `force_console_print` | Output the action result to console, instead of creating the comment |`false`| +| `cmake_args` | Additional CMake arguments | `` | +| `force_console_print` | Output the action result to console, instead of creating the comment | `false` | **NOTE: `apt_pckgs` will run before `init_script`, just in case you need some packages installed before running the script** @@ -147,7 +166,7 @@ jobs: - uses: actions/checkout@v3 - name: CodeQuality - uses: JacobDomagala/StaticAnalysis@main + uses: eljonny/StaticAnalysis-MoreCPP@morecpp-latest with: language: "Python" pylint_args: "--rcfile=.pylintrc --recursive=true" diff --git a/action.yml b/action.yml index 73e3e42..2dd17e8 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ -name: "Static analysis for C++(Clang-19)/Python project" -description: "Static analysis with cppcheck & clang-tidy for C++, pylint for Python. Posts results to PRs or console." +name: "Static Analysis for C++ and Python projects" +description: "Static analysis with cppcheck, clang-tidy, fbinfer, and flawfinder for C++, pylint for Python. Posts results to PRs or console." inputs: github_token: @@ -21,7 +21,7 @@ inputs: description: 'Title for comment with the raport. This should be an unique name' default: Static analysis result exclude_dir: - description: 'Directories (space separated) which should be excluded from the raport' + description: 'Directories (space separated) which should be excluded from the report' apt_pckgs: description: 'Additional (space separated) packages that need to be installed in order for project to compile' init_script: @@ -35,7 +35,15 @@ inputs: description: 'cppcheck (space separated) arguments that will be used' default: --enable=all --suppress=missingIncludeSystem --inline-suppr --inconclusive clang_tidy_args: - description: 'clang-tidy arguments that will be used (example: -checks="*,fuchsia-*,google-*,zircon-*"' + description: 'clang-tidy arguments that will be used (example: -checks="*,fuchsia-*,google-*,zircon-*")' + fbinfer_args: + description: 'Arguments that will be passed to infer' + flawfinder_args: + description: 'Flawfinder arguments that will be used' + default: --minlevel=0 --context --dataonly --quiet --columns --error-level=0 + flawfinder_targets: + description: 'Directories (space separated) containing C/C++ header and/or files to be checked' + default: ${{github.workspace}}/src ${{github.workspace}}/include report_pr_changes_only: description: 'Only post the issues found within the changes introduced in this Pull Request' default: false diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..b12daae --- /dev/null +++ b/codecov.yml @@ -0,0 +1,6 @@ +ignore: + - "local/**" + - "test/test_*.py" + - "docker/**" + - ".github/**" + - ".pytest_cache/**" diff --git a/docker/static_analysis.dockerfile b/docker/static_analysis.dockerfile index 57ddc9f..62ae41d 100644 --- a/docker/static_analysis.dockerfile +++ b/docker/static_analysis.dockerfile @@ -1,40 +1,50 @@ -FROM ubuntu:24.04 AS base +FROM debian:stable-slim AS base -# Define versions as environment variables -ENV CLANG_VERSION=20 \ - CPPCHECK_VERSION=2.16.0 \ - CXX=clang++ \ - CC=clang \ - DEBIAN_FRONTEND=noninteractive +ENV CLANG_VERSION=19 +ENV CPPCHECK_VERSION=2.17.1 +ENV INFER_VERSION=1.2.0 +ENV CXX=clang++ +ENV CC=clang +ENV DEBIAN_FRONTEND=noninteractive -# Copy the llvm.sh installation script -COPY llvm.sh /llvm.sh +ENV INFER_EP=https://github.com/facebook/infer/releases/download/v$INFER_VERSION/infer-linux-x86_64-v$INFER_VERSION.tar.xz # Install dependencies -RUN apt-get update && apt-get install -y \ - build-essential python3 python3-pip git wget libssl-dev ninja-build \ - lsb-release software-properties-common gnupg \ - # Execute the LLVM install script with the version number - && chmod +x /llvm.sh && /llvm.sh $CLANG_VERSION \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* \ - # Install Python packages - && pip3 install --break-system-packages PyGithub pylint \ - # Create symlinks for clang and clang++ - && ln -s "$(which clang++-$CLANG_VERSION)" /usr/bin/clang++ \ - && ln -s "$(which clang-$CLANG_VERSION)" /usr/bin/clang \ - && ln -s /usr/bin/python3 /usr/bin/python +RUN apt-get update && apt-get full-upgrade -y +RUN apt-get install -y apt-utils +RUN apt-get install -y build-essential python3 \ + python3-pip git wget libssl-dev ninja-build \ + gnupg lsb-release software-properties-common \ + flawfinder curl cmake -WORKDIR /opt +RUN curl -sSL $INFER_EP | tar -C /opt -xJ +RUN ln -s /opt/infer-linux-x86_64-v$INFER_VERSION/bin/infer /usr/local/bin/infer + +# Copy the llvm.sh installation script +COPY --chmod=500 llvm.sh /opt/llvm/llvm.sh + +# Execute the LLVM install script with the version number +WORKDIR /opt/llvm +RUN ./llvm.sh $CLANG_VERSION -# Build CMake from source -RUN git clone https://github.com/Kitware/CMake.git \ - && cd CMake \ - && ./bootstrap && make -j$(nproc) && make install +# Clean up +RUN apt-get clean +RUN rm -rf /var/lib/apt/lists/* + +# Install Python packages +RUN pip3 install --break-system-packages PyGithub pylint sarif-om + +# Create symlinks for clang and clang++ +RUN ln -s "$(which clang++-$CLANG_VERSION)" /usr/bin/clang++ +RUN ln -s "$(which clang-$CLANG_VERSION)" /usr/bin/clang + +# Create a symlink for python +RUN ln -s /usr/bin/python3 /usr/bin/python # Install cppcheck -RUN git clone https://github.com/danmar/cppcheck.git \ - && cd cppcheck \ - && git checkout tags/$CPPCHECK_VERSION \ - && mkdir build && cd build \ - && cmake -G Ninja .. && ninja all && ninja install +WORKDIR /opt +RUN git clone https://github.com/danmar/cppcheck.git +WORKDIR /opt/cppcheck +RUN git checkout tags/$CPPCHECK_VERSION +WORKDIR /opt/cppcheck/build +RUN cmake -G Ninja .. && ninja all && ninja install diff --git a/entrypoint_cpp.sh b/entrypoint_cpp.sh index d840545..5449587 100644 --- a/entrypoint_cpp.sh +++ b/entrypoint_cpp.sh @@ -9,19 +9,27 @@ print_to_console=${print_to_console:-false} use_extra_directory=${use_extra_directory:-false} common_ancestor=${common_ancestor:-""} -CLANG_TIDY_ARGS="${INPUT_CLANG_TIDY_ARGS//$'\n'/}" +FLAWFINDER_ARGS="${INPUT_FLAWFINDER_ARGS//$'\n'/}" +FLAWFINDER_TGTS="${INPUT_FLAWFINDER_TARGETS//$'\n'/}" CPPCHECK_ARGS="${INPUT_CPPCHECK_ARGS//$'\n'/}" +INFER_ARGS="${INPUT_FBINFER_ARGS//$'\n'/}" +CLANG_TIDY_ARGS="${INPUT_CLANG_TIDY_ARGS//$'\n'/}" + +WS_BASE="$GITHUB_WORKSPACE/build" +WS_INFER="$GITHUB_WORKSPACE/build/infer-out" cd build if [ "$INPUT_REPORT_PR_CHANGES_ONLY" = true ]; then if [ -z "$preselected_files" ]; then # Create empty files + touch flawfinder.txt touch cppcheck.txt + touch infer.json touch clang_tidy.txt cd / - python3 -m src.static_analysis_cpp -cc "${GITHUB_WORKSPACE}/build/cppcheck.txt" -ct "${GITHUB_WORKSPACE}/build/clang_tidy.txt" -o "$print_to_console" -fk "$use_extra_directory" --common "$common_ancestor" --head "origin/$GITHUB_HEAD_REF" + python3 -m src.static_analysis_cpp -ff "$WS_BASE/flawfinder.txt" -cc "$WS_BASE/cppcheck.txt" -fi "$WS_BASE/infer.json" -ct "$WS_BASE/clang_tidy.txt" -o "$print_to_console" -fk "$use_extra_directory" --common "$common_ancestor" --head "origin/$GITHUB_HEAD_REF" exit 0 fi fi @@ -34,52 +42,80 @@ if [ "$INPUT_USE_CMAKE" = true ]; then fi if [ -z "$INPUT_EXCLUDE_DIR" ]; then - files_to_check=$(python3 /src/get_files_to_check.py -dir="$GITHUB_WORKSPACE" -preselected="$preselected_files" -lang="c++") debug_print "Running: files_to_check=python3 /src/get_files_to_check.py -dir=\"$GITHUB_WORKSPACE\" -preselected=\"$preselected_files\" -lang=\"c++\")" + files_to_check=$(python3 /src/get_files_to_check.py -dir="$GITHUB_WORKSPACE" -preselected="$preselected_files" -lang="c++") else - files_to_check=$(python3 /src/get_files_to_check.py -exclude="$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR" -dir="$GITHUB_WORKSPACE" -preselected="$preselected_files" -lang="c++") - debug_print "Running: files_to_check=python3 /src/get_files_to_check.py -exclude=\"$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR\" -dir=\"$GITHUB_WORKSPACE\" -preselected=\"$preselected_files\" -lang=\"c++\")" + debug_print "Running: files_to_check=python3 /src/get_files_to_check.py -exclude=\"$INPUT_EXCLUDE_DIR\" -dir=\"$GITHUB_WORKSPACE\" -preselected=\"$preselected_files\" -lang=\"c++\")" + files_to_check=$(python3 /src/get_files_to_check.py -exclude="$INPUT_EXCLUDE_DIR" -dir="$GITHUB_WORKSPACE" -preselected="$preselected_files" -lang="c++") fi +debug_print "FLAWFINDER_ARGS = $FLAWFINDER_ARGS" +debug_print "FLAWFINDER_TGTS = $FLAWFINDER_TGTS" debug_print "Files to check = $files_to_check" debug_print "CPPCHECK_ARGS = $CPPCHECK_ARGS" debug_print "CLANG_TIDY_ARGS = $CLANG_TIDY_ARGS" - -num_proc=$(nproc) +debug_print "INFER_ARGS = $INFER_ARGS" +debug_print "WS_BASE = $WS_BASE" +debug_print "WS_INFER = $WS_INFER" if [ -z "$files_to_check" ]; then echo "No files to check" + else - if [ "$INPUT_USE_CMAKE" = true ]; then - for file in $files_to_check; do - exclude_arg="" - if [ -n "$INPUT_EXCLUDE_DIR" ]; then - exclude_arg="-i$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR" + cpp_files_to_check="" + for file in $files_to_check; do + file_extension="${file##*.}" + if [[ "${file_extension,,}" =~ (c(c|p(pm?)?|\+\+)?|(c|i)xx) ]]; then + if [ -z "${cpp_files_to_check}" ]; then + cpp_files_to_check="$file" + else + cpp_files_to_check="$cpp_files_to_check $file" fi + fi + done - # Replace '/' with '_' + debug_print "CPPCheck will check the following files: $cpp_files_to_check" + + for ffdir in $FLAWFINDER_TGTS; do + dir_name=$(echo "$ffdir" | tr '/' '_') + + debug_print "Running flawfinder $FLAWFINDER_ARGS --sarif for files in /$GITHUB_WORKSPACE/$ffdir..." + eval flawfinder "$FLAWFINDER_ARGS" --sarif "/$GITHUB_WORKSPACE/$ffdir" > "flawfinder_$dir_name.sarif" 2>&1 || true + done + + debug_print "Aggregating flawfinder results into $WS_BASE/flawfinder.sarif..." + python3 -m src.join_sarif --files "$(ls flawfinder_*.sarif)" --output "$WS_BASE/flawfinder.sarif" + + if [ "$INPUT_USE_CMAKE" = true ]; then + for file in $cpp_files_to_check; do 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.json $CPPCHECK_ARGS --file-filter=$file --output-format=sarif --output-file=cppcheck_$file_name.sarif" + eval cppcheck --project=compile_commands.json "$CPPCHECK_ARGS" --file-filter="$file" --output-format=sarif --output-file="cppcheck_$file_name.sarif" || true done - cat cppcheck_*.txt > cppcheck.txt + debug_print "Aggregating cppcheck results into $WS_BASE/cppcheck.sarif..." + python3 -m src.join_sarif --files "$(ls cppcheck_*.sarif)" --output "$WS_BASE/cppcheck.sarif" + + debug_print "Running infer run --no-progress-bar --compilation-database compile_commands.json $INFER_ARGS --sarif..." + eval infer run --no-progress-bar --compilation-database compile_commands.json "$INFER_ARGS" --sarif || true # 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-19 $CLANG_TIDY_ARGS -p $(pwd) $files_to_check >>clang_tidy.txt 2>&1" + eval run-clang-tidy-19 "$CLANG_TIDY_ARGS" -p "$(pwd)" "$files_to_check" >clang_tidy.txt 2>&1 || true else - # Excludes for clang-tidy are handled in python script - 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 cppcheck $cpp_files_to_check $CPPCHECK_ARGS --output-format=sarif --output-file=cppcheck.sarif ..." + eval cppcheck "$cpp_files_to_check" "$CPPCHECK_ARGS" --output-format=sarif --output-file=cppcheck.sarif || true + + debug_print "Running infer run --no-progress-bar --sarif $INFER_ARGS..." + eval infer run --no-progress-bar --sarif "$INFER_ARGS" || 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 + debug_print "Running run-clang-tidy-19 $CLANG_TIDY_ARGS $files_to_check >>clang_tidy.txt 2>&1" + eval run-clang-tidy-19 "$CLANG_TIDY_ARGS" "$files_to_check" >clang_tidy.txt 2>&1 || true fi cd / - python3 -m src.static_analysis_cpp -cc "${GITHUB_WORKSPACE}/build/cppcheck.txt" -ct "${GITHUB_WORKSPACE}/build/clang_tidy.txt" -o "$print_to_console" -fk "$use_extra_directory" --common "$common_ancestor" --head "origin/$GITHUB_HEAD_REF" + python3 -m src.static_analysis_cpp -ff "$WS_BASE/flawfinder.sarif" -cc "$WS_BASE/cppcheck.sarif" -fi "$WS_INFER/report.sarif" -ct "$WS_BASE/clang_tidy.txt" -o "$print_to_console" -fk "$use_extra_directory" --common "$common_ancestor" --head "origin/$GITHUB_HEAD_REF" fi diff --git a/local/run_cppcheck.sh b/local/run_cppcheck.sh new file mode 100644 index 0000000..fc8fee7 --- /dev/null +++ b/local/run_cppcheck.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +RUN_FINDFILES_SCRIPT="$1" +SRC_DIR="$2" +EXCLUDES="$3" +CPPCHECK_ARGS="$4" + +#EXCLUDES="/out/ /.git /.vs /3rdparty/ /cmake/ /custom-overlay/ /infer-out/ /test/ /test_package/ /demo/ /_packages/ /res/" +#CPPCHECK_ARGS="--quiet --enable=all --inconclusive --suppress=missingIncludeSystem --suppress=unusedFunction --std=c++11 --inline-suppr --force --check-level=exhaustive --suppress='*:*3rdparty*' --checkers-report=cppcheck-checkers.report --suppress=checkersReport" + +echo "Running $RUN_FINDFILES_SCRIPT on $SRC_DIR with the following exclusions: $EXCLUDES" + +files_to_check=$(python3 "$RUN_FINDFILES_SCRIPT" -exclude="$EXCLUDES" -dir="$SRC_DIR" -lang="c++") + +for file in $files_to_check; do + file_extension="${file##*.}" + if [[ "${file_extension,,}" =~ (c(c|p(pm?)?|\+\+)?|(c|i)xx) ]]; then + # Replace '/' with '_' + file_name=$(echo "$file" | tr '/' '_') + + echo "Running cppcheck --project=compile_commands.json $CPPCHECK_ARGS --file-filter=$file --output-format=sarif --output-file=cppcheck_$file_name.txt" + eval cppcheck --project=compile_commands.json "$CPPCHECK_ARGS" --file-filter="$file" --output-format=sarif --output-file="cppcheck_$file_name.sarif" || true + else + echo "Skipping cppcheck run for $file" + fi +done diff --git a/src/get_files_to_check.py b/src/get_files_to_check.py index 7b1b7e4..a42aa06 100644 --- a/src/get_files_to_check.py +++ b/src/get_files_to_check.py @@ -1,4 +1,7 @@ import argparse +import os +import re + from pathlib import Path @@ -18,12 +21,20 @@ def get_files_to_check(directory_in, excludes_in, preselected_files, lang): str: A space-separated string of file paths that meet the search criteria. """ - exclude_prefixes = [f"{directory_in}/build"] + if os.sep.__eq__("\\"): + directory_in = str(directory_in).replace("/", os.sep) + else: + directory_in = str(directory_in).replace("\\", os.sep) + + dirin = f"{directory_in}{os.sep}build" + while not dirin.find(f"{os.sep}{os.sep}") == -1: + dirin = dirin.replace(f"{os.sep}{os.sep}", f"{os.sep}") + exclude_prefixes = [re.escape(str(dirin))] if excludes_in is not None: excludes_list = excludes_in.split(" ") for exclude in excludes_list: - exclude_prefixes.append(str(exclude)) + exclude_prefixes.append(re.escape(str(exclude))) if lang == "c++": supported_extensions = (".h", ".hpp", ".hcc", ".c", ".cc", ".cpp", ".cxx") @@ -35,12 +46,15 @@ def get_files_to_check(directory_in, excludes_in, preselected_files, lang): all_files = [] if len(preselected_files) == 0: + print(f"Compiling regex for exclude prefixes: {exclude_prefixes}") + regex_exclude = re.compile(f"{"|".join(exclude_prefixes)}") for path in Path(directory_in).rglob("*.*"): - path_ = str(path.resolve()) - if path_.endswith(supported_extensions) and not path_.startswith( - tuple(exclude_prefixes) - ): - all_files.append(path_) + if not regex_exclude.search(str(path)): + path_ = str(path.resolve()) + if path_.endswith(supported_extensions) and not path_.startswith( + tuple(exclude_prefixes) + ): + all_files.append(path_) else: for file in preselected_files: if not file.startswith(directory_in): @@ -60,7 +74,9 @@ def get_files_to_check(directory_in, excludes_in, preselected_files, lang): parser.add_argument("-dir", help="Source directory", required=True) parser.add_argument("-lang", help="Programming language", required=True) - directory = parser.parse_args().dir + directory = str(parser.parse_args().dir).replace("//", "/") + while not directory.find("//") == -1: + directory = str(parser.parse_args().dir).replace("//", "/") preselected = parser.parse_args().preselected excludes = parser.parse_args().exclude language = parser.parse_args().lang diff --git a/src/join_sarif.py b/src/join_sarif.py new file mode 100644 index 0000000..3f2b5d7 --- /dev/null +++ b/src/join_sarif.py @@ -0,0 +1,49 @@ +import argparse +import json +import jsonpickle + +from sarif_om import SarifLog + +def join_sarif(files): + joined = None + for file in files: + with open(file) as sarif_file: + sarif_json = json.load(sarif_file) + + schema_key = "$schema" + del sarif_json[schema_key] + + if joined == None: + joined = SarifLog(**sarif_json) + continue + + to_join = SarifLog(**sarif_json) + for run in to_join.runs: + joined.runs.append(run) + + return joined + +def write_joined_sarif(joined_sarif, output_file): + with open(output_file, "w") as out: + out.write(jsonpickle.encode(joined_sarif)) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "-f", + "--files", + help="Space-separated list of SARIF files to join.", + required=True, + ) + parser.add_argument( + "-o", + "--output", + help="Aggregate the SARIF results to this output file.", + required=True, + ) + + files_to_join = str(parser.parse_args().files).split(" ") + joined_sarif = join_sarif(files_to_join) + + output_file = parser.parse_args().output + write_joined_sarif(joined_sarif, output_file) diff --git a/src/sa_utils.py b/src/sa_utils.py index 25ba8cb..a3b1130 100644 --- a/src/sa_utils.py +++ b/src/sa_utils.py @@ -24,7 +24,7 @@ "!Maximum character count per GitHub comment has been reached!" " Not all warnings/errors has been parsed!" ) -COMMENT_MAX_SIZE = 65000 +COMMENT_MAX_SIZE = 64984 CURRENT_COMMENT_LENGTH = 0 @@ -224,7 +224,7 @@ def is_excluded_dir(line): if not exclude_dir: return False - excluded_dir = f"{WORK_DIR}/{exclude_dir}" + excluded_dir = f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}{exclude_dir}" debug_print( f"{line} and {excluded_dir} with result {line.startswith(excluded_dir)}" ) @@ -245,7 +245,7 @@ def get_file_line_end(file_in, file_line_start_in): or the total number of lines in the file, whichever is smaller. """ - with open(f"{WORK_DIR}/{file_in}", encoding="utf-8") as file: + with open(f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}{file_in}", encoding="utf-8") as file: num_lines = sum(1 for line in file) return min(file_line_start_in + 5, num_lines) @@ -285,40 +285,116 @@ def generate_description( return output_string, description +def generate_desc_from_sarif( + is_note, was_note, file_line_start, issue_description, output_string +): + """Generate description for an issue -def create_or_edit_comment(comment_body): + is_note -- is the current issue a Note: or not + was_note -- was the previous issue a Note: or not + file_line_start -- line to which the issue corresponds + issue_description -- the description from cppcheck/clang-tidy + output_string -- entire description (can be altered if the current/previous issue is/was Note:) """ - Creates or edits a comment on a pull request with the given comment body. + global CURRENT_COMMENT_LENGTH - Args: - - comment_body: A string containing the full comment body to be created or edited. + if not is_note: + description = ( + f"\n```diff\n!Line: {file_line_start} - {issue_description}\n``` \n" + ) + else: + if not was_note: + # Previous line consists of ```diff ```, so remove the closing ``` + # and append the with Note: ...` + + # 12 here means "``` \n
\n"` + num_chars_to_remove = 12 + else: + # Previous line is Note: so it ends with "``` \n" + num_chars_to_remove = 6 + + output_string = output_string[:-num_chars_to_remove] + CURRENT_COMMENT_LENGTH -= num_chars_to_remove + description = f"\n!Line: {file_line_start} - {issue_description}``` \n" + + return output_string, description + + +def generate_output( + is_note, prefix_and_file_path, file_line_start, file_line_end, description +): + """ + Generate a formatted output string based on the details of a code issue. + + This function takes information about a code issue and constructs a string that + includes details such as the location of the issue in the codebase, the affected code + lines, and a description of the issue. If the issue is a note, only the description + is returned. If the issue occurs in a different repository than the target, it + also fetches the lines where the issue was detected. + + Parameters: + - is_note (bool): Whether the issue is just a note or a code issue. + - file_path (str): Path to the file where the issue was detected. + - file_line_start (int): The line number in the file where the issue starts. + - file_line_end (int): The line number in the file where the issue ends. + - description (str): Description of the issue. Returns: - - None. + - str: Formatted string with details of the issue. + + Note: + - This function relies on several global variables like TARGET_REPO_NAME, REPO_NAME, + FILES_WITH_ISSUES, and SHA which should be set before calling this function. """ - github = Github(GITHUB_TOKEN) - repo = github.get_repo(TARGET_REPO_NAME) - pull_request = repo.get_pull(int(PR_NUM)) + # We assume that the file is not empty! + # In case the tool will reffer to line 0 (meaning entire file) + file_line_start = max(1, file_line_start) + file_line_end = max(1, file_line_end) - comments = pull_request.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 not is_note: + prefix, file_path = prefix_and_file_path + if TARGET_REPO_NAME != REPO_NAME: + if file_path not in FILES_WITH_ISSUES: + try: + with open(f"{prefix}{os.sep}{file_path}", encoding="utf-8") as file: + lines = file.readlines() + FILES_WITH_ISSUES[file_path] = lines + except FileNotFoundError: + print(f"Error: The file '{prefix}/{file_path}' was not found.") - if found_id != -1 and comment_to_edit: - comment_to_edit.edit(body=comment_body) + modified_content = FILES_WITH_ISSUES[file_path][ + file_line_start - 1 : file_line_end - 1 + ] + + debug_print( + f"generate_output for following file: \nfile_path={file_path} \nmodified_content={modified_content}\n" + ) + + 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 Issue found in file [{REPO_NAME}/{file_path}]({file_url})\n" + f"```{LANG}\n" + f"{file_content}" + f"\n``` \n" + f"{description}
\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}
\n" + ) else: - pull_request.create_issue_comment(body=comment_body) + new_line = description + return new_line -def generate_output( +def generate_outp_from_sarif( is_note, prefix_and_file_path, file_line_start, file_line_end, description ): """ @@ -355,7 +431,7 @@ def generate_output( if TARGET_REPO_NAME != REPO_NAME: if file_path not in FILES_WITH_ISSUES: try: - with open(f"{prefix}/{file_path}", encoding="utf-8") as file: + with open(f"{prefix}{os.sep}{file_path}", encoding="utf-8") as file: lines = file.readlines() FILES_WITH_ISSUES[file_path] = lines except FileNotFoundError: @@ -393,6 +469,38 @@ def generate_output( return new_line +def create_or_edit_comment(comment_body): + """ + Creates or edits a comment on a pull request with the given comment body. + + Args: + - comment_body: A string containing the full comment body to be created or edited. + + Returns: + - None. + """ + + github = Github(GITHUB_TOKEN) + repo = github.get_repo(TARGET_REPO_NAME) + pull_request = repo.get_pull(int(PR_NUM)) + + comments = pull_request.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 and comment_to_edit: + comment_to_edit.edit(body=comment_body) + else: + pull_request.create_issue_comment(body=comment_body) + + def extract_info(line, prefix): """ Extracts information from a given line containing file path, line number, and issue description. @@ -413,7 +521,7 @@ def extract_info(line, prefix): """ # Clean up line - line = line.replace(prefix, "").lstrip("/") + line = line.replace(prefix, "").lstrip(os.sep) # Get the line starting position /path/to/file:line and trim it file_path_end_idx = line.index(":") diff --git a/src/static_analysis_cpp.py b/src/static_analysis_cpp.py index ff8fa5b..7dc94ff 100644 --- a/src/static_analysis_cpp.py +++ b/src/static_analysis_cpp.py @@ -1,6 +1,9 @@ +import json import os import sys +from sarif_om import SarifLog + from src import sa_utils as utils @@ -14,6 +17,15 @@ def append_issue(is_note, per_issue_string, new_line, list_of_issues): return per_issue_string +def append_sarif_issue(is_note, per_issue_string, new_line, list_of_issues): + if not is_note: + if len(per_issue_string) > 0 and (per_issue_string not in list_of_issues): + list_of_issues.append(per_issue_string) + per_issue_string = new_line + else: + per_issue_string += new_line + + return per_issue_string def create_comment_for_output( tool_output, prefix, files_changed_in_pr, output_to_console @@ -92,6 +104,73 @@ def create_comment_for_output( return output_string, len(list_of_issues) +def create_comment_for_sarif( + sarif_output, files_changed_in_pr, output_to_console +): + """ + Generates a comment for a GitHub pull request based on SARIF output. + + Parameters: + sarif_output (sarif_json_om): The unparsed output to parse. + files_changed_in_pr (dict): A dictionary containing the files that were + changed in the pull request and the lines that were modified. + output_to_console (bool): Whether or not to output the results to the console. + + Returns: + tuple: A tuple containing the generated comment and the number of issues found. + """ + list_of_issues = [] + per_issue_string = "" + was_note = False + + for result in sarif_output.results: + file_path = result.locations[0].physicalLocation.artifactLocation.uri.split(":")[1] + """ + These go into the new functions probably + + rule_id = result.ruleId + file_path = result.locations[0].physicalLocation.artifactLocation.uri.split(":")[1] + level = result.level + violation_line = result.locations[0].physicalLocation.region.startLine, + violation_char = result.locations[0].physicalLocation.region.startColumn, + issue_detail = result.message.text + location_ctx = result.codeFlows[0].threadFlows[0].locations[0].location.message.text + """ + + # In case where we only output to console, skip the next part + if output_to_console: + per_issue_string = append_sarif_issue(result) + continue + + if utils.is_part_of_pr_changes(file_path, result, files_changed_in_pr): + per_issue_string, description = utils.generate_desc_from_sarif( + was_note, + result, + per_issue_string, + ) + was_note = result.level == "note" + new_line = utils.generate_outp_from_sarif( + result, + description + ) + + if utils.check_for_char_limit(new_line): + per_issue_string = append_sarif_issue(result, per_issue_string, new_line, list_of_issues) + utils.CURRENT_COMMENT_LENGTH += len(new_line) + + else: + utils.CURRENT_COMMENT_LENGTH = utils.COMMENT_MAX_SIZE + return "\n".join(list_of_issues), len(list_of_issues) + + # Append any unprocessed issues + if len(per_issue_string) > 0 and (per_issue_string not in list_of_issues): + list_of_issues.append(per_issue_string) + + output_string = "\n".join(list_of_issues) + + utils.debug_print(f"\nFinal output_string = \n{output_string}\n") + + return output_string, len(list_of_issues) def read_files_and_parse_results(): """Reads the output files generated by cppcheck and clang-tidy and creates comments @@ -110,11 +189,17 @@ def read_files_and_parse_results(): - output_to_console (bool): Whether output was generated to the console. """ - # Get cppcheck and clang-tidy files + # Get flawfinder, cppcheck, fbinfer, and clang-tidy files parser = utils.create_common_input_vars_parser() + parser.add_argument( + "-ff", "--flawfinder", help="Output file name for flawfinder", required=True + ) parser.add_argument( "-cc", "--cppcheck", help="Output file name for cppcheck", required=True ) + parser.add_argument( + "-fi", "--fbinfer", help="Output file name for FB Infer", required=True + ) parser.add_argument( "-ct", "--clangtidy", help="Output file name for clang-tidy", required=True ) @@ -123,13 +208,38 @@ def read_files_and_parse_results(): # Make sure to use Head repository utils.REPO_NAME = os.getenv("INPUT_PR_REPO") + flawfinder_file_name = parser.parse_args().flawfinder cppcheck_file_name = parser.parse_args().cppcheck + fbinfer_file_name = parser.parse_args().fbinfer clangtidy_file_name = parser.parse_args().clangtidy output_to_console = parser.parse_args().output_to_console == "true" + flawfinder_content = "" + with open(flawfinder_file_name, "r", encoding="utf-8") as file: + sarif_json = json.load(file) + + schema_key = "$schema" + del sarif_json[schema_key] + + flawfinder_content = SarifLog(**sarif_json).runs[0] + cppcheck_content = "" with open(cppcheck_file_name, "r", encoding="utf-8") as file: - cppcheck_content = file.readlines() + sarif_json = json.load(file) + + schema_key = "$schema" + del sarif_json[schema_key] + + cppcheck_content = SarifLog(**sarif_json).runs[0] + + fbinfer_content = "" + with open(fbinfer_file_name, "r", encoding="utf-8") as file: + sarif_json = json.load(file) + + schema_key = "$schema" + del sarif_json[schema_key] + + fbinfer_content = SarifLog(**sarif_json).runs[0] clang_tidy_content = "" with open(clangtidy_file_name, "r", encoding="utf-8") as file: @@ -139,9 +249,12 @@ def read_files_and_parse_results(): feature_branch = parser.parse_args().head line_prefix = f"{utils.WORK_DIR}" + issue_key = f"{utils.JSON_ISSUE_KEY}" utils.debug_print( + f"flawfinder result: \n {flawfinder_content} \n" f"cppcheck result: \n {cppcheck_content} \n" + f"fbinfer result: \n {fbinfer_content} \n" f"clang-tidy result: \n {clang_tidy_content} \n" f"line_prefix: {line_prefix} \n" ) @@ -150,34 +263,51 @@ def read_files_and_parse_results(): if not output_to_console and (utils.ONLY_PR_CHANGES == "true"): files_changed_in_pr = utils.get_changed_files(common_ancestor, feature_branch) - cppcheck_comment, cppcheck_issues_found = create_comment_for_output( - cppcheck_content, line_prefix, files_changed_in_pr, output_to_console + flawfinder_comment, flawfinder_issues_found = create_comment_for_sarif( + flawfinder_content, files_changed_in_pr, output_to_console + ) + cppcheck_comment, cppcheck_issues_found = create_comment_for_sarif( + cppcheck_content, files_changed_in_pr, output_to_console + ) + fbinfer_comment, fbinfer_issues_found = create_comment_for_sarif( + fbinfer_content, files_changed_in_pr, output_to_console ) clang_tidy_comment, clang_tidy_issues_found = create_comment_for_output( clang_tidy_content, line_prefix, files_changed_in_pr, output_to_console ) - if output_to_console and (cppcheck_issues_found or clang_tidy_issues_found): + if output_to_console and (flawfinder_issues_found or cppcheck_issues_found or fbinfer_issues_found or clang_tidy_issues_found): print("##[error] Issues found!\n") error_color = "\u001b[31m" + if flawfinder_issues_found: + print(f"{error_color}flawfinder results: {flawfinder_comment}") + if cppcheck_issues_found: print(f"{error_color}cppcheck results: {cppcheck_comment}") + if fbinfer_issues_found: + print(f"{error_color}fbinfer results: {fbinfer_comment}") + if clang_tidy_issues_found: print(f"{error_color}clang-tidy results: {clang_tidy_comment}") return ( + flawfinder_comment, cppcheck_comment, + fbinfer_comment, clang_tidy_comment, + flawfinder_issues_found, cppcheck_issues_found, + fbinfer_issues_found, clang_tidy_issues_found, output_to_console, ) def prepare_comment_body( - cppcheck_comment, clang_tidy_comment, cppcheck_issues_found, clang_tidy_issues_found + flawfinder_comment, cppcheck_comment, fbinfer_comment, clang_tidy_comment, + flawfinder_issues_found, cppcheck_issues_found, fbinfer_issues_found, clang_tidy_issues_found ): """ Generates a comment body based on the results of the cppcheck and clang-tidy analysis. @@ -192,7 +322,10 @@ def prepare_comment_body( str: The final comment body that will be posted as a comment on the pull request. """ - if cppcheck_issues_found == 0 and clang_tidy_issues_found == 0: + SEPARATOR = "\n\n\n *** \n" + + if flawfinder_issues_found == 0 and cppcheck_issues_found == 0 and \ + fbinfer_issues_found == 0 and clang_tidy_issues_found == 0: full_comment_body = ( '##

:white_check_mark:' f"{utils.COMMENT_TITLE} - no issues found! :white_check_mark:

" @@ -202,6 +335,16 @@ def prepare_comment_body( f'##

:zap: {utils.COMMENT_TITLE} :zap:

\n\n' ) + if len(flawfinder_comment) > 0: + full_comment_body += ( + f"
:red_circle: flawfinder found " + f"{flawfinder_issues_found} {'issues' if flawfinder_issues_found > 1 else 'issue'}!" + " Click here to see details.
" + f"{flawfinder_comment}
" + ) + + full_comment_body += SEPARATOR + if len(cppcheck_comment) > 0: full_comment_body += ( f"
:red_circle: cppcheck found " @@ -210,7 +353,17 @@ def prepare_comment_body( f"{cppcheck_comment}
" ) - full_comment_body += "\n\n *** \n" + full_comment_body += SEPARATOR + + if len(fbinfer_comment) > 0: + full_comment_body += ( + f"
:red_circle: FB Infer found " + f"{fbinfer_issues_found} {'issues' if fbinfer_issues_found > 1 else 'issue'}!" + " Click here to see details.
" + f"{fbinfer_comment}
" + ) + + full_comment_body += SEPARATOR if len(clang_tidy_comment) > 0: full_comment_body += ( @@ -232,20 +385,28 @@ def prepare_comment_body( if __name__ == "__main__": ( + flawfinder_comment_in, cppcheck_comment_in, + fbinfer_comment_in, clang_tidy_comment_in, + flawfinder_issues_found_in, cppcheck_issues_found_in, + fbinfer_issues_found_in, clang_tidy_issues_found_in, output_to_console_in, ) = read_files_and_parse_results() if not output_to_console_in: comment_body_in = prepare_comment_body( + flawfinder_comment_in, cppcheck_comment_in, + fbinfer_comment_in, clang_tidy_comment_in, + flawfinder_issues_found_in, cppcheck_issues_found_in, + fbinfer_issues_found_in, clang_tidy_issues_found_in, ) utils.create_or_edit_comment(comment_body_in) - sys.exit(cppcheck_issues_found_in + clang_tidy_issues_found_in) + sys.exit(flawfinder_issues_found_in + cppcheck_issues_found_in + fbinfer_issues_found_in + clang_tidy_issues_found_in) diff --git a/src/static_analysis_python.py b/src/static_analysis_python.py index f01f710..fcad463 100644 --- a/src/static_analysis_python.py +++ b/src/static_analysis_python.py @@ -4,7 +4,6 @@ from src import sa_utils as utils - def parse_pylint_json( pylint_json_in, output_to_console, common_ancestor, feature_branch ): diff --git a/test/data/sarif/cc-empty.sarif b/test/data/sarif/cc-empty.sarif new file mode 100644 index 0000000..ea9eaf5 --- /dev/null +++ b/test/data/sarif/cc-empty.sarif @@ -0,0 +1,17 @@ +{ + "$schema": "https:\/\/docs.oasis-open.org\/sarif\/sarif\/v2.1.0\/errata01\/os\/schemas\/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [], + "tool": { + "driver": { + "informationUri": "https:\/\/cppcheck.sourceforge.io", + "name": "Cppcheck", + "rules": [], + "semanticVersion": "2.18" + } + } + } + ], + "version": "2.1.0" +} \ No newline at end of file diff --git a/test/data/sarif/cc-example.sarif b/test/data/sarif/cc-example.sarif new file mode 100644 index 0000000..fe9eeb8 --- /dev/null +++ b/test/data/sarif/cc-example.sarif @@ -0,0 +1,975 @@ +{ + "$schema": "https:\/\/docs.oasis-open.org\/sarif\/sarif\/v2.1.0\/errata01\/os\/schemas\/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPAssertions.h" + }, + "region": { + "endColumn": 24, + "endLine": 78, + "startColumn": 24, + "startLine": 78 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPAssertions.h" + }, + "region": { + "endColumn": 24, + "endLine": 109, + "startColumn": 24, + "startLine": 109 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPAssertions.h" + }, + "region": { + "endColumn": 24, + "endLine": 136, + "startColumn": 24, + "startLine": 136 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPAssertions.h" + }, + "region": { + "endColumn": 24, + "endLine": 163, + "startColumn": 24, + "startLine": 163 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPAssertions.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 92, + "startColumn": 20, + "startLine": 92 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPAssertions.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 107, + "startColumn": 20, + "startLine": 107 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 461, + "startColumn": 20, + "startLine": 461 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 14, + "endLine": 199, + "startColumn": 14, + "startLine": 199 + } + } + } + ], + "message": { + "text": "Technically the member function 'TestCPP::TestCase::clearStdoutCapture' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 467, + "startColumn": 20, + "startLine": 467 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 14, + "endLine": 207, + "startColumn": 14, + "startLine": 207 + } + } + } + ], + "message": { + "text": "Technically the member function 'TestCPP::TestCase::clearLogCapture' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 473, + "startColumn": 20, + "startLine": 473 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 14, + "endLine": 215, + "startColumn": 14, + "startLine": 215 + } + } + } + ], + "message": { + "text": "Technically the member function 'TestCPP::TestCase::clearStderrCapture' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 292, + "startColumn": 20, + "startLine": 292 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 14, + "endLine": 292, + "startColumn": 14, + "startLine": 292 + } + } + } + ], + "message": { + "text": "Technically the member function 'TestCPP::TestCase::logFailure' can be const." + }, + "ruleId": "functionConst" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 15, + "endLine": 171, + "startColumn": 15, + "startLine": 171 + } + } + } + ], + "message": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "ruleId": "throwInNoexceptFunction" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 15, + "endLine": 262, + "startColumn": 15, + "startLine": 262 + } + } + } + ], + "message": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "ruleId": "throwInNoexceptFunction" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 27, + "endLine": 132, + "startColumn": 27, + "startLine": 132 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 39, + "endLine": 114, + "startColumn": 39, + "startLine": 114 + } + } + } + ], + "message": { + "text": "Function 'TestCase' argument 1 names different: declaration 'testName' definition 'name'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 30, + "endLine": 133, + "startColumn": 30, + "startLine": 133 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 42, + "endLine": 115, + "startColumn": 42, + "startLine": 115 + } + } + } + ], + "message": { + "text": "Function 'TestCase' argument 2 names different: declaration 'test' definition 'testFn'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 18, + "endLine": 134, + "startColumn": 18, + "startLine": 134 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 30, + "endLine": 116, + "startColumn": 30, + "startLine": 116 + } + } + } + ], + "message": { + "text": "Function 'TestCase' argument 3 names different: declaration 'testPassedMessage' definition 'msg'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 37, + "endLine": 299, + "startColumn": 37, + "startLine": 299 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 43, + "endLine": 304, + "startColumn": 43, + "startLine": 304 + } + } + } + ], + "message": { + "text": "Function 'logTestFailure' argument 1 names different: declaration 'failureMessage' definition 'reason'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestCase.h" + }, + "region": { + "endColumn": 36, + "endLine": 180, + "startColumn": 36, + "startLine": 180 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 42, + "endLine": 371, + "startColumn": 42, + "startLine": 371 + } + } + } + ], + "message": { + "text": "Function 'setNotifyPassed' argument 1 names different: declaration 'shouldNotify' definition 'notify'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 40, + "endLine": 479, + "startColumn": 40, + "startLine": 479 + } + } + } + ], + "message": { + "text": "Function parameter 'against' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 37, + "endLine": 484, + "startColumn": 37, + "startLine": 484 + } + } + } + ], + "message": { + "text": "Function parameter 'against' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 40, + "endLine": 489, + "startColumn": 40, + "startLine": 489 + } + } + } + ], + "message": { + "text": "Function parameter 'against' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 40, + "endLine": 494, + "startColumn": 40, + "startLine": 494 + } + } + } + ], + "message": { + "text": "Function parameter 'source' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 55, + "endLine": 494, + "startColumn": 55, + "startLine": 494 + } + } + } + ], + "message": { + "text": "Function parameter 'against' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 35, + "endLine": 145, + "startColumn": 35, + "startLine": 145 + } + } + } + ], + "message": { + "text": "Parameter 'o' can be declared as reference to const" + }, + "ruleId": "constParameterReference" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 46, + "endLine": 234, + "startColumn": 46, + "startLine": 234 + } + } + } + ], + "message": { + "text": "Parameter 'rhs' can be declared as reference to const" + }, + "ruleId": "constParameterReference" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestCase.cpp" + }, + "region": { + "endColumn": 53, + "endLine": 292, + "startColumn": 53, + "startLine": 292 + } + } + } + ], + "message": { + "text": "Parameter 'reason' can be declared as reference to const" + }, + "ruleId": "constParameterReference" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestSuite.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'TestSuite::lastRunSucceeded' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestSuite.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'TestSuite::lastRunSuccessCount' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestSuite.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'TestSuite::lastRunFailCount' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestSuite.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'TestSuite::totalRuntime' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestSuite.h" + }, + "region": { + "endColumn": 16, + "endLine": 117, + "startColumn": 16, + "startLine": 117 + } + } + } + ], + "message": { + "text": "Technically the member function 'TestCPP::TestSuite::addTests' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPTestSuite.cpp" + }, + "region": { + "endColumn": 25, + "endLine": 73, + "startColumn": 25, + "startLine": 73 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPTestSuite.h" + }, + "region": { + "endColumn": 18, + "endLine": 153, + "startColumn": 18, + "startLine": 153 + } + } + } + ], + "message": { + "text": "Technically the member function 'TestCPP::TestSuite::getLastRunFailCount' can be const." + }, + "ruleId": "functionConst" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/TestCPPUtil.cpp" + }, + "region": { + "endColumn": 32, + "endLine": 59, + "startColumn": 32, + "startLine": 59 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "include\/internal\/TestCPPUtil.h" + }, + "region": { + "endColumn": 23, + "endLine": 81, + "startColumn": 23, + "startLine": 81 + } + } + } + ], + "message": { + "text": "Technically the member function 'TestCPP::TestObjName::getName' can be const." + }, + "ruleId": "functionConst" + } + ], + "tool": { + "driver": { + "informationUri": "https:\/\/cppcheck.sourceforge.io", + "name": "Cppcheck", + "rules": [ + { + "fullDescription": { + "text": "Parameter 'failureMessage' is passed by value. It could be passed as a const reference which is usually faster and recommended in C++." + }, + "help": { + "text": "Parameter 'failureMessage' is passed by value. It could be passed as a const reference which is usually faster and recommended in C++." + }, + "id": "passedByValue", + "properties": { + "precision": "high" + }, + "shortDescription": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + } + }, + { + "fullDescription": { + "text": "The member function 'TestCPP::TestCase::clearStdoutCapture' can be made a static function. Making a function static can bring a performance benefit since no 'this' instance is passed to the function. This change should not cause compiler errors but it does not necessarily make sense conceptually. Think about your design and the task of the function first - is it a function that must not access members of class instances? And maybe it is more appropriate to move this function to an unnamed namespace." + }, + "help": { + "text": "The member function 'TestCPP::TestCase::clearStdoutCapture' can be made a static function. Making a function static can bring a performance benefit since no 'this' instance is passed to the function. This change should not cause compiler errors but it does not necessarily make sense conceptually. Think about your design and the task of the function first - is it a function that must not access members of class instances? And maybe it is more appropriate to move this function to an unnamed namespace." + }, + "id": "functionStatic", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Technically the member function 'TestCPP::TestCase::clearStdoutCapture' can be static (but you may consider moving to unnamed namespace)." + } + }, + { + "fullDescription": { + "text": "The member function 'TestCPP::TestCase::logFailure' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "help": { + "text": "The member function 'TestCPP::TestCase::logFailure' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "id": "functionConst", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Technically the member function 'TestCPP::TestCase::logFailure' can be const." + } + }, + { + "fullDescription": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "help": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "id": "throwInNoexceptFunction", + "properties": { + "precision": "high", + "security-severity": 9.9000000000000004, + "tags": [ + "security" + ] + }, + "shortDescription": { + "text": "Exception thrown in function declared not to throw exceptions." + } + }, + { + "fullDescription": { + "text": "Function 'TestCase' argument 1 names different: declaration 'testName' definition 'name'." + }, + "help": { + "text": "Function 'TestCase' argument 1 names different: declaration 'testName' definition 'name'." + }, + "id": "funcArgNamesDifferent", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Function 'TestCase' argument 1 names different: declaration 'testName' definition 'name'." + } + }, + { + "fullDescription": { + "text": "Parameter 'o' can be declared as reference to const" + }, + "help": { + "text": "Parameter 'o' can be declared as reference to const" + }, + "id": "constParameterReference", + "properties": { + "precision": "high" + }, + "shortDescription": { + "text": "Parameter 'o' can be declared as reference to const" + } + }, + { + "fullDescription": { + "text": "Member variable 'TestSuite::lastRunSucceeded' is not initialized in the constructor. Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior." + }, + "help": { + "text": "Member variable 'TestSuite::lastRunSucceeded' is not initialized in the constructor. Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior." + }, + "id": "uninitMemberVar", + "properties": { + "precision": "high" + }, + "shortDescription": { + "text": "Member variable 'TestSuite::lastRunSucceeded' is not initialized in the constructor." + } + } + ], + "semanticVersion": "2.18" + } + } + } + ], + "version": "2.1.0" +} \ No newline at end of file diff --git a/test/data/sarif/ff-empty.sarif b/test/data/sarif/ff-empty.sarif new file mode 100644 index 0000000..739907a --- /dev/null +++ b/test/data/sarif/ff-empty.sarif @@ -0,0 +1,34 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Flawfinder", + "version": "2.0.19", + "informationUri": "https://dwheeler.com/flawfinder/", + "rules": [], + "supportedTaxonomies": [ + { + "name": "CWE", + "guid": "FFC64C90-42B6-44CE-8BEB-F6B7DAE649E5" + } + ] + } + }, + "columnKind": "utf16CodeUnits", + "results": [], + "externalPropertyFileReferences": { + "taxonomies": [ + { + "location": { + "uri": "https://raw.githubusercontent.com/sarif-standard/taxonomies/main/CWE_v4.4.sarif" + }, + "guid": "FFC64C90-42B6-44CE-8BEB-F6B7DAE649E5" + } + ] + } + } + ] +} diff --git a/test/data/sarif/ff-example.sarif b/test/data/sarif/ff-example.sarif new file mode 100644 index 0000000..b55b527 --- /dev/null +++ b/test/data/sarif/ff-example.sarif @@ -0,0 +1,102 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Flawfinder", + "version": "2.0.19", + "informationUri": "https://dwheeler.com/flawfinder/", + "rules": [ + { + "id": "FF1013", + "name": "buffer/char", + "shortDescription": { + "text": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)." + }, + "defaultConfiguration": { + "level": "note" + }, + "helpUri": "https://cwe.mitre.org/data/definitions/119.html", + "relationships": [ + { + "target": { + "id": "CWE-119", + "toolComponent": { + "name": "CWE", + "guid": "FFC64C90-42B6-44CE-8BEB-F6B7DAE649E5" + } + }, + "kinds": [ + "incomparable" + ] + }, + { + "target": { + "id": "CWE-120", + "toolComponent": { + "name": "CWE", + "guid": "FFC64C90-42B6-44CE-8BEB-F6B7DAE649E5" + } + }, + "kinds": [ + "relevant" + ] + } + ] + } + ], + "supportedTaxonomies": [ + { + "name": "CWE", + "guid": "FFC64C90-42B6-44CE-8BEB-F6B7DAE649E5" + } + ] + } + }, + "columnKind": "utf16CodeUnits", + "results": [ + { + "ruleId": "FF1013", + "level": "note", + "message": { + "text": "buffer/char:Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "include/internal/TestCPPUtil.h", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 107, + "startColumn": 33, + "endColumn": 54, + "snippet": { + "text": " alignas(T) unsigned char data[sizeof(T)];" + } + } + } + } + ], + "fingerprints": { + "contextHash/v1": "ef5b18490d0e1f5791c730ded175b6ec23adf7ef7dcba962fd7e0ef5bc5e920a" + }, + "rank": 0.4 + } + ], + "externalPropertyFileReferences": { + "taxonomies": [ + { + "location": { + "uri": "https://raw.githubusercontent.com/sarif-standard/taxonomies/main/CWE_v4.4.sarif" + }, + "guid": "FFC64C90-42B6-44CE-8BEB-F6B7DAE649E5" + } + ] + } + } + ] +} diff --git a/test/data/sarif/fi-empty.sarif b/test/data/sarif/fi-empty.sarif new file mode 100644 index 0000000..1dd9f4d --- /dev/null +++ b/test/data/sarif/fi-empty.sarif @@ -0,0 +1,17 @@ +{ + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Infer", + "informationUri": "https://github.com/facebook/infer", + "version": "1.2.0", + "rules": [] + } + }, + "results": [] + } + ] +} \ No newline at end of file diff --git a/test/data/sarif/fi-example.sarif b/test/data/sarif/fi-example.sarif new file mode 100644 index 0000000..ce0beb8 --- /dev/null +++ b/test/data/sarif/fi-example.sarif @@ -0,0 +1,888 @@ +{ + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Infer", + "informationUri": "https://github.com/facebook/infer", + "version": "1.2.0", + "rules": [ + { + "id": "PULSE_UNNECESSARY_COPY_ASSIGNMENT_MOVABLE", + "shortDescription": { + "text": "Unnecessary Copy Assignment Movable" + }, + "helpUri": "https://fbinfer.com/docs/next/all-issue-types#pulse_unnecessary_copy_assignment_movable" + }, + { + "id": "PULSE_CONST_REFABLE", + "shortDescription": { + "text": "Const Refable Parameter" + }, + "helpUri": "https://fbinfer.com/docs/next/all-issue-types#pulse_const_refable" + }, + { + "id": "USE_AFTER_DELETE", + "shortDescription": { + "text": "Use After Delete" + }, + "helpUri": "https://fbinfer.com/docs/next/all-issue-types#use_after_delete" + }, + { + "id": "PULSE_UNNECESSARY_COPY_ASSIGNMENT", + "shortDescription": { + "text": "Unnecessary Copy Assignment" + }, + "helpUri": "https://fbinfer.com/docs/next/all-issue-types#pulse_unnecessary_copy_assignment" + } + ] + } + }, + "results": [ + { + "message": { + "text": "Function parameter `func` is passed by-value but not modified inside this function, resulting in a potential unnecessary copy at the function's callsites. Change the type of the parameter to `const &`." + }, + "level": "error", + "ruleId": "PULSE_CONST_REFABLE", + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "nestingLevel": 0, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:include/internal/TestCPPTestCase.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/include/internal/TestCPPTestCase.h" + }, + "region": { + "startLine": 345, + "startColumn": 9 + } + }, + "message": { + "text": "Parameter func with type `std::function<_fn_>`" + } + } + } + ] + } + ] + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:include/internal/TestCPPTestCase.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/include/internal/TestCPPTestCase.h" + }, + "region": { + "startLine": 345, + "startColumn": 9 + } + } + } + ], + "fingerprints": { + "hash/v1": "ce82b7ba7661167341bb2251cf82d2ae", + "key": "TestCPPTestCase.h|duration_>|PULSE_CONST_REFABLE" + } + }, + { + "message": { + "text": "Function parameter `shouldNotThrow` is passed by-value but not modified inside this function, resulting in a potential unnecessary copy at the function's callsites. Change the type of the parameter to `const &`." + }, + "level": "error", + "ruleId": "PULSE_CONST_REFABLE", + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "nestingLevel": 0, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPAssertions.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPAssertions.cpp" + }, + "region": { + "startLine": 136, + "startColumn": 5 + } + }, + "message": { + "text": "Parameter shouldNotThrow with type `std::function<_fn_>`" + } + } + } + ] + } + ] + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPAssertions.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPAssertions.cpp" + }, + "region": { + "startLine": 136, + "startColumn": 5 + } + } + } + ], + "fingerprints": { + "hash/v1": "eaa17da93f34d6f8ed47c66db7882399", + "key": "TestCPPAssertions.cpp|assertNoThrows|PULSE_CONST_REFABLE" + } + }, + { + "message": { + "text": "`testFn` is copy assigned into field `test` but is not modified afterwards. Rather than copying into the field, move into it instead." + }, + "level": "error", + "ruleId": "PULSE_UNNECESSARY_COPY_ASSIGNMENT", + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "nestingLevel": 0, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 122, + "startColumn": 9 + } + }, + "message": { + "text": "copy assigned here (with type `std::function<_fn_>&`)" + } + } + } + ] + } + ] + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 122, + "startColumn": 9 + } + } + } + ], + "fingerprints": { + "hash/v1": "b787e827e8dd0832f56f127b2fbd8cde", + "key": "TestCPPTestCase.cpp|TestCase|PULSE_UNNECESSARY_COPY_ASSIGNMENT" + } + }, + { + "message": { + "text": "`name` is copy assigned into field `testName` but is not modified afterwards. Rather than copying into the field, move into it instead." + }, + "level": "error", + "ruleId": "PULSE_UNNECESSARY_COPY_ASSIGNMENT_MOVABLE", + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "nestingLevel": 0, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 124, + "startColumn": 9 + } + }, + "message": { + "text": "copy assigned here (with type `TestCPP::TestObjName&&`)" + } + } + } + ] + } + ] + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 124, + "startColumn": 9 + } + } + } + ], + "fingerprints": { + "hash/v1": "52f9f9e4330352c605e3a05044586b78", + "key": "TestCPPTestCase.cpp|TestCase|PULSE_UNNECESSARY_COPY_ASSIGNMENT_MOVABLE" + } + }, + { + "message": { + "text": "call to `std::unique_ptr::operator=()` eventually accesses `TestCPP::Util::no_destroy::get().__infer_backing_pointer` that was invalidated by `delete` on line 203." + }, + "level": "error", + "ruleId": "USE_AFTER_DELETE", + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "nestingLevel": 0, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "invalidation part of the trace starts here" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "global variable `TestCPP::TestCase::stdoutBuffer` accessed here" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "in call to `TestCPP::Util::no_destroy,std::allocator>,_fn_(*)>>::get`" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:include/internal/TestCPPUtil.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/include/internal/TestCPPUtil.h" + }, + "region": { + "startLine": 111, + "startColumn": 13 + } + }, + "message": { + "text": "parameter `this` of TestCPP::Util::no_destroy,std::allocator>,_fn_(*)>>::get" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:include/internal/TestCPPUtil.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/include/internal/TestCPPUtil.h" + }, + "region": { + "startLine": 111, + "startColumn": 24 + } + }, + "message": { + "text": "returned" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "return from call to `TestCPP::Util::no_destroy,std::allocator>,_fn_(*)>>::get`" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "in call to `std::operator!=<533d66becc097987>`" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:/usr/include/c++/13/bits/unique_ptr.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/usr/include/c++/13/bits/unique_ptr.h" + }, + "region": { + "startLine": 860, + "startColumn": 5 + } + }, + "message": { + "text": "parameter `__x` of std::operator!=<533d66becc097987>" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:/usr/include/c++/13/bits/unique_ptr.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/usr/include/c++/13/bits/unique_ptr.h" + }, + "region": { + "startLine": 862, + "startColumn": 20 + } + }, + "message": { + "text": "in call to `std::unique_ptr::operator_bool()` (modelled)" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:/usr/include/c++/13/bits/unique_ptr.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/usr/include/c++/13/bits/unique_ptr.h" + }, + "region": { + "startLine": 862, + "startColumn": 7 + } + }, + "message": { + "text": "returned" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "return from call to `std::operator!=<533d66becc097987>`" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "taking \"then\" branch" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 199, + "startColumn": 17 + } + }, + "message": { + "text": "global variable `TestCPP::TestCase::stdoutCaptureCasesDestroyed` accessed here" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 199, + "startColumn": 17 + } + }, + "message": { + "text": "in call to `std::atomic::operator_T()` (modelled)" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 200, + "startColumn": 17 + } + }, + "message": { + "text": "global variable `TestCPP::TestCase::stdoutCaptureCasesConstructed` accessed here" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 200, + "startColumn": 17 + } + }, + "message": { + "text": "in call to `std::atomic::operator_T()` (modelled)" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 199, + "startColumn": 17 + } + }, + "message": { + "text": "is assigned to the constant 1" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 199, + "startColumn": 17 + } + }, + "message": { + "text": "taking \"then\" branch" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 203, + "startColumn": 24 + } + }, + "message": { + "text": "in call to `std::unique_ptr::get()` (modelled)" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 203, + "startColumn": 17 + } + }, + "message": { + "text": "was invalidated by `delete`" + } + } + }, + { + "nestingLevel": 0, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "use-after-lifetime part of the trace starts here" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "global variable `TestCPP::TestCase::stdoutBuffer` accessed here" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "in call to `TestCPP::Util::no_destroy,std::allocator>,_fn_(*)>>::get`" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:include/internal/TestCPPUtil.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/include/internal/TestCPPUtil.h" + }, + "region": { + "startLine": 111, + "startColumn": 13 + } + }, + "message": { + "text": "parameter `this` of TestCPP::Util::no_destroy,std::allocator>,_fn_(*)>>::get" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:include/internal/TestCPPUtil.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/include/internal/TestCPPUtil.h" + }, + "region": { + "startLine": 111, + "startColumn": 24 + } + }, + "message": { + "text": "returned" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "return from call to `TestCPP::Util::no_destroy,std::allocator>,_fn_(*)>>::get`" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "in call to `std::operator!=<533d66becc097987>`" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:/usr/include/c++/13/bits/unique_ptr.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/usr/include/c++/13/bits/unique_ptr.h" + }, + "region": { + "startLine": 860, + "startColumn": 5 + } + }, + "message": { + "text": "parameter `__x` of std::operator!=<533d66becc097987>" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 197, + "startColumn": 13 + } + }, + "message": { + "text": "return from call to `std::operator!=<533d66becc097987>`" + } + } + }, + { + "nestingLevel": 1, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 204, + "startColumn": 17 + } + }, + "message": { + "text": "when calling `std::unique_ptr,std::allocator>,_fn_(*)>::operator=` here" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:/usr/include/c++/13/bits/unique_ptr.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/usr/include/c++/13/bits/unique_ptr.h" + }, + "region": { + "startLine": 439, + "startColumn": 7 + } + }, + "message": { + "text": "parameter `this` of std::unique_ptr,std::allocator>,_fn_(*)>::operator=" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:/usr/include/c++/13/bits/unique_ptr.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/usr/include/c++/13/bits/unique_ptr.h" + }, + "region": { + "startLine": 442, + "startColumn": 2 + } + }, + "message": { + "text": "in call to `std::unique_ptr::reset(T*)` (modelled)" + } + } + }, + { + "nestingLevel": 2, + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:/usr/include/c++/13/bits/unique_ptr.h", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/usr/include/c++/13/bits/unique_ptr.h" + }, + "region": { + "startLine": 442, + "startColumn": 2 + } + }, + "message": { + "text": "invalid access occurs here" + } + } + } + ] + } + ] + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:src/TestCPPTestCase.cpp", + "uriBaseId": "/mnt/c/Users/jonat/Code/TestCPP/src/TestCPPTestCase.cpp" + }, + "region": { + "startLine": 204, + "startColumn": 17 + } + } + } + ], + "fingerprints": { + "hash/v1": "7a09e3db5fda863aff928a8b56e14543", + "key": "TestCPPTestCase.cpp|__infer_inner_destructor_~TestCase|USE_AFTER_DELETE" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type1.cpp.sarif b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type1.cpp.sarif new file mode 100644 index 0000000..1be9997 --- /dev/null +++ b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type1.cpp.sarif @@ -0,0 +1,167 @@ +{ + "$schema": "https:\/\/docs.oasis-open.org\/sarif\/sarif\/v2.1.0\/errata01\/os\/schemas\/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type1.h" + }, + "region": { + "endColumn": 24, + "endLine": 78, + "startColumn": 24, + "startLine": 78 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type1.h" + }, + "region": { + "endColumn": 24, + "endLine": 109, + "startColumn": 24, + "startLine": 109 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type1.h" + }, + "region": { + "endColumn": 24, + "endLine": 136, + "startColumn": 24, + "startLine": 136 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type1.h" + }, + "region": { + "endColumn": 24, + "endLine": 163, + "startColumn": 24, + "startLine": 163 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type1.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 92, + "startColumn": 20, + "startLine": 92 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type1.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 107, + "startColumn": 20, + "startLine": 107 + } + } + } + ], + "message": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + }, + "ruleId": "passedByValue" + } + ], + "tool": { + "driver": { + "informationUri": "https:\/\/cppcheck.sourceforge.io", + "name": "Cppcheck", + "rules": [ + { + "fullDescription": { + "text": "Parameter 'failureMessage' is passed by value. It could be passed as a const reference which is usually faster and recommended in C++." + }, + "help": { + "text": "Parameter 'failureMessage' is passed by value. It could be passed as a const reference which is usually faster and recommended in C++." + }, + "id": "passedByValue", + "properties": { + "precision": "high" + }, + "shortDescription": { + "text": "Function parameter 'failureMessage' should be passed by const reference." + } + } + ], + "semanticVersion": "2.18" + } + } + } + ], + "version": "2.1.0" +} + diff --git a/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type2.cpp.sarif b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type2.cpp.sarif new file mode 100644 index 0000000..684f828 --- /dev/null +++ b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type2.cpp.sarif @@ -0,0 +1,18 @@ +{ + "$schema": "https:\/\/docs.oasis-open.org\/sarif\/sarif\/v2.1.0\/errata01\/os\/schemas\/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [], + "tool": { + "driver": { + "informationUri": "https:\/\/cppcheck.sourceforge.io", + "name": "Cppcheck", + "rules": [], + "semanticVersion": "2.18" + } + } + } + ], + "version": "2.1.0" +} + diff --git a/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type3.cpp.sarif b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type3.cpp.sarif new file mode 100644 index 0000000..90274d4 --- /dev/null +++ b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type3.cpp.sarif @@ -0,0 +1,649 @@ +{ + "$schema": "https:\/\/docs.oasis-open.org\/sarif\/sarif\/v2.1.0\/errata01\/os\/schemas\/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 461, + "startColumn": 20, + "startLine": 461 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 14, + "endLine": 199, + "startColumn": 14, + "startLine": 199 + } + } + } + ], + "message": { + "text": "Technically the member function 'project::Type3::fn1' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 467, + "startColumn": 20, + "startLine": 467 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 14, + "endLine": 207, + "startColumn": 14, + "startLine": 207 + } + } + } + ], + "message": { + "text": "Technically the member function 'project::Type3::fn2' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 473, + "startColumn": 20, + "startLine": 473 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 14, + "endLine": 215, + "startColumn": 14, + "startLine": 215 + } + } + } + ], + "message": { + "text": "Technically the member function 'project::Type3::fn3' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 20, + "endLine": 292, + "startColumn": 20, + "startLine": 292 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 14, + "endLine": 292, + "startColumn": 14, + "startLine": 292 + } + } + } + ], + "message": { + "text": "Technically the member function 'project::Type3::fn4' can be const." + }, + "ruleId": "functionConst" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 15, + "endLine": 171, + "startColumn": 15, + "startLine": 171 + } + } + } + ], + "message": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "ruleId": "throwInNoexceptFunction" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 15, + "endLine": 262, + "startColumn": 15, + "startLine": 262 + } + } + } + ], + "message": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "ruleId": "throwInNoexceptFunction" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 27, + "endLine": 132, + "startColumn": 27, + "startLine": 132 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 39, + "endLine": 114, + "startColumn": 39, + "startLine": 114 + } + } + } + ], + "message": { + "text": "Function 'Type3' argument 1 names different: declaration 'arg1decl' definition 'arg1def'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 30, + "endLine": 133, + "startColumn": 30, + "startLine": 133 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 42, + "endLine": 115, + "startColumn": 42, + "startLine": 115 + } + } + } + ], + "message": { + "text": "Function 'Type3' argument 2 names different: declaration 'arg2decl' definition 'arg2def'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 18, + "endLine": 134, + "startColumn": 18, + "startLine": 134 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 30, + "endLine": 116, + "startColumn": 30, + "startLine": 116 + } + } + } + ], + "message": { + "text": "Function 'Type3' argument 3 names different: declaration 'arg3decl' definition 'arg3def'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 37, + "endLine": 299, + "startColumn": 37, + "startLine": 299 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 43, + "endLine": 304, + "startColumn": 43, + "startLine": 304 + } + } + } + ], + "message": { + "text": "Function 'fn5' argument 1 names different: declaration 'fn5arg1decl' definition 'fn5arg1def'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type3.h" + }, + "region": { + "endColumn": 36, + "endLine": 180, + "startColumn": 36, + "startLine": 180 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 42, + "endLine": 371, + "startColumn": 42, + "startLine": 371 + } + } + } + ], + "message": { + "text": "Function 'fn6' argument 1 names different: declaration 'fn6arg1decl' definition 'fn6arg1def'." + }, + "ruleId": "funcArgNamesDifferent" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 40, + "endLine": 479, + "startColumn": 40, + "startLine": 479 + } + } + } + ], + "message": { + "text": "Function parameter 'fnArg1NonConst' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 37, + "endLine": 484, + "startColumn": 37, + "startLine": 484 + } + } + } + ], + "message": { + "text": "Function parameter 'fnArg1NonConst' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 40, + "endLine": 489, + "startColumn": 40, + "startLine": 489 + } + } + } + ], + "message": { + "text": "Function parameter 'fnArg1NonConst' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 40, + "endLine": 494, + "startColumn": 40, + "startLine": 494 + } + } + } + ], + "message": { + "text": "Function parameter 'fnArg2NonConst' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 55, + "endLine": 494, + "startColumn": 55, + "startLine": 494 + } + } + } + ], + "message": { + "text": "Function parameter 'fnArg1NonConst' should be passed by const reference." + }, + "ruleId": "passedByValue" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 35, + "endLine": 145, + "startColumn": 35, + "startLine": 145 + } + } + } + ], + "message": { + "text": "Parameter 'fnArg3NonConst' can be declared as reference to const" + }, + "ruleId": "constParameterReference" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 46, + "endLine": 234, + "startColumn": 46, + "startLine": 234 + } + } + } + ], + "message": { + "text": "Parameter 'rhs' can be declared as reference to const" + }, + "ruleId": "constParameterReference" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type3.cpp" + }, + "region": { + "endColumn": 53, + "endLine": 292, + "startColumn": 53, + "startLine": 292 + } + } + } + ], + "message": { + "text": "Parameter 'fn5arg1def' can be declared as reference to const" + }, + "ruleId": "constParameterReference" + } + ], + "tool": { + "driver": { + "informationUri": "https:\/\/cppcheck.sourceforge.io", + "name": "Cppcheck", + "rules": [ + { + "fullDescription": { + "text": "The member function 'project::Type3::fn1' can be made a static function. Making a function static can bring a performance benefit since no 'this' instance is passed to the function. This change should not cause compiler errors but it does not necessarily make sense conceptually. Think about your design and the task of the function first - is it a function that must not access members of class instances? And maybe it is more appropriate to move this function to an unnamed namespace." + }, + "help": { + "text": "The member function 'project::Type3::fn1' can be made a static function. Making a function static can bring a performance benefit since no 'this' instance is passed to the function. This change should not cause compiler errors but it does not necessarily make sense conceptually. Think about your design and the task of the function first - is it a function that must not access members of class instances? And maybe it is more appropriate to move this function to an unnamed namespace." + }, + "id": "functionStatic", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Technically the member function 'project::Type3::fn1' can be static (but you may consider moving to unnamed namespace)." + } + }, + { + "fullDescription": { + "text": "The member function 'project::Type3::fn4' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "help": { + "text": "The member function 'project::Type3::fn4' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "id": "functionConst", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Technically the member function 'project::Type3::fn4' can be const." + } + }, + { + "fullDescription": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "help": { + "text": "Exception thrown in function declared not to throw exceptions." + }, + "id": "throwInNoexceptFunction", + "properties": { + "precision": "high", + "security-severity": 9.9000000000000004, + "tags": [ + "security" + ] + }, + "shortDescription": { + "text": "Exception thrown in function declared not to throw exceptions." + } + }, + { + "fullDescription": { + "text": "Function 'Type3' argument 1 names different: declaration 'arg1decl' definition 'arg1def'." + }, + "help": { + "text": "Function 'Type3' argument 1 names different: declaration 'arg1decl' definition 'arg1def'." + }, + "id": "funcArgNamesDifferent", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Function 'Type3' argument 1 names different: declaration 'arg1decl' definition 'arg1def'." + } + }, + { + "fullDescription": { + "text": "Parameter 'fnArg1NonConst' is passed by value. It could be passed as a const reference which is usually faster and recommended in C++." + }, + "help": { + "text": "Parameter 'fnArg1NonConst' is passed by value. It could be passed as a const reference which is usually faster and recommended in C++." + }, + "id": "passedByValue", + "properties": { + "precision": "high" + }, + "shortDescription": { + "text": "Function parameter 'fnArg1NonConst' should be passed by const reference." + } + }, + { + "fullDescription": { + "text": "Parameter 'fnArg3NonConst' can be declared as reference to const" + }, + "help": { + "text": "Parameter 'fnArg3NonConst' can be declared as reference to const" + }, + "id": "constParameterReference", + "properties": { + "precision": "high" + }, + "shortDescription": { + "text": "Parameter 'fnArg3NonConst' can be declared as reference to const" + } + } + ], + "semanticVersion": "2.18" + } + } + } + ], + "version": "2.1.0" +} + diff --git a/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type4.cpp.sarif b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type4.cpp.sarif new file mode 100644 index 0000000..70d1a7f --- /dev/null +++ b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type4.cpp.sarif @@ -0,0 +1,210 @@ +{ + "$schema": "https:\/\/docs.oasis-open.org\/sarif\/sarif\/v2.1.0\/errata01\/os\/schemas\/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type4.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'Type4::member1' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type4.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'Type4::member2' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type4.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'Type4::member3' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type4.h" + }, + "region": { + "endColumn": 9, + "endLine": 80, + "startColumn": 9, + "startLine": 80 + } + } + } + ], + "message": { + "text": "Member variable 'Type4::member4' is not initialized in the constructor." + }, + "ruleId": "uninitMemberVar" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type4.h" + }, + "region": { + "endColumn": 16, + "endLine": 117, + "startColumn": 16, + "startLine": 117 + } + } + } + ], + "message": { + "text": "Technically the member function 'project::Type4::fn1' can be static (but you may consider moving to unnamed namespace)." + }, + "ruleId": "functionStatic" + }, + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type4.cpp" + }, + "region": { + "endColumn": 25, + "endLine": 73, + "startColumn": 25, + "startLine": 73 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type4.h" + }, + "region": { + "endColumn": 18, + "endLine": 153, + "startColumn": 18, + "startLine": 153 + } + } + } + ], + "message": { + "text": "Technically the member function 'project::Type4::fn2' can be const." + }, + "ruleId": "functionConst" + } + ], + "tool": { + "driver": { + "informationUri": "https:\/\/cppcheck.sourceforge.io", + "name": "Cppcheck", + "rules": [ + { + "fullDescription": { + "text": "Member variable 'Type4::member1' is not initialized in the constructor. Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior." + }, + "help": { + "text": "Member variable 'Type4::member1' is not initialized in the constructor. Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior." + }, + "id": "uninitMemberVar", + "properties": { + "precision": "high" + }, + "shortDescription": { + "text": "Member variable 'Type4::member1' is not initialized in the constructor." + } + }, + { + "fullDescription": { + "text": "The member function 'project::Type4::fn1' can be made a static function. Making a function static can bring a performance benefit since no 'this' instance is passed to the function. This change should not cause compiler errors but it does not necessarily make sense conceptually. Think about your design and the task of the function first - is it a function that must not access members of class instances? And maybe it is more appropriate to move this function to an unnamed namespace." + }, + "help": { + "text": "The member function 'project::Type4::fn1' can be made a static function. Making a function static can bring a performance benefit since no 'this' instance is passed to the function. This change should not cause compiler errors but it does not necessarily make sense conceptually. Think about your design and the task of the function first - is it a function that must not access members of class instances? And maybe it is more appropriate to move this function to an unnamed namespace." + }, + "id": "functionStatic", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Technically the member function 'project::Type4::fn1' can be static (but you may consider moving to unnamed namespace)." + } + }, + { + "fullDescription": { + "text": "The member function 'project::Type4::fn2' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "help": { + "text": "The member function 'project::Type4::fn2' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "id": "functionConst", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Technically the member function 'project::Type4::fn2' can be const." + } + } + ], + "semanticVersion": "2.18" + } + } + } + ], + "version": "2.1.0" +} + diff --git a/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type5.cpp.sarif b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type5.cpp.sarif new file mode 100644 index 0000000..317086f --- /dev/null +++ b/test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type5.cpp.sarif @@ -0,0 +1,70 @@ +{ + "$schema": "https:\/\/docs.oasis-open.org\/sarif\/sarif\/v2.1.0\/errata01\/os\/schemas\/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/src\/Type5.cpp" + }, + "region": { + "endColumn": 32, + "endLine": 59, + "startColumn": 32, + "startLine": 59 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "\/mnt\/c\/code\/project\/include\/internal\/Type5.h" + }, + "region": { + "endColumn": 23, + "endLine": 81, + "startColumn": 23, + "startLine": 81 + } + } + } + ], + "message": { + "text": "Technically the member function 'project::Type6::fn1' can be const." + }, + "ruleId": "functionConst" + } + ], + "tool": { + "driver": { + "informationUri": "https:\/\/cppcheck.sourceforge.io", + "name": "Cppcheck", + "rules": [ + { + "fullDescription": { + "text": "The member function 'project::Type6::fn1' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "help": { + "text": "The member function 'project::Type6::fn1' can be made a const function. Making this function 'const' should not cause compiler errors. Even though the function can be made const function technically it may not make sense conceptually. Think about your design and the task of the function first - is it a function that must not change object internal state?" + }, + "id": "functionConst", + "properties": { + "precision": "medium" + }, + "shortDescription": { + "text": "Technically the member function 'project::Type6::fn1' can be const." + } + } + ], + "semanticVersion": "2.18" + } + } + } + ], + "version": "2.1.0" +} + diff --git a/test/test_get_files_to_check.py b/test/test_get_files_to_check.py new file mode 100644 index 0000000..17a626c --- /dev/null +++ b/test/test_get_files_to_check.py @@ -0,0 +1,84 @@ +import os +import sys +import unittest + +try: + PROJECT_PATH = f"{os.sep}".join(os.path.abspath(__file__).split(os.sep)[:-2]) + sys.path.append(PROJECT_PATH) + +except Exception as exception: + print(f"Can not add project path to system path! Exiting!\nERROR: {exception}") + raise SystemExit(1) from exception + +from src import get_files_to_check as gftc +from utils import helper_functions as util + +class TestGetFilesToCheck(unittest.TestCase): + """Unit tests for get_files_to_check module""" + + def test_get_files_to_check(self): + """ + Test the `get_files_to_check()` function. + + This test case checks whether the `get_files_to_check()` function correctly generates a + list of file paths to check for static analysis issues in a given directory, excluding + any directories that should be skipped. + + The test case creates a mock directory structure and a set of directories to skip, + and expects the generated list of file paths to match a pre-defined expected list of + file paths. + """ + + pwd = os.path.dirname(os.path.realpath(__file__)) + + # Excludes == None + expected = [ + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.cpp", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.hpp", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_1{os.sep}ExcludedFile1.hpp", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_2{os.sep}ExcludedFile2.hpp", + ] + result = gftc.get_files_to_check( + f"{pwd}{os.sep}utils{os.sep}dummy_project", None, "", "c++" + ) + + self.assertEqual(util.to_list_and_sort(result), expected) + + # Single exclude_dir + expected = [ + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.cpp", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.hpp", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_2{os.sep}ExcludedFile2.hpp", + ] + result = gftc.get_files_to_check( + f"{pwd}{os.sep}utils{os.sep}dummy_project", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_1", + "", + "c++", + ) + + self.assertEqual(util.to_list_and_sort(result), expected) + + # Multiple exclude_dir + expected = [ + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.cpp", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.hpp", + ] + result = gftc.get_files_to_check( + f"{pwd}{os.sep}utils{os.sep}dummy_project", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_1 {pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_2", + "", + "c++", + ) + + # Preselected files present + expected = [f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.cpp"] + result = gftc.get_files_to_check( + f"{pwd}{os.sep}utils{os.sep}dummy_project", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_1 {pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_2", + f"{pwd}{os.sep}utils{os.sep}dummy_project{os.sep}DummyFile.cpp {pwd}{os.sep}utils{os.sep}dummy_project{os.sep}exclude_dir_1{os.sep}ExcludedFile1.hpp", + "c++", + ) + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_join_sarif.py b/test/test_join_sarif.py new file mode 100644 index 0000000..5bf1462 --- /dev/null +++ b/test/test_join_sarif.py @@ -0,0 +1,53 @@ +import filecmp +import itertools +import json +import jsonpickle +import os +import sys +import tempfile +import unittest + +try: + PROJECT_PATH = f"{os.sep}".join(os.path.abspath(__file__).split(os.sep)[:-2]) + sys.path.append(PROJECT_PATH) + +except Exception as exception: + print(f"Can not add project path to system path! Exiting!\nERROR: {exception}") + raise SystemExit(1) from exception + +from pathlib import Path +from src import join_sarif as sarops +from utils import helper_functions as utils + +class TestJoinSarif(unittest.TestCase): + """Unit tests for join_sarif module""" + + @classmethod + def setUpClass(cls): + dir = f"{f"{os.sep}".join(os.path.abspath(__file__).split(os.sep)[:-1])}{os.sep}data{os.sep}sarif{os.sep}join" + cls.to_join_tjs_js, cls.to_join_tjs_gh, cls.to_join_twjs_jsw, cls.to_join_twjs_jsjd = itertools.tee( + Path(dir).rglob("cppcheck__mnt_c_code_project_src_Type*.cpp.sarif"), + 4 + ) + + def test_join_sarif(self): + joined = sarops.join_sarif(self.to_join_tjs_js) + hashes = utils.genhashes(self.to_join_tjs_gh) + + for run in joined.runs: + run_hash = hash(json.dumps(run, sort_keys=True, ensure_ascii=True, default=lambda v: repr(v) + str(hash(v)))) + self.assertIn(run_hash, hashes) + + def test_write_joined_sarif(self): + with tempfile.NamedTemporaryFile("w", delete_on_close=False) as written_file: + written_file.close() + sarops.write_joined_sarif(sarops.join_sarif(self.to_join_twjs_jsw), written_file.name) + + with tempfile.NamedTemporaryFile("w", delete_on_close=False) as joined_file: + joined_file.close() + json_sarif = jsonpickle.encode(sarops.join_sarif(self.to_join_twjs_jsjd)) + open(joined_file.name, "w").write(json_sarif) + self.assertTrue(filecmp.cmp(written_file.name, joined_file.name, False)) + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_sa_utils.py b/test/test_sa_utils.py new file mode 100644 index 0000000..6c4c13b --- /dev/null +++ b/test/test_sa_utils.py @@ -0,0 +1,40 @@ +import unittest +import os +import sys + +try: + PROJECT_PATH = f"{os.sep}".join(os.path.abspath(__file__).split(os.sep)[:-2]) + sys.path.append(PROJECT_PATH) +except Exception as exception: + print(f"Can not add project path to system path! Exiting!\nERROR: {exception}") + raise SystemExit(1) from exception + +from pytest import MonkeyPatch + +class TestUtils(unittest.TestCase): + """Unit tests for utils_sa module""" + + maxDiff = None + + @classmethod + def mock_env(self, mp: MonkeyPatch): + mp.setenv("GITHUB_WORKSPACE", f"{PROJECT_PATH}{os.sep}test{os.sep}utils{os.sep}dummy_project") + mp.setenv("INPUT_VERBOSE", "True") + mp.setenv("INPUT_REPORT_PR_CHANGES_ONLY", "False") + mp.setenv("INPUT_REPO", "RepoName") + mp.setenv("GITHUB_SHA", "1234") + mp.setenv("INPUT_COMMENT_TITLE", "title") + + def test_get_lines_changed_from_patch(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import sa_utils + + patch = "@@ -43,6 +48,8 @@\n@@ -0,0 +1 @@" + + lines = sa_utils.get_lines_changed_from_patch(patch) + self.assertEqual(lines, [(48, 56), (1, 1)]) + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_static_analysis_cpp.py b/test/test_static_analysis_cpp.py index d8974d8..483fc81 100644 --- a/test/test_static_analysis_cpp.py +++ b/test/test_static_analysis_cpp.py @@ -1,7 +1,6 @@ import unittest import os import sys -import utils.helper_functions as utils try: PROJECT_PATH = f"{os.sep}".join(os.path.abspath(__file__).split(os.sep)[:-2]) @@ -10,216 +9,171 @@ print(f"Can not add project path to system path! Exiting!\nERROR: {exception}") raise SystemExit(1) from exception -os.environ["GITHUB_WORKSPACE"] = f"{PROJECT_PATH}/test/utils/dummy_project" -os.environ["INPUT_VERBOSE"] = "True" -os.environ["INPUT_REPORT_PR_CHANGES_ONLY"] = "False" -os.environ["INPUT_REPO"] = "RepoName" -os.environ["GITHUB_SHA"] = "1234" -os.environ["INPUT_COMMENT_TITLE"] = "title" +from pytest import MonkeyPatch -from src import static_analysis_cpp, get_files_to_check +class TestStaticAnalysisCpp(unittest.TestCase): + """Unit tests for static_analysis_cpp""" -def to_list_and_sort(string_in): - # create list (of strings) from space separated string - # and then sort it - list_out = string_in.split(" ") - list_out.sort() + maxDiff = None - return list_out + @classmethod + def mock_env(self, mp: MonkeyPatch): + mp.setenv("GITHUB_WORKSPACE", f"{PROJECT_PATH}{os.sep}test{os.sep}utils{os.sep}dummy_project") + mp.setenv("INPUT_VERBOSE", "True") + mp.setenv("INPUT_REPORT_PR_CHANGES_ONLY", "False") + mp.setenv("INPUT_REPO", "RepoName") + mp.setenv("GITHUB_SHA", "1234") + mp.setenv("INPUT_COMMENT_TITLE", "title") + def test_create_comment_for_output(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import static_analysis_cpp + + cppcheck_content = [ + f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}DummyFile.cpp:8:23: style: Error message\n", + " Part of code\n", + " ^\n", + f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}DummyFile.cpp:6:12: note: Note message\n", + " Part of code\n", + " ^\n", + f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}DummyFile.cpp:7:4: note: Another note message\n", + " Part of code\n", + " ^\n", + f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}DummyFile.cpp:3:0: style: Error message\n", + " Part of code\n", + " ^\n", + ] + + files_changed_in_pr = { + f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}DummyFile.hpp": ("added", (1, 10)), + f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}DummyFile.cpp": ("added", (1, 10)), + } + result = static_analysis_cpp.create_comment_for_output( + cppcheck_content, os.getenv("GITHUB_WORKSPACE"), files_changed_in_pr, False + ) + + sha = os.getenv("GITHUB_SHA") + repo_name = os.getenv("INPUT_REPO") + expected = ( + f"\n\nhttps://github.com/{repo_name}/blob/{sha}/DummyFile.cpp#L8-L9 \n" + f"```diff\n!Line: 8 - style: Error message" + f"\n\n!Line: 6 - note: Note message" + f"\n!Line: 7 - note: Another note message\n``` " + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/DummyFile.cpp#L3-L8 \n" + f"```diff\n!Line: 3 - style: Error message\n\n``` \n
\n" + ) + + print(result) + + self.assertEqual(result, (expected, 2)) + + def test_prepare_comment_body_empty(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import static_analysis_cpp + from utils import helper_functions as utils + + comment_title = os.getenv("INPUT_COMMENT_TITLE") + comment_body = static_analysis_cpp.prepare_comment_body("", "", "", "", 0, 0, 0, 0) + + # Empty results + expected_comment_body = utils.generate_comment(comment_title, "", 0, "cppcheck") + + self.assertEqual(expected_comment_body, comment_body) + + def test_prepare_comment_body_single_cppcheck(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import static_analysis_cpp + from utils import helper_functions as utils + + comment_title = os.getenv("INPUT_COMMENT_TITLE") + + cppcheck_issues_found = 1 + cppcheck_comment = "dummy issue" + expected_comment_body = utils.generate_comment( + comment_title, cppcheck_comment, cppcheck_issues_found, + "cppcheck" + ) + + comment_body = static_analysis_cpp.prepare_comment_body( + "", cppcheck_comment, "", "", + 0, cppcheck_issues_found, 0, 0 + ) + + self.assertEqual(expected_comment_body, comment_body) + + def test_prepare_comment_body_multi_cppcheck(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import static_analysis_cpp + from utils import helper_functions as utils + + comment_title = os.getenv("INPUT_COMMENT_TITLE") + + cppcheck_issues_found = 4 + cppcheck_comment = "dummy issues" + expected_comment_body = utils.generate_comment( + comment_title, cppcheck_comment, cppcheck_issues_found, + "cppcheck" + ) + + comment_body = static_analysis_cpp.prepare_comment_body( + "", cppcheck_comment, "", "", + 0, cppcheck_issues_found, 0, 0 + ) + + self.assertEqual(expected_comment_body, comment_body) + + def test_prepare_comment_body_single_clang_tidy(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import static_analysis_cpp + from utils import helper_functions as utils + + comment_title = os.getenv("INPUT_COMMENT_TITLE") -class TestStaticAnalysisCpp(unittest.TestCase): - """Unit tests for static_analysis_cpp""" + clang_tidy_issues_found = 1 + clang_tidy_comment = "dummy issue" + expected_comment_body = utils.generate_comment( + comment_title, clang_tidy_comment, clang_tidy_issues_found, "clang-tidy" + ) - maxDiff = None + comment_body = static_analysis_cpp.prepare_comment_body( + "", "", "", clang_tidy_comment, + 0, 0, 0, clang_tidy_issues_found + ) - def test_create_comment_for_output(self): - """ - Test the `create_comment_for_output()` function. - - This test case checks whether the `create_comment_for_output()` function correctly - generates a GitHub comment that displays static analysis issues for a given set of - files. - - The test case creates a mock set of files and static analysis issues, and expects the - generated GitHub comment to match a pre-defined expected string. - """ - - cppcheck_content = [ - "/github/workspace/DummyFile.cpp:8:23: style: Error message\n", - " Part of code\n", - " ^\n", - "/github/workspace/DummyFile.cpp:6:12: note: Note message\n", - " Part of code\n", - " ^\n", - "/github/workspace/DummyFile.cpp:7:4: note: Another note message\n", - " Part of code\n", - " ^\n", - "/github/workspace/DummyFile.cpp:3:0: style: Error message\n", - " Part of code\n", - " ^\n", - ] - - files_changed_in_pr = { - "/github/workspace/DummyFile.hpp": ("added", (1, 10)), - "/github/workspace/DummyFile.cpp": ("added", (1, 10)), - } - result = static_analysis_cpp.create_comment_for_output( - cppcheck_content, "/github/workspace", files_changed_in_pr, False - ) - - sha = os.getenv("GITHUB_SHA") - repo_name = os.getenv("INPUT_REPO") - expected = ( - f"\n\nhttps://github.com/{repo_name}/blob/{sha}/DummyFile.cpp#L8-L9 \n" - f"```diff\n!Line: 8 - style: Error message" - f"\n\n!Line: 6 - note: Note message" - f"\n!Line: 7 - note: Another note message\n``` " - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/DummyFile.cpp#L3-L8 \n" - f"```diff\n!Line: 3 - style: Error message\n\n``` \n
\n" - ) - - print(result) - - self.assertEqual(result, (expected, 2)) - - def test_prepare_comment_body(self): - """ - Test the `prepare_comment_body()` function. - - This test case checks whether the `prepare_comment_body()` function correctly generates - the body text of a GitHub comment for a given set of static analysis issues. - - The test case creates mock input parameters representing different types of static - analysis issues, and expects the generated comment body to match a pre-defined expected - string. - """ - - comment_title = os.getenv("INPUT_COMMENT_TITLE") - comment_body = static_analysis_cpp.prepare_comment_body("", "", 0, 0) - - # Empty results - expected_comment_body = utils.generate_comment(comment_title, "", 0, "cppcheck") - - self.assertEqual(expected_comment_body, comment_body) - - # Multiple cppcheck issues - cppcheck_issues_found = 4 - cppcheck_comment = "dummy issues" - expected_comment_body = utils.generate_comment( - comment_title, cppcheck_comment, cppcheck_issues_found, "cppcheck" - ) - - comment_body = static_analysis_cpp.prepare_comment_body( - cppcheck_comment, "", cppcheck_issues_found, 0 - ) - - self.assertEqual(expected_comment_body, comment_body) - - # Single cppcheck issue - cppcheck_issues_found = 1 - cppcheck_comment = "dummy issue" - expected_comment_body = utils.generate_comment( - comment_title, cppcheck_comment, cppcheck_issues_found, "cppcheck" - ) - - comment_body = static_analysis_cpp.prepare_comment_body( - cppcheck_comment, "", cppcheck_issues_found, 0 - ) - - self.assertEqual(expected_comment_body, comment_body) - - # Multiple clang-tidy issues - clang_tidy_issues_found = 4 - clang_tidy_comment = "dummy issues" - expected_comment_body = utils.generate_comment( - comment_title, clang_tidy_comment, clang_tidy_issues_found, "clang-tidy" - ) - - comment_body = static_analysis_cpp.prepare_comment_body( - "", clang_tidy_comment, 0, clang_tidy_issues_found - ) - - self.assertEqual(expected_comment_body, comment_body) - - # Single clang-tidy issue - clang_tidy_issues_found = 1 - clang_tidy_comment = "dummy issue" - expected_comment_body = utils.generate_comment( - comment_title, clang_tidy_comment, clang_tidy_issues_found, "clang-tidy" - ) - - comment_body = static_analysis_cpp.prepare_comment_body( - "", clang_tidy_comment, 0, clang_tidy_issues_found - ) - - self.assertEqual(expected_comment_body, comment_body) - - def test_get_files_to_check(self): - """ - Test the `get_files_to_check()` function. - - This test case checks whether the `get_files_to_check()` function correctly generates a - list of file paths to check for static analysis issues in a given directory, excluding - any directories that should be skipped. - - The test case creates a mock directory structure and a set of directories to skip, - and expects the generated list of file paths to match a pre-defined expected list of - file paths. - """ - - pwd = os.path.dirname(os.path.realpath(__file__)) - - # Excludes == None - expected = [ - f"{pwd}/utils/dummy_project/DummyFile.cpp", - f"{pwd}/utils/dummy_project/DummyFile.hpp", - f"{pwd}/utils/dummy_project/exclude_dir_1/ExcludedFile1.hpp", - f"{pwd}/utils/dummy_project/exclude_dir_2/ExcludedFile2.hpp", - ] - result = get_files_to_check.get_files_to_check( - f"{pwd}/utils/dummy_project", None, "", "c++" - ) - - self.assertEqual(to_list_and_sort(result), expected) - - # Single exclude_dir - expected = [ - f"{pwd}/utils/dummy_project/DummyFile.cpp", - f"{pwd}/utils/dummy_project/DummyFile.hpp", - f"{pwd}/utils/dummy_project/exclude_dir_2/ExcludedFile2.hpp", - ] - result = get_files_to_check.get_files_to_check( - f"{pwd}/utils/dummy_project", - f"{pwd}/utils/dummy_project/exclude_dir_1", - "", - "c++", - ) - - self.assertEqual(to_list_and_sort(result), expected) - - # Multiple exclude_dir - expected = [ - f"{pwd}/utils/dummy_project/DummyFile.cpp", - f"{pwd}/utils/dummy_project/DummyFile.hpp", - ] - result = get_files_to_check.get_files_to_check( - f"{pwd}/utils/dummy_project", - f"{pwd}/utils/dummy_project/exclude_dir_1 {pwd}/utils/dummy_project/exclude_dir_2", - "", - "c++", - ) - - # Preselected files present - expected = [f"{pwd}/utils/dummy_project/DummyFile.cpp"] - result = get_files_to_check.get_files_to_check( - f"{pwd}/utils/dummy_project", - f"{pwd}/utils/dummy_project/exclude_dir_1 {pwd}/utils/dummy_project/exclude_dir_2", - f"{pwd}/utils/dummy_project/DummyFile.cpp {pwd}/utils/dummy_project/exclude_dir_1/ExcludedFile1.hpp", - "c++", - ) + self.assertEqual(expected_comment_body, comment_body) + + def test_prepare_comment_body_multi_clang_tidy(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import static_analysis_cpp + from utils import helper_functions as utils + + comment_title = os.getenv("INPUT_COMMENT_TITLE") + + clang_tidy_issues_found = 4 + clang_tidy_comment = "dummy issues" + expected_comment_body = utils.generate_comment( + comment_title, clang_tidy_comment, clang_tidy_issues_found, "clang-tidy" + ) + + comment_body = static_analysis_cpp.prepare_comment_body( + "", "", "", clang_tidy_comment, + 0, 0, 0, clang_tidy_issues_found + ) + self.assertEqual(expected_comment_body, comment_body) if __name__ == "__main__": unittest.main() diff --git a/test/test_static_analysis_python.py b/test/test_static_analysis_python.py index c1a8894..23840cc 100644 --- a/test/test_static_analysis_python.py +++ b/test/test_static_analysis_python.py @@ -10,204 +10,201 @@ print(f"Can not add project path to system path! Exiting!\nERROR: {exception}") raise SystemExit(1) from exception -os.environ["GITHUB_WORKSPACE"] = f"{PROJECT_PATH}/test/utils/dummy_project" -os.environ["INPUT_VERBOSE"] = "True" -os.environ["INPUT_REPORT_PR_CHANGES_ONLY"] = "False" -os.environ["INPUT_REPO"] = "RepoName" -os.environ["GITHUB_SHA"] = "1234" -os.environ["INPUT_COMMENT_TITLE"] = "title" - -from src import static_analysis_python - +from pytest import MonkeyPatch class TestStaticAnalysisPython(unittest.TestCase): + """Unit tests for static_analysis_python""" maxDiff = None - def test_create_comment_for_output(self): - """ - Test the `create_comment_for_output()` function. - - This test case checks whether the `create_comment_for_output()` function correctly - generates a GitHub comment that displays static analysis issues for a given set of - files. - - The test case creates a mock set of files and static analysis issues, and expects the - generated GitHub comment to match a pre-defined expected string. - """ - - pylint_content = r""" [ - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 5, - "column": 0, - "endLine": 5, - "endColumn": 5, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"shift\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 8, - "column": 0, - "endLine": 8, - "endColumn": 7, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"letters\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 9, - "column": 0, - "endLine": 9, - "endColumn": 7, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 13, - "column": 12, - "endLine": 13, - "endColumn": 19, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 15, - "column": 12, - "endLine": 15, - "endColumn": 13, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"x\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 16, - "column": 12, - "endLine": 16, - "endColumn": 19, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 20, - "column": 12, - "endLine": 20, - "endColumn": 19, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 22, - "column": 12, - "endLine": 22, - "endColumn": 13, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"x\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "dummy", - "obj": "", - "line": 23, - "column": 12, - "endLine": 23, - "endColumn": 19, - "path": "dummy.py", - "symbol": "invalid-name", - "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", - "message-id": "C0103" - } - ]""" - - files_changed_in_pr = {"/github/workspace/dummy.py": ("added", (1, 25))} - result = static_analysis_python.create_comment_for_output( - json.loads(pylint_content), files_changed_in_pr, False - ) - - sha = os.getenv("GITHUB_SHA") - repo_name = os.getenv("INPUT_REPO") - expected = ( - f"\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L5-L10 \n" - "```diff" - '\n!Line: 5 - C0103: Constant name "shift" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L8-L13 \n" - "```diff\n" - '!Line: 8 - C0103: Constant name "letters" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L9-L14 \n" - "```diff\n" - '!Line: 9 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "```" - " \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L13-L18 \n" - "```diff\n" - '!Line: 13 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L15-L20 \n" - "```diff\n" - '!Line: 15 - C0103: Constant name "x" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L16-L21 \n" - "```diff\n" - '!Line: 16 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L20-L25 \n" - "```diff\n" - '!Line: 20 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L22-L25 \n" - "```diff\n" - '!Line: 22 - C0103: Constant name "x" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
" - f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L23-L25 \n" - "```diff\n" - '!Line: 23 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' - "``` \n
\n" - ) - - print(result) - - self.assertEqual(result, (expected, 9)) + @classmethod + def mock_env(self, mp: MonkeyPatch): + mp.setenv("GITHUB_WORKSPACE", f"{PROJECT_PATH}{os.sep}test{os.sep}utils{os.sep}dummy_project") + mp.setenv("INPUT_VERBOSE", "True") + mp.setenv("INPUT_REPORT_PR_CHANGES_ONLY", "False") + mp.setenv("INPUT_REPO", "RepoName") + mp.setenv("GITHUB_SHA", "1234") + mp.setenv("INPUT_COMMENT_TITLE", "title") + def test_create_comment_for_output(self): + with MonkeyPatch.context() as mp: + self.mock_env(mp) + + from src import static_analysis_python + + pylint_content = r""" [ + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 5, + "column": 0, + "endLine": 5, + "endColumn": 5, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"shift\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 8, + "column": 0, + "endLine": 8, + "endColumn": 7, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"letters\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 9, + "column": 0, + "endLine": 9, + "endColumn": 7, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 13, + "column": 12, + "endLine": 13, + "endColumn": 19, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 15, + "column": 12, + "endLine": 15, + "endColumn": 13, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"x\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 16, + "column": 12, + "endLine": 16, + "endColumn": 19, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 20, + "column": 12, + "endLine": 20, + "endColumn": 19, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 22, + "column": 12, + "endLine": 22, + "endColumn": 13, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"x\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + }, + { + "type": "convention", + "module": "dummy", + "obj": "", + "line": 23, + "column": 12, + "endLine": 23, + "endColumn": 19, + "path": "dummy.py", + "symbol": "invalid-name", + "message": "Constant name \"encoded\" doesn't conform to UPPER_CASE naming style", + "message-id": "C0103" + } + ]""" + + files_changed_in_pr = { + f"{os.getenv("GITHUB_WORKSPACE")}{os.sep}dummy.py": ("added", (1, 25)) + } + result = static_analysis_python.create_comment_for_output( + json.loads(pylint_content), files_changed_in_pr, False + ) + + sha = os.getenv("GITHUB_SHA") + repo_name = os.getenv("INPUT_REPO") + expected = ( + f"\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L5-L10 \n" + "```diff" + '\n!Line: 5 - C0103: Constant name "shift" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L8-L13 \n" + "```diff\n" + '!Line: 8 - C0103: Constant name "letters" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L9-L14 \n" + "```diff\n" + '!Line: 9 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "```" + " \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L13-L18 \n" + "```diff\n" + '!Line: 13 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L15-L20 \n" + "```diff\n" + '!Line: 15 - C0103: Constant name "x" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L16-L21 \n" + "```diff\n" + '!Line: 16 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L20-L25 \n" + "```diff\n" + '!Line: 20 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L22-L25 \n" + "```diff\n" + '!Line: 22 - C0103: Constant name "x" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
" + f"\n\n\n\nhttps://github.com/{repo_name}/blob/{sha}/dummy.py#L23-L25 \n" + "```diff\n" + '!Line: 23 - C0103: Constant name "encoded" doesn\'t conform to UPPER_CASE naming style (invalid-name)\n' + "``` \n
\n" + ) + + print(result) + + self.assertEqual(result, (expected, 9)) if __name__ == "__main__": unittest.main() diff --git a/test/test_utils.py b/test/test_utils.py deleted file mode 100644 index adbb77c..0000000 --- a/test/test_utils.py +++ /dev/null @@ -1,28 +0,0 @@ -import unittest -import os -import sys - -try: - PROJECT_PATH = f"{os.sep}".join(os.path.abspath(__file__).split(os.sep)[:-2]) - sys.path.append(PROJECT_PATH) -except Exception as exception: - print(f"Can not add project path to system path! Exiting!\nERROR: {exception}") - raise SystemExit(1) from exception - -from src import sa_utils - - -class TestUtils(unittest.TestCase): - """Unit tests for utils_sa module""" - - maxDiff = None - - def test_get_lines_changed_from_patch(self): - patch = "@@ -43,6 +48,8 @@\n@@ -0,0 +1 @@" - - lines = sa_utils.get_lines_changed_from_patch(patch) - self.assertEqual(lines, [(48, 56), (1, 1)]) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/utils/dummy_project/DummyFile.hpp b/test/utils/dummy_project/DummyFile.hpp index 21054e8..524b57a 100644 --- a/test/utils/dummy_project/DummyFile.hpp +++ b/test/utils/dummy_project/DummyFile.hpp @@ -1,3 +1,3 @@ inline void func() { int anotherUnused; -} \ No newline at end of file +} diff --git a/test/utils/helper_functions.py b/test/utils/helper_functions.py index cee3548..ac42862 100644 --- a/test/utils/helper_functions.py +++ b/test/utils/helper_functions.py @@ -1,4 +1,18 @@ +import json + +from sarif_om import SarifLog + +def to_list_and_sort(string_in): + # create list (of strings) from space separated string + # and then sort it + list_out = string_in.split(" ") + list_out.sort() + + return list_out + def generate_comment(comment_title, content, issues_found, tool_name): + SEPARATOR = "\n\n\n *** \n" + if issues_found == 0: return ( '##

:white_check_mark:' @@ -8,8 +22,13 @@ def generate_comment(comment_title, content, issues_found, tool_name): expected_comment_body = ( f'##

:zap: {comment_title} :zap:

\n\n' ) - if tool_name == "clang-tidy": - expected_comment_body += "\n\n *** \n" + + if tool_name == "cppcheck": + expected_comment_body += SEPARATOR + elif tool_name == "fbinfer": + expected_comment_body += SEPARATOR + SEPARATOR + elif tool_name == "clang-tidy": + expected_comment_body += SEPARATOR + SEPARATOR + SEPARATOR expected_comment_body += ( f"
:red_circle: {tool_name} found " @@ -18,9 +37,29 @@ def generate_comment(comment_title, content, issues_found, tool_name): f"{content}
" ) - if tool_name == "cppcheck": - expected_comment_body += "\n\n *** \n" + if tool_name == "flawfinder": + expected_comment_body += SEPARATOR + SEPARATOR + SEPARATOR + elif tool_name == "cppcheck": + expected_comment_body += SEPARATOR + SEPARATOR + elif tool_name == "fbinfer": + expected_comment_body += SEPARATOR else: expected_comment_body += "
\n" return expected_comment_body + +def genhashes(to_join): + hashes = [] + + for sarif_run in to_join: + with open(sarif_run) as file: + sarif_json = json.load(file) + + schema_key = "$schema" + del sarif_json[schema_key] + + sarif = SarifLog(**sarif_json) + for run in sarif.runs: + hashes.append(hash(json.dumps(run, sort_keys=True, ensure_ascii=True, default=lambda v: repr(v) + str(hash(v))))) + + return hashes