Skip to content

Commit 11c2b38

Browse files
committed
report_converter: add support for ruff json input
1 parent 4467991 commit 11c2b38

File tree

7 files changed

+273
-0
lines changed

7 files changed

+273
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
8+
9+
import json
10+
import logging
11+
import os
12+
13+
from typing import Dict, List
14+
15+
from codechecker_report_converter.report import File, get_or_create_file, \
16+
Report
17+
18+
from ..analyzer_result import AnalyzerResultBase
19+
20+
21+
LOG = logging.getLogger('report-converter')
22+
23+
24+
class AnalyzerResult(AnalyzerResultBase):
25+
""" Transform analyzer result in json format of the ruff analyzer. """
26+
27+
TOOL_NAME = 'ruff'
28+
NAME = 'ruff'
29+
URL = 'https://docs.astral.sh/ruff/'
30+
31+
def get_reports(self, file_path: str) -> List[Report]:
32+
""" Get reports from the given analyzer result. """
33+
reports: List[Report] = []
34+
35+
if not os.path.exists(file_path):
36+
LOG.error("Report file does not exist: %s", file_path)
37+
return reports
38+
39+
try:
40+
with open(file_path, 'r',
41+
encoding="utf-8", errors="ignore") as f:
42+
bugs = json.load(f)
43+
except (IOError, json.decoder.JSONDecodeError):
44+
LOG.error("Failed to parse the given analyzer result '%s'. Please "
45+
"give a valid json file generated by ruff.",
46+
file_path)
47+
return reports
48+
49+
file_cache: Dict[str, File] = {}
50+
for bug in bugs:
51+
fp = bug.get('filename')
52+
if not os.path.exists(fp):
53+
LOG.warning("Source file does not exists: %s", fp)
54+
continue
55+
56+
reports.append(Report(
57+
get_or_create_file(os.path.abspath(fp), file_cache),
58+
int(bug['location']['row']),
59+
int(bug['location']['column']),
60+
bug['message'],
61+
bug['code']))
62+
63+
return reports
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# copied from pylint, could
2+
all:
3+
ruff check --exit-zero --output-format=json --output-file=./simple.json files/simple.py
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
5+
6+
def foo(x):
7+
return 1
8+
9+
10+
foo(0)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>diagnostics</key>
6+
<array>
7+
<dict>
8+
<key>category</key>
9+
<string>unknown</string>
10+
<key>check_name</key>
11+
<string>F401</string>
12+
<key>description</key>
13+
<string>`json` imported but unused</string>
14+
<key>issue_hash_content_of_line_in_context</key>
15+
<string>faa5e0e305cb745ebab5a254dfd0ce66</string>
16+
<key>location</key>
17+
<dict>
18+
<key>col</key>
19+
<integer>8</integer>
20+
<key>file</key>
21+
<integer>0</integer>
22+
<key>line</key>
23+
<integer>3</integer>
24+
</dict>
25+
<key>path</key>
26+
<array>
27+
<dict>
28+
<key>depth</key>
29+
<integer>0</integer>
30+
<key>kind</key>
31+
<string>event</string>
32+
<key>location</key>
33+
<dict>
34+
<key>col</key>
35+
<integer>8</integer>
36+
<key>file</key>
37+
<integer>0</integer>
38+
<key>line</key>
39+
<integer>3</integer>
40+
</dict>
41+
<key>message</key>
42+
<string>`json` imported but unused</string>
43+
</dict>
44+
</array>
45+
<key>type</key>
46+
<string>ruff</string>
47+
</dict>
48+
</array>
49+
<key>files</key>
50+
<array>
51+
<string>files/simple.py</string>
52+
</array>
53+
<key>metadata</key>
54+
<dict>
55+
<key>analyzer</key>
56+
<dict>
57+
<key>name</key>
58+
<string>ruff</string>
59+
</dict>
60+
<key>generated_by</key>
61+
<dict>
62+
<key>name</key>
63+
<string>report-converter</string>
64+
<key>version</key>
65+
<string>x.y.z</string>
66+
</dict>
67+
</dict>
68+
</dict>
69+
</plist>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[
2+
{
3+
"cell": null,
4+
"code": "F401",
5+
"end_location": {
6+
"column": 12,
7+
"row": 3
8+
},
9+
"filename": "/localdata1/zoel_ml/codechecker/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/files/simple.py",
10+
"fix": {
11+
"applicability": "safe",
12+
"edits": [
13+
{
14+
"content": "",
15+
"end_location": {
16+
"column": 1,
17+
"row": 4
18+
},
19+
"location": {
20+
"column": 1,
21+
"row": 3
22+
}
23+
}
24+
],
25+
"message": "Remove unused import: `json`"
26+
},
27+
"location": {
28+
"column": 8,
29+
"row": 3
30+
},
31+
"message": "`json` imported but unused",
32+
"noqa_row": 3,
33+
"url": "https://docs.astral.sh/ruff/rules/unused-import"
34+
}
35+
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
8+
9+
"""
10+
This module tests the correctness of the RuffAnalyzerResult, which
11+
used in sequence transform ruff output to a plist file.
12+
"""
13+
14+
15+
import os
16+
import plistlib
17+
import shutil
18+
import tempfile
19+
import unittest
20+
21+
from codechecker_report_converter.analyzers.ruff import analyzer_result
22+
from codechecker_report_converter.report.parser import plist
23+
24+
25+
class RuffAnalyzerResultTestCase(unittest.TestCase):
26+
""" Test the output of the RuffAnalyzerResult. """
27+
28+
def setUp(self):
29+
""" Setup the test. """
30+
self.analyzer_result = analyzer_result.AnalyzerResult()
31+
self.cc_result_dir = tempfile.mkdtemp()
32+
self.test_files = os.path.join(os.path.dirname(__file__),
33+
'ruff_output_test_files')
34+
35+
def tearDown(self):
36+
""" Clean temporary directory. """
37+
shutil.rmtree(self.cc_result_dir)
38+
39+
def test_no_json_file(self):
40+
""" Test transforming single plist file. """
41+
analyzer_result = os.path.join(self.test_files, 'files',
42+
'simple.py')
43+
44+
ret = self.analyzer_result.transform(
45+
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
46+
file_name="{source_file}_{analyzer}")
47+
self.assertFalse(ret)
48+
49+
def test_transform_dir(self):
50+
""" Test transforming single plist file. """
51+
analyzer_result = os.path.join(self.test_files)
52+
53+
ret = self.analyzer_result.transform(
54+
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
55+
file_name="{source_file}_{analyzer}")
56+
self.assertFalse(ret)
57+
58+
def test_transform_single_file(self):
59+
""" Test transforming single json file. """
60+
analyzer_result = os.path.join(self.test_files, 'simple.json')
61+
self.analyzer_result.transform(
62+
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
63+
file_name="{source_file}_{analyzer}")
64+
65+
plist_file = os.path.join(self.cc_result_dir,
66+
'simple.py_ruff.plist')
67+
68+
with open(plist_file, mode='rb') as pfile:
69+
res = plistlib.load(pfile)
70+
71+
# Use relative path for this test.
72+
res['files'][0] = os.path.join('files', 'simple.py')
73+
74+
self.assertTrue(res['metadata']['generated_by']['version'])
75+
res['metadata']['generated_by']['version'] = "x.y.z"
76+
77+
plist_file = os.path.join(self.test_files,
78+
'simple.expected.plist')
79+
with open(plist_file, mode='rb') as pfile:
80+
exp = plistlib.load(pfile)
81+
82+
self.assertEqual(res, exp)
83+
84+
85+
if __name__ == '__main__':
86+
unittest.main()

0 commit comments

Comments
 (0)