diff --git a/doc/conf.py b/doc/conf.py index 3f0c1177b4..739ee312d3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. +# documentation root, use 'os.path.abspath' to make it absolute, like shown here. sys.path.append(os.path.abspath("exts")) # -- General configuration ----------------------------------------------------- diff --git a/pylint/message/message.py b/pylint/message/message.py index 4efa3f1244..11961d9af9 100644 --- a/pylint/message/message.py +++ b/pylint/message/message.py @@ -77,3 +77,16 @@ def format(self, template: str) -> str: cf. https://docs.python.org/2/library/string.html#formatstrings """ return template.format(**asdict(self)) + + @property + def location(self) -> MessageLocationTuple: + return MessageLocationTuple( + self.abspath, + self.path, + self.module, + self.obj, + self.line, + self.column, + self.end_line, + self.end_column, + ) diff --git a/pylint/reporters/json_reporter.py b/pylint/reporters/json_reporter.py index 8adb783027..c47aac5987 100644 --- a/pylint/reporters/json_reporter.py +++ b/pylint/reporters/json_reporter.py @@ -7,16 +7,43 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING +import sys +from typing import TYPE_CHECKING, Optional +from pylint.interfaces import UNDEFINED +from pylint.message import Message from pylint.reporters.base_reporter import BaseReporter +from pylint.typing import MessageLocationTuple + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter from pylint.reporters.ureports.nodes import Section +# Since message-id is an invalid name we need to use the alternative syntax +OldJsonExport = TypedDict( + "OldJsonExport", + { + "type": str, + "module": str, + "obj": str, + "line": int, + "column": int, + "endLine": Optional[int], + "endColumn": Optional[int], + "path": str, + "symbol": str, + "message": str, + "message-id": str, + }, +) + -class JSONReporter(BaseReporter): +class BaseJSONReporter(BaseReporter): """Report messages and layouts in JSON.""" name = "json" @@ -24,22 +51,7 @@ class JSONReporter(BaseReporter): def display_messages(self, layout: Section | None) -> None: """Launch layouts display.""" - json_dumpable = [ - { - "type": msg.category, - "module": msg.module, - "obj": msg.obj, - "line": msg.line, - "column": msg.column, - "endLine": msg.end_line, - "endColumn": msg.end_column, - "path": msg.path, - "symbol": msg.symbol, - "message": msg.msg or "", - "message-id": msg.msg_id, - } - for msg in self.messages - ] + json_dumpable = [self.serialize(message) for message in self.messages] print(json.dumps(json_dumpable, indent=4), file=self.out) def display_reports(self, layout: Section) -> None: @@ -48,6 +60,62 @@ def display_reports(self, layout: Section) -> None: def _display(self, layout: Section) -> None: """Do nothing.""" + @staticmethod + def serialize(message: Message) -> OldJsonExport: + raise NotImplementedError + + @staticmethod + def deserialize(message_as_json: OldJsonExport) -> Message: + raise NotImplementedError + + +class JSONReporter(BaseJSONReporter): + + """ + TODO: 3.0: Remove this JSONReporter in favor of the new one handling abs-path + and confidence. + + TODO: 2.15: Add a new JSONReporter handling abs-path, confidence and scores. + (Ultimately all other breaking change related to json for 3.0). + """ + + @staticmethod + def serialize(message: Message) -> OldJsonExport: + return { + "type": message.category, + "module": message.module, + "obj": message.obj, + "line": message.line, + "column": message.column, + "endLine": message.end_line, + "endColumn": message.end_column, + "path": message.path, + "symbol": message.symbol, + "message": message.msg or "", + "message-id": message.msg_id, + } + + @staticmethod + def deserialize(message_as_json: OldJsonExport) -> Message: + return Message( + msg_id=message_as_json["message-id"], + symbol=message_as_json["symbol"], + msg=message_as_json["message"], + location=MessageLocationTuple( + # TODO: 3.0: Add abs-path and confidence in a new JSONReporter + abspath=message_as_json["path"], + path=message_as_json["path"], + module=message_as_json["module"], + obj=message_as_json["obj"], + line=message_as_json["line"], + column=message_as_json["column"], + end_line=message_as_json["endLine"], + end_column=message_as_json["endColumn"], + ), + # TODO: 3.0: Make confidence available in a new JSONReporter + confidence=UNDEFINED, + ) + def register(linter: PyLinter) -> None: linter.register_reporter(JSONReporter) diff --git a/tests/message/unittest_message.py b/tests/message/unittest_message.py index d0805e337d..edb803daf7 100644 --- a/tests/message/unittest_message.py +++ b/tests/message/unittest_message.py @@ -55,5 +55,7 @@ def build_message( ) e1234 = build_message(e1234_message_definition, e1234_location_values) w1234 = build_message(w1234_message_definition, w1234_location_values) + assert e1234.location == e1234_location_values + assert w1234.location == w1234_location_values assert e1234.format(template) == expected assert w1234.format(template) == "8:11:12: W1234: message (msg-symbol)" diff --git a/tests/unittest_reporters_json.py b/tests/reporters/unittest_json_reporter.py similarity index 71% rename from tests/unittest_reporters_json.py rename to tests/reporters/unittest_json_reporter.py index 2a0843e7f2..90a67fcebd 100644 --- a/tests/unittest_reporters_json.py +++ b/tests/reporters/unittest_json_reporter.py @@ -10,10 +10,15 @@ from io import StringIO from typing import Any +import pytest + from pylint import checkers +from pylint.interfaces import UNDEFINED from pylint.lint import PyLinter +from pylint.message import Message from pylint.reporters import JSONReporter from pylint.reporters.ureports.nodes import EvaluationSection +from pylint.typing import MessageLocationTuple expected_score_message = "Expected score message" @@ -98,3 +103,35 @@ def get_linter_result(score: bool, message: dict[str, Any]) -> list[dict[str, An reporter.display_messages(None) report_result = json.loads(output.getvalue()) return report_result + + +@pytest.mark.parametrize( + "message", + [ + pytest.param( + Message( + msg_id="C0111", + symbol="missing-docstring", + location=MessageLocationTuple( + # abs-path and path must be equal because one of them is removed + # in the JsonReporter + abspath=__file__, + path=__file__, + module="unittest_json_reporter", + obj="obj", + line=1, + column=3, + end_line=3, + end_column=5, + ), + msg="This is the actual message", + confidence=UNDEFINED, + ), + id="everything-defined", + ) + ], +) +def test_serialize_deserialize(message): + # TODO: 3.0: Add confidence handling, add path and abs path handling or a new JSONReporter + json_message = JSONReporter.serialize(message) + assert message == JSONReporter.deserialize(json_message) diff --git a/tests/unittest_reporting.py b/tests/reporters/unittest_reporting.py similarity index 100% rename from tests/unittest_reporting.py rename to tests/reporters/unittest_reporting.py