base_checker.py 11 KB

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