message_state_handler.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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 sys
  6. import tokenize
  7. from collections import defaultdict
  8. from typing import TYPE_CHECKING
  9. from pylint import exceptions, interfaces
  10. from pylint.constants import (
  11. MSG_STATE_CONFIDENCE,
  12. MSG_STATE_SCOPE_CONFIG,
  13. MSG_STATE_SCOPE_MODULE,
  14. MSG_TYPES,
  15. MSG_TYPES_LONG,
  16. )
  17. from pylint.interfaces import HIGH
  18. from pylint.message import MessageDefinition
  19. from pylint.typing import ManagedMessage
  20. from pylint.utils.pragma_parser import (
  21. OPTION_PO,
  22. InvalidPragmaError,
  23. UnRecognizedOptionError,
  24. parse_pragma,
  25. )
  26. if sys.version_info >= (3, 8):
  27. from typing import Literal
  28. else:
  29. from typing_extensions import Literal
  30. if TYPE_CHECKING:
  31. from pylint.lint.pylinter import PyLinter
  32. class _MessageStateHandler:
  33. """Class that handles message disabling & enabling and processing of inline
  34. pragma's.
  35. """
  36. def __init__(self, linter: PyLinter) -> None:
  37. self.linter = linter
  38. self._msgs_state: dict[str, bool] = {}
  39. self._options_methods = {
  40. "enable": self.enable,
  41. "disable": self.disable,
  42. "disable-next": self.disable_next,
  43. }
  44. self._bw_options_methods = {
  45. "disable-msg": self._options_methods["disable"],
  46. "enable-msg": self._options_methods["enable"],
  47. }
  48. self._pragma_lineno: dict[str, int] = {}
  49. # TODO: 3.0: Update key type to str when current_name is always str
  50. self._stashed_messages: defaultdict[
  51. tuple[str | None, str], list[tuple[str | None, str]]
  52. ] = defaultdict(list)
  53. """Some messages in the options (for --enable and --disable) are encountered
  54. too early to warn about them.
  55. i.e. before all option providers have been fully parsed. Thus, this dict stores
  56. option_value and msg_id needed to (later) emit the messages keyed on module names.
  57. """
  58. def _set_one_msg_status(
  59. self, scope: str, msg: MessageDefinition, line: int | None, enable: bool
  60. ) -> None:
  61. """Set the status of an individual message."""
  62. if scope in {"module", "line"}:
  63. assert isinstance(line, int) # should always be int inside module scope
  64. self.linter.file_state.set_msg_status(msg, line, enable, scope)
  65. if not enable and msg.symbol != "locally-disabled":
  66. self.linter.add_message(
  67. "locally-disabled", line=line, args=(msg.symbol, msg.msgid)
  68. )
  69. else:
  70. msgs = self._msgs_state
  71. msgs[msg.msgid] = enable
  72. def _get_messages_to_set(
  73. self, msgid: str, enable: bool, ignore_unknown: bool = False
  74. ) -> list[MessageDefinition]:
  75. """Do some tests and find the actual messages of which the status should be set."""
  76. message_definitions: list[MessageDefinition] = []
  77. if msgid == "all":
  78. for _msgid in MSG_TYPES:
  79. message_definitions.extend(
  80. self._get_messages_to_set(_msgid, enable, ignore_unknown)
  81. )
  82. return message_definitions
  83. # msgid is a category?
  84. category_id = msgid.upper()
  85. if category_id not in MSG_TYPES:
  86. category_id_formatted = MSG_TYPES_LONG.get(category_id)
  87. else:
  88. category_id_formatted = category_id
  89. if category_id_formatted is not None:
  90. for _msgid in self.linter.msgs_store._msgs_by_category[
  91. category_id_formatted
  92. ]:
  93. message_definitions.extend(
  94. self._get_messages_to_set(_msgid, enable, ignore_unknown)
  95. )
  96. return message_definitions
  97. # msgid is a checker name?
  98. if msgid.lower() in self.linter._checkers:
  99. for checker in self.linter._checkers[msgid.lower()]:
  100. for _msgid in checker.msgs:
  101. message_definitions.extend(
  102. self._get_messages_to_set(_msgid, enable, ignore_unknown)
  103. )
  104. return message_definitions
  105. # msgid is report id?
  106. if msgid.lower().startswith("rp"):
  107. if enable:
  108. self.linter.enable_report(msgid)
  109. else:
  110. self.linter.disable_report(msgid)
  111. return message_definitions
  112. try:
  113. # msgid is a symbolic or numeric msgid.
  114. message_definitions = self.linter.msgs_store.get_message_definitions(msgid)
  115. except exceptions.UnknownMessageError:
  116. if not ignore_unknown:
  117. raise
  118. return message_definitions
  119. def _set_msg_status(
  120. self,
  121. msgid: str,
  122. enable: bool,
  123. scope: str = "package",
  124. line: int | None = None,
  125. ignore_unknown: bool = False,
  126. ) -> None:
  127. """Do some tests and then iterate over message definitions to set state."""
  128. assert scope in {"package", "module", "line"}
  129. message_definitions = self._get_messages_to_set(msgid, enable, ignore_unknown)
  130. for message_definition in message_definitions:
  131. self._set_one_msg_status(scope, message_definition, line, enable)
  132. # sync configuration object
  133. self.linter.config.enable = []
  134. self.linter.config.disable = []
  135. for msgid_or_symbol, is_enabled in self._msgs_state.items():
  136. symbols = [
  137. m.symbol
  138. for m in self.linter.msgs_store.get_message_definitions(msgid_or_symbol)
  139. ]
  140. if is_enabled:
  141. self.linter.config.enable += symbols
  142. else:
  143. self.linter.config.disable += symbols
  144. def _register_by_id_managed_msg(
  145. self, msgid_or_symbol: str, line: int | None, is_disabled: bool = True
  146. ) -> None:
  147. """If the msgid is a numeric one, then register it to inform the user
  148. it could furnish instead a symbolic msgid.
  149. """
  150. if msgid_or_symbol[1:].isdigit():
  151. try:
  152. symbol = self.linter.msgs_store.message_id_store.get_symbol(
  153. msgid=msgid_or_symbol
  154. )
  155. except exceptions.UnknownMessageError:
  156. return
  157. managed = ManagedMessage(
  158. self.linter.current_name, msgid_or_symbol, symbol, line, is_disabled
  159. )
  160. self.linter._by_id_managed_msgs.append(managed)
  161. def disable(
  162. self,
  163. msgid: str,
  164. scope: str = "package",
  165. line: int | None = None,
  166. ignore_unknown: bool = False,
  167. ) -> None:
  168. """Disable a message for a scope."""
  169. self._set_msg_status(
  170. msgid, enable=False, scope=scope, line=line, ignore_unknown=ignore_unknown
  171. )
  172. self._register_by_id_managed_msg(msgid, line)
  173. def disable_next(
  174. self,
  175. msgid: str,
  176. _: str = "package",
  177. line: int | None = None,
  178. ignore_unknown: bool = False,
  179. ) -> None:
  180. """Disable a message for the next line."""
  181. if not line:
  182. raise exceptions.NoLineSuppliedError
  183. self._set_msg_status(
  184. msgid,
  185. enable=False,
  186. scope="line",
  187. line=line + 1,
  188. ignore_unknown=ignore_unknown,
  189. )
  190. self._register_by_id_managed_msg(msgid, line + 1)
  191. def enable(
  192. self,
  193. msgid: str,
  194. scope: str = "package",
  195. line: int | None = None,
  196. ignore_unknown: bool = False,
  197. ) -> None:
  198. """Enable a message for a scope."""
  199. self._set_msg_status(
  200. msgid, enable=True, scope=scope, line=line, ignore_unknown=ignore_unknown
  201. )
  202. self._register_by_id_managed_msg(msgid, line, is_disabled=False)
  203. def disable_noerror_messages(self) -> None:
  204. """Disable message categories other than `error` and `fatal`."""
  205. for msgcat in self.linter.msgs_store._msgs_by_category:
  206. if msgcat in {"E", "F"}:
  207. continue
  208. self.disable(msgcat)
  209. def list_messages_enabled(self) -> None:
  210. emittable, non_emittable = self.linter.msgs_store.find_emittable_messages()
  211. enabled: list[str] = []
  212. disabled: list[str] = []
  213. for message in emittable:
  214. if self.is_message_enabled(message.msgid):
  215. enabled.append(f" {message.symbol} ({message.msgid})")
  216. else:
  217. disabled.append(f" {message.symbol} ({message.msgid})")
  218. print("Enabled messages:")
  219. for msg in enabled:
  220. print(msg)
  221. print("\nDisabled messages:")
  222. for msg in disabled:
  223. print(msg)
  224. print("\nNon-emittable messages with current interpreter:")
  225. for msg_def in non_emittable:
  226. print(f" {msg_def.symbol} ({msg_def.msgid})")
  227. print("")
  228. def _get_message_state_scope(
  229. self,
  230. msgid: str,
  231. line: int | None = None,
  232. confidence: interfaces.Confidence | None = None,
  233. ) -> Literal[0, 1, 2] | None:
  234. """Returns the scope at which a message was enabled/disabled."""
  235. if confidence is None:
  236. confidence = interfaces.UNDEFINED
  237. if confidence.name not in self.linter.config.confidence:
  238. return MSG_STATE_CONFIDENCE # type: ignore[return-value] # mypy does not infer Literal correctly
  239. try:
  240. if line in self.linter.file_state._module_msgs_state[msgid]:
  241. return MSG_STATE_SCOPE_MODULE # type: ignore[return-value]
  242. except (KeyError, TypeError):
  243. return MSG_STATE_SCOPE_CONFIG # type: ignore[return-value]
  244. return None
  245. def _is_one_message_enabled(self, msgid: str, line: int | None) -> bool:
  246. """Checks state of a single message for the current file.
  247. This function can't be cached as it depends on self.file_state which can
  248. change.
  249. """
  250. if line is None:
  251. return self._msgs_state.get(msgid, True)
  252. try:
  253. return self.linter.file_state._module_msgs_state[msgid][line]
  254. except KeyError:
  255. # Check if the message's line is after the maximum line existing in ast tree.
  256. # This line won't appear in the ast tree and won't be referred in
  257. # self.file_state._module_msgs_state
  258. # This happens for example with a commented line at the end of a module.
  259. max_line_number = self.linter.file_state.get_effective_max_line_number()
  260. if max_line_number and line > max_line_number:
  261. fallback = True
  262. lines = self.linter.file_state._raw_module_msgs_state.get(msgid, {})
  263. # Doesn't consider scopes, as a 'disable' can be in a
  264. # different scope than that of the current line.
  265. closest_lines = reversed(
  266. [
  267. (message_line, enable)
  268. for message_line, enable in lines.items()
  269. if message_line <= line
  270. ]
  271. )
  272. _, fallback_iter = next(closest_lines, (None, None))
  273. if fallback_iter is not None:
  274. fallback = fallback_iter
  275. return self._msgs_state.get(msgid, fallback)
  276. return self._msgs_state.get(msgid, True)
  277. def is_message_enabled(
  278. self,
  279. msg_descr: str,
  280. line: int | None = None,
  281. confidence: interfaces.Confidence | None = None,
  282. ) -> bool:
  283. """Return whether this message is enabled for the current file, line and
  284. confidence level.
  285. This function can't be cached right now as the line is the line of
  286. the currently analysed file (self.file_state), if it changes, then the
  287. result for the same msg_descr/line might need to change.
  288. :param msg_descr: Either the msgid or the symbol for a MessageDefinition
  289. :param line: The line of the currently analysed file
  290. :param confidence: The confidence of the message
  291. """
  292. if confidence and confidence.name not in self.linter.config.confidence:
  293. return False
  294. try:
  295. msgids = self.linter.msgs_store.message_id_store.get_active_msgids(
  296. msg_descr
  297. )
  298. except exceptions.UnknownMessageError:
  299. # The linter checks for messages that are not registered
  300. # due to version mismatch, just treat them as message IDs
  301. # for now.
  302. msgids = [msg_descr]
  303. return any(self._is_one_message_enabled(msgid, line) for msgid in msgids)
  304. def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
  305. """Process tokens from the current module to search for module/block level
  306. options.
  307. See func_block_disable_msg.py test case for expected behaviour.
  308. """
  309. control_pragmas = {"disable", "disable-next", "enable"}
  310. prev_line = None
  311. saw_newline = True
  312. seen_newline = True
  313. for tok_type, content, start, _, _ in tokens:
  314. if prev_line and prev_line != start[0]:
  315. saw_newline = seen_newline
  316. seen_newline = False
  317. prev_line = start[0]
  318. if tok_type in (tokenize.NL, tokenize.NEWLINE):
  319. seen_newline = True
  320. if tok_type != tokenize.COMMENT:
  321. continue
  322. match = OPTION_PO.search(content)
  323. if match is None:
  324. continue
  325. try: # pylint: disable = too-many-try-statements
  326. for pragma_repr in parse_pragma(match.group(2)):
  327. if pragma_repr.action in {"disable-all", "skip-file"}:
  328. if pragma_repr.action == "disable-all":
  329. self.linter.add_message(
  330. "deprecated-pragma",
  331. line=start[0],
  332. args=("disable-all", "skip-file"),
  333. )
  334. self.linter.add_message("file-ignored", line=start[0])
  335. self._ignore_file = True
  336. return
  337. try:
  338. meth = self._options_methods[pragma_repr.action]
  339. except KeyError:
  340. meth = self._bw_options_methods[pragma_repr.action]
  341. # found a "(dis|en)able-msg" pragma deprecated suppression
  342. self.linter.add_message(
  343. "deprecated-pragma",
  344. line=start[0],
  345. args=(
  346. pragma_repr.action,
  347. pragma_repr.action.replace("-msg", ""),
  348. ),
  349. )
  350. for msgid in pragma_repr.messages:
  351. # Add the line where a control pragma was encountered.
  352. if pragma_repr.action in control_pragmas:
  353. self._pragma_lineno[msgid] = start[0]
  354. if (pragma_repr.action, msgid) == ("disable", "all"):
  355. self.linter.add_message(
  356. "deprecated-pragma",
  357. line=start[0],
  358. args=("disable=all", "skip-file"),
  359. )
  360. self.linter.add_message("file-ignored", line=start[0])
  361. self._ignore_file = True
  362. return
  363. # If we did not see a newline between the previous line and now,
  364. # we saw a backslash so treat the two lines as one.
  365. l_start = start[0]
  366. if not saw_newline:
  367. l_start -= 1
  368. try:
  369. meth(msgid, "module", l_start)
  370. except (
  371. exceptions.DeletedMessageError,
  372. exceptions.MessageBecameExtensionError,
  373. ) as e:
  374. self.linter.add_message(
  375. "useless-option-value",
  376. args=(pragma_repr.action, e),
  377. line=start[0],
  378. confidence=HIGH,
  379. )
  380. except exceptions.UnknownMessageError:
  381. self.linter.add_message(
  382. "unknown-option-value",
  383. args=(pragma_repr.action, msgid),
  384. line=start[0],
  385. confidence=HIGH,
  386. )
  387. except UnRecognizedOptionError as err:
  388. self.linter.add_message(
  389. "unrecognized-inline-option", args=err.token, line=start[0]
  390. )
  391. continue
  392. except InvalidPragmaError as err:
  393. self.linter.add_message(
  394. "bad-inline-option", args=err.token, line=start[0]
  395. )
  396. continue