file_state.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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 collections
  6. import sys
  7. import warnings
  8. from collections import defaultdict
  9. from collections.abc import Iterator
  10. from typing import TYPE_CHECKING, Dict
  11. from astroid import nodes
  12. from pylint.constants import (
  13. INCOMPATIBLE_WITH_USELESS_SUPPRESSION,
  14. MSG_STATE_SCOPE_MODULE,
  15. WarningScope,
  16. )
  17. if sys.version_info >= (3, 8):
  18. from typing import Literal
  19. else:
  20. from typing_extensions import Literal
  21. if TYPE_CHECKING:
  22. from pylint.message import MessageDefinition, MessageDefinitionStore
  23. MessageStateDict = Dict[str, Dict[int, bool]]
  24. class FileState:
  25. """Hold internal state specific to the currently analyzed file."""
  26. def __init__(
  27. self,
  28. modname: str | None = None,
  29. msg_store: MessageDefinitionStore | None = None,
  30. node: nodes.Module | None = None,
  31. *,
  32. is_base_filestate: bool = False,
  33. ) -> None:
  34. if modname is None:
  35. warnings.warn(
  36. "FileState needs a string as modname argument. "
  37. "This argument will be required in pylint 3.0",
  38. DeprecationWarning,
  39. stacklevel=2,
  40. )
  41. if msg_store is None:
  42. warnings.warn(
  43. "FileState needs a 'MessageDefinitionStore' as msg_store argument. "
  44. "This argument will be required in pylint 3.0",
  45. DeprecationWarning,
  46. stacklevel=2,
  47. )
  48. self.base_name = modname
  49. self._module_msgs_state: MessageStateDict = {}
  50. self._raw_module_msgs_state: MessageStateDict = {}
  51. self._ignored_msgs: defaultdict[
  52. tuple[str, int], set[int]
  53. ] = collections.defaultdict(set)
  54. self._suppression_mapping: dict[tuple[str, int], int] = {}
  55. self._module = node
  56. if node:
  57. self._effective_max_line_number = node.tolineno
  58. else:
  59. self._effective_max_line_number = None
  60. self._msgs_store = msg_store
  61. self._is_base_filestate = is_base_filestate
  62. """If this FileState is the base state made during initialization of
  63. PyLinter.
  64. """
  65. def collect_block_lines(
  66. self, msgs_store: MessageDefinitionStore, module_node: nodes.Module
  67. ) -> None:
  68. """Walk the AST to collect block level options line numbers."""
  69. warnings.warn(
  70. "'collect_block_lines' has been deprecated and will be removed in pylint 3.0.",
  71. DeprecationWarning,
  72. stacklevel=2,
  73. )
  74. for msg, lines in self._module_msgs_state.items():
  75. self._raw_module_msgs_state[msg] = lines.copy()
  76. orig_state = self._module_msgs_state.copy()
  77. self._module_msgs_state = {}
  78. self._suppression_mapping = {}
  79. self._effective_max_line_number = module_node.tolineno
  80. for msgid, lines in orig_state.items():
  81. for msgdef in msgs_store.get_message_definitions(msgid):
  82. self._set_state_on_block_lines(msgs_store, module_node, msgdef, lines)
  83. def _set_state_on_block_lines(
  84. self,
  85. msgs_store: MessageDefinitionStore,
  86. node: nodes.NodeNG,
  87. msg: MessageDefinition,
  88. msg_state: dict[int, bool],
  89. ) -> None:
  90. """Recursively walk (depth first) AST to collect block level options
  91. line numbers and set the state correctly.
  92. """
  93. for child in node.get_children():
  94. self._set_state_on_block_lines(msgs_store, child, msg, msg_state)
  95. # first child line number used to distinguish between disable
  96. # which are the first child of scoped node with those defined later.
  97. # For instance in the code below:
  98. #
  99. # 1. def meth8(self):
  100. # 2. """test late disabling"""
  101. # 3. pylint: disable=not-callable, useless-suppression
  102. # 4. print(self.blip)
  103. # 5. pylint: disable=no-member, useless-suppression
  104. # 6. print(self.bla)
  105. #
  106. # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
  107. #
  108. # this is necessary to disable locally messages applying to class /
  109. # function using their fromlineno
  110. if (
  111. isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef))
  112. and node.body
  113. ):
  114. firstchildlineno = node.body[0].fromlineno
  115. else:
  116. firstchildlineno = node.tolineno
  117. self._set_message_state_in_block(msg, msg_state, node, firstchildlineno)
  118. def _set_message_state_in_block(
  119. self,
  120. msg: MessageDefinition,
  121. lines: dict[int, bool],
  122. node: nodes.NodeNG,
  123. firstchildlineno: int,
  124. ) -> None:
  125. """Set the state of a message in a block of lines."""
  126. first = node.fromlineno
  127. last = node.tolineno
  128. for lineno, state in list(lines.items()):
  129. original_lineno = lineno
  130. if first > lineno or last < lineno:
  131. continue
  132. # Set state for all lines for this block, if the
  133. # warning is applied to nodes.
  134. if msg.scope == WarningScope.NODE:
  135. if lineno > firstchildlineno:
  136. state = True
  137. first_, last_ = node.block_range(lineno)
  138. # pylint: disable=useless-suppression
  139. # For block nodes first_ is their definition line. For example, we
  140. # set the state of line zero for a module to allow disabling
  141. # invalid-name for the module. For example:
  142. # 1. # pylint: disable=invalid-name
  143. # 2. ...
  144. # OR
  145. # 1. """Module docstring"""
  146. # 2. # pylint: disable=invalid-name
  147. # 3. ...
  148. #
  149. # But if we already visited line 0 we don't need to set its state again
  150. # 1. # pylint: disable=invalid-name
  151. # 2. # pylint: enable=invalid-name
  152. # 3. ...
  153. # The state should come from line 1, not from line 2
  154. # Therefore, if the 'fromlineno' is already in the states we just start
  155. # with the lineno we were originally visiting.
  156. # pylint: enable=useless-suppression
  157. if (
  158. first_ == node.fromlineno
  159. and first_ >= firstchildlineno
  160. and node.fromlineno in self._module_msgs_state.get(msg.msgid, ())
  161. ):
  162. first_ = lineno
  163. else:
  164. first_ = lineno
  165. last_ = last
  166. for line in range(first_, last_ + 1):
  167. # Do not override existing entries. This is especially important
  168. # when parsing the states for a scoped node where some line-disables
  169. # have already been parsed.
  170. if (
  171. (
  172. isinstance(node, nodes.Module)
  173. and node.fromlineno <= line < lineno
  174. )
  175. or (
  176. not isinstance(node, nodes.Module)
  177. and node.fromlineno < line < lineno
  178. )
  179. ) and line in self._module_msgs_state.get(msg.msgid, ()):
  180. continue
  181. if line in lines: # state change in the same block
  182. state = lines[line]
  183. original_lineno = line
  184. self._set_message_state_on_line(msg, line, state, original_lineno)
  185. del lines[lineno]
  186. def _set_message_state_on_line(
  187. self,
  188. msg: MessageDefinition,
  189. line: int,
  190. state: bool,
  191. original_lineno: int,
  192. ) -> None:
  193. """Set the state of a message on a line."""
  194. # Update suppression mapping
  195. if not state:
  196. self._suppression_mapping[(msg.msgid, line)] = original_lineno
  197. else:
  198. self._suppression_mapping.pop((msg.msgid, line), None)
  199. # Update message state for respective line
  200. try:
  201. self._module_msgs_state[msg.msgid][line] = state
  202. except KeyError:
  203. self._module_msgs_state[msg.msgid] = {line: state}
  204. def set_msg_status(
  205. self,
  206. msg: MessageDefinition,
  207. line: int,
  208. status: bool,
  209. scope: str = "package",
  210. ) -> None:
  211. """Set status (enabled/disable) for a given message at a given line."""
  212. assert line > 0
  213. assert self._module
  214. # TODO: 3.0: Remove unnecessary assertion
  215. assert self._msgs_store
  216. if scope != "line":
  217. # Expand the status to cover all relevant block lines
  218. self._set_state_on_block_lines(
  219. self._msgs_store, self._module, msg, {line: status}
  220. )
  221. else:
  222. self._set_message_state_on_line(msg, line, status, line)
  223. # Store the raw value
  224. try:
  225. self._raw_module_msgs_state[msg.msgid][line] = status
  226. except KeyError:
  227. self._raw_module_msgs_state[msg.msgid] = {line: status}
  228. def handle_ignored_message(
  229. self, state_scope: Literal[0, 1, 2] | None, msgid: str, line: int | None
  230. ) -> None:
  231. """Report an ignored message.
  232. state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
  233. depending on whether the message was disabled locally in the module,
  234. or globally.
  235. """
  236. if state_scope == MSG_STATE_SCOPE_MODULE:
  237. assert isinstance(line, int) # should always be int inside module scope
  238. try:
  239. orig_line = self._suppression_mapping[(msgid, line)]
  240. self._ignored_msgs[(msgid, orig_line)].add(line)
  241. except KeyError:
  242. pass
  243. def iter_spurious_suppression_messages(
  244. self,
  245. msgs_store: MessageDefinitionStore,
  246. ) -> Iterator[
  247. tuple[
  248. Literal["useless-suppression", "suppressed-message"],
  249. int,
  250. tuple[str] | tuple[str, int],
  251. ]
  252. ]:
  253. for warning, lines in self._raw_module_msgs_state.items():
  254. for line, enable in lines.items():
  255. if (
  256. not enable
  257. and (warning, line) not in self._ignored_msgs
  258. and warning not in INCOMPATIBLE_WITH_USELESS_SUPPRESSION
  259. ):
  260. yield "useless-suppression", line, (
  261. msgs_store.get_msg_display_string(warning),
  262. )
  263. # don't use iteritems here, _ignored_msgs may be modified by add_message
  264. for (warning, from_), ignored_lines in list(self._ignored_msgs.items()):
  265. for line in ignored_lines:
  266. yield "suppressed-message", line, (
  267. msgs_store.get_msg_display_string(warning),
  268. from_,
  269. )
  270. def get_effective_max_line_number(self) -> int | None:
  271. return self._effective_max_line_number # type: ignore[no-any-return]