base_checker.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import abc
  6. import functools
  7. from collections.abc import Iterable, Sequence
  8. from inspect import cleandoc
  9. from tokenize import TokenInfo
  10. from typing import TYPE_CHECKING, Any
  11. from astroid import nodes
  12. from pylint.config.arguments_provider import _ArgumentsProvider
  13. from pylint.constants import _MSG_ORDER, MAIN_CHECKER_NAME, WarningScope
  14. from pylint.exceptions import InvalidMessageError
  15. from pylint.interfaces import Confidence
  16. from pylint.message.message_definition import MessageDefinition
  17. from pylint.typing import (
  18. ExtraMessageOptions,
  19. MessageDefinitionTuple,
  20. OptionDict,
  21. Options,
  22. ReportsCallable,
  23. )
  24. from pylint.utils import get_rst_section, get_rst_title
  25. if TYPE_CHECKING:
  26. from pylint.lint import PyLinter
  27. @functools.total_ordering
  28. class BaseChecker(_ArgumentsProvider):
  29. # checker name (you may reuse an existing one)
  30. name: str = ""
  31. # ordered list of options to control the checker behaviour
  32. options: Options = ()
  33. # messages issued by this checker
  34. msgs: dict[str, MessageDefinitionTuple] = {}
  35. # reports issued by this checker
  36. reports: tuple[tuple[str, str, ReportsCallable], ...] = ()
  37. # mark this checker as enabled or not.
  38. enabled: bool = True
  39. def __init__(self, linter: PyLinter) -> None:
  40. """Checker instances should have the linter as argument."""
  41. if self.name is not None:
  42. self.name = self.name.lower()
  43. self.linter = linter
  44. _ArgumentsProvider.__init__(self, linter)
  45. def __gt__(self, other: Any) -> bool:
  46. """Permits sorting checkers for stable doc and tests.
  47. The main checker is always the first one, then builtin checkers in alphabetical
  48. order, then extension checkers in alphabetical order.
  49. """
  50. if not isinstance(other, BaseChecker):
  51. return False
  52. if self.name == MAIN_CHECKER_NAME:
  53. return False
  54. if other.name == MAIN_CHECKER_NAME:
  55. return True
  56. self_is_builtin = type(self).__module__.startswith("pylint.checkers")
  57. if self_is_builtin ^ type(other).__module__.startswith("pylint.checkers"):
  58. return not self_is_builtin
  59. return self.name > other.name
  60. def __eq__(self, other: object) -> bool:
  61. """Permit to assert Checkers are equal."""
  62. if not isinstance(other, BaseChecker):
  63. return False
  64. return f"{self.name}{self.msgs}" == f"{other.name}{other.msgs}"
  65. def __hash__(self) -> int:
  66. """Make Checker hashable."""
  67. return hash(f"{self.name}{self.msgs}")
  68. def __repr__(self) -> str:
  69. status = "Checker" if self.enabled else "Disabled checker"
  70. msgs = "', '".join(self.msgs.keys())
  71. return f"{status} '{self.name}' (responsible for '{msgs}')"
  72. def __str__(self) -> str:
  73. """This might be incomplete because multiple classes inheriting BaseChecker
  74. can have the same name.
  75. See: MessageHandlerMixIn.get_full_documentation()
  76. """
  77. return self.get_full_documentation(
  78. msgs=self.msgs, options=self._options_and_values(), reports=self.reports
  79. )
  80. def get_full_documentation(
  81. self,
  82. msgs: dict[str, MessageDefinitionTuple],
  83. options: Iterable[tuple[str, OptionDict, Any]],
  84. reports: Sequence[tuple[str, str, ReportsCallable]],
  85. doc: str | None = None,
  86. module: str | None = None,
  87. show_options: bool = True,
  88. ) -> str:
  89. result = ""
  90. checker_title = f"{self.name.replace('_', ' ').title()} checker"
  91. if module:
  92. # Provide anchor to link against
  93. result += f".. _{module}:\n\n"
  94. result += f"{get_rst_title(checker_title, '~')}\n"
  95. if module:
  96. result += f"This checker is provided by ``{module}``.\n"
  97. result += f"Verbatim name of the checker is ``{self.name}``.\n\n"
  98. if doc:
  99. # Provide anchor to link against
  100. result += get_rst_title(f"{checker_title} Documentation", "^")
  101. result += f"{cleandoc(doc)}\n\n"
  102. # options might be an empty generator and not be False when cast to boolean
  103. options_list = list(options)
  104. if options_list:
  105. if show_options:
  106. result += get_rst_title(f"{checker_title} Options", "^")
  107. result += f"{get_rst_section(None, options_list)}\n"
  108. else:
  109. result += f"See also :ref:`{self.name} checker's options' documentation <{self.name}-options>`\n\n"
  110. if msgs:
  111. result += get_rst_title(f"{checker_title} Messages", "^")
  112. for msgid, msg in sorted(
  113. msgs.items(), key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[0])
  114. ):
  115. msg_def = self.create_message_definition_from_tuple(msgid, msg)
  116. result += f"{msg_def.format_help(checkerref=False)}\n"
  117. result += "\n"
  118. if reports:
  119. result += get_rst_title(f"{checker_title} Reports", "^")
  120. for report in reports:
  121. result += f":{report[0]}: {report[1]}\n"
  122. result += "\n"
  123. result += "\n"
  124. return result
  125. def add_message(
  126. self,
  127. msgid: str,
  128. line: int | None = None,
  129. node: nodes.NodeNG | None = None,
  130. args: Any = None,
  131. confidence: Confidence | None = None,
  132. col_offset: int | None = None,
  133. end_lineno: int | None = None,
  134. end_col_offset: int | None = None,
  135. ) -> None:
  136. self.linter.add_message(
  137. msgid, line, node, args, confidence, col_offset, end_lineno, end_col_offset
  138. )
  139. def check_consistency(self) -> None:
  140. """Check the consistency of msgid.
  141. msg ids for a checker should be a string of len 4, where the two first
  142. characters are the checker id and the two last the msg id in this
  143. checker.
  144. :raises InvalidMessageError: If the checker id in the messages are not
  145. always the same.
  146. """
  147. checker_id = None
  148. existing_ids = []
  149. for message in self.messages:
  150. # Id's for shared messages such as the 'deprecated-*' messages
  151. # can be inconsistent with their checker id.
  152. if message.shared:
  153. continue
  154. if checker_id is not None and checker_id != message.msgid[1:3]:
  155. error_msg = "Inconsistent checker part in message id "
  156. error_msg += f"'{message.msgid}' (expected 'x{checker_id}xx' "
  157. error_msg += f"because we already had {existing_ids})."
  158. raise InvalidMessageError(error_msg)
  159. checker_id = message.msgid[1:3]
  160. existing_ids.append(message.msgid)
  161. def create_message_definition_from_tuple(
  162. self, msgid: str, msg_tuple: MessageDefinitionTuple
  163. ) -> MessageDefinition:
  164. if isinstance(self, (BaseTokenChecker, BaseRawFileChecker)):
  165. default_scope = WarningScope.LINE
  166. else:
  167. default_scope = WarningScope.NODE
  168. options: ExtraMessageOptions = {}
  169. if len(msg_tuple) == 4:
  170. (msg, symbol, descr, msg_options) = msg_tuple
  171. options = ExtraMessageOptions(**msg_options)
  172. elif len(msg_tuple) == 3:
  173. (msg, symbol, descr) = msg_tuple
  174. else:
  175. error_msg = """Messages should have a msgid, a symbol and a description. Something like this :
  176. "W1234": (
  177. "message",
  178. "message-symbol",
  179. "Message description with detail.",
  180. ...
  181. ),
  182. """
  183. raise InvalidMessageError(error_msg)
  184. options.setdefault("scope", default_scope)
  185. return MessageDefinition(self, msgid, msg, descr, symbol, **options)
  186. @property
  187. def messages(self) -> list[MessageDefinition]:
  188. return [
  189. self.create_message_definition_from_tuple(msgid, msg_tuple)
  190. for msgid, msg_tuple in sorted(self.msgs.items())
  191. ]
  192. def open(self) -> None:
  193. """Called before visiting project (i.e. set of modules)."""
  194. def close(self) -> None:
  195. """Called after visiting project (i.e set of modules)."""
  196. def get_map_data(self) -> Any:
  197. return None
  198. # pylint: disable-next=unused-argument
  199. def reduce_map_data(self, linter: PyLinter, data: list[Any]) -> None:
  200. return None
  201. class BaseTokenChecker(BaseChecker):
  202. """Base class for checkers that want to have access to the token stream."""
  203. @abc.abstractmethod
  204. def process_tokens(self, tokens: list[TokenInfo]) -> None:
  205. """Should be overridden by subclasses."""
  206. raise NotImplementedError()
  207. class BaseRawFileChecker(BaseChecker):
  208. """Base class for checkers which need to parse the raw file."""
  209. @abc.abstractmethod
  210. def process_module(self, node: nodes.Module) -> None:
  211. """Process a module.
  212. The module's content is accessible via ``astroid.stream``
  213. """
  214. raise NotImplementedError()