Skip to content

Add explicit serialization / deserialization to the JsonReporter #7077

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

Merged
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
2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----------------------------------------------------
Expand Down
13 changes: 13 additions & 0 deletions pylint/message/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
104 changes: 86 additions & 18 deletions pylint/reporters/json_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,51 @@
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"
extension = "json"

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:
Expand All @@ -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)
2 changes: 2 additions & 0 deletions tests/message/unittest_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
File renamed without changes.