third_party.pylibs.pylint.src/pylint/checkers/base_checker.py
2022-04-12 11:45:27 +02:00

204 lines
7.4 KiB
Python

# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
import functools
import warnings
from inspect import cleandoc
from typing import TYPE_CHECKING, Any, Optional
from astroid import nodes
from pylint.config.arguments_provider import _ArgumentsProvider
from pylint.constants import _MSG_ORDER, WarningScope
from pylint.exceptions import InvalidMessageError
from pylint.interfaces import Confidence, IRawChecker, ITokenChecker, implements
from pylint.message.message_definition import MessageDefinition
from pylint.typing import Options
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 = ""
# options level (0 will be displaying in --help, 1 in --long-help)
level = 1
# ordered list of options to control the checker behaviour
options: Options = ()
# messages issued by this checker
msgs: Any = {}
# reports issued by this checker
reports: Any = ()
# 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):
"""Permit to sort a list of Checker by name."""
return f"{self.name}{self.msgs}" > f"{other.name}{other.msgs}"
def __eq__(self, other):
"""Permit to assert Checkers are equal."""
return f"{self.name}{self.msgs}" == f"{other.name}{other.msgs}"
def __hash__(self):
"""Make Checker hashable."""
return hash(f"{self.name}{self.msgs}")
def __repr__(self):
status = "Checker" if self.enabled else "Disabled checker"
msgs = "', '".join(self.msgs.keys())
return f"{status} '{self.name}' (responsible for '{msgs}')"
def __str__(self):
"""This might be incomplete because multiple classes inheriting BaseChecker
can have the same name.
See: MessageHandlerMixIn.get_full_documentation()
"""
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
return self.get_full_documentation(
msgs=self.msgs, options=self.options_and_values(), reports=self.reports
)
def get_full_documentation(self, msgs, options, reports, doc=None, module=None):
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(options)
if options:
result += get_rst_title(f"{checker_title} Options", "^")
result += f"{get_rst_section(None, options)}\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 = self.create_message_definition_from_tuple(msgid, msg)
result += f"{msg.format_help(checkerref=False)}\n"
result += "\n"
if reports:
result += get_rst_title(f"{checker_title} Reports", "^")
for report in reports:
result += (
":%s: %s\n" % report[:2] # pylint: disable=consider-using-f-string
)
result += "\n"
result += "\n"
return result
def add_message(
self,
msgid: str,
line: Optional[int] = None,
node: Optional[nodes.NodeNG] = None,
args: Any = None,
confidence: Optional[Confidence] = None,
col_offset: Optional[int] = None,
end_lineno: Optional[int] = None,
end_col_offset: Optional[int] = None,
) -> None:
self.linter.add_message(
msgid, line, node, args, confidence, col_offset, end_lineno, end_col_offset
)
def check_consistency(self):
"""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:
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, msg_tuple):
if implements(self, (IRawChecker, ITokenChecker)):
default_scope = WarningScope.LINE
else:
default_scope = WarningScope.NODE
options = {}
if len(msg_tuple) > 3:
(msg, symbol, descr, options) = msg_tuple
elif len(msg_tuple) > 2:
(msg, symbol, descr) = msg_tuple
else:
error_msg = """Messages should have a msgid and a symbol. 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:
return [
self.create_message_definition_from_tuple(msgid, msg_tuple)
for msgid, msg_tuple in sorted(self.msgs.items())
]
# dummy methods implementing the IChecker interface
def get_message_definition(self, msgid):
for message_definition in self.messages:
if message_definition.msgid == msgid:
return message_definition
error_msg = f"MessageDefinition for '{msgid}' does not exists. "
error_msg += f"Choose from {[m.msgid for m in self.messages]}."
raise InvalidMessageError(error_msg)
def open(self):
"""Called before visiting project (i.e. set of modules)."""
def close(self):
"""Called after visiting project (i.e set of modules)."""
class BaseTokenChecker(BaseChecker):
"""Base class for checkers that want to have access to the token stream."""
def process_tokens(self, tokens):
"""Should be overridden by subclasses."""
raise NotImplementedError()