Coverage for C:\Repos\ekr-pylint\pylint\lint\message_state_handler.py: 13%

202 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 sys 

8import tokenize 

9from typing import TYPE_CHECKING 

10 

11from pylint import exceptions, interfaces 

12from pylint.constants import ( 

13 MSG_STATE_CONFIDENCE, 

14 MSG_STATE_SCOPE_CONFIG, 

15 MSG_STATE_SCOPE_MODULE, 

16 MSG_TYPES, 

17 MSG_TYPES_LONG, 

18) 

19from pylint.message import MessageDefinition 

20from pylint.typing import ManagedMessage 

21from pylint.utils.pragma_parser import ( 

22 OPTION_PO, 

23 InvalidPragmaError, 

24 UnRecognizedOptionError, 

25 parse_pragma, 

26) 

27 

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

29 from typing import Literal 

30else: 

31 from typing_extensions import Literal 

32 

33 

34if TYPE_CHECKING: 

35 from pylint.lint.pylinter import PyLinter 

36 

37 

38class _MessageStateHandler: 

39 """Class that handles message disabling & enabling and processing of inline pragma's.""" 

40 

41 def __init__(self, linter: PyLinter) -> None: 

42 self.linter = linter 

43 self._msgs_state: dict[str, bool] = {} 

44 self._options_methods = { 

45 "enable": self.enable, 

46 "disable": self.disable, 

47 "disable-next": self.disable_next, 

48 } 

49 self._bw_options_methods = { 

50 "disable-msg": self._options_methods["disable"], 

51 "enable-msg": self._options_methods["enable"], 

52 } 

53 self._pragma_lineno: dict[str, int] = {} 

54 

55 def _set_one_msg_status( 

56 self, scope: str, msg: MessageDefinition, line: int | None, enable: bool 

57 ) -> None: 

58 """Set the status of an individual message.""" 

59 if scope == "module": 

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

61 

62 self.linter.file_state.set_msg_status(msg, line, enable) 

63 if not enable and msg.symbol != "locally-disabled": 

64 self.linter.add_message( 

65 "locally-disabled", line=line, args=(msg.symbol, msg.msgid) 

66 ) 

67 else: 

68 msgs = self._msgs_state 

69 msgs[msg.msgid] = enable 

70 

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 return message_definitions 

82 

83 # msgid is a category? 

84 category_id = msgid.upper() 

85 if category_id not in MSG_TYPES: 

86 category_id_formatted = MSG_TYPES_LONG.get(category_id) 

87 else: 

88 category_id_formatted = category_id 

89 if category_id_formatted is not None: 

90 for _msgid in self.linter.msgs_store._msgs_by_category[ 

91 category_id_formatted 

92 ]: 

93 message_definitions.extend( 

94 self._get_messages_to_set(_msgid, enable, ignore_unknown) 

95 ) 

96 return message_definitions 

97 

98 # msgid is a checker name? 

99 if msgid.lower() in self.linter._checkers: 

100 for checker in self.linter._checkers[msgid.lower()]: 

101 for _msgid in checker.msgs: 

102 message_definitions.extend( 

103 self._get_messages_to_set(_msgid, enable, ignore_unknown) 

104 ) 

105 return message_definitions 

106 

107 # msgid is report id? 

108 if msgid.lower().startswith("rp"): 

109 if enable: 

110 self.linter.enable_report(msgid) 

111 else: 

112 self.linter.disable_report(msgid) 

113 return message_definitions 

114 

115 try: 

116 # msgid is a symbolic or numeric msgid. 

117 message_definitions = self.linter.msgs_store.get_message_definitions(msgid) 

118 except exceptions.UnknownMessageError: 

119 if not ignore_unknown: 

120 raise 

121 return message_definitions 

122 

123 def _set_msg_status( 

124 self, 

125 msgid: str, 

126 enable: bool, 

127 scope: str = "package", 

128 line: int | None = None, 

129 ignore_unknown: bool = False, 

130 ) -> None: 

131 """Do some tests and then iterate over message definitions to set state.""" 

132 assert scope in {"package", "module"} 

133 

134 message_definitions = self._get_messages_to_set(msgid, enable, ignore_unknown) 

135 

136 for message_definition in message_definitions: 

137 self._set_one_msg_status(scope, message_definition, line, enable) 

138 

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 

152 def _register_by_id_managed_msg( 

153 self, msgid_or_symbol: str, line: int | None, is_disabled: bool = True 

154 ) -> None: 

155 """If the msgid is a numeric one, then register it to inform the user 

156 it could furnish instead a symbolic msgid. 

157 """ 

158 if msgid_or_symbol[1:].isdigit(): 

159 try: 

160 symbol = self.linter.msgs_store.message_id_store.get_symbol( 

161 msgid=msgid_or_symbol 

162 ) 

163 except exceptions.UnknownMessageError: 

164 return 

165 managed = ManagedMessage( 

166 self.linter.current_name, msgid_or_symbol, symbol, line, is_disabled 

167 ) 

168 self.linter._by_id_managed_msgs.append(managed) 

169 

170 def disable( 

171 self, 

172 msgid: str, 

173 scope: str = "package", 

174 line: int | None = None, 

175 ignore_unknown: bool = False, 

176 ) -> None: 

177 """Disable a message for a scope.""" 

178 self._set_msg_status( 

179 msgid, enable=False, scope=scope, line=line, ignore_unknown=ignore_unknown 

180 ) 

181 self._register_by_id_managed_msg(msgid, line) 

182 

183 def disable_next( 

184 self, 

185 msgid: str, 

186 scope: str = "package", 

187 line: int | None = None, 

188 ignore_unknown: bool = False, 

189 ) -> None: 

190 """Disable a message for the next line.""" 

191 if not line: 

192 raise exceptions.NoLineSuppliedError 

193 self._set_msg_status( 

194 msgid, 

195 enable=False, 

196 scope=scope, 

197 line=line + 1, 

198 ignore_unknown=ignore_unknown, 

199 ) 

200 self._register_by_id_managed_msg(msgid, line + 1) 

201 

202 def enable( 

203 self, 

204 msgid: str, 

205 scope: str = "package", 

206 line: int | None = None, 

207 ignore_unknown: bool = False, 

208 ) -> None: 

209 """Enable a message for a scope.""" 

210 self._set_msg_status( 

211 msgid, enable=True, scope=scope, line=line, ignore_unknown=ignore_unknown 

212 ) 

213 self._register_by_id_managed_msg(msgid, line, is_disabled=False) 

214 

215 def disable_noerror_messages(self) -> None: 

216 for msgcat, msgids in self.linter.msgs_store._msgs_by_category.items(): 

217 # enable only messages with 'error' severity and above ('fatal') 

218 if msgcat in {"E", "F"}: 

219 for msgid in msgids: 

220 self.enable(msgid) 

221 else: 

222 for msgid in msgids: 

223 self.disable(msgid) 

224 

225 def list_messages_enabled(self) -> None: 

226 emittable, non_emittable = self.linter.msgs_store.find_emittable_messages() 

227 enabled: list[str] = [] 

228 disabled: list[str] = [] 

229 for message in emittable: 

230 if self.is_message_enabled(message.msgid): 

231 enabled.append(f" {message.symbol} ({message.msgid})") 

232 else: 

233 disabled.append(f" {message.symbol} ({message.msgid})") 

234 print("Enabled messages:") 

235 for msg in enabled: 

236 print(msg) 

237 print("\nDisabled messages:") 

238 for msg in disabled: 

239 print(msg) 

240 print("\nNon-emittable messages with current interpreter:") 

241 for msg_def in non_emittable: 

242 print(f" {msg_def.symbol} ({msg_def.msgid})") 

243 print("") 

244 

245 def _get_message_state_scope( 

246 self, 

247 msgid: str, 

248 line: int | None = None, 

249 confidence: interfaces.Confidence | None = None, 

250 ) -> Literal[0, 1, 2] | None: 

251 """Returns the scope at which a message was enabled/disabled.""" 

252 if confidence is None: 

253 confidence = interfaces.UNDEFINED 

254 if confidence.name not in self.linter.config.confidence: 

255 return MSG_STATE_CONFIDENCE # type: ignore[return-value] # mypy does not infer Literal correctly 

256 try: 

257 if line in self.linter.file_state._module_msgs_state[msgid]: 

258 return MSG_STATE_SCOPE_MODULE # type: ignore[return-value] 

259 except (KeyError, TypeError): 

260 return MSG_STATE_SCOPE_CONFIG # type: ignore[return-value] 

261 return None 

262 

263 def _is_one_message_enabled(self, msgid: str, line: int | None) -> bool: 

264 """Checks state of a single message for the current file. 

265 

266 This function can't be cached as it depends on self.file_state which can 

267 change. 

268 """ 

269 if line is None: 

270 return self._msgs_state.get(msgid, True) 

271 try: 

272 return self.linter.file_state._module_msgs_state[msgid][line] 

273 except KeyError: 

274 # Check if the message's line is after the maximum line existing in ast tree. 

275 # This line won't appear in the ast tree and won't be referred in 

276 # self.file_state._module_msgs_state 

277 # This happens for example with a commented line at the end of a module. 

278 max_line_number = self.linter.file_state.get_effective_max_line_number() 

279 if max_line_number and line > max_line_number: 

280 fallback = True 

281 lines = self.linter.file_state._raw_module_msgs_state.get(msgid, {}) 

282 

283 # Doesn't consider scopes, as a 'disable' can be in a 

284 # different scope than that of the current line. 

285 closest_lines = reversed( 

286 [ 

287 (message_line, enable) 

288 for message_line, enable in lines.items() 

289 if message_line <= line 

290 ] 

291 ) 

292 _, fallback_iter = next(closest_lines, (None, None)) 

293 if fallback_iter is not None: 

294 fallback = fallback_iter 

295 

296 return self._msgs_state.get(msgid, fallback) 

297 return self._msgs_state.get(msgid, True) 

298 

299 def is_message_enabled( 

300 self, 

301 msg_descr: str, 

302 line: int | None = None, 

303 confidence: interfaces.Confidence | None = None, 

304 ) -> bool: 

305 """Return whether this message is enabled for the current file, line and confidence level. 

306 

307 This function can't be cached right now as the line is the line of 

308 the currently analysed file (self.file_state), if it changes, then the 

309 result for the same msg_descr/line might need to change. 

310 

311 :param msg_descr: Either the msgid or the symbol for a MessageDefinition 

312 :param line: The line of the currently analysed file 

313 :param confidence: The confidence of the message 

314 """ 

315 if confidence and confidence.name not in self.linter.config.confidence: 

316 return False 

317 try: 

318 msgids = self.linter.msgs_store.message_id_store.get_active_msgids( 

319 msg_descr 

320 ) 

321 except exceptions.UnknownMessageError: 

322 # The linter checks for messages that are not registered 

323 # due to version mismatch, just treat them as message IDs 

324 # for now. 

325 msgids = [msg_descr] 

326 return any(self._is_one_message_enabled(msgid, line) for msgid in msgids) 

327 

328 def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: 

329 """Process tokens from the current module to search for module/block level 

330 options. 

331 

332 See func_block_disable_msg.py test case for expected behaviour. 

333 """ 

334 control_pragmas = {"disable", "disable-next", "enable"} 

335 prev_line = None 

336 saw_newline = True 

337 seen_newline = True 

338 for (tok_type, content, start, _, _) in tokens: 

339 if prev_line and prev_line != start[0]: 

340 saw_newline = seen_newline 

341 seen_newline = False 

342 

343 prev_line = start[0] 

344 if tok_type in (tokenize.NL, tokenize.NEWLINE): 

345 seen_newline = True 

346 

347 if tok_type != tokenize.COMMENT: 

348 continue 

349 match = OPTION_PO.search(content) 

350 if match is None: 

351 continue 

352 try: 

353 for pragma_repr in parse_pragma(match.group(2)): 

354 if pragma_repr.action in {"disable-all", "skip-file"}: 

355 if pragma_repr.action == "disable-all": 

356 self.linter.add_message( 

357 "deprecated-pragma", 

358 line=start[0], 

359 args=("disable-all", "skip-file"), 

360 ) 

361 self.linter.add_message("file-ignored", line=start[0]) 

362 self._ignore_file = True 

363 return 

364 try: 

365 meth = self._options_methods[pragma_repr.action] 

366 except KeyError: 

367 meth = self._bw_options_methods[pragma_repr.action] 

368 # found a "(dis|en)able-msg" pragma deprecated suppression 

369 self.linter.add_message( 

370 "deprecated-pragma", 

371 line=start[0], 

372 args=( 

373 pragma_repr.action, 

374 pragma_repr.action.replace("-msg", ""), 

375 ), 

376 ) 

377 for msgid in pragma_repr.messages: 

378 # Add the line where a control pragma was encountered. 

379 if pragma_repr.action in control_pragmas: 

380 self._pragma_lineno[msgid] = start[0] 

381 

382 if (pragma_repr.action, msgid) == ("disable", "all"): 

383 self.linter.add_message( 

384 "deprecated-pragma", 

385 line=start[0], 

386 args=("disable=all", "skip-file"), 

387 ) 

388 self.linter.add_message("file-ignored", line=start[0]) 

389 self._ignore_file = True 

390 return 

391 # If we did not see a newline between the previous line and now, 

392 # we saw a backslash so treat the two lines as one. 

393 l_start = start[0] 

394 if not saw_newline: 

395 l_start -= 1 

396 try: 

397 meth(msgid, "module", l_start) 

398 except exceptions.UnknownMessageError: 

399 msg = f"{pragma_repr.action}. Don't recognize message {msgid}." 

400 self.linter.add_message( 

401 "bad-option-value", args=msg, line=start[0] 

402 ) 

403 except UnRecognizedOptionError as err: 

404 self.linter.add_message( 

405 "unrecognized-inline-option", args=err.token, line=start[0] 

406 ) 

407 continue 

408 except InvalidPragmaError as err: 

409 self.linter.add_message( 

410 "bad-inline-option", args=err.token, line=start[0] 

411 ) 

412 continue