Add typing in ``pylint.reporters`` (#5004)

* Add typing and fix small issue in pylint.reporters

Fix typing error in pylint/checkers/imports.py. Add typing
of report related code outside of pylint.reporters.

* Remove unused argument in pylint.reporters.VNode constructor

* Simplify and specify the typing in reporters nodes

Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
diff --git a/pylint/checkers/__init__.py b/pylint/checkers/__init__.py
index 584f476..d15c876 100644
--- a/pylint/checkers/__init__.py
+++ b/pylint/checkers/__init__.py
@@ -61,7 +61,7 @@
     columns: Iterable[str],
 ) -> List[str]:
     """get values listed in <columns> from <stats> and <old_stats>,
-    and return a formated list of values, designed to be given to a
+    and return a formatted list of values, designed to be given to a
     ureport.Table object
     """
     lines: List[str] = []
diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py
index b8609e9..4c75b59 100644
--- a/pylint/checkers/imports.py
+++ b/pylint/checkers/imports.py
@@ -193,7 +193,7 @@
     report's section
     """
     outputfile = _dependencies_graph(filename, dep_info)
-    sect.append(Paragraph(f"{gtype}imports graph has been written to {outputfile}"))
+    sect.append(Paragraph((f"{gtype}imports graph has been written to {outputfile}",)))
 
 
 # the import checker itself ###################################################
diff --git a/pylint/interfaces.py b/pylint/interfaces.py
index cac7e76..2e1ca6d 100644
--- a/pylint/interfaces.py
+++ b/pylint/interfaces.py
@@ -17,6 +17,7 @@
 
 """Interfaces for Pylint objects"""
 from collections import namedtuple
+from typing import Tuple
 
 from astroid import nodes
 
@@ -40,7 +41,7 @@
         return implements(instance, cls)
 
 
-def implements(obj, interface):
+def implements(obj: "Interface", interface: Tuple[type, type]) -> bool:
     """Return true if the give object (maybe an instance or class) implements
     the interface.
     """
@@ -101,4 +102,4 @@
         """display results encapsulated in the layout tree"""
 
 
-__all__ = ("IRawChecker", "IAstroidChecker", "ITokenChecker", "IReporter")
+__all__ = ("IRawChecker", "IAstroidChecker", "ITokenChecker", "IReporter", "IChecker")
diff --git a/pylint/lint/report_functions.py b/pylint/lint/report_functions.py
index 12c4b03..201b28b 100644
--- a/pylint/lint/report_functions.py
+++ b/pylint/lint/report_functions.py
@@ -5,7 +5,7 @@
 from typing import DefaultDict, Dict, List, Tuple, Union
 
 from pylint import checkers, exceptions
-from pylint.reporters.ureports import nodes as report_nodes
+from pylint.reporters.ureports.nodes import Table
 from pylint.typing import CheckerStats
 
 
@@ -19,7 +19,7 @@
     lines += checkers.table_lines_from_stats(
         stats, previous_stats, ("convention", "refactor", "warning", "error")
     )
-    sect.append(report_nodes.Table(children=lines, cols=4, rheaders=1))
+    sect.append(Table(children=lines, cols=4, rheaders=1))
 
 
 def report_messages_stats(
@@ -41,7 +41,7 @@
     lines = ["message id", "occurrences"]
     for value, msg_id in in_order:
         lines += [msg_id, str(value)]
-    sect.append(report_nodes.Table(children=lines, cols=2, rheaders=1))
+    sect.append(Table(children=lines, cols=2, rheaders=1))
 
 
 def report_messages_by_module_stats(
@@ -61,7 +61,7 @@
         total: int = stats[m_type]  # type: ignore
         for module in module_stats.keys():
             mod_total = module_stats[module][m_type]
-            percent = 0 if total == 0 else float((mod_total) * 100) / total
+            percent = 0 if total == 0 else float(mod_total * 100) / total
             by_mod[module][m_type] = percent
     sorted_result = []
     for module, mod_info in by_mod.items():
@@ -86,4 +86,4 @@
             lines.append(f"{val:.2f}")
     if len(lines) == 5:
         raise exceptions.EmptyReportError()
-    sect.append(report_nodes.Table(children=lines, cols=5, rheaders=1))
+    sect.append(Table(children=lines, cols=5, rheaders=1))
diff --git a/pylint/reporters/__init__.py b/pylint/reporters/__init__.py
index 79b13e0..39cf5fb 100644
--- a/pylint/reporters/__init__.py
+++ b/pylint/reporters/__init__.py
@@ -21,7 +21,7 @@
 # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
 
 """utilities methods and classes for reporters"""
-
+from typing import TYPE_CHECKING
 
 from pylint import utils
 from pylint.reporters.base_reporter import BaseReporter
@@ -30,10 +30,13 @@
 from pylint.reporters.multi_reporter import MultiReporter
 from pylint.reporters.reports_handler_mix_in import ReportsHandlerMixIn
 
+if TYPE_CHECKING:
+    from pylint.lint.pylinter import PyLinter
 
-def initialize(linter):
+
+def initialize(linter: "PyLinter") -> None:
     """initialize linter with reporters in this package"""
-    utils.register_plugins(linter, __path__[0])
+    utils.register_plugins(linter, __path__[0])  # type: ignore # Fixed in https://github.com/python/mypy/pull/9454
 
 
 __all__ = [
diff --git a/pylint/reporters/collecting_reporter.py b/pylint/reporters/collecting_reporter.py
index 309c604..145c3c8 100644
--- a/pylint/reporters/collecting_reporter.py
+++ b/pylint/reporters/collecting_reporter.py
@@ -8,11 +8,11 @@
 
     name = "collector"
 
-    def __init__(self):
+    def __init__(self) -> None:
         BaseReporter.__init__(self)
         self.messages = []
 
-    def reset(self):
+    def reset(self) -> None:
         self.messages = []
 
     _display = None
diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py
index 245c10f..2ac361b 100644
--- a/pylint/reporters/multi_reporter.py
+++ b/pylint/reporters/multi_reporter.py
@@ -43,7 +43,7 @@
 
         self.set_output(output)
 
-    def __del__(self):
+    def __del__(self) -> None:
         self.close_output_files()
 
     @property
diff --git a/pylint/reporters/reports_handler_mix_in.py b/pylint/reporters/reports_handler_mix_in.py
index 450d383..de3bbe2 100644
--- a/pylint/reporters/reports_handler_mix_in.py
+++ b/pylint/reporters/reports_handler_mix_in.py
@@ -2,9 +2,10 @@
 # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
 
 import collections
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Tuple
 
 from pylint.exceptions import EmptyReportError
+from pylint.interfaces import IChecker
 from pylint.reporters.ureports.nodes import Section
 from pylint.typing import CheckerStats
 
@@ -17,9 +18,11 @@
     related methods for the main lint class
     """
 
-    def __init__(self):
-        self._reports = collections.defaultdict(list)
-        self._reports_state = {}
+    def __init__(self) -> None:
+        self._reports: DefaultDict[
+            IChecker, List[Tuple[str, str, Callable]]
+        ] = collections.defaultdict(list)
+        self._reports_state: Dict[str, bool] = {}
 
     def report_order(self):
         """Return a list of reports, sorted in the order
@@ -27,7 +30,9 @@
         """
         return list(self._reports)
 
-    def register_report(self, reportid, r_title, r_cb, checker):
+    def register_report(
+        self, reportid: str, r_title: str, r_cb: Callable, checker: IChecker
+    ) -> None:
         """register a report
 
         reportid is the unique identifier for the report
@@ -38,17 +43,17 @@
         reportid = reportid.upper()
         self._reports[checker].append((reportid, r_title, r_cb))
 
-    def enable_report(self, reportid):
+    def enable_report(self, reportid: str) -> None:
         """disable the report of the given id"""
         reportid = reportid.upper()
         self._reports_state[reportid] = True
 
-    def disable_report(self, reportid):
+    def disable_report(self, reportid: str) -> None:
         """disable the report of the given id"""
         reportid = reportid.upper()
         self._reports_state[reportid] = False
 
-    def report_is_enabled(self, reportid):
+    def report_is_enabled(self, reportid: str) -> bool:
         """return true if the report associated to the given identifier is
         enabled
         """
@@ -58,7 +63,7 @@
         self: "PyLinter",
         stats: CheckerStats,
         old_stats: CheckerStats,
-    ):
+    ) -> Section:
         """render registered reports"""
         sect = Section("Report", f"{self.stats['statement']} statements analysed.")
         for checker in self.report_order():
diff --git a/pylint/reporters/ureports/base_writer.py b/pylint/reporters/ureports/base_writer.py
index f0651f9..a7fd167 100644
--- a/pylint/reporters/ureports/base_writer.py
+++ b/pylint/reporters/ureports/base_writer.py
@@ -18,7 +18,15 @@
 import os
 import sys
 from io import StringIO
-from typing import Iterator, TextIO
+from typing import TYPE_CHECKING, Iterator, List, TextIO, Union
+
+if TYPE_CHECKING:
+    from pylint.reporters.ureports.nodes import (
+        EvaluationSection,
+        Paragraph,
+        Section,
+        Table,
+    )
 
 
 class BaseWriter:
@@ -39,34 +47,36 @@
         layout.accept(self)
         self.end_format()
 
-    def format_children(self, layout):
+    def format_children(
+        self, layout: Union["EvaluationSection", "Paragraph", "Section"]
+    ) -> None:
         """recurse on the layout children and call their accept method
         (see the Visitor pattern)
         """
         for child in getattr(layout, "children", ()):
             child.accept(self)
 
-    def writeln(self, string=""):
+    def writeln(self, string: str = "") -> None:
         """write a line in the output buffer"""
         self.write(string + os.linesep)
 
-    def write(self, string):
+    def write(self, string: str) -> None:
         """write a string in the output buffer"""
         self.out.write(string)
 
-    def begin_format(self):
+    def begin_format(self) -> None:
         """begin to format a layout"""
         self.section = 0
 
-    def end_format(self):
+    def end_format(self) -> None:
         """finished to format a layout"""
 
-    def get_table_content(self, table):
+    def get_table_content(self, table: "Table") -> List[List[str]]:
         """trick to get table content without actually writing it
 
         return an aligned list of lists containing table cells values as string
         """
-        result = [[]]
+        result: List[List[str]] = [[]]
         cols = table.cols
         for cell in self.compute_content(table):
             if cols == 0:
diff --git a/pylint/reporters/ureports/nodes.py b/pylint/reporters/ureports/nodes.py
index 0288761..1a04057 100644
--- a/pylint/reporters/ureports/nodes.py
+++ b/pylint/reporters/ureports/nodes.py
@@ -14,21 +14,21 @@
 
 A micro report is a tree of layout and content objects.
 """
-from typing import Optional
+from typing import Any, Iterable, Iterator, List, Optional, Union
+
+from pylint.reporters.ureports.text_writer import TextWriter
 
 
 class VNode:
-    def __init__(self, nid=None):
-        self.id = nid
-        # navigation
-        self.parent = None
-        self.children = []
-        self.visitor_name = self.__class__.__name__.lower()
+    def __init__(self) -> None:
+        self.parent: Optional["BaseLayout"] = None
+        self.children: List["VNode"] = []
+        self.visitor_name: str = self.__class__.__name__.lower()
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator["VNode"]:
         return iter(self.children)
 
-    def accept(self, visitor, *args, **kwargs):
+    def accept(self, visitor: TextWriter, *args: Any, **kwargs: Any) -> None:
         func = getattr(visitor, f"visit_{self.visitor_name}")
         return func(self, *args, **kwargs)
 
@@ -44,8 +44,8 @@
     * children : components in this table (i.e. the table's cells)
     """
 
-    def __init__(self, children=(), **kwargs):
-        super().__init__(**kwargs)
+    def __init__(self, children: Iterable[Union["Text", str]] = ()) -> None:
+        super().__init__()
         for child in children:
             if isinstance(child, VNode):
                 self.append(child)
@@ -63,14 +63,14 @@
         self.children.insert(index, child)
         child.parent = self
 
-    def parents(self):
+    def parents(self) -> List["BaseLayout"]:
         """return the ancestor nodes"""
         assert self.parent is not self
         if self.parent is None:
             return []
         return [self.parent] + self.parent.parents()
 
-    def add_text(self, text):
+    def add_text(self, text: str) -> None:
         """shortcut to add text data"""
         self.children.append(Text(text))
 
@@ -85,11 +85,8 @@
     * data : the text value as an encoded or unicode string
     """
 
-    def __init__(self, data, escaped=True, **kwargs):
-        super().__init__(**kwargs)
-        # if isinstance(data, unicode):
-        #    data = data.encode('ascii')
-        assert isinstance(data, str), data.__class__
+    def __init__(self, data: str, escaped: bool = True) -> None:
+        super().__init__()
         self.escaped = escaped
         self.data = data
 
@@ -117,22 +114,28 @@
     as a first paragraph
     """
 
-    def __init__(self, title=None, description=None, **kwargs):
-        super().__init__(**kwargs)
+    def __init__(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        children: Iterable[Union["Text", str]] = (),
+    ) -> None:
+        super().__init__(children=children)
         if description:
             self.insert(0, Paragraph([Text(description)]))
         if title:
             self.insert(0, Title(children=(title,)))
-        self.report_id: Optional[str] = None
+        self.report_id: str = ""  # Used in ReportHandlerMixin.make_reports
 
 
 class EvaluationSection(Section):
-    def __init__(self, message, **kwargs):
-        super().__init__(**kwargs)
+    def __init__(
+        self, message: str, children: Iterable[Union["Text", str]] = ()
+    ) -> None:
+        super().__init__(children=children)
         title = Paragraph()
         title.append(Text("-" * len(message)))
         self.append(title)
-
         message_body = Paragraph()
         message_body.append(Text(message))
         self.append(message_body)
@@ -169,8 +172,15 @@
     * title : the table's optional title
     """
 
-    def __init__(self, cols, title=None, rheaders=0, cheaders=0, **kwargs):
-        super().__init__(**kwargs)
+    def __init__(
+        self,
+        cols: int,
+        title: Optional[str] = None,
+        rheaders: int = 0,
+        cheaders: int = 0,
+        children: Iterable[Union["Text", str]] = (),
+    ) -> None:
+        super().__init__(children=children)
         assert isinstance(cols, int)
         self.cols = cols
         self.title = title
diff --git a/pylint/reporters/ureports/text_writer.py b/pylint/reporters/ureports/text_writer.py
index a48d73a..a00392b 100644
--- a/pylint/reporters/ureports/text_writer.py
+++ b/pylint/reporters/ureports/text_writer.py
@@ -11,7 +11,20 @@
 
 """Text formatting drivers for ureports"""
 
-from pylint.reporters.ureports import BaseWriter
+from typing import TYPE_CHECKING, List
+
+from pylint.reporters.ureports.base_writer import BaseWriter
+
+if TYPE_CHECKING:
+    from pylint.reporters.ureports.nodes import (
+        EvaluationSection,
+        Paragraph,
+        Section,
+        Table,
+        Text,
+        Title,
+        VerbatimText,
+    )
 
 TITLE_UNDERLINES = ["", "=", "-", "`", ".", "~", "^"]
 BULLETS = ["*", "-"]
@@ -22,11 +35,11 @@
     (ReStructured inspiration but not totally handled yet)
     """
 
-    def begin_format(self):
-        super().begin_format()
+    def __init__(self):
+        super().__init__()
         self.list_level = 0
 
-    def visit_section(self, layout):
+    def visit_section(self, layout: "Section") -> None:
         """display a section as text"""
         self.section += 1
         self.writeln()
@@ -34,14 +47,14 @@
         self.section -= 1
         self.writeln()
 
-    def visit_evaluationsection(self, layout):
+    def visit_evaluationsection(self, layout: "EvaluationSection") -> None:
         """Display an evaluation section as a text."""
         self.section += 1
         self.format_children(layout)
         self.section -= 1
         self.writeln()
 
-    def visit_title(self, layout):
+    def visit_title(self, layout: "Title") -> None:
         title = "".join(list(self.compute_content(layout)))
         self.writeln(title)
         try:
@@ -49,12 +62,12 @@
         except IndexError:
             print("FIXME TITLE TOO DEEP. TURNING TITLE INTO TEXT")
 
-    def visit_paragraph(self, layout):
+    def visit_paragraph(self, layout: "Paragraph") -> None:
         """enter a paragraph"""
         self.format_children(layout)
         self.writeln()
 
-    def visit_table(self, layout):
+    def visit_table(self, layout: "Table") -> None:
         """display a table as text"""
         table_content = self.get_table_content(layout)
         # get columns width
@@ -65,33 +78,36 @@
         self.default_table(layout, table_content, cols_width)
         self.writeln()
 
-    def default_table(self, layout, table_content, cols_width):
+    def default_table(
+        self, layout: "Table", table_content: List[List[str]], cols_width: List[int]
+    ) -> None:
         """format a table"""
         cols_width = [size + 1 for size in cols_width]
         format_strings = " ".join(["%%-%ss"] * len(cols_width))
-        format_strings = format_strings % tuple(cols_width)
-        format_strings = format_strings.split(" ")
+        format_strings %= tuple(cols_width)
+
         table_linesep = "\n+" + "+".join("-" * w for w in cols_width) + "+\n"
         headsep = "\n+" + "+".join("=" * w for w in cols_width) + "+\n"
 
         self.write(table_linesep)
+        split_strings = format_strings.split(" ")
         for index, line in enumerate(table_content):
             self.write("|")
             for line_index, at_index in enumerate(line):
-                self.write(format_strings[line_index] % at_index)
+                self.write(split_strings[line_index] % at_index)
                 self.write("|")
             if index == 0 and layout.rheaders:
                 self.write(headsep)
             else:
                 self.write(table_linesep)
 
-    def visit_verbatimtext(self, layout):
+    def visit_verbatimtext(self, layout: "VerbatimText") -> None:
         """display a verbatim layout as text (so difficult ;)"""
         self.writeln("::\n")
         for line in layout.data.splitlines():
             self.writeln("    " + line)
         self.writeln()
 
-    def visit_text(self, layout):
+    def visit_text(self, layout: "Text") -> None:
         """add some text"""
         self.write(f"{layout.data}")