message_state_handler.py 18 KB

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