| # 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 abc |
| import functools |
| from collections.abc import Iterable, Sequence |
| from inspect import cleandoc |
| from tokenize import TokenInfo |
| from typing import TYPE_CHECKING, Any |
| |
| from astroid import nodes |
| |
| from pylint.config.arguments_provider import _ArgumentsProvider |
| from pylint.constants import _MSG_ORDER, MAIN_CHECKER_NAME, WarningScope |
| from pylint.exceptions import InvalidMessageError |
| from pylint.interfaces import Confidence |
| from pylint.message.message_definition import MessageDefinition |
| from pylint.typing import ( |
| ExtraMessageOptions, |
| MessageDefinitionTuple, |
| OptionDict, |
| Options, |
| ReportsCallable, |
| ) |
| from pylint.utils import get_rst_section, get_rst_title |
| |
| if TYPE_CHECKING: |
| from pylint.lint import PyLinter |
| |
| |
| @functools.total_ordering |
| class BaseChecker(_ArgumentsProvider): |
| # checker name (you may reuse an existing one) |
| name: str = "" |
| # ordered list of options to control the checker behaviour |
| options: Options = () |
| # messages issued by this checker |
| msgs: dict[str, MessageDefinitionTuple] = {} |
| # reports issued by this checker |
| reports: tuple[tuple[str, str, ReportsCallable], ...] = () |
| # mark this checker as enabled or not. |
| enabled: bool = True |
| |
| def __init__(self, linter: PyLinter) -> None: |
| """Checker instances should have the linter as argument.""" |
| if self.name is not None: |
| self.name = self.name.lower() |
| self.linter = linter |
| _ArgumentsProvider.__init__(self, linter) |
| |
| def __gt__(self, other: Any) -> bool: |
| """Permits sorting checkers for stable doc and tests. |
| |
| The main checker is always the first one, then builtin checkers in alphabetical |
| order, then extension checkers in alphabetical order. |
| """ |
| if not isinstance(other, BaseChecker): |
| return False |
| if self.name == MAIN_CHECKER_NAME: |
| return False |
| if other.name == MAIN_CHECKER_NAME: |
| return True |
| self_is_builtin = type(self).__module__.startswith("pylint.checkers") |
| if self_is_builtin ^ type(other).__module__.startswith("pylint.checkers"): |
| return not self_is_builtin |
| return self.name > other.name |
| |
| def __eq__(self, other: object) -> bool: |
| """Permit to assert Checkers are equal.""" |
| if not isinstance(other, BaseChecker): |
| return False |
| return f"{self.name}{self.msgs}" == f"{other.name}{other.msgs}" |
| |
| def __hash__(self) -> int: |
| """Make Checker hashable.""" |
| return hash(f"{self.name}{self.msgs}") |
| |
| def __repr__(self) -> str: |
| status = "Checker" if self.enabled else "Disabled checker" |
| msgs = "', '".join(self.msgs.keys()) |
| return f"{status} '{self.name}' (responsible for '{msgs}')" |
| |
| def __str__(self) -> str: |
| """This might be incomplete because multiple classes inheriting BaseChecker |
| can have the same name. |
| |
| See: MessageHandlerMixIn.get_full_documentation() |
| """ |
| return self.get_full_documentation( |
| msgs=self.msgs, options=self._options_and_values(), reports=self.reports |
| ) |
| |
| def get_full_documentation( |
| self, |
| msgs: dict[str, MessageDefinitionTuple], |
| options: Iterable[tuple[str, OptionDict, Any]], |
| reports: Sequence[tuple[str, str, ReportsCallable]], |
| doc: str | None = None, |
| module: str | None = None, |
| show_options: bool = True, |
| ) -> str: |
| result = "" |
| checker_title = f"{self.name.replace('_', ' ').title()} checker" |
| if module: |
| # Provide anchor to link against |
| result += f".. _{module}:\n\n" |
| result += f"{get_rst_title(checker_title, '~')}\n" |
| if module: |
| result += f"This checker is provided by ``{module}``.\n" |
| result += f"Verbatim name of the checker is ``{self.name}``.\n\n" |
| if doc: |
| # Provide anchor to link against |
| result += get_rst_title(f"{checker_title} Documentation", "^") |
| result += f"{cleandoc(doc)}\n\n" |
| # options might be an empty generator and not be False when cast to boolean |
| options_list = list(options) |
| if options_list: |
| if show_options: |
| result += get_rst_title(f"{checker_title} Options", "^") |
| result += f"{get_rst_section(None, options_list)}\n" |
| else: |
| result += f"See also :ref:`{self.name} checker's options' documentation <{self.name}-options>`\n\n" |
| if msgs: |
| result += get_rst_title(f"{checker_title} Messages", "^") |
| for msgid, msg in sorted( |
| msgs.items(), key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1]) |
| ): |
| msg_def = self.create_message_definition_from_tuple(msgid, msg) |
| result += f"{msg_def.format_help(checkerref=False)}\n" |
| result += "\n" |
| if reports: |
| result += get_rst_title(f"{checker_title} Reports", "^") |
| for report in reports: |
| result += f":{report[0]}: {report[1]}\n" |
| result += "\n" |
| result += "\n" |
| return result |
| |
| def add_message( |
| self, |
| msgid: str, |
| line: int | None = None, |
| node: nodes.NodeNG | None = None, |
| args: Any = None, |
| confidence: Confidence | None = None, |
| col_offset: int | None = None, |
| end_lineno: int | None = None, |
| end_col_offset: int | None = None, |
| ) -> None: |
| self.linter.add_message( |
| msgid, line, node, args, confidence, col_offset, end_lineno, end_col_offset |
| ) |
| |
| def check_consistency(self) -> None: |
| """Check the consistency of msgid. |
| |
| msg ids for a checker should be a string of len 4, where the two first |
| characters are the checker id and the two last the msg id in this |
| checker. |
| |
| :raises InvalidMessageError: If the checker id in the messages are not |
| always the same. |
| """ |
| checker_id = None |
| existing_ids = [] |
| for message in self.messages: |
| # Id's for shared messages such as the 'deprecated-*' messages |
| # can be inconsistent with their checker id. |
| if message.shared: |
| continue |
| if checker_id is not None and checker_id != message.msgid[1:3]: |
| error_msg = "Inconsistent checker part in message id " |
| error_msg += f"'{message.msgid}' (expected 'x{checker_id}xx' " |
| error_msg += f"because we already had {existing_ids})." |
| raise InvalidMessageError(error_msg) |
| checker_id = message.msgid[1:3] |
| existing_ids.append(message.msgid) |
| |
| def create_message_definition_from_tuple( |
| self, msgid: str, msg_tuple: MessageDefinitionTuple |
| ) -> MessageDefinition: |
| if isinstance(self, (BaseTokenChecker, BaseRawFileChecker)): |
| default_scope = WarningScope.LINE |
| else: |
| default_scope = WarningScope.NODE |
| options: ExtraMessageOptions = {} |
| if len(msg_tuple) == 4: |
| (msg, symbol, descr, msg_options) = msg_tuple |
| options = ExtraMessageOptions(**msg_options) |
| elif len(msg_tuple) == 3: |
| (msg, symbol, descr) = msg_tuple |
| else: |
| error_msg = """Messages should have a msgid, a symbol and a description. Something like this : |
| |
| "W1234": ( |
| "message", |
| "message-symbol", |
| "Message description with detail.", |
| ... |
| ), |
| """ |
| raise InvalidMessageError(error_msg) |
| options.setdefault("scope", default_scope) |
| return MessageDefinition(self, msgid, msg, descr, symbol, **options) |
| |
| @property |
| def messages(self) -> list[MessageDefinition]: |
| return [ |
| self.create_message_definition_from_tuple(msgid, msg_tuple) |
| for msgid, msg_tuple in sorted(self.msgs.items()) |
| ] |
| |
| def open(self) -> None: |
| """Called before visiting project (i.e. set of modules).""" |
| |
| def close(self) -> None: |
| """Called after visiting project (i.e set of modules).""" |
| |
| def get_map_data(self) -> Any: |
| return None |
| |
| # pylint: disable-next=unused-argument |
| def reduce_map_data(self, linter: PyLinter, data: list[Any]) -> None: |
| return None |
| |
| |
| class BaseTokenChecker(BaseChecker): |
| """Base class for checkers that want to have access to the token stream.""" |
| |
| @abc.abstractmethod |
| def process_tokens(self, tokens: list[TokenInfo]) -> None: |
| """Should be overridden by subclasses.""" |
| raise NotImplementedError() |
| |
| |
| class BaseRawFileChecker(BaseChecker): |
| """Base class for checkers which need to parse the raw file.""" |
| |
| @abc.abstractmethod |
| def process_module(self, node: nodes.Module) -> None: |
| """Process a module. |
| |
| The module's content is accessible via ``astroid.stream`` |
| """ |
| raise NotImplementedError() |