diff --git a/docs/supported_code_analyzers.md b/docs/supported_code_analyzers.md index 3ce580e5c9..523096e6cb 100644 --- a/docs/supported_code_analyzers.md +++ b/docs/supported_code_analyzers.md @@ -23,10 +23,13 @@ CodeChecker result directory which can be stored to a CodeChecker server. | | [Sparse](/docs/tools/report-converter.md#sparse) | ✓ | | | [cpplint](/docs/tools/report-converter.md#cpplint) | ✓ | | | [GNU GCC Static Analyzer](/docs/tools/report-converter.md#gcc) | ✓ | +| | [PVS-Studio](/docs/tools/report-converter.md#PVS-Studio) | ✓ | | **C#** | [Roslynator.DotNet.Cli](/docs/tools/report-converter.md#roslynatordotnetcli) | ✓ | +| | [PVS-Studio](/docs/tools/report-converter.md#PVS-Studio) | ✓ | | **Java** | [FindBugs](http://findbugs.sourceforge.net/) | ✗ | | | [SpotBugs](/docs/tools/report-converter.md#spotbugs) | ✓ | | | [Facebook Infer](/docs/tools/report-converter.md#facebook-infer) | ✓ | +| | [PVS-Studio](/docs/tools/report-converter.md#PVS-Studio) | ✓ | | **Python** | [Pylint](/docs/tools/report-converter.md#pylint) | ✓ | | | [Pyflakes](/docs/tools/report-converter.md#pyflakes) | ✓ | | | [mypy](http://mypy-lang.org/) | ✗ | diff --git a/docs/tools/report-converter.md b/docs/tools/report-converter.md index 35eb9faf07..a09c04299a 100644 --- a/docs/tools/report-converter.md +++ b/docs/tools/report-converter.md @@ -22,6 +22,7 @@ a CodeChecker server. * [TSLint](#tslint) * [Golint](#golint) * [Pyflakes](#pyflakes) + * [PVS-Studio](#PVS-Studio) * [Markdownlint](#markdownlint) * [Coccinelle](#coccinelle) * [Smatch](#smatch) @@ -399,6 +400,21 @@ report-converter -t pyflakes -o ./codechecker_pyflakes_reports ./pyflakes_report CodeChecker store ./codechecker_pyflakes_reports -n pyflakes ``` +### [PVS-Studio](https://pvs-studio.com/en) +[PVS-Studio](https://pvs-studio.com/en) is a static analyzer on guard of code quality, +security (SAST), and code safety for C, C++, C# and Java. + +Detailed documentation on how to run the analysis can be found [on our website](https://pvs-studio.com/en/docs/). + +```sh +# Use 'report-converter' to create a CodeChecker report directory from the +# JSON report of PVS-Studio. +report-converter -t pvs-studio -o ./codechecker_pvs_studio_reports ./PVS-Studio.json + +# Store the PVS-Studio reports with CodeChecker. +CodeChecker store ./codechecker_pvs_studio_reports -n pvs_studio +``` + ### [TSLint](https://palantir.github.io/tslint) [TSLint](https://palantir.github.io/tslint) is a static analysis tool for `TypeScript`. diff --git a/tools/report-converter/codechecker_report_converter/analyzers/pvs_studio/__init__.py b/tools/report-converter/codechecker_report_converter/analyzers/pvs_studio/__init__.py new file mode 100644 index 0000000000..4259749345 --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/pvs_studio/__init__.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- diff --git a/tools/report-converter/codechecker_report_converter/analyzers/pvs_studio/analyzer_result.py b/tools/report-converter/codechecker_report_converter/analyzers/pvs_studio/analyzer_result.py new file mode 100644 index 0000000000..8a72986761 --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/pvs_studio/analyzer_result.py @@ -0,0 +1,82 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- + +import logging +from typing import List + +from codechecker_report_converter.report import (Report, + get_or_create_file, + File) + +from typing import Dict +import os +import json +from ..analyzer_result import AnalyzerResultBase + +LOG = logging.getLogger('report-converter') + + +class AnalyzerResult(AnalyzerResultBase): + """ Transform analyzer result of the PVS-Studio analyzer. """ + + TOOL_NAME = 'pvs-studio' + NAME = 'PVS-Studio' + URL = 'https://pvs-studio.com/en/' + + __severities = ["UNSPECIFIED", "HIGH", "MEDIUM", "LOW"] + + def get_reports(self, file_path: str) -> List[Report]: + """ Get reports from the PVS-Studio analyzer result. """ + + reports: List[Report] = [] + + if not os.path.exists(file_path): + LOG.info("Report file does not exist: %s", file_path) + return reports + + try: + with open(file_path, + "r", + encoding="UTF-8", + errors="ignore") as report_file: + bugs = json.load(report_file)['warnings'] + except (IOError, json.decoder.JSONDecodeError): + LOG.warning("Failed to parse the given analyzer result '%s'. " + "Please give a valid json file " + "generated by PVS-Studio.", + file_path) + return reports + + file_cache: Dict[str, File] = {} + for bug in bugs: + bug_positions = bug['positions'] + + for position in bug_positions: + if not os.path.exists(position['file']): + LOG.warning( + "Source file does not exist: %s", + position['file'] + ) + continue + + reports.append(Report( + get_or_create_file( + os.path.abspath(position['file']), + file_cache + ), + position['line'], + position['column'] if position.get('column') else 0, + bug['message'], + bug['code'], + severity=self.get_diagnostic_severity(bug.get('level')) + )) + + return reports + + def get_diagnostic_severity(self, level: int) -> str: + return self.__severities[level] diff --git a/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/files/sample.cpp b/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/files/sample.cpp new file mode 100644 index 0000000000..d7d878e754 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/files/sample.cpp @@ -0,0 +1,13 @@ +#include "stdafx.h" + +int main() +{ + int a = 5; + int b = 6; + + if (a < b) { + return 1; + } + + return 0; +} diff --git a/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/sample.json b/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/sample.json new file mode 100644 index 0000000000..d497858686 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/sample.json @@ -0,0 +1,27 @@ +{ + "version": 2, + "warnings": [ + { + "code": "V547", + "cwe": 571, + "level": 1, + "positions": [ + { + "file": "files/sample.cpp", + "line": 8, + "endLine": 8, + "navigation": { + "previousLine": 4979, + "currentLine": 11857, + "nextLine": 11235, + "columns": 0 + } + } + ], + "projects": [], + "message": "Expression 'a < b' is always true.", + "favorite": false, + "falseAlarm": false + } + ] +} diff --git a/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/sample.plist b/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/sample.plist new file mode 100644 index 0000000000..374b101ea3 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/pvs_studio_output_test_files/sample.plist @@ -0,0 +1,69 @@ + + + + + diagnostics + + + category + unknown + check_name + V547 + description + Expression 'a < b' is always true. + issue_hash_content_of_line_in_context + ba476be198a6e52c12427e1d8606baa8 + location + + col + 0 + file + 0 + line + 8 + + path + + + depth + 0 + kind + event + location + + col + 0 + file + 0 + line + 8 + + message + Expression 'a < b' is always true. + + + type + pvs-studio + + + files + + files\sample.cpp + + metadata + + analyzer + + name + pvs-studio + + generated_by + + name + report-converter + version + 0.1.0 + + + + diff --git a/tools/report-converter/tests/unit/analyzers/test_pvs_studio_parser.py b/tools/report-converter/tests/unit/analyzers/test_pvs_studio_parser.py new file mode 100644 index 0000000000..8dadd24a56 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/test_pvs_studio_parser.py @@ -0,0 +1,129 @@ +import unittest +import tempfile +import shutil +import plistlib +import os +import json + +from codechecker_report_converter.analyzers.pvs_studio import analyzer_result +from codechecker_report_converter.report.parser import plist + + +class PvsStudioAnalyzerResultTestCase(unittest.TestCase): + """ Test the output of PVS-Studio's AnalyzerResult. """ + + def setUp(self) -> None: + """ Set up the test. """ + self.analyzer_result = analyzer_result.AnalyzerResult() + self.test_files = os.path.join( + os.path.dirname(__file__), + 'pvs_studio_output_test_files' + ) + self.result_dir = tempfile.mkdtemp() + + def tearDown(self) -> None: + """Clean temporary directory. """ + shutil.rmtree(self.result_dir) + + def test_no_report_output_file(self) -> None: + """ Test transforming single cpp file. """ + result = os.path.join(self.test_files, "files", "sample.cpp") + + is_success = self.analyzer_result.transform( + analyzer_result_file_paths=[result], + output_dir_path=self.result_dir, + export_type=plist.EXTENSION, + file_name="{source_file}_{analyzer}" + ) + self.assertFalse(is_success) + self.assertFalse(is_success) + + def test_transform_dir(self) -> None: + """ Test transforming a directory. """ + result = os.path.join(self.test_files) + + is_success = self.analyzer_result.transform( + analyzer_result_file_paths=[result], + output_dir_path=self.result_dir, + export_type=plist.EXTENSION, + file_name="{source_file}_{analyzer}" + ) + + self.assertFalse(is_success) + + def test_transform_single_file(self) -> None: + """ Test transforming single output file. """ + self.make_report_valid() + result = os.path.join(self.test_files, 'sample.json') + + is_success = self.analyzer_result.transform( + analyzer_result_file_paths=[result], + output_dir_path=self.result_dir, + export_type=plist.EXTENSION, + file_name="{source_file}_{analyzer}" + ) + + self.assertTrue(is_success) + + plist_file = os.path.join(self.result_dir, + 'sample.cpp_pvs-studio.plist') + + with open(plist_file, mode='rb') as pfile: + res = plistlib.load(pfile) + + plist_file = os.path.join(self.test_files, + 'sample.plist') + with open(plist_file, mode='rb') as pfile: + exp = plistlib.load(pfile) + + self.assertEqual( + res["diagnostics"][0]["check_name"], + exp["diagnostics"][0]["check_name"] + ) + + self.assertEqual( + res["diagnostics"][0]["location"]["line"], + exp["diagnostics"][0]["location"]["line"] + ) + + self.assertEqual( + res["diagnostics"][0]["issue_hash_content_of_line_in_context"], + exp["diagnostics"][0]["issue_hash_content_of_line_in_context"] + ) + + @staticmethod + def make_report_valid() -> None: + """ The method sets absolute paths in PVS-Studio report + and .plist sample. """ + + samples_path = os.path.join( + os.path.dirname(__file__), + "pvs_studio_output_test_files" + ) + + path_to_file = os.path.join( + samples_path, + "files", + "sample.cpp" + ) + + report_path = os.path.join(samples_path, "sample.json") + with open(report_path, 'r', encoding="utf-8") as file: + data = json.loads(file.read()) + data["warnings"][0]["positions"][0]["file"] = path_to_file + + with open(report_path, "w", encoding="utf-8") as file: + file.write(json.dumps(data)) + + path_to_plist = os.path.join(samples_path, "sample.plist") + + with open(path_to_plist, "rb") as plist_file: + data = plistlib.load(plist_file) + data["files"][0] = path_to_file + + with open(path_to_plist, "wb") as plist_file: + plistlib.dump(data, plist_file) + + +if __name__ == "__main__": + unittest.main()