# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt from __future__ import annotations import tokenize from collections import defaultdict from typing import TYPE_CHECKING, Literal from pylint import exceptions, interfaces from pylint.constants import ( MSG_STATE_CONFIDENCE, MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, MSG_TYPES, MSG_TYPES_LONG, ) from pylint.interfaces import HIGH from pylint.message import MessageDefinition from pylint.typing import ManagedMessage, MessageDefinitionTuple from pylint.utils.pragma_parser import ( OPTION_PO, InvalidPragmaError, UnRecognizedOptionError, parse_pragma, ) if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter class _MessageStateHandler: """Class that handles message disabling & enabling and processing of inline pragma's. """ def __init__(self, linter: PyLinter) -> None: self.linter = linter self.default_enabled_messages: dict[str, MessageDefinitionTuple] = { k: v for k, v in self.linter.msgs.items() if len(v) == 3 or v[3].get("default_enabled", True) } self._msgs_state: dict[str, bool] = {} self._options_methods = { "enable": self.enable, "disable": self.disable, "disable-next": self.disable_next, } self._bw_options_methods = { "disable-msg": self._options_methods["disable"], "enable-msg": self._options_methods["enable"], } self._pragma_lineno: dict[str, int] = {} self._stashed_messages: defaultdict[ tuple[str, str], list[tuple[str | None, str]] ] = defaultdict(list) """Some messages in the options (for --enable and --disable) are encountered too early to warn about them. i.e. before all option providers have been fully parsed. Thus, this dict stores option_value and msg_id needed to (later) emit the messages keyed on module names. """ def _set_one_msg_status( self, scope: str, msg: MessageDefinition, line: int | None, enable: bool ) -> None: """Set the status of an individual message.""" if scope in {"module", "line"}: assert isinstance(line, int) # should always be int inside module scope self.linter.file_state.set_msg_status(msg, line, enable, scope) if not enable and msg.symbol != "locally-disabled": self.linter.add_message( "locally-disabled", line=line, args=(msg.symbol, msg.msgid) ) else: msgs = self._msgs_state msgs[msg.msgid] = enable def _get_messages_to_set( self, msgid: str, enable: bool, ignore_unknown: bool = False ) -> list[MessageDefinition]: """Do some tests and find the actual messages of which the status should be set.""" message_definitions: list[MessageDefinition] = [] if msgid == "all": for _msgid in MSG_TYPES: message_definitions.extend( self._get_messages_to_set(_msgid, enable, ignore_unknown) ) if not enable: # "all" should not disable pylint's own warnings message_definitions = list( filter( lambda m: m.msgid not in self.default_enabled_messages, message_definitions, ) ) return message_definitions # msgid is a category? category_id = msgid.upper() if category_id not in MSG_TYPES: category_id_formatted = MSG_TYPES_LONG.get(category_id) else: category_id_formatted = category_id if category_id_formatted is not None: for _msgid in self.linter.msgs_store._msgs_by_category[ category_id_formatted ]: message_definitions.extend( self._get_messages_to_set(_msgid, enable, ignore_unknown) ) return message_definitions # msgid is a checker name? if msgid.lower() in self.linter._checkers: for checker in self.linter._checkers[msgid.lower()]: for _msgid in checker.msgs: message_definitions.extend( self._get_messages_to_set(_msgid, enable, ignore_unknown) ) return message_definitions # msgid is report id? if msgid.lower().startswith("rp"): if enable: self.linter.enable_report(msgid) else: self.linter.disable_report(msgid) return message_definitions try: # msgid is a symbolic or numeric msgid. message_definitions = self.linter.msgs_store.get_message_definitions(msgid) except exceptions.UnknownMessageError: if not ignore_unknown: raise return message_definitions def _set_msg_status( self, msgid: str, enable: bool, scope: str = "package", line: int | None = None, ignore_unknown: bool = False, ) -> None: """Do some tests and then iterate over message definitions to set state.""" assert scope in {"package", "module", "line"} message_definitions = self._get_messages_to_set(msgid, enable, ignore_unknown) for message_definition in message_definitions: self._set_one_msg_status(scope, message_definition, line, enable) # sync configuration object self.linter.config.enable = [] self.linter.config.disable = [] for msgid_or_symbol, is_enabled in self._msgs_state.items(): symbols = [ m.symbol for m in self.linter.msgs_store.get_message_definitions(msgid_or_symbol) ] if is_enabled: self.linter.config.enable += symbols else: self.linter.config.disable += symbols def _register_by_id_managed_msg( self, msgid_or_symbol: str, line: int | None, is_disabled: bool = True ) -> None: """If the msgid is a numeric one, then register it to inform the user it could furnish instead a symbolic msgid. """ if msgid_or_symbol[1:].isdigit(): try: symbol = self.linter.msgs_store.message_id_store.get_symbol( msgid=msgid_or_symbol ) except exceptions.UnknownMessageError: return managed = ManagedMessage( self.linter.current_name, msgid_or_symbol, symbol, line, is_disabled ) self.linter._by_id_managed_msgs.append(managed) def disable( self, msgid: str, scope: str = "package", line: int | None = None, ignore_unknown: bool = False, ) -> None: """Disable a message for a scope.""" self._set_msg_status( msgid, enable=False, scope=scope, line=line, ignore_unknown=ignore_unknown ) self._register_by_id_managed_msg(msgid, line) def disable_next( self, msgid: str, _: str = "package", line: int | None = None, ignore_unknown: bool = False, ) -> None: """Disable a message for the next line.""" if not line: raise exceptions.NoLineSuppliedError self._set_msg_status( msgid, enable=False, scope="line", line=line + 1, ignore_unknown=ignore_unknown, ) self._register_by_id_managed_msg(msgid, line + 1) def enable( self, msgid: str, scope: str = "package", line: int | None = None, ignore_unknown: bool = False, ) -> None: """Enable a message for a scope.""" self._set_msg_status( msgid, enable=True, scope=scope, line=line, ignore_unknown=ignore_unknown ) self._register_by_id_managed_msg(msgid, line, is_disabled=False) def disable_noerror_messages(self) -> None: """Disable message categories other than `error` and `fatal`.""" for msgcat in self.linter.msgs_store._msgs_by_category: if msgcat in {"E", "F"}: continue self.disable(msgcat) def list_messages_enabled(self) -> None: emittable, non_emittable = self.linter.msgs_store.find_emittable_messages() enabled: list[str] = [] disabled: list[str] = [] for message in emittable: if self.is_message_enabled(message.msgid): enabled.append(f" {message.symbol} ({message.msgid})") else: disabled.append(f" {message.symbol} ({message.msgid})") print("Enabled messages:") for msg in enabled: print(msg) print("\nDisabled messages:") for msg in disabled: print(msg) print("\nNon-emittable messages with current interpreter:") for msg_def in non_emittable: print(f" {msg_def.symbol} ({msg_def.msgid})") print("") def _get_message_state_scope( self, msgid: str, line: int | None = None, confidence: interfaces.Confidence | None = None, ) -> Literal[0, 1, 2] | None: """Returns the scope at which a message was enabled/disabled.""" if confidence is None: confidence = interfaces.UNDEFINED if confidence.name not in self.linter.config.confidence: return MSG_STATE_CONFIDENCE # type: ignore[return-value] # mypy does not infer Literal correctly try: if line in self.linter.file_state._module_msgs_state[msgid]: return MSG_STATE_SCOPE_MODULE # type: ignore[return-value] except (KeyError, TypeError): return MSG_STATE_SCOPE_CONFIG # type: ignore[return-value] return None def _is_one_message_enabled(self, msgid: str, line: int | None) -> bool: """Checks state of a single message for the current file. This function can't be cached as it depends on self.file_state which can change. """ if line is None: return self._msgs_state.get(msgid, True) try: return self.linter.file_state._module_msgs_state[msgid][line] except KeyError: # Check if the message's line is after the maximum line existing in ast tree. # This line won't appear in the ast tree and won't be referred in # self.file_state._module_msgs_state # This happens for example with a commented line at the end of a module. max_line_number = self.linter.file_state.get_effective_max_line_number() if max_line_number and line > max_line_number: fallback = True lines = self.linter.file_state._raw_module_msgs_state.get(msgid, {}) # Doesn't consider scopes, as a 'disable' can be in a # different scope than that of the current line. closest_lines = reversed( [ (message_line, enable) for message_line, enable in lines.items() if message_line <= line ] ) _, fallback_iter = next(closest_lines, (None, None)) if fallback_iter is not None: fallback = fallback_iter return self._msgs_state.get(msgid, fallback) return self._msgs_state.get(msgid, True) def is_message_enabled( self, msg_descr: str, line: int | None = None, confidence: interfaces.Confidence | None = None, ) -> bool: """Is this message enabled for the current file ? Optionally, is it enabled for this line and confidence level ? The current file is implicit and mandatory. As a result this function can't be cached right now as the line is the line of the currently analysed file (self.file_state), if it changes, then the result for the same msg_descr/line might need to change. :param msg_descr: Either the msgid or the symbol for a MessageDefinition :param line: The line of the currently analysed file :param confidence: The confidence of the message """ if confidence and confidence.name not in self.linter.config.confidence: return False try: msgids = self.linter.msgs_store.message_id_store.get_active_msgids( msg_descr ) except exceptions.UnknownMessageError: # The linter checks for messages that are not registered # due to version mismatch, just treat them as message IDs # for now. msgids = [msg_descr] return any(self._is_one_message_enabled(msgid, line) for msgid in msgids) def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: """Process tokens from the current module to search for module/block level options. See func_block_disable_msg.py test case for expected behaviour. """ control_pragmas = {"disable", "disable-next", "enable"} prev_line = None saw_newline = True seen_newline = True for tok_type, content, start, _, _ in tokens: if prev_line and prev_line != start[0]: saw_newline = seen_newline seen_newline = False prev_line = start[0] if tok_type in (tokenize.NL, tokenize.NEWLINE): seen_newline = True if tok_type != tokenize.COMMENT: continue match = OPTION_PO.search(content) if match is None: continue try: # pylint: disable = too-many-try-statements for pragma_repr in parse_pragma(match.group(2)): if pragma_repr.action in {"disable-all", "skip-file"}: if pragma_repr.action == "disable-all": self.linter.add_message( "deprecated-pragma", line=start[0], args=("disable-all", "skip-file"), ) self.linter.add_message("file-ignored", line=start[0]) self._ignore_file = True return try: meth = self._options_methods[pragma_repr.action] except KeyError: meth = self._bw_options_methods[pragma_repr.action] # found a "(dis|en)able-msg" pragma deprecated suppression self.linter.add_message( "deprecated-pragma", line=start[0], args=( pragma_repr.action, pragma_repr.action.replace("-msg", ""), ), ) for msgid in pragma_repr.messages: # Add the line where a control pragma was encountered. if pragma_repr.action in control_pragmas: self._pragma_lineno[msgid] = start[0] if (pragma_repr.action, msgid) == ("disable", "all"): self.linter.add_message( "deprecated-pragma", line=start[0], args=("disable=all", "skip-file"), ) self.linter.add_message("file-ignored", line=start[0]) self._ignore_file = True return # If we did not see a newline between the previous line and now, # we saw a backslash so treat the two lines as one. l_start = start[0] if not saw_newline: l_start -= 1 try: meth(msgid, "module", l_start) except ( exceptions.DeletedMessageError, exceptions.MessageBecameExtensionError, ) as e: self.linter.add_message( "useless-option-value", args=(pragma_repr.action, e), line=start[0], confidence=HIGH, ) except exceptions.UnknownMessageError: self.linter.add_message( "unknown-option-value", args=(pragma_repr.action, msgid), line=start[0], confidence=HIGH, ) except UnRecognizedOptionError as err: self.linter.add_message( "unrecognized-inline-option", args=err.token, line=start[0] ) continue except InvalidPragmaError as err: self.linter.add_message( "bad-inline-option", args=err.token, line=start[0] ) continue