file_state.py 9.4 KB

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