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 6 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -------------------------------------------------------------------------
#
# 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.error("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.error("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.error(
feeelin marked this conversation as resolved.
Show resolved Hide resolved
"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,103 @@
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)

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. """
result = os.path.join(self.test_files, 'sample.json')

self.make_report_valid()
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)

# Use relative path for this test.
res['files'][0] = os.path.join('files', 'sample.cpp')
Copy link
Member

Choose a reason for hiding this comment

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

Why are you modifying transformation result plist after calling the transform()?
Shouldn't we have there an absolute path to sample.cpp? This should be compared to the location of sample.cpp in the sample.plist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably forgot to remove that line when I added the method to set absolute paths in the json report. Fixed it.


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, exp)

@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.

samples_path = os.path.join(
os.path.dirname(__file__),
"pvs_studio_output_test_files"
)
report_path = os.path.join(samples_path, "sample.json")
with open(report_path, 'r') as file:
data = json.loads(file.read())
data["warnings"][0]["positions"][0]["file"] = os.path.join(
samples_path,
"files",
"sample.cpp"
)

with open(report_path, "w") as file:
file.write(json.dumps(data))


if __name__ == "__main__":
unittest.main()
Loading