Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PVS-Studio Static Code Analyzer support #4356

Merged
merged 11 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/supported_code_analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/) | ✗ |
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>
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"]
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please add an assertion about the generated HASH? That would prove that the transform() generates the correct hash for the report.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right, I added this.


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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a docstring comment to this function explaining what it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right, I added this.

""" 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does PVS studio support relative path to the source files?
If it does, I think we could test the transform() function for .json files with relative paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, PVS-Studio uses absolute paths to source files.


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()
Loading