base_checker.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. # Copyright (c) 2006-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  2. # Copyright (c) 2013-2014 Google, Inc.
  3. # Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
  4. # Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
  5. # Copyright (c) 2014 Brett Cannon <brett@python.org>
  6. # Copyright (c) 2014 Arun Persaud <arun@nubati.net>
  7. # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
  8. # Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
  9. # Copyright (c) 2017-2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
  10. # Copyright (c) 2018-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  11. # Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
  12. # Copyright (c) 2019 Bruno P. Kinoshita <kinow@users.noreply.github.com>
  13. # Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
  14. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  15. # Copyright (c) 2021 bot <bot@noreply.github.com>
  16. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  17. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  18. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  19. import functools
  20. from inspect import cleandoc
  21. from typing import Any, Optional
  22. from astroid import nodes
  23. from pylint.config import OptionsProviderMixIn
  24. from pylint.constants import _MSG_ORDER, WarningScope
  25. from pylint.exceptions import InvalidMessageError
  26. from pylint.interfaces import Confidence, IRawChecker, ITokenChecker, implements
  27. from pylint.message.message_definition import MessageDefinition
  28. from pylint.utils import get_rst_section, get_rst_title
  29. @functools.total_ordering
  30. class BaseChecker(OptionsProviderMixIn):
  31. # checker name (you may reuse an existing one)
  32. name: str = ""
  33. # options level (0 will be displaying in --help, 1 in --long-help)
  34. level = 1
  35. # ordered list of options to control the checker behaviour
  36. options: Any = ()
  37. # messages issued by this checker
  38. msgs: Any = {}
  39. # reports issued by this checker
  40. reports: Any = ()
  41. # mark this checker as enabled or not.
  42. enabled: bool = True
  43. def __init__(self, linter=None):
  44. """checker instances should have the linter as argument
  45. :param ILinter linter: is an object implementing ILinter."""
  46. if self.name is not None:
  47. self.name = self.name.lower()
  48. super().__init__()
  49. self.linter = linter
  50. def __gt__(self, other):
  51. """Permit to sort a list of Checker by name."""
  52. return f"{self.name}{self.msgs}".__gt__(f"{other.name}{other.msgs}")
  53. def __repr__(self):
  54. status = "Checker" if self.enabled else "Disabled checker"
  55. msgs = "', '".join(self.msgs.keys())
  56. return f"{status} '{self.name}' (responsible for '{msgs}')"
  57. def __str__(self):
  58. """This might be incomplete because multiple class inheriting BaseChecker
  59. can have the same name. Cf MessageHandlerMixIn.get_full_documentation()"""
  60. return self.get_full_documentation(
  61. msgs=self.msgs, options=self.options_and_values(), reports=self.reports
  62. )
  63. def get_full_documentation(self, msgs, options, reports, doc=None, module=None):
  64. result = ""
  65. checker_title = f"{self.name.replace('_', ' ').title()} checker"
  66. if module:
  67. # Provide anchor to link against
  68. result += f".. _{module}:\n\n"
  69. result += f"{get_rst_title(checker_title, '~')}\n"
  70. if module:
  71. result += f"This checker is provided by ``{module}``.\n"
  72. result += f"Verbatim name of the checker is ``{self.name}``.\n\n"
  73. if doc:
  74. # Provide anchor to link against
  75. result += get_rst_title(f"{checker_title} Documentation", "^")
  76. result += f"{cleandoc(doc)}\n\n"
  77. # options might be an empty generator and not be False when casted to boolean
  78. options = list(options)
  79. if options:
  80. result += get_rst_title(f"{checker_title} Options", "^")
  81. result += f"{get_rst_section(None, options)}\n"
  82. if msgs:
  83. result += get_rst_title(f"{checker_title} Messages", "^")
  84. for msgid, msg in sorted(
  85. msgs.items(), key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1])
  86. ):
  87. msg = self.create_message_definition_from_tuple(msgid, msg)
  88. result += f"{msg.format_help(checkerref=False)}\n"
  89. result += "\n"
  90. if reports:
  91. result += get_rst_title(f"{checker_title} Reports", "^")
  92. for report in reports:
  93. result += (
  94. ":%s: %s\n" % report[:2] # pylint: disable=consider-using-f-string
  95. )
  96. result += "\n"
  97. result += "\n"
  98. return result
  99. def add_message(
  100. self,
  101. msgid: str,
  102. line: Optional[int] = None,
  103. node: Optional[nodes.NodeNG] = None,
  104. args: Any = None,
  105. confidence: Optional[Confidence] = None,
  106. col_offset: Optional[int] = None,
  107. end_lineno: Optional[int] = None,
  108. end_col_offset: Optional[int] = None,
  109. ) -> None:
  110. self.linter.add_message(
  111. msgid, line, node, args, confidence, col_offset, end_lineno, end_col_offset
  112. )
  113. def check_consistency(self):
  114. """Check the consistency of msgid.
  115. msg ids for a checker should be a string of len 4, where the two first
  116. characters are the checker id and the two last the msg id in this
  117. checker.
  118. :raises InvalidMessageError: If the checker id in the messages are not
  119. always the same."""
  120. checker_id = None
  121. existing_ids = []
  122. for message in self.messages:
  123. if checker_id is not None and checker_id != message.msgid[1:3]:
  124. error_msg = "Inconsistent checker part in message id "
  125. error_msg += f"'{message.msgid}' (expected 'x{checker_id}xx' "
  126. error_msg += f"because we already had {existing_ids})."
  127. raise InvalidMessageError(error_msg)
  128. checker_id = message.msgid[1:3]
  129. existing_ids.append(message.msgid)
  130. def create_message_definition_from_tuple(self, msgid, msg_tuple):
  131. if implements(self, (IRawChecker, ITokenChecker)):
  132. default_scope = WarningScope.LINE
  133. else:
  134. default_scope = WarningScope.NODE
  135. options = {}
  136. if len(msg_tuple) > 3:
  137. (msg, symbol, descr, options) = msg_tuple
  138. elif len(msg_tuple) > 2:
  139. (msg, symbol, descr) = msg_tuple
  140. else:
  141. error_msg = """Messages should have a msgid and a symbol. Something like this :
  142. "W1234": (
  143. "message",
  144. "message-symbol",
  145. "Message description with detail.",
  146. ...
  147. ),
  148. """
  149. raise InvalidMessageError(error_msg)
  150. options.setdefault("scope", default_scope)
  151. return MessageDefinition(self, msgid, msg, descr, symbol, **options)
  152. @property
  153. def messages(self) -> list:
  154. return [
  155. self.create_message_definition_from_tuple(msgid, msg_tuple)
  156. for msgid, msg_tuple in sorted(self.msgs.items())
  157. ]
  158. # dummy methods implementing the IChecker interface
  159. def get_message_definition(self, msgid):
  160. for message_definition in self.messages:
  161. if message_definition.msgid == msgid:
  162. return message_definition
  163. error_msg = f"MessageDefinition for '{msgid}' does not exists. "
  164. error_msg += f"Choose from {[m.msgid for m in self.messages]}."
  165. raise InvalidMessageError(error_msg)
  166. def open(self):
  167. """called before visiting project (i.e set of modules)"""
  168. def close(self):
  169. """called after visiting project (i.e set of modules)"""
  170. class BaseTokenChecker(BaseChecker):
  171. """Base class for checkers that want to have access to the token stream."""
  172. def process_tokens(self, tokens):
  173. """Should be overridden by subclasses."""
  174. raise NotImplementedError()