Coverage for C:\Repos\ekr-pylint\pylint\utils\file_state.py: 21%

107 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

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 

5from __future__ import annotations 

6 

7import collections 

8import sys 

9import warnings 

10from collections import defaultdict 

11from collections.abc import Iterator 

12from typing import TYPE_CHECKING, Dict 

13 

14from astroid import nodes 

15 

16from pylint.constants import ( 

17 INCOMPATIBLE_WITH_USELESS_SUPPRESSION, 

18 MSG_STATE_SCOPE_MODULE, 

19 WarningScope, 

20) 

21 

22if sys.version_info >= (3, 8): 

23 from typing import Literal 

24else: 

25 from typing_extensions import Literal 

26 

27if TYPE_CHECKING: 

28 from pylint.message import MessageDefinition, MessageDefinitionStore 

29 

30 

31MessageStateDict = Dict[str, Dict[int, bool]] 

32 

33 

34class FileState: 

35 """Hold internal state specific to the currently analyzed file.""" 

36 

37 def __init__( 

38 self, 

39 modname: str | None = None, 

40 msg_store: MessageDefinitionStore | None = None, 

41 node: nodes.Module | None = None, 

42 *, 

43 is_base_filestate: bool = False, 

44 ) -> None: 

45 if modname is None: 

46 warnings.warn( 

47 "FileState needs a string as modname argument. " 

48 "This argument will be required in pylint 3.0", 

49 DeprecationWarning, 

50 ) 

51 if msg_store is None: 

52 warnings.warn( 

53 "FileState needs a 'MessageDefinitionStore' as msg_store argument. " 

54 "This argument will be required in pylint 3.0", 

55 DeprecationWarning, 

56 ) 

57 self.base_name = modname 

58 self._module_msgs_state: MessageStateDict = {} 

59 self._raw_module_msgs_state: MessageStateDict = {} 

60 self._ignored_msgs: defaultdict[ 

61 tuple[str, int], set[int] 

62 ] = collections.defaultdict(set) 

63 self._suppression_mapping: dict[tuple[str, int], int] = {} 

64 self._module = node 

65 if node: 

66 self._effective_max_line_number = node.tolineno 

67 else: 

68 self._effective_max_line_number = None 

69 self._msgs_store = msg_store 

70 self._is_base_filestate = is_base_filestate 

71 """If this FileState is the base state made during initialization of PyLinter.""" 

72 

73 def collect_block_lines( 

74 self, msgs_store: MessageDefinitionStore, module_node: nodes.Module 

75 ) -> None: 

76 """Walk the AST to collect block level options line numbers.""" 

77 warnings.warn( 

78 "'collect_block_lines' has been deprecated and will be removed in pylint 3.0.", 

79 DeprecationWarning, 

80 ) 

81 for msg, lines in self._module_msgs_state.items(): 

82 self._raw_module_msgs_state[msg] = lines.copy() 

83 orig_state = self._module_msgs_state.copy() 

84 self._module_msgs_state = {} 

85 self._suppression_mapping = {} 

86 self._effective_max_line_number = module_node.tolineno 

87 for msgid, lines in orig_state.items(): 

88 for msgdef in msgs_store.get_message_definitions(msgid): 

89 self._set_state_on_block_lines(msgs_store, module_node, msgdef, lines) 

90 

91 def _set_state_on_block_lines( 

92 self, 

93 msgs_store: MessageDefinitionStore, 

94 node: nodes.NodeNG, 

95 msg: MessageDefinition, 

96 msg_state: dict[int, bool], 

97 ) -> None: 

98 """Recursively walk (depth first) AST to collect block level options 

99 line numbers and set the state correctly. 

100 """ 

101 for child in node.get_children(): 

102 self._set_state_on_block_lines(msgs_store, child, msg, msg_state) 

103 # first child line number used to distinguish between disable 

104 # which are the first child of scoped node with those defined later. 

105 # For instance in the code below: 

106 # 

107 # 1. def meth8(self): 

108 # 2. """test late disabling""" 

109 # 3. pylint: disable=not-callable, useless-suppression 

110 # 4. print(self.blip) 

111 # 5. pylint: disable=no-member, useless-suppression 

112 # 6. print(self.bla) 

113 # 

114 # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6 

115 # 

116 # this is necessary to disable locally messages applying to class / 

117 # function using their fromlineno 

118 if ( 

119 isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef)) 

120 and node.body 

121 ): 

122 firstchildlineno = node.body[0].fromlineno 

123 else: 

124 firstchildlineno = node.tolineno 

125 self._set_message_state_in_block(msg, msg_state, node, firstchildlineno) 

126 

127 def _set_message_state_in_block( 

128 self, 

129 msg: MessageDefinition, 

130 lines: dict[int, bool], 

131 node: nodes.NodeNG, 

132 firstchildlineno: int, 

133 ) -> None: 

134 """Set the state of a message in a block of lines.""" 

135 first = node.fromlineno 

136 last = node.tolineno 

137 for lineno, state in list(lines.items()): 

138 original_lineno = lineno 

139 if first > lineno or last < lineno: 

140 continue 

141 # Set state for all lines for this block, if the 

142 # warning is applied to nodes. 

143 if msg.scope == WarningScope.NODE: 

144 if lineno > firstchildlineno: 

145 state = True 

146 first_, last_ = node.block_range(lineno) 

147 # pylint: disable=useless-suppression 

148 # For block nodes first_ is their definition line. For example, we 

149 # set the state of line zero for a module to allow disabling 

150 # invalid-name for the module. For example: 

151 # 1. # pylint: disable=invalid-name 

152 # 2. ... 

153 # OR 

154 # 1. """Module docstring""" 

155 # 2. # pylint: disable=invalid-name 

156 # 3. ... 

157 # 

158 # But if we already visited line 0 we don't need to set its state again 

159 # 1. # pylint: disable=invalid-name 

160 # 2. # pylint: enable=invalid-name 

161 # 3. ... 

162 # The state should come from line 1, not from line 2 

163 # Therefore, if the 'fromlineno' is already in the states we just start 

164 # with the lineno we were originally visiting. 

165 # pylint: enable=useless-suppression 

166 if ( 

167 first_ == node.fromlineno 

168 and first_ >= firstchildlineno 

169 and node.fromlineno in self._module_msgs_state.get(msg.msgid, ()) 

170 ): 

171 first_ = lineno 

172 

173 else: 

174 first_ = lineno 

175 last_ = last 

176 for line in range(first_, last_ + 1): 

177 # Do not override existing entries. This is especially important 

178 # when parsing the states for a scoped node where some line-disables 

179 # have already been parsed. 

180 if ( 

181 ( 

182 isinstance(node, nodes.Module) 

183 and node.fromlineno <= line < lineno 

184 ) 

185 or ( 

186 not isinstance(node, nodes.Module) 

187 and node.fromlineno < line < lineno 

188 ) 

189 ) and line in self._module_msgs_state.get(msg.msgid, ()): 

190 continue 

191 if line in lines: # state change in the same block 

192 state = lines[line] 

193 original_lineno = line 

194 

195 # Update suppression mapping 

196 if not state: 

197 self._suppression_mapping[(msg.msgid, line)] = original_lineno 

198 else: 

199 self._suppression_mapping.pop((msg.msgid, line), None) 

200 

201 # Update message state for respective line 

202 try: 

203 self._module_msgs_state[msg.msgid][line] = state 

204 except KeyError: 

205 self._module_msgs_state[msg.msgid] = {line: state} 

206 del lines[lineno] 

207 

208 def set_msg_status(self, msg: MessageDefinition, line: int, status: bool) -> None: 

209 """Set status (enabled/disable) for a given message at a given line.""" 

210 assert line > 0 

211 assert self._module 

212 # TODO: 3.0: Remove unnecessary assertion 

213 assert self._msgs_store 

214 

215 # Expand the status to cover all relevant block lines 

216 self._set_state_on_block_lines( 

217 self._msgs_store, self._module, msg, {line: status} 

218 ) 

219 

220 # Store the raw value 

221 try: 

222 self._raw_module_msgs_state[msg.msgid][line] = status 

223 except KeyError: 

224 self._raw_module_msgs_state[msg.msgid] = {line: status} 

225 

226 def handle_ignored_message( 

227 self, state_scope: Literal[0, 1, 2] | None, msgid: str, line: int | None 

228 ) -> None: 

229 """Report an ignored message. 

230 

231 state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG, 

232 depending on whether the message was disabled locally in the module, 

233 or globally. 

234 """ 

235 if state_scope == MSG_STATE_SCOPE_MODULE: 

236 assert isinstance(line, int) # should always be int inside module scope 

237 

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 

244 def iter_spurious_suppression_messages( 

245 self, 

246 msgs_store: MessageDefinitionStore, 

247 ) -> Iterator[ 

248 tuple[ 

249 Literal["useless-suppression", "suppressed-message"], 

250 int, 

251 tuple[str] | tuple[str, int], 

252 ] 

253 ]: 

254 for warning, lines in self._raw_module_msgs_state.items(): 

255 for line, enable in lines.items(): 

256 if ( 

257 not enable 

258 and (warning, line) not in self._ignored_msgs 

259 and warning not in INCOMPATIBLE_WITH_USELESS_SUPPRESSION 

260 ): 

261 yield "useless-suppression", line, ( 

262 msgs_store.get_msg_display_string(warning), 

263 ) 

264 # don't use iteritems here, _ignored_msgs may be modified by add_message 

265 for (warning, from_), ignored_lines in list(self._ignored_msgs.items()): 

266 for line in ignored_lines: 

267 yield "suppressed-message", line, ( 

268 msgs_store.get_msg_display_string(warning), 

269 from_, 

270 ) 

271 

272 def get_effective_max_line_number(self) -> int | None: 

273 return self._effective_max_line_number