Skip to content

Commit

Permalink
Merge pull request #4356 from feeelin/master
Browse files Browse the repository at this point in the history
PVS-Studio Static Code Analyzer support
  • Loading branch information
bruntib authored Oct 30, 2024
2 parents 1e9f8f0 + 9061b09 commit 3320e9a
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/supported_code_analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,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/) ||
Expand Down
16 changes: 16 additions & 0 deletions docs/tools/report-converter.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ a CodeChecker server.
* [TSLint](#tslint)
* [Golint](#golint)
* [Pyflakes](#pyflakes)
* [PVS-Studio](#PVS-Studio)
* [Markdownlint](#markdownlint)
* [Coccinelle](#coccinelle)
* [Smatch](#smatch)
Expand Down Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
#
# -------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include "stdafx.h"

int main()
{
int a = 5;
int b = 6;

if (a < b) {
return 1;
}

return 0;
}
Original file line number Diff line number Diff line change
@@ -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
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>diagnostics</key>
<array>
<dict>
<key>category</key>
<string>unknown</string>
<key>check_name</key>
<string>V547</string>
<key>description</key>
<string>Expression 'a &lt; b' is always true.</string>
<key>issue_hash_content_of_line_in_context</key>
<string>ba476be198a6e52c12427e1d8606baa8</string>
<key>location</key>
<dict>
<key>col</key>
<integer>0</integer>
<key>file</key>
<integer>0</integer>
<key>line</key>
<integer>8</integer>
</dict>
<key>path</key>
<array>
<dict>
<key>depth</key>
<integer>0</integer>
<key>kind</key>
<string>event</string>
<key>location</key>
<dict>
<key>col</key>
<integer>0</integer>
<key>file</key>
<integer>0</integer>
<key>line</key>
<integer>8</integer>
</dict>
<key>message</key>
<string>Expression 'a &lt; b' is always true.</string>
</dict>
</array>
<key>type</key>
<string>pvs-studio</string>
</dict>
</array>
<key>files</key>
<array>
<string>files\sample.cpp</string>
</array>
<key>metadata</key>
<dict>
<key>analyzer</key>
<dict>
<key>name</key>
<string>pvs-studio</string>
</dict>
<key>generated_by</key>
<dict>
<key>name</key>
<string>report-converter</string>
<key>version</key>
<string>0.1.0</string>
</dict>
</dict>
</dict>
</plist>
129 changes: 129 additions & 0 deletions tools/report-converter/tests/unit/analyzers/test_pvs_studio_parser.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 3320e9a

Please sign in to comment.