diff --git a/tools/report-converter/codechecker_report_converter/analyzers/ruff/__init__.py b/tools/report-converter/codechecker_report_converter/analyzers/ruff/__init__.py new file mode 100644 index 0000000000..4259749345 --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/ruff/__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/ruff/analyzer_result.py b/tools/report-converter/codechecker_report_converter/analyzers/ruff/analyzer_result.py new file mode 100644 index 0000000000..538d77102a --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/ruff/analyzer_result.py @@ -0,0 +1,63 @@ +# ------------------------------------------------------------------------- +# +# 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 json +import logging +import os + +from typing import Dict, List + +from codechecker_report_converter.report import File, get_or_create_file, \ + Report + +from ..analyzer_result import AnalyzerResultBase + + +LOG = logging.getLogger('report-converter') + + +class AnalyzerResult(AnalyzerResultBase): + """ Transform analyzer result in json format of the ruff analyzer. """ + + TOOL_NAME = 'ruff' + NAME = 'ruff' + URL = 'https://docs.astral.sh/ruff/' + + def get_reports(self, file_path: str) -> List[Report]: + """ Get reports from the given 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 f: + bugs = json.load(f) + except (IOError, json.decoder.JSONDecodeError): + LOG.error("Failed to parse the given analyzer result '%s'. Please " + "give a valid json file generated by ruff.", + file_path) + return reports + + file_cache: Dict[str, File] = {} + for bug in bugs: + fp = bug.get('filename') + if not os.path.exists(fp): + LOG.warning("Source file does not exists: %s", fp) + continue + + reports.append(Report( + get_or_create_file(os.path.abspath(fp), file_cache), + int(bug['location']['row']), + int(bug['location']['column']), + bug['message'], + bug['code'])) + + return reports diff --git a/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/Makefile b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/Makefile new file mode 100644 index 0000000000..a735bab413 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/Makefile @@ -0,0 +1,3 @@ +# copied from pylint, could +all: + ruff check --exit-zero --output-format=json --output-file=./simple.json files/simple.py diff --git a/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/files/simple.py b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/files/simple.py new file mode 100644 index 0000000000..48ac7aa500 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/files/simple.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +import json + + +def foo(x): + return 1 + + +foo(0) diff --git a/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/simple.expected.plist b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/simple.expected.plist new file mode 100644 index 0000000000..1a8d7bff06 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/simple.expected.plist @@ -0,0 +1,69 @@ + + + + + diagnostics + + + category + unknown + check_name + F401 + description + `json` imported but unused + issue_hash_content_of_line_in_context + faa5e0e305cb745ebab5a254dfd0ce66 + location + + col + 8 + file + 0 + line + 3 + + path + + + depth + 0 + kind + event + location + + col + 8 + file + 0 + line + 3 + + message + `json` imported but unused + + + type + ruff + + + files + + files/simple.py + + metadata + + analyzer + + name + ruff + + generated_by + + name + report-converter + version + x.y.z + + + + diff --git a/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/simple.json b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/simple.json new file mode 100644 index 0000000000..4822b21b9a --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/simple.json @@ -0,0 +1,35 @@ +[ + { + "cell": null, + "code": "F401", + "end_location": { + "column": 12, + "row": 3 + }, + "filename": "/localdata1/zoel_ml/codechecker/tools/report-converter/tests/unit/analyzers/ruff_output_test_files/files/simple.py", + "fix": { + "applicability": "safe", + "edits": [ + { + "content": "", + "end_location": { + "column": 1, + "row": 4 + }, + "location": { + "column": 1, + "row": 3 + } + } + ], + "message": "Remove unused import: `json`" + }, + "location": { + "column": 8, + "row": 3 + }, + "message": "`json` imported but unused", + "noqa_row": 3, + "url": "https://docs.astral.sh/ruff/rules/unused-import" + } +] \ No newline at end of file diff --git a/tools/report-converter/tests/unit/analyzers/test_ruff_parser.py b/tools/report-converter/tests/unit/analyzers/test_ruff_parser.py new file mode 100644 index 0000000000..ee6f3bcf73 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/test_ruff_parser.py @@ -0,0 +1,86 @@ +# ------------------------------------------------------------------------- +# +# 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 +# +# ------------------------------------------------------------------------- + +""" +This module tests the correctness of the RuffAnalyzerResult, which +used in sequence transform ruff output to a plist file. +""" + + +import os +import plistlib +import shutil +import tempfile +import unittest + +from codechecker_report_converter.analyzers.ruff import analyzer_result +from codechecker_report_converter.report.parser import plist + + +class RuffAnalyzerResultTestCase(unittest.TestCase): + """ Test the output of the RuffAnalyzerResult. """ + + def setUp(self): + """ Setup the test. """ + self.analyzer_result = analyzer_result.AnalyzerResult() + self.cc_result_dir = tempfile.mkdtemp() + self.test_files = os.path.join(os.path.dirname(__file__), + 'ruff_output_test_files') + + def tearDown(self): + """ Clean temporary directory. """ + shutil.rmtree(self.cc_result_dir) + + def test_no_json_file(self): + """ Test transforming single plist file. """ + analyzer_result = os.path.join(self.test_files, 'files', + 'simple.py') + + ret = self.analyzer_result.transform( + [analyzer_result], self.cc_result_dir, plist.EXTENSION, + file_name="{source_file}_{analyzer}") + self.assertFalse(ret) + + def test_transform_dir(self): + """ Test transforming single plist file. """ + analyzer_result = os.path.join(self.test_files) + + ret = self.analyzer_result.transform( + [analyzer_result], self.cc_result_dir, plist.EXTENSION, + file_name="{source_file}_{analyzer}") + self.assertFalse(ret) + + def test_transform_single_file(self): + """ Test transforming single json file. """ + analyzer_result = os.path.join(self.test_files, 'simple.json') + self.analyzer_result.transform( + [analyzer_result], self.cc_result_dir, plist.EXTENSION, + file_name="{source_file}_{analyzer}") + + plist_file = os.path.join(self.cc_result_dir, + 'simple.py_ruff.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', 'simple.py') + + self.assertTrue(res['metadata']['generated_by']['version']) + res['metadata']['generated_by']['version'] = "x.y.z" + + plist_file = os.path.join(self.test_files, + 'simple.expected.plist') + with open(plist_file, mode='rb') as pfile: + exp = plistlib.load(pfile) + + self.assertEqual(res, exp) + + +if __name__ == '__main__': + unittest.main()