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
« 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
5from __future__ import annotations
7import sys
8import tokenize
9from typing import TYPE_CHECKING
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)
28if sys.version_info >= (3, 8):
29 from typing import Literal
30else:
31 from typing_extensions import Literal
34if TYPE_CHECKING:
35 from pylint.lint.pylinter import PyLinter
38class _MessageStateHandler:
39 """Class that handles message disabling & enabling and processing of inline pragma's."""
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] = {}
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
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
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
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
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
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
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
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"}
134 message_definitions = self._get_messages_to_set(msgid, enable, ignore_unknown)
136 for message_definition in message_definitions:
137 self._set_one_msg_status(scope, message_definition, line, enable)
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
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)
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)
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)
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)
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)
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("")
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
263 def _is_one_message_enabled(self, msgid: str, line: int | None) -> bool:
264 """Checks state of a single message for the current file.
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, {})
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
296 return self._msgs_state.get(msgid, fallback)
297 return self._msgs_state.get(msgid, True)
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.
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.
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)
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.
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
343 prev_line = start[0]
344 if tok_type in (tokenize.NL, tokenize.NEWLINE):
345 seen_newline = True
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]
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