file_state.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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. import collections
  4. import sys
  5. from typing import (
  6. TYPE_CHECKING,
  7. DefaultDict,
  8. Dict,
  9. Iterator,
  10. Optional,
  11. Set,
  12. Tuple,
  13. Union,
  14. )
  15. from astroid import nodes
  16. from pylint.constants import MSG_STATE_SCOPE_MODULE, WarningScope
  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__(self, modname: Optional[str] = None) -> None:
  27. self.base_name = modname
  28. self._module_msgs_state: MessageStateDict = {}
  29. self._raw_module_msgs_state: MessageStateDict = {}
  30. self._ignored_msgs: DefaultDict[
  31. Tuple[str, int], Set[int]
  32. ] = collections.defaultdict(set)
  33. self._suppression_mapping: Dict[Tuple[str, int], int] = {}
  34. self._effective_max_line_number: Optional[int] = None
  35. def collect_block_lines(
  36. self, msgs_store: "MessageDefinitionStore", module_node: nodes.Module
  37. ) -> None:
  38. """Walk the AST to collect block level options line numbers."""
  39. for msg, lines in self._module_msgs_state.items():
  40. self._raw_module_msgs_state[msg] = lines.copy()
  41. orig_state = self._module_msgs_state.copy()
  42. self._module_msgs_state = {}
  43. self._suppression_mapping = {}
  44. self._effective_max_line_number = module_node.tolineno
  45. self._collect_block_lines(msgs_store, module_node, orig_state)
  46. def _collect_block_lines(
  47. self,
  48. msgs_store: "MessageDefinitionStore",
  49. node: nodes.NodeNG,
  50. msg_state: MessageStateDict,
  51. ) -> None:
  52. """Recursively walk (depth first) AST to collect block level options
  53. line numbers.
  54. """
  55. for child in node.get_children():
  56. self._collect_block_lines(msgs_store, child, msg_state)
  57. first = node.fromlineno
  58. last = node.tolineno
  59. # first child line number used to distinguish between disable
  60. # which are the first child of scoped node with those defined later.
  61. # For instance in the code below:
  62. #
  63. # 1. def meth8(self):
  64. # 2. """test late disabling"""
  65. # 3. pylint: disable=not-callable, useless-suppression
  66. # 4. print(self.blip)
  67. # 5. pylint: disable=no-member, useless-suppression
  68. # 6. print(self.bla)
  69. #
  70. # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
  71. #
  72. # this is necessary to disable locally messages applying to class /
  73. # function using their fromlineno
  74. if (
  75. isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef))
  76. and node.body
  77. ):
  78. firstchildlineno = node.body[0].fromlineno
  79. else:
  80. firstchildlineno = last
  81. for msgid, lines in msg_state.items():
  82. for lineno, state in list(lines.items()):
  83. original_lineno = lineno
  84. if first > lineno or last < lineno:
  85. continue
  86. # Set state for all lines for this block, if the
  87. # warning is applied to nodes.
  88. message_definitions = msgs_store.get_message_definitions(msgid)
  89. for message_definition in message_definitions:
  90. if message_definition.scope == WarningScope.NODE:
  91. if lineno > firstchildlineno:
  92. state = True
  93. first_, last_ = node.block_range(lineno)
  94. else:
  95. first_ = lineno
  96. last_ = last
  97. for line in range(first_, last_ + 1):
  98. # do not override existing entries
  99. if line in self._module_msgs_state.get(msgid, ()):
  100. continue
  101. if line in lines: # state change in the same block
  102. state = lines[line]
  103. original_lineno = line
  104. if not state:
  105. self._suppression_mapping[(msgid, line)] = original_lineno
  106. try:
  107. self._module_msgs_state[msgid][line] = state
  108. except KeyError:
  109. self._module_msgs_state[msgid] = {line: state}
  110. del lines[lineno]
  111. def set_msg_status(self, msg: "MessageDefinition", line: int, status: bool) -> None:
  112. """Set status (enabled/disable) for a given message at a given line"""
  113. assert line > 0
  114. try:
  115. self._module_msgs_state[msg.msgid][line] = status
  116. except KeyError:
  117. self._module_msgs_state[msg.msgid] = {line: status}
  118. def handle_ignored_message(
  119. self, state_scope: Optional[Literal[0, 1, 2]], msgid: str, line: int
  120. ) -> None:
  121. """Report an ignored message.
  122. state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
  123. depending on whether the message was disabled locally in the module,
  124. or globally.
  125. """
  126. if state_scope == MSG_STATE_SCOPE_MODULE:
  127. try:
  128. orig_line = self._suppression_mapping[(msgid, line)]
  129. self._ignored_msgs[(msgid, orig_line)].add(line)
  130. except KeyError:
  131. pass
  132. def iter_spurious_suppression_messages(
  133. self,
  134. msgs_store: "MessageDefinitionStore",
  135. ) -> Iterator[
  136. Tuple[
  137. Literal["useless-suppression", "suppressed-message"],
  138. int,
  139. Union[Tuple[str], Tuple[str, int]],
  140. ]
  141. ]:
  142. for warning, lines in self._raw_module_msgs_state.items():
  143. for line, enable in lines.items():
  144. if not enable and (warning, line) not in self._ignored_msgs:
  145. # ignore cyclic-import check which can show false positives
  146. # here due to incomplete context
  147. if warning != "R0401":
  148. yield "useless-suppression", line, (
  149. msgs_store.get_msg_display_string(warning),
  150. )
  151. # don't use iteritems here, _ignored_msgs may be modified by add_message
  152. for (warning, from_), ignored_lines in list(self._ignored_msgs.items()):
  153. for line in ignored_lines:
  154. yield "suppressed-message", line, (
  155. msgs_store.get_msg_display_string(warning),
  156. from_,
  157. )
  158. def get_effective_max_line_number(self) -> Optional[int]:
  159. return self._effective_max_line_number