Skip to content
Open
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
29 changes: 29 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)
* [Ruff](#ruff)
* [PVS-Studio](#PVS-Studio)
* [Markdownlint](#markdownlint)
* [Coccinelle](#coccinelle)
Expand Down Expand Up @@ -132,6 +133,13 @@ Supported analyzers:
</details>

## Supported analyzer outputs
The list below can go out of sync with the actual code. For an always

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think, the first sentence is unnecessary here.

up-to-date usage example for a given analyzer straight from the source, run:
```sh
report-converter --example <TYPE>
# e.g. report-converter --example ruff
```

### Sanitizers
#### Undefined Behaviour Sanitizer
[UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html)
Expand Down Expand Up @@ -409,6 +417,27 @@ report-converter -t pyflakes -o ./codechecker_pyflakes_reports ./pyflakes_report
CodeChecker store ./codechecker_pyflakes_reports -n pyflakes
```

### Ruff
[Ruff](https://docs.astral.sh/ruff/) is an extremely fast static analysis
tool (linter) for `Python` code.

The recommended way of running Ruff is to generate a `json` output file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

report-converter accepts only json from rust. So, it is not simply "recommended", but "required" by report-converter.


The following example shows you how to run Ruff and store the results
found by Ruff to the CodeChecker database.

```sh
# Run ruff and generate a json output file.
ruff check --output-format json /path/to/my/project > ./ruff_reports.json

# Use 'report-converter' to create a CodeChecker report directory from the
# analyzer result of ruff.
report-converter -t ruff -o ./codechecker_ruff_reports ./ruff_reports.json

# Store the ruff reports with CodeChecker.
CodeChecker store ./codechecker_ruff_reports -n ruff
```

### PVS-Studio
[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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ class AnalyzerResultBase(metaclass=ABCMeta):
# Link to the official analyzer website.
URL: str = ''

# An example shell command (or short sequence of commands) showing how
# to produce an analyzer result file that this parser can consume, and
# how to feed it to 'report-converter'. Every new parser MUST provide
# this so that usage instructions live next to the code and can't
# silently drift out of sync with docs/tools/report-converter.md (see
# https://github.com/Ericsson/codechecker/issues/4992). Users can view
# it directly with 'report-converter --example <TOOL_NAME>'.
EXAMPLE_CMD: str = ''

def transform(
self,
analyzer_result_file_paths: Iterable[str],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ class AnalyzerResult(AnalyzerResultBase):
TOOL_NAME = 'ruff'
NAME = 'ruff'
URL = 'https://docs.astral.sh/ruff/'
EXAMPLE_CMD = """\
# Run ruff and generate a json output file.
ruff check --output-format json /path/to/my/project > ./ruff_reports.json

# Use 'report-converter' to create a CodeChecker report directory from the
# analyzer result of ruff.
report-converter -t ruff -o ./codechecker_ruff_reports ./ruff_reports.json

# Store the ruff reports with CodeChecker.
CodeChecker store ./codechecker_ruff_reports -n ruff"""

def get_reports(self, file_path: str) -> List[Report]:
""" Get reports from the given analyzer result. """
Expand Down
36 changes: 36 additions & 0 deletions tools/report-converter/codechecker_report_converter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ class RawDescriptionDefaultHelpFormatter(
descriptions. """


class PrintExampleCmdAction(argparse.Action):
"""
Argparse action which immediately prints a usage example for the given
analyzer TYPE and exits - similarly to the built-in '--version'/'--help'
actions, this bypasses this program's other required arguments (input,
--output, --type), since the user is only asking for documentation, not
actually running a conversion.
"""

def __call__(self, parser, namespace, values, option_string=None):
analyzer_parser = supported_converters[values]

if not analyzer_parser.EXAMPLE_CMD:
LOG.warning(
"No usage example is available yet for '%s'. Please see "
"docs/tools/report-converter.md for more information.",
values)
else:
print(f"Example commands to produce a '{values}' output which "
"'report-converter' can parse:\n")
print(analyzer_parser.EXAMPLE_CMD)

parser.exit()


# Load supported converters dynamically.
supported_converters = {}
analyzers_dir_path = os.path.join(os.path.dirname(
Expand Down Expand Up @@ -263,6 +288,17 @@ def main():
for tool_name in sorted(supported_converters)])),
formatter_class=RawDescriptionDefaultHelpFormatter
)

parser.add_argument('--example',
action=PrintExampleCmdAction,
metavar='TYPE',
choices=supported_converters,
default=argparse.SUPPRESS,
help="Print an example command showing how to "
"produce an analyzer result file of the given "
"TYPE that 'report-converter' can parse, then "
"exit. Currently supported types are: " +
', '.join(sorted(supported_converters)) + ".")
__add_arguments_to_parser(parser)

args = parser.parse_args()
Expand Down
115 changes: 115 additions & 0 deletions tools/report-converter/tests/unit/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# -------------------------------------------------------------------------
#
# 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
#
# -------------------------------------------------------------------------

"""
Tests for the report-converter CLI, specifically the '--example' flag and
the EXAMPLE_CMD interface it relies on (see
https://github.com/Ericsson/codechecker/issues/4992).
"""

import io
import unittest
from contextlib import redirect_stdout
from unittest.mock import patch

from codechecker_report_converter import cli


# TOOL_NAMEs of analyzers that do not yet provide an EXAMPLE_CMD.
#
# This list must not grow: any newly added parser is required to define a
# non-empty EXAMPLE_CMD (see AnalyzerResultBase.EXAMPLE_CMD). Existing
# parsers on this list predate that requirement and should have one added
# as a follow-up; when a parser is updated, remove it from this list.
LEGACY_ANALYZERS_WITHOUT_EXAMPLE_CMD = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should include the examples for all analyzer tools in this PR.
In fact, I'm thinking if we could force the existence of EXAMPLE member variable for each analyzer. Would you think, that it could rather be a virtual function? Or maybe the test could fail if any of the supported analyzers don't fill in the EXAMPLE variable.

'clang-tidy', 'clang-tidy-yaml', 'coccinelle', 'cppcheck', 'cpplint',
'eslint', 'gcc', 'golint', 'fbinfer', 'jscpd', 'kernel-doc', 'mdl',
'pmd', 'pvs-studio', 'pyflakes', 'pylint', 'roslynator', 'smatch',
'sparse', 'sphinx', 'spotbugs', 'tslint',
'asan', 'lsan', 'msan', 'tsan', 'ubsan',
}


class ExampleCmdInterfaceTest(unittest.TestCase):
"""
Ensures new analyzer parsers can't be added without a usage example,
while allowing existing ones to be migrated gradually.
"""

def test_new_analyzers_must_define_example_cmd(self):
for tool_name, analyzer_result in cli.supported_converters.items():
if tool_name in LEGACY_ANALYZERS_WITHOUT_EXAMPLE_CMD:
continue

self.assertTrue(
analyzer_result.EXAMPLE_CMD,
f"'{tool_name}' must define a non-empty EXAMPLE_CMD class "
"attribute (see AnalyzerResultBase.EXAMPLE_CMD) so users "
"can discover how to produce compatible input via "
"'report-converter --example {tool_name}'.")

def test_legacy_exemption_list_has_no_stale_entries(self):
"""
Keeps the exemption list itself honest: every entry must still
correspond to a supported analyzer that genuinely lacks an
EXAMPLE_CMD. Once a legacy parser is updated, it must be removed
from this list rather than left behind.
"""
for tool_name in LEGACY_ANALYZERS_WITHOUT_EXAMPLE_CMD:
self.assertIn(tool_name, cli.supported_converters)
self.assertFalse(
cli.supported_converters[tool_name].EXAMPLE_CMD,
f"'{tool_name}' now defines an EXAMPLE_CMD - please remove "
"it from LEGACY_ANALYZERS_WITHOUT_EXAMPLE_CMD.")


class ExampleFlagTest(unittest.TestCase):
""" Tests for the 'report-converter --example TYPE' flag. """

def test_example_flag_prints_example_and_exits_zero(self):
"""
For an analyzer with an EXAMPLE_CMD, '--example' should print it
and exit successfully, without requiring the tool's other
required arguments (input, --output, --type).
"""
out = io.StringIO()
with patch('sys.argv', ['report-converter', '--example', 'ruff']), \
redirect_stdout(out):
with self.assertRaises(SystemExit) as ctx:
cli.main()

self.assertEqual(ctx.exception.code, 0)
self.assertIn('ruff check', out.getvalue())
self.assertIn('report-converter -t ruff', out.getvalue())

def test_example_flag_on_legacy_analyzer_does_not_crash(self):
"""
For an analyzer that doesn't have an EXAMPLE_CMD yet, '--example'
should exit cleanly (with a warning) instead of crashing.
"""
with patch('sys.argv', ['report-converter', '--example', 'pylint']):
with self.assertRaises(SystemExit) as ctx:
cli.main()

self.assertEqual(ctx.exception.code, 0)

def test_example_flag_rejects_unknown_analyzer(self):
"""
An unsupported TYPE should be rejected the same way argparse
rejects any other invalid 'choices' value.
"""
argv = ['report-converter', '--example', 'not_a_real_analyzer']
with patch('sys.argv', argv):
with self.assertRaises(SystemExit) as ctx:
cli.main()

self.assertEqual(ctx.exception.code, 2)


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