From 69fd014b483aa9951128f4060ebe8ff2ac8b5818 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Brunner?= <stephane.brunner@camptocamp.com>
Date: Sat, 18 Jan 2025 10:58:07 +0100
Subject: [PATCH] Add more fields in message

---
 .pre-commit-config.yaml              |  2 +-
 prospector/formatters/base.py        | 11 ++++++++++-
 prospector/formatters/pylint.py      |  3 +++
 prospector/message.py                | 16 +++++++++++++++-
 prospector/tools/bandit/__init__.py  | 13 +++++++++++--
 prospector/tools/mypy/__init__.py    |  2 +-
 prospector/tools/pylint/collector.py | 10 ++++++++--
 prospector/tools/ruff/__init__.py    |  6 ++++--
 8 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d689c335..babb0002 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -41,7 +41,7 @@ repos:
               pyproject.toml|
           )$
   - repo: https://github.com/PyCQA/prospector
-    rev: v1.10.3
+    rev: v1.13.3
     hooks:
       - id: prospector
         additional_dependencies:
diff --git a/prospector/formatters/base.py b/prospector/formatters/base.py
index f2d3db03..3509fea9 100644
--- a/prospector/formatters/base.py
+++ b/prospector/formatters/base.py
@@ -39,9 +39,18 @@ def _message_to_dict(self, message: Message) -> dict[str, Any]:
             "line": message.location.line,
             "character": message.location.character,
         }
-        return {
+        if message.location.line_end is not None and message.location.line_end != -1:
+            loc["lineEnd"] = message.location.line_end
+        if message.location.character_end is not None and message.location.character_end != -1:
+            loc["characterEnd"] = message.location.character_end
+        result = {
             "source": message.source,
             "code": message.code,
             "location": loc,
             "message": message.message,
+            "isFixable": message.is_fixable,
         }
+        if message.doc_url:
+            result["docUrl"] = message.doc_url
+
+        return result
diff --git a/prospector/formatters/pylint.py b/prospector/formatters/pylint.py
index 0aa23ca4..456c73cc 100644
--- a/prospector/formatters/pylint.py
+++ b/prospector/formatters/pylint.py
@@ -49,6 +49,9 @@ def render_messages(self) -> list[str]:
                 else f"{template_code}: %(message)s"
             )
 
+            message_str = message.message.strip()
+            if message.doc_url:
+                message_str += f" (See: {message.doc_url})"
             output.append(
                 template
                 % {
diff --git a/prospector/message.py b/prospector/message.py
index 34f5942e..6f3b08b7 100644
--- a/prospector/message.py
+++ b/prospector/message.py
@@ -12,6 +12,8 @@ def __init__(
         function: Optional[str],
         line: Optional[int],
         character: Optional[int],
+        line_end: Optional[int] = None,
+        character_end: Optional[int] = None,
     ):
         if isinstance(path, Path):
             self._path = path.absolute()
@@ -25,6 +27,8 @@ def __init__(
         self.function = function or None
         self.line = None if line == -1 else line
         self.character = None if character == -1 else character
+        self.line_end = line_end
+        self.character_end = character_end
 
     @property
     def path(self) -> Optional[Path]:
@@ -69,11 +73,21 @@ def __lt__(self, other: "Location") -> bool:
 
 
 class Message:
-    def __init__(self, source: str, code: str, location: Location, message: str):
+    def __init__(
+        self,
+        source: str,
+        code: str,
+        location: Location,
+        message: str,
+        doc_url: Optional[str] = None,
+        is_fixable: bool = False,
+    ):
         self.source = source
         self.code = code
         self.location = location
         self.message = message
+        self.doc_url = doc_url
+        self.is_fixable = is_fixable
 
     def __repr__(self) -> str:
         return f"{self.source}-{self.code}"
diff --git a/prospector/tools/bandit/__init__.py b/prospector/tools/bandit/__init__.py
index d302cc90..59b7faf2 100644
--- a/prospector/tools/bandit/__init__.py
+++ b/prospector/tools/bandit/__init__.py
@@ -1,6 +1,7 @@
 from typing import TYPE_CHECKING, Any, Optional
 
 from bandit.cli.main import _get_profile, _init_extensions
+from bandit.core import docs_utils
 from bandit.core.config import BanditConfig
 from bandit.core.constants import RANKING
 from bandit.core.manager import BanditManager
@@ -66,7 +67,15 @@ def run(self, found_files: FileFinder) -> list[Message]:
         results = self.manager.get_issue_list(sev_level=RANKING[self.severity], conf_level=RANKING[self.confidence])
         messages = []
         for result in results:
-            loc = Location(result.fname, None, "", int(result.lineno), 0)
-            msg = Message("bandit", result.test_id, loc, result.text)
+            loc = Location(
+                result.fname,
+                None,
+                "",
+                result.lineno,
+                result.col_offset,
+                line_end=result.linerange[-1] if result.linerange else result.lineno,
+                character_end=result.end_col_offset,
+            )
+            msg = Message("bandit", result.test_id, loc, result.text, doc_url=docs_utils.get_url(result.test_id))
             messages.append(msg)
         return messages
diff --git a/prospector/tools/mypy/__init__.py b/prospector/tools/mypy/__init__.py
index a897383b..45275e45 100644
--- a/prospector/tools/mypy/__init__.py
+++ b/prospector/tools/mypy/__init__.py
@@ -87,7 +87,7 @@ def configure(self, prospector_config: "ProspectorConfig", _: Any) -> None:
                     self.options.append(f"--{name}-{v}")
                 continue
 
-            raise BadToolConfig("mypy", f"The option {name} has an unsupported balue type: {type(value)}")
+            raise BadToolConfig("mypy", f"The option {name} has an unsupported value type: {type(value)}")
 
     def run(self, found_files: FileFinder) -> list[Message]:
         paths = [str(path) for path in found_files.python_modules]
diff --git a/prospector/tools/pylint/collector.py b/prospector/tools/pylint/collector.py
index 1002bc10..613685b5 100644
--- a/prospector/tools/pylint/collector.py
+++ b/prospector/tools/pylint/collector.py
@@ -17,7 +17,7 @@ def __init__(self, message_store: MessageDefinitionStore) -> None:
         self._messages: list[Message] = []
 
     def handle_message(self, msg: PylintMessage) -> None:
-        loc = Location(msg.abspath, msg.module, msg.obj, msg.line, msg.column)
+        loc = Location(msg.abspath, msg.module, msg.obj, msg.line, msg.column, msg.end_line, msg.end_column)
 
         # At this point pylint will give us the code but we want the
         # more user-friendly symbol
@@ -31,7 +31,13 @@ def handle_message(self, msg: PylintMessage) -> None:
         else:
             msg_symbol = msg_data[0].symbol
 
-        message = Message("pylint", msg_symbol, loc, msg.msg)
+        message = Message(
+            "pylint",
+            msg_symbol,
+            loc,
+            msg.msg,
+            doc_url=f"https://pylint.readthedocs.io/en/latest/user_guide/messages/{msg.category}/{msg.symbol}.html",
+        )
         self._messages.append(message)
 
     def get_messages(self) -> list[Message]:
diff --git a/prospector/tools/ruff/__init__.py b/prospector/tools/ruff/__init__.py
index 6aca01ba..f6e9c4fc 100644
--- a/prospector/tools/ruff/__init__.py
+++ b/prospector/tools/ruff/__init__.py
@@ -59,8 +59,6 @@ def run(self, found_files: FileFinder) -> list[Message]:
             return messages
         for message in json.loads(completed_process.stdout):
             sub_message = {}
-            if message.get("url"):
-                sub_message["See"] = message["url"]
             if message.get("fix") and message["fix"].get("applicability"):
                 sub_message["Fix applicability"] = message["fix"]["applicability"]
             message_str = message.get("message", "")
@@ -77,8 +75,12 @@ def run(self, found_files: FileFinder) -> list[Message]:
                         None,
                         line=message.get("location", {}).get("row"),
                         character=message.get("location", {}).get("column"),
+                        line_end=message.get("end_location", {}).get("row"),
+                        character_end=message.get("end_location", {}).get("column"),
                     ),
                     message_str,
+                    doc_url=message.get("url"),
+                    is_fixable=bool((message.get("fix") or {}).get("applicability") in ("safe", "unsafe")),
                 )
             )
         return messages