From 2b9f3cabfe937c25fd042b979bc0caaba0f543f4 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Sat, 15 Mar 2025 18:22:39 -0700 Subject: [PATCH 01/16] Revamp README and workflows for my own fork, start adding fbinfer and flawfinder --- .github/workflows/linter.yml | 4 +- .github/workflows/shellcheck.yml | 4 +- .github/workflows/test_action.yml | 21 +++++---- .github/workflows/unit_tests.yml | 3 ++ Dockerfile | 3 +- LICENSE | 42 +++++++++--------- README.md | 46 +++++++++++-------- action.yml | 16 +++++-- docker/static_analysis.dockerfile | 74 ++++++++++++++++++------------- 9 files changed, 125 insertions(+), 88 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 417ca8e..e21c0f6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -5,6 +5,8 @@ on: branches: - main pull_request: + branches: + - main jobs: check: @@ -14,7 +16,7 @@ jobs: - uses: actions/checkout@v3 - name: CodeQuality - uses: JacobDomagala/StaticAnalysis@main + uses: eljonny/StaticAnalysis@morecpp-latest with: language: "Python" pylint_args: "--rcfile=.pylintrc --recursive=true" diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index abb4b72..e928c0e 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -5,6 +5,8 @@ on: 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..52d2b44 100644 --- a/.github/workflows/test_action.yml +++ b/.github/workflows/test_action.yml @@ -1,3 +1,4 @@ +==== BASE ==== name: Test Action on: @@ -5,6 +6,8 @@ on: branches: - main pull_request: + branches: + - main jobs: check: @@ -31,8 +34,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/eljonny/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 +51,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/StaticAnalysisTestRepo.git" + cd StaticAnalysisTestRepo git checkout test-branch-fork git commit -as --amend --no-edit git push -f @@ -61,16 +64,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..9362f03 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -5,6 +5,8 @@ on: branches: - main pull_request: + branches: + - main jobs: check: @@ -23,3 +25,4 @@ jobs: - 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..dc88768 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -The MIT License (MIT) - -Copyright (c) 2021 GitHub, Inc. and contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License (MIT) + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index a5e48c5..319cf62 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@ -[![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/actions/workflows/linter.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis/actions/workflows/linter.yml?query=branch%3Amain) +[![Test Action](https://github.com/eljonny/StaticAnalysis/actions/workflows/test_action.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis/actions/workflows/test_action.yml?query=branch%3Amain) +[![Unit Tests](https://github.com/eljonny/StaticAnalysis/actions/workflows/unit_tests.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis/actions/workflows/unit_tests.yml?query=branch%3Amain) # 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?tab=readme-ov-file#c) and [**Python**](https://github.com/eljonny/StaticAnalysis?tab=readme-ov-file#python)) ## Pull Request comment @@ -19,20 +24,20 @@ Created comment will contain code snippets with the issue description. When this Note that it's possible that the amount of issues detected can make the comment's body to be greater than the GitHub's character limit per PR comment (which is 65536). In that case, the created comment will contain only the issues found up to that point, and the information that the limit of characters was reached. ## Output example (C++) -![output](https://github.com/JacobDomagala/StaticAnalysis/wiki/output_example.png) +![output](https://github.com/eljonny/StaticAnalysis/wiki/output_example.png) ## Non Pull Request For non Pull Requests, the output will be printed to GitHub's output console. This behaviour can also be forced via `force_console_print` input. ## Output example (C++) -![output](https://github.com/JacobDomagala/StaticAnalysis/wiki/console_output_example.png) +![output](https://github.com/eljonny/StaticAnalysis/wiki/console_output_example.png)

# C++ -While it's recommended that your project is CMake-based, it's not required (see the [**Inputs**](https://github.com/JacobDomagala/StaticAnalysis#inputs) section below). We also recommend using a ```.clang-tidy``` file in your root directory. If your project requires additional packages to be installed, you can use the `apt_pckgs` and/or `init_script` input variables to install them (see the [**Workflow example**](https://github.com/JacobDomagala/StaticAnalysis#workflow-example) or [**Inputs**](https://github.com/JacobDomagala/StaticAnalysis#inputs) sections below). If your repository allows contributions from forks, you must use this Action with the `pull_request_target` trigger event, as the GitHub API won't allow PR comments otherwise. +While it's recommended that your project is CMake-based, it's not required (see the [**Inputs**](https://github.com/eljonny/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/eljonny/StaticAnalysis#workflow-example) or [**Inputs**](https://github.com/eljonny/StaticAnalysis#inputs) sections below). If your repository allows contributions from forks, you must use this Action with the `pull_request_target` trigger event, as the GitHub API won't allow PR comments otherwise. By default, **cppcheck** runs with the following flags: ```--enable=all --suppress=missingIncludeSystem --inline-suppr --inconclusive``` @@ -52,7 +57,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) @@ -82,7 +86,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-latest with: language: c++ @@ -108,18 +112,21 @@ jobs: | 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, the easiest way to use it is with a JSON compilation database | `run --compilation-database path/to/compile_commands.json` | +| `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 source and/or header files that will be analyzed with flawfinder | `${{github.workspace}}/src ${{github.workspace}}/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 | `-B ${{github.workspace}}/build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -S ${{github.workspace}}` | +| `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 +154,7 @@ jobs: - uses: actions/checkout@v3 - name: CodeQuality - uses: JacobDomagala/StaticAnalysis@main + uses: eljonny/StaticAnalysis@morecpp-latest with: language: "Python" pylint_args: "--rcfile=.pylintrc --recursive=true" @@ -170,3 +177,4 @@ jobs: | `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** + diff --git a/action.yml b/action.yml index 73e3e42..b36eecc 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: @@ -35,7 +35,16 @@ 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' + default: run --compilation-database ${{github.workspace}}/build/compile_commands.json + 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 @@ -44,6 +53,7 @@ inputs: default: true cmake_args: description: 'Additional CMake arguments' + default: -B ${{github.workspace}}/build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -S ${{github.workspace}} force_console_print: description: 'Output the action result to console, instead of creating the comment' default: false diff --git a/docker/static_analysis.dockerfile b/docker/static_analysis.dockerfile index 57ddc9f..ea647c4 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 + +# 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 From 18bb221424974f10272faf485747f8a34f0942b8 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Sat, 15 Mar 2025 18:25:44 -0700 Subject: [PATCH 02/16] Not an issue to use the upstream for python linting --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index e21c0f6..c6263cc 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v3 - name: CodeQuality - uses: eljonny/StaticAnalysis@morecpp-latest + uses: JacobDomagala/StaticAnalysis@master with: language: "Python" pylint_args: "--rcfile=.pylintrc --recursive=true" From aa30bbc79fe9d822cb267f9d995357e94c76cb77 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Sat, 15 Mar 2025 19:53:47 -0700 Subject: [PATCH 03/16] Add example workflow options for infer and flawfinder, fix defaults, sh In the C++ workflow eg in the README.md, examples for fbinfer_args, flawfinder_args, and flawfinder_targets are now present that I added. Fixed defaults for cmake_args, flawfinder_targets, and fbinfer_args in the action.yml and README. Preprocess fbinfer_args, flawfinder_args, and flawfinder_targets in line with the way cppcheck and clang_tidy arg preprocessing is handled. Add -fi and -ff options to python3 -m src.static_analysis_cpp commands, note that I have not yet added these to the python code yet. -fi receives the file name for the infer report, and -ff receives the file name for the flawfinder report. Output the values of INFER_ARGS, FLAWFINDER_ARGS, and FLAWFINDER_TGTS for debugging where necessary. Added calls to infer for CMake and non-CMake sections. Add call to flawfinder, that does not depend at all on whether CMake is being used. Ordered the variables and operations so everything is consistent. --- README.md | 15 ++++++++++++--- action.yml | 2 -- entrypoint_cpp.sh | 48 +++++++++++++++++++++++++++++++++++++---------- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 319cf62..f22a76a 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,15 @@ 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 @@ -120,12 +129,12 @@ jobs: | `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-*'`) | `` | -| `fbinfer_args` | FB Infer arguments that will be used, the easiest way to use it is with a JSON compilation database | `run --compilation-database path/to/compile_commands.json` | +| `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 source and/or header files that will be analyzed with flawfinder | `${{github.workspace}}/src ${{github.workspace}}/include` | +| `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 | `-B ${{github.workspace}}/build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -S ${{github.workspace}}` | +| `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** diff --git a/action.yml b/action.yml index b36eecc..5a816da 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,6 @@ inputs: description: 'clang-tidy arguments that will be used (example: -checks="*,fuchsia-*,google-*,zircon-*")' fbinfer_args: description: 'Arguments that will be passed to infer' - default: run --compilation-database ${{github.workspace}}/build/compile_commands.json flawfinder_args: description: 'Flawfinder arguments that will be used' default: --minlevel=0 --context --dataonly --quiet --columns --error-level=0 @@ -53,7 +52,6 @@ inputs: default: true cmake_args: description: 'Additional CMake arguments' - default: -B ${{github.workspace}}/build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -S ${{github.workspace}} force_console_print: description: 'Output the action result to console, instead of creating the comment' default: false diff --git a/entrypoint_cpp.sh b/entrypoint_cpp.sh index d840545..f37ef4c 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 @@ -41,15 +49,30 @@ else 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++\")" 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" +debug_print "INFER_ARGS = $INFER_ARGS" +debug_print "WS_BASE = $WS_BASE" +debug_print "WS_INFER = $WS_INFER" num_proc=$(nproc) if [ -z "$files_to_check" ]; then echo "No files to check" + else + for ffdir in $FLAWFINDER_TGTS; do + dir_name=$(echo "$ffdir" | tr '/' '_') + + debug_print "Running flawfinder $FLAWFINDER_ARGS for files in /$GITHUB_WORKSPACE/$ffdir..." + eval flawfinder $FLAWFINDER_ARGS /$GITHUB_WORKSPACE/$ffdir >>flawfinder_$dir_name.txt 2>&1 || true + done + + cat flawfinder_*.txt > flawfinder.txt + if [ "$INPUT_USE_CMAKE" = true ]; then for file in $files_to_check; do exclude_arg="" @@ -66,20 +89,25 @@ else cat cppcheck_*.txt > cppcheck.txt + debug_print "Running infer run --no-progress-bar --compilation-database compile_commands.json $INFER_ARGS..." + eval infer run --no-progress-bar --compilation-database compile_commands.json $INFER_ARGS || 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 $files_to_check $CPPCHECK_ARGS --output-file=cppcheck.txt ..." + eval cppcheck "$files_to_check" "$CPPCHECK_ARGS" --output-file=cppcheck.txt || true + + debug_print "Running infer run --no-progress-bar $INFER_ARGS..." + eval infer run --no-progress-bar $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.txt" -cc "$WS_BASE/cppcheck.txt" -fi "$WS_INFER/report.json" -ct "$WS_BASE/clang_tidy.txt" -o "$print_to_console" -fk "$use_extra_directory" --common "$common_ancestor" --head "origin/$GITHUB_HEAD_REF" fi From 62df528b8654ea6dfe74787b5cb6838fc1640816 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Sat, 15 Mar 2025 19:55:41 -0700 Subject: [PATCH 04/16] Stop using -j for cppcheck because it hinders certain checks Also remove associated variable since it's no longer used to fix SC2034 Double quote arguments to prevent unwanted globbing/splitting to fix SC2086. --- entrypoint_cpp.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/entrypoint_cpp.sh b/entrypoint_cpp.sh index f37ef4c..3b2555c 100644 --- a/entrypoint_cpp.sh +++ b/entrypoint_cpp.sh @@ -58,8 +58,6 @@ debug_print "INFER_ARGS = $INFER_ARGS" debug_print "WS_BASE = $WS_BASE" debug_print "WS_INFER = $WS_INFER" -num_proc=$(nproc) - if [ -z "$files_to_check" ]; then echo "No files to check" @@ -68,7 +66,7 @@ else dir_name=$(echo "$ffdir" | tr '/' '_') debug_print "Running flawfinder $FLAWFINDER_ARGS for files in /$GITHUB_WORKSPACE/$ffdir..." - eval flawfinder $FLAWFINDER_ARGS /$GITHUB_WORKSPACE/$ffdir >>flawfinder_$dir_name.txt 2>&1 || true + eval flawfinder "$FLAWFINDER_ARGS" "/$GITHUB_WORKSPACE/$ffdir" >>"flawfinder_$dir_name.txt" 2>&1 || true done cat flawfinder_*.txt > flawfinder.txt @@ -90,7 +88,7 @@ else cat cppcheck_*.txt > cppcheck.txt debug_print "Running infer run --no-progress-bar --compilation-database compile_commands.json $INFER_ARGS..." - eval infer run --no-progress-bar --compilation-database compile_commands.json $INFER_ARGS || true + eval infer run --no-progress-bar --compilation-database compile_commands.json "$INFER_ARGS" || true # Excludes for clang-tidy are handled in python script debug_print "Running run-clang-tidy-19 $CLANG_TIDY_ARGS -p $(pwd) $files_to_check >>clang_tidy.txt 2>&1" @@ -101,7 +99,7 @@ else eval cppcheck "$files_to_check" "$CPPCHECK_ARGS" --output-file=cppcheck.txt || true debug_print "Running infer run --no-progress-bar $INFER_ARGS..." - eval infer run --no-progress-bar $INFER_ARGS || true + eval infer run --no-progress-bar "$INFER_ARGS" || 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 From 34801e1521b5500deef88ee4370b61c032e0158c Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Sat, 29 Mar 2025 03:10:56 -0700 Subject: [PATCH 05/16] Prepare for SARIF integration, fix spelling, FF and Infer in ep_cpp.sh Start to move away from line output parsing so there's a common format being used as much as possible for gathering static analysis issues data/results. I chose SARIF since CPPCheck, FlawFinder, and Infer all support this format, and it is a format supported by a number of other static analysis tools that could then be integrated without consideration of any particular custom stdout format. This opens up the possibility for other languages and tools that also support this format to be easily integrated into this Action. The first step was ensuring all tools that can support it are using versions with SARIF support; this is complete, only cppcheck needed to be upgraded in the docker image that supports the runner, and is now set to version 2.17.1. The second step in this was to install the Python 3 SARIF Object Model library into the docker image that supports the runner; this is complete. The third step was to add the proper command line arguments to the tools that get called in the entrypoint_cpp.sh; this is also complete. The fourth step was to add execution of a new python script that joins multiple SARIF files (one for each source file analyzed with cppcheck and one for each target directory analyzed with flawfinder) to entrypoint_cpp.sh; this is now complete also. The fifth step is to actually write the new join_sarif Python 3 module, which is almost complete but not yet ready. The final step is to integrate SARIF into the Action code and tests; this is not yet complete, but I am working on it and have already made significant progress, and should be something I can finish in the next 2-3 weeks, time permitting. Fixed spelling in the Action definition. I finished integrating FlawFinder and Infer into entrypoint_cpp.sh script, which included adding -ff and -fi arguments to the call to the static_analysis_cpp Python 3 module. Fixed/improved aspects of entrypoint_cpp.sh. - debug_print for get_files_to_check.py that says it is going to run the script now prints prior to it running so what is being printed aligns with what is actually happening during the script run. - Fixed an issue where all the header files in the project being analyzed were getting passed to cppcheck, which does not support direct analysis of header files. - Fix exclude_arg not working with cppcheck by removing it; it is unnecessary based on how I want cppcheck to be run. Removed excess whitespace in test/test_utils.py. Added trailing newline to test/utils/dummy_project/DummyFile.hpp. --- action.yml | 2 +- docker/static_analysis.dockerfile | 2 +- entrypoint_cpp.sh | 56 +++++++++++++++----------- test/test_utils.py | 1 - test/utils/dummy_project/DummyFile.hpp | 2 +- 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/action.yml b/action.yml index 5a816da..2dd17e8 100644 --- a/action.yml +++ b/action.yml @@ -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: diff --git a/docker/static_analysis.dockerfile b/docker/static_analysis.dockerfile index ea647c4..62ae41d 100644 --- a/docker/static_analysis.dockerfile +++ b/docker/static_analysis.dockerfile @@ -32,7 +32,7 @@ RUN apt-get clean RUN rm -rf /var/lib/apt/lists/* # Install Python packages -RUN pip3 install --break-system-packages PyGithub pylint +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++ diff --git a/entrypoint_cpp.sh b/entrypoint_cpp.sh index 3b2555c..5449587 100644 --- a/entrypoint_cpp.sh +++ b/entrypoint_cpp.sh @@ -42,11 +42,11 @@ 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" @@ -62,44 +62,54 @@ if [ -z "$files_to_check" ]; then echo "No files to check" else + 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 + + 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 for files in /$GITHUB_WORKSPACE/$ffdir..." - eval flawfinder "$FLAWFINDER_ARGS" "/$GITHUB_WORKSPACE/$ffdir" >>"flawfinder_$dir_name.txt" 2>&1 || true + 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 - cat flawfinder_*.txt > flawfinder.txt + 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 $files_to_check; do - exclude_arg="" - if [ -n "$INPUT_EXCLUDE_DIR" ]; then - exclude_arg="-i$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR" - fi - - # Replace '/' with '_' + 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..." - eval infer run --no-progress-bar --compilation-database compile_commands.json "$INFER_ARGS" || true + 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-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 - debug_print "Running cppcheck $files_to_check $CPPCHECK_ARGS --output-file=cppcheck.txt ..." - eval cppcheck "$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 $INFER_ARGS..." - eval infer run --no-progress-bar "$INFER_ARGS" || 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-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 @@ -107,5 +117,5 @@ else cd / - python3 -m src.static_analysis_cpp -ff "$WS_BASE/flawfinder.txt" -cc "$WS_BASE/cppcheck.txt" -fi "$WS_INFER/report.json" -ct "$WS_BASE/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/test/test_utils.py b/test/test_utils.py index adbb77c..fbcf4ff 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -11,7 +11,6 @@ from src import sa_utils - class TestUtils(unittest.TestCase): """Unit tests for utils_sa module""" 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 +} From c7023437a1f5e4d7b2e1cf86d8c45f694f5a19b4 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Tue, 3 Jun 2025 19:38:35 -0700 Subject: [PATCH 06/16] This is the beginning of adding fbinfer and flawfinder to SA-MCPP I have updated the workflows and added the necessary repository secrets to hopefully allow them to work, and updated the names to be unique to the project. Added a new workflow for code coverage and test results upload to CodeCov for visualization and analysis. Fixed the TestRepo clone URL. Added a new file for running cppcheck locally. Fix a number of cross-platform issues with directory separators, so it is consistent across all of the codebase. Ensure proper regular expression escaping in paths. get_files_to_check now uses a regular-expression-based method for excluding files that is more consistent and works in more cross- platform situations than previous. Fix double-path-separators often showing up in selected paths. Fix line endings (all are now LF-only) Reduced the number of characters in a comment to account for the additional SA tools Use the GITHUB_WORKSPACE env in more places to fix a large number of pathing issues Begin adding SARIF integration; this is a WIP. Formatting, remove excess whitespace Add SARIF test data Add more tests for existing code, add tests for join_sarif Use monkeypatch to ensure the environment is correct for each test Add genhashes to test helper functions --- .github/workflows/coverage.yml | 41 + .github/workflows/linter.yml | 2 +- .github/workflows/shellcheck.yml | 2 +- .github/workflows/test_action.yml | 5 +- .github/workflows/unit_tests.yml | 2 +- LICENSE | 42 +- local/run_cppcheck.sh | 24 + src/get_files_to_check.py | 32 +- src/join_sarif.py | 49 + src/sa_utils.py | 162 ++- src/static_analysis_cpp.py | 179 +++- src/static_analysis_python.py | 1 - test/data/sarif/cc-empty.sarif | 17 + test/data/sarif/cc-example.sarif | 975 ++++++++++++++++++ test/data/sarif/ff-empty.sarif | 34 + test/data/sarif/ff-example.sarif | 102 ++ test/data/sarif/fi-empty.sarif | 17 + test/data/sarif/fi-example.sarif | 888 ++++++++++++++++ ...ck__mnt_c_code_project_src_Type1.cpp.sarif | 167 +++ ...ck__mnt_c_code_project_src_Type2.cpp.sarif | 18 + ...ck__mnt_c_code_project_src_Type3.cpp.sarif | 649 ++++++++++++ ...ck__mnt_c_code_project_src_Type4.cpp.sarif | 210 ++++ ...ck__mnt_c_code_project_src_Type5.cpp.sarif | 70 ++ test/test_get_files_to_check.py | 84 ++ test/test_join_sarif.py | 53 + test/test_sa_utils.py | 40 + test/test_static_analysis_cpp.py | 358 +++---- test/test_static_analysis_python.py | 379 ++++--- test/test_utils.py | 27 - test/utils/helper_functions.py | 47 +- 30 files changed, 4180 insertions(+), 496 deletions(-) create mode 100644 .github/workflows/coverage.yml create mode 100644 local/run_cppcheck.sh create mode 100644 src/join_sarif.py create mode 100644 test/data/sarif/cc-empty.sarif create mode 100644 test/data/sarif/cc-example.sarif create mode 100644 test/data/sarif/ff-empty.sarif create mode 100644 test/data/sarif/ff-example.sarif create mode 100644 test/data/sarif/fi-empty.sarif create mode 100644 test/data/sarif/fi-example.sarif create mode 100644 test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type1.cpp.sarif create mode 100644 test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type2.cpp.sarif create mode 100644 test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type3.cpp.sarif create mode 100644 test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type4.cpp.sarif create mode 100644 test/data/sarif/join/cppcheck__mnt_c_code_project_src_Type5.cpp.sarif create mode 100644 test/test_get_files_to_check.py create mode 100644 test/test_join_sarif.py create mode 100644 test/test_sa_utils.py delete mode 100644 test/test_utils.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..9c54e21 --- /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 + - 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 c6263cc..95b0b97 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,4 +1,4 @@ -name: Linter +name: StaticAnalysis-MoreCPP Project Code Linting and Style Checks on: push: diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index e928c0e..99096df 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -1,4 +1,4 @@ -name: "Shellcheck" +name: "StaticAnalysis-MoreCPP Shellcheck" on: push: diff --git a/.github/workflows/test_action.yml b/.github/workflows/test_action.yml index 52d2b44..3109656 100644 --- a/.github/workflows/test_action.yml +++ b/.github/workflows/test_action.yml @@ -1,5 +1,4 @@ -==== BASE ==== -name: Test Action +name: StaticAnalysis-MoreCPP Test GitHub Action on: push: @@ -34,7 +33,7 @@ jobs: BRANCH_NAME=${GITHUB_HEAD_REF} fi - git clone "https://${{secrets.TOKEN}}@github.com/eljonny/StaticAnalysisTestRepo.git" + 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)" diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 9362f03..85239c3 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -1,4 +1,4 @@ -name: Unit Tests +name: StaticAnalysis-MoreCPP Unit Tests on: push: diff --git a/LICENSE b/LICENSE index dc88768..fbea532 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -The MIT License (MIT) - -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 -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License (MIT) + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/local/run_cppcheck.sh b/local/run_cppcheck.sh new file mode 100644 index 0000000..7ae62b6 --- /dev/null +++ b/local/run_cppcheck.sh @@ -0,0 +1,24 @@ +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..34182c1 --- /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..1972370 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. @@ -191,8 +321,11 @@ def prepare_comment_body( Returns: str: The final comment body that will be posted as a comment on the pull request. """ + + SEPARATOR = "\n\n\n *** \n" - if cppcheck_issues_found == 0 and clang_tidy_issues_found == 0: + 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..28bd95a --- /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 fbcf4ff..0000000 --- a/test/test_utils.py +++ /dev/null @@ -1,27 +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/helper_functions.py b/test/utils/helper_functions.py index cee3548..30c4744 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 From 83e20ec286916db0b96ac6d7f432072f77e01d4b Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Tue, 3 Jun 2025 19:46:53 -0700 Subject: [PATCH 07/16] Fix whitespace --- src/join_sarif.py | 6 +++--- src/static_analysis_cpp.py | 14 +++++++------- test/test_join_sarif.py | 2 +- test/utils/helper_functions.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/join_sarif.py b/src/join_sarif.py index 34182c1..3f2b5d7 100644 --- a/src/join_sarif.py +++ b/src/join_sarif.py @@ -9,18 +9,18 @@ def join_sarif(files): 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): diff --git a/src/static_analysis_cpp.py b/src/static_analysis_cpp.py index 1972370..7dc94ff 100644 --- a/src/static_analysis_cpp.py +++ b/src/static_analysis_cpp.py @@ -217,28 +217,28 @@ def read_files_and_parse_results(): 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: 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 = "" @@ -321,7 +321,7 @@ def prepare_comment_body( Returns: str: The final comment body that will be posted as a comment on the pull request. """ - + SEPARATOR = "\n\n\n *** \n" if flawfinder_issues_found == 0 and cppcheck_issues_found == 0 and \ diff --git a/test/test_join_sarif.py b/test/test_join_sarif.py index 28bd95a..5bf1462 100644 --- a/test/test_join_sarif.py +++ b/test/test_join_sarif.py @@ -37,7 +37,7 @@ def test_join_sarif(self): 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() diff --git a/test/utils/helper_functions.py b/test/utils/helper_functions.py index 30c4744..ac42862 100644 --- a/test/utils/helper_functions.py +++ b/test/utils/helper_functions.py @@ -57,7 +57,7 @@ def genhashes(to_join): 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))))) From f92d130c0ac6e2e9e5eb0c38e2527fd097cacf1f Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Tue, 3 Jun 2025 23:15:26 -0700 Subject: [PATCH 08/16] Missing pytest-cov dependency --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9c54e21..cd98359 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest PyGithub + pip install pytest PyGithub pytest-cov - name: Test with coverage run: | pytest --cov-report xml:coverage.xml From e62ec097a7166059d1ddd03d6e7c149bbed65779 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Tue, 3 Jun 2025 23:20:44 -0700 Subject: [PATCH 09/16] Missing sarif-om and jsonpickle dependencies --- .github/workflows/coverage.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cd98359..3923495 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest PyGithub pytest-cov + pip install pytest PyGithub pytest-cov sarif-om jsonpickle - name: Test with coverage run: | pytest --cov-report xml:coverage.xml diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 85239c3..fc89c60 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -21,7 +21,7 @@ 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 From 1d91c1a10123feb4ba29c08b95aed5ce0d908593 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Tue, 3 Jun 2025 23:56:59 -0700 Subject: [PATCH 10/16] I understand better what's going on with this workflow, it should now function as expected. --- .github/workflows/test_action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_action.yml b/.github/workflows/test_action.yml index 3109656..8fae109 100644 --- a/.github/workflows/test_action.yml +++ b/.github/workflows/test_action.yml @@ -50,8 +50,8 @@ jobs: git push -f # test pull_request_target - git clone "https://${{secrets.TOKEN}}@github.com/eljonnyTest/StaticAnalysisTestRepo.git" - cd StaticAnalysisTestRepo + 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 From 5406cf3b0a7549864ceb2de355c0b55dc49d990d Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Wed, 4 Jun 2025 00:02:03 -0700 Subject: [PATCH 11/16] Use a known working version of StaticAnalysis. Exclude the dummy_project, scan the test/utils/ dir otherwise. --- .github/workflows/linter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 95b0b97..b2870da 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -16,12 +16,12 @@ jobs: - uses: actions/checkout@v3 - name: CodeQuality - uses: JacobDomagala/StaticAnalysis@master + 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 From 3dbd326183ec6f4630b582f8e03d22f0ec192def Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Wed, 4 Jun 2025 00:04:21 -0700 Subject: [PATCH 12/16] Fix SC2148 --- local/run_cppcheck.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/local/run_cppcheck.sh b/local/run_cppcheck.sh index 7ae62b6..97049b9 100644 --- a/local/run_cppcheck.sh +++ b/local/run_cppcheck.sh @@ -1,3 +1,5 @@ +#!/bin/bash + RUN_FINDFILES_SCRIPT="$1" SRC_DIR="$2" EXCLUDES="$3" From 705aec4f234cba5fc40adb9f40c0bce30ec372e7 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Wed, 4 Jun 2025 00:06:27 -0700 Subject: [PATCH 13/16] Fix SC2086 --- local/run_cppcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/run_cppcheck.sh b/local/run_cppcheck.sh index 97049b9..fc8fee7 100644 --- a/local/run_cppcheck.sh +++ b/local/run_cppcheck.sh @@ -10,7 +10,7 @@ CPPCHECK_ARGS="$4" 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++") +files_to_check=$(python3 "$RUN_FINDFILES_SCRIPT" -exclude="$EXCLUDES" -dir="$SRC_DIR" -lang="c++") for file in $files_to_check; do file_extension="${file##*.}" From 5c861509c7e3c40066df04054f48ad61befd0068 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Wed, 4 Jun 2025 00:29:07 -0700 Subject: [PATCH 14/16] Add codecov.yml for ignores; use ubuntu-latest, the other may not be supported. --- .github/workflows/linter.yml | 2 +- codecov.yml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 codecov.yml diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index b2870da..c66e180 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -11,7 +11,7 @@ on: jobs: check: name: Run Linter - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 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/**" From c8882a59e6b2008d3814b5d695c62dec462978f9 Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Wed, 4 Jun 2025 01:16:08 -0700 Subject: [PATCH 15/16] README work Fixed all links. Added badges for shellcheck, Code Coverage, and CodeCov. --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f22a76a..de2c547 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ -[![Linter](https://github.com/eljonny/StaticAnalysis/actions/workflows/linter.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis/actions/workflows/linter.yml?query=branch%3Amain) -[![Test Action](https://github.com/eljonny/StaticAnalysis/actions/workflows/test_action.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis/actions/workflows/test_action.yml?query=branch%3Amain) -[![Unit Tests](https://github.com/eljonny/StaticAnalysis/actions/workflows/unit_tests.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis/actions/workflows/unit_tests.yml?query=branch%3Amain) +[![Linter](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/linter.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/linter.yml?query=branch%3Amain) +[![Test Action](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/test_action.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/test_action.yml?query=branch%3Amain) +[![Unit Tests](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/unit_tests.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/unit_tests.yml?query=branch%3Amain) +[![Shell Script Check](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/shellcheck.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/shellcheck.yml?query=branch%3Amain) +[![Code Coverage](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/coverage.yml?query=branch%3Amain) +[![codecov](https://codecov.io/gh/eljonny/StaticAnalysis-MoreCPP/graph/badge.svg?token=QJZvQ3D9aK)](https://codecov.io/gh/eljonny/StaticAnalysis-MoreCPP) # Static Analysis @@ -15,7 +18,7 @@ This GitHub action is designed for C++/Python projects and performs static analy 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/eljonny/StaticAnalysis?tab=readme-ov-file#c) and [**Python**](https://github.com/eljonny/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 @@ -24,20 +27,20 @@ Created comment will contain code snippets with the issue description. When this Note that it's possible that the amount of issues detected can make the comment's body to be greater than the GitHub's character limit per PR comment (which is 65536). In that case, the created comment will contain only the issues found up to that point, and the information that the limit of characters was reached. ## Output example (C++) -![output](https://github.com/eljonny/StaticAnalysis/wiki/output_example.png) +![output](https://github.com/JacobDomagala/StaticAnalysis/wiki/output_example.png) ## Non Pull Request For non Pull Requests, the output will be printed to GitHub's output console. This behaviour can also be forced via `force_console_print` input. ## Output example (C++) -![output](https://github.com/eljonny/StaticAnalysis/wiki/console_output_example.png) +![output](https://github.com/JacobDomagala/StaticAnalysis/wiki/console_output_example.png)

# C++ -While it's recommended that your project is CMake-based, it's not required (see the [**Inputs**](https://github.com/eljonny/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/eljonny/StaticAnalysis#workflow-example) or [**Inputs**](https://github.com/eljonny/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``` @@ -76,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}\" @@ -86,7 +89,7 @@ jobs: apt install -y libvulkan1 mesa-vulkan-drivers vulkan-utils" > init_script.sh - name: Run static analysis - uses: eljonny/StaticAnalysis@morecpp-latest + uses: eljonny/StaticAnalysis-MoreCPP@morecpp-latest with: language: c++ @@ -163,7 +166,7 @@ jobs: - uses: actions/checkout@v3 - name: CodeQuality - uses: eljonny/StaticAnalysis@morecpp-latest + uses: eljonny/StaticAnalysis-MoreCPP@morecpp-latest with: language: "Python" pylint_args: "--rcfile=.pylintrc --recursive=true" @@ -186,4 +189,3 @@ jobs: | `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** - From 28bbb3a969d13706b770ff7472844896956264ac Mon Sep 17 00:00:00 2001 From: Jonathan Hyry Date: Wed, 4 Jun 2025 01:24:05 -0700 Subject: [PATCH 16/16] Just use the default links. No branch specifier required. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index de2c547..792242f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -[![Linter](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/linter.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/linter.yml?query=branch%3Amain) -[![Test Action](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/test_action.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/test_action.yml?query=branch%3Amain) -[![Unit Tests](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/unit_tests.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/unit_tests.yml?query=branch%3Amain) -[![Shell Script Check](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/shellcheck.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/shellcheck.yml?query=branch%3Amain) -[![Code Coverage](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/eljonny/StaticAnalysis-MoreCPP/actions/workflows/coverage.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