misc.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. # Copyright (c) 2006, 2009-2013 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  2. # Copyright (c) 2012-2014 Google, Inc.
  3. # Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
  4. # Copyright (c) 2014 Brett Cannon <brett@python.org>
  5. # Copyright (c) 2014 Alexandru Coman <fcoman@bitdefender.com>
  6. # Copyright (c) 2014 Arun Persaud <arun@nubati.net>
  7. # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
  8. # Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
  9. # Copyright (c) 2016 glegoux <gilles.legoux@gmail.com>
  10. # Copyright (c) 2017-2020 hippo91 <guillaume.peillex@gmail.com>
  11. # Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
  12. # Copyright (c) 2018 Rogalski, Lukasz <lukasz.rogalski@intel.com>
  13. # Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
  14. # Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
  15. # Copyright (c) 2019-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  16. # Copyright (c) 2020 wtracy <afishionado@gmail.com>
  17. # Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
  18. # Copyright (c) 2020 Benny <benny.mueller91@gmail.com>
  19. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  20. # Copyright (c) 2021 Nick Drozd <nicholasdrozd@gmail.com>
  21. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  22. # Copyright (c) 2021 Konstantina Saketou <56515303+ksaketou@users.noreply.github.com>
  23. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  24. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  25. """Check source code is ascii only or has an encoding declaration (PEP 263)"""
  26. import re
  27. import tokenize
  28. from typing import List, Optional
  29. from astroid import nodes
  30. from pylint.checkers import BaseChecker
  31. from pylint.interfaces import IRawChecker, ITokenChecker
  32. from pylint.typing import ManagedMessage
  33. from pylint.utils.pragma_parser import OPTION_PO, PragmaParserError, parse_pragma
  34. class ByIdManagedMessagesChecker(BaseChecker):
  35. """Checks for messages that are enabled or disabled by id instead of symbol."""
  36. __implements__ = IRawChecker
  37. name = "miscellaneous"
  38. msgs = {
  39. "I0023": (
  40. "%s",
  41. "use-symbolic-message-instead",
  42. "Used when a message is enabled or disabled by id.",
  43. )
  44. }
  45. options = ()
  46. def _clear_by_id_managed_msgs(self) -> None:
  47. self.linter._by_id_managed_msgs.clear()
  48. def _get_by_id_managed_msgs(self) -> List[ManagedMessage]:
  49. return self.linter._by_id_managed_msgs
  50. def process_module(self, node: nodes.Module) -> None:
  51. """Inspect the source file to find messages activated or deactivated by id."""
  52. managed_msgs = self._get_by_id_managed_msgs()
  53. for (mod_name, msgid, symbol, lineno, is_disabled) in managed_msgs:
  54. if mod_name == node.name:
  55. verb = "disable" if is_disabled else "enable"
  56. txt = f"'{msgid}' is cryptic: use '# pylint: {verb}={symbol}' instead"
  57. self.add_message("use-symbolic-message-instead", line=lineno, args=txt)
  58. self._clear_by_id_managed_msgs()
  59. class EncodingChecker(BaseChecker):
  60. """checks for:
  61. * warning notes in the code like FIXME, XXX
  62. * encoding issues.
  63. """
  64. __implements__ = (IRawChecker, ITokenChecker)
  65. # configuration section name
  66. name = "miscellaneous"
  67. msgs = {
  68. "W0511": (
  69. "%s",
  70. "fixme",
  71. "Used when a warning note as FIXME or XXX is detected.",
  72. )
  73. }
  74. options = (
  75. (
  76. "notes",
  77. {
  78. "type": "csv",
  79. "metavar": "<comma separated values>",
  80. "default": ("FIXME", "XXX", "TODO"),
  81. "help": (
  82. "List of note tags to take in consideration, "
  83. "separated by a comma."
  84. ),
  85. },
  86. ),
  87. (
  88. "notes-rgx",
  89. {
  90. "type": "string",
  91. "metavar": "<regexp>",
  92. "help": "Regular expression of note tags to take in consideration.",
  93. },
  94. ),
  95. )
  96. def open(self):
  97. super().open()
  98. notes = "|".join(re.escape(note) for note in self.config.notes)
  99. if self.config.notes_rgx:
  100. regex_string = fr"#\s*({notes}|{self.config.notes_rgx})\b"
  101. else:
  102. regex_string = fr"#\s*({notes})\b"
  103. self._fixme_pattern = re.compile(regex_string, re.I)
  104. def _check_encoding(
  105. self, lineno: int, line: bytes, file_encoding: str
  106. ) -> Optional[str]:
  107. try:
  108. return line.decode(file_encoding)
  109. except UnicodeDecodeError:
  110. pass
  111. except LookupError:
  112. if (
  113. line.startswith(b"#")
  114. and "coding" in str(line)
  115. and file_encoding in str(line)
  116. ):
  117. msg = f"Cannot decode using encoding '{file_encoding}', bad encoding"
  118. self.add_message("syntax-error", line=lineno, args=msg)
  119. return None
  120. def process_module(self, node: nodes.Module) -> None:
  121. """inspect the source file to find encoding problem"""
  122. encoding = node.file_encoding if node.file_encoding else "ascii"
  123. with node.stream() as stream:
  124. for lineno, line in enumerate(stream):
  125. self._check_encoding(lineno + 1, line, encoding)
  126. def process_tokens(self, tokens):
  127. """inspect the source to find fixme problems"""
  128. if not self.config.notes:
  129. return
  130. comments = (
  131. token_info for token_info in tokens if token_info.type == tokenize.COMMENT
  132. )
  133. for comment in comments:
  134. comment_text = comment.string[1:].lstrip() # trim '#' and whitespaces
  135. # handle pylint disable clauses
  136. disable_option_match = OPTION_PO.search(comment_text)
  137. if disable_option_match:
  138. try:
  139. values = []
  140. try:
  141. for pragma_repr in (
  142. p_rep
  143. for p_rep in parse_pragma(disable_option_match.group(2))
  144. if p_rep.action == "disable"
  145. ):
  146. values.extend(pragma_repr.messages)
  147. except PragmaParserError:
  148. # Printing useful information dealing with this error is done in the lint package
  149. pass
  150. if set(values) & set(self.config.notes):
  151. continue
  152. except ValueError:
  153. self.add_message(
  154. "bad-inline-option",
  155. args=disable_option_match.group(1).strip(),
  156. line=comment.start[0],
  157. )
  158. continue
  159. # emit warnings if necessary
  160. match = self._fixme_pattern.search("#" + comment_text.lower())
  161. if match:
  162. self.add_message(
  163. "fixme",
  164. col_offset=comment.start[1] + 1,
  165. args=comment_text,
  166. line=comment.start[0],
  167. )
  168. def register(linter):
  169. """required method to auto register this checker"""
  170. linter.register_checker(EncodingChecker(linter))
  171. linter.register_checker(ByIdManagedMessagesChecker(linter))