| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE |
| # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt |
| |
| """JSON reporter.""" |
| |
| from __future__ import annotations |
| |
| import json |
| from typing import TYPE_CHECKING, Optional, TypedDict |
| |
| from pylint.interfaces import CONFIDENCE_MAP, UNDEFINED |
| from pylint.message import Message |
| from pylint.reporters.base_reporter import BaseReporter |
| from pylint.typing import MessageLocationTuple |
| |
| 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): |
| """Report messages and layouts in JSON. |
| |
| Consider using JSON2Reporter instead, as it is superior and this reporter |
| is no longer maintained. |
| """ |
| |
| name = "json" |
| extension = "json" |
| |
| def display_messages(self, layout: Section | None) -> None: |
| """Launch layouts display.""" |
| 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: |
| """Don't do anything in this reporter.""" |
| |
| def _display(self, layout: Section) -> None: |
| """Do nothing.""" |
| |
| @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( |
| 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"], |
| ), |
| confidence=UNDEFINED, |
| ) |
| |
| |
| class JSONMessage(TypedDict): |
| type: str |
| message: str |
| messageId: str |
| symbol: str |
| confidence: str |
| module: str |
| path: str |
| absolutePath: str |
| line: int |
| endLine: int | None |
| column: int |
| endColumn: int | None |
| obj: str |
| |
| |
| class JSON2Reporter(BaseReporter): |
| name = "json2" |
| extension = "json2" |
| |
| def display_reports(self, layout: Section) -> None: |
| """Don't do anything in this reporter.""" |
| |
| def _display(self, layout: Section) -> None: |
| """Do nothing.""" |
| |
| def display_messages(self, layout: Section | None) -> None: |
| """Launch layouts display.""" |
| output = { |
| "messages": [self.serialize(message) for message in self.messages], |
| "statistics": self.serialize_stats(), |
| } |
| print(json.dumps(output, indent=4), file=self.out) |
| |
| @staticmethod |
| def serialize(message: Message) -> JSONMessage: |
| return JSONMessage( |
| type=message.category, |
| symbol=message.symbol, |
| message=message.msg or "", |
| messageId=message.msg_id, |
| confidence=message.confidence.name, |
| module=message.module, |
| obj=message.obj, |
| line=message.line, |
| column=message.column, |
| endLine=message.end_line, |
| endColumn=message.end_column, |
| path=message.path, |
| absolutePath=message.abspath, |
| ) |
| |
| @staticmethod |
| def deserialize(message_as_json: JSONMessage) -> Message: |
| return Message( |
| msg_id=message_as_json["messageId"], |
| symbol=message_as_json["symbol"], |
| msg=message_as_json["message"], |
| location=MessageLocationTuple( |
| abspath=message_as_json["absolutePath"], |
| 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"], |
| ), |
| confidence=CONFIDENCE_MAP[message_as_json["confidence"]], |
| ) |
| |
| def serialize_stats(self) -> dict[str, str | int | dict[str, int]]: |
| """Serialize the linter stats into something JSON dumpable.""" |
| stats = self.linter.stats |
| |
| counts_dict = { |
| "fatal": stats.fatal, |
| "error": stats.error, |
| "warning": stats.warning, |
| "refactor": stats.refactor, |
| "convention": stats.convention, |
| "info": stats.info, |
| } |
| |
| # Calculate score based on the evaluation option |
| evaluation = self.linter.config.evaluation |
| try: |
| note: int = eval( # pylint: disable=eval-used |
| evaluation, {}, {**counts_dict, "statement": stats.statement or 1} |
| ) |
| except Exception as ex: # pylint: disable=broad-except |
| score: str | int = f"An exception occurred while rating: {ex}" |
| else: |
| score = round(note, 2) |
| |
| return { |
| "messageTypeCount": counts_dict, |
| "modulesLinted": len(stats.by_module), |
| "score": score, |
| } |
| |
| |
| def register(linter: PyLinter) -> None: |
| linter.register_reporter(JSONReporter) |
| linter.register_reporter(JSON2Reporter) |