Coverage for C:\Repos\ekr-pylint\pylint\checkers\base_checker.py: 34%
138 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 abc
8import functools
9import warnings
10from collections.abc import Iterator
11from inspect import cleandoc
12from tokenize import TokenInfo
13from typing import TYPE_CHECKING, Any
15from astroid import nodes
17from pylint.config.arguments_provider import _ArgumentsProvider
18from pylint.constants import _MSG_ORDER, MAIN_CHECKER_NAME, WarningScope
19from pylint.exceptions import InvalidMessageError
20from pylint.interfaces import Confidence, IRawChecker, ITokenChecker, implements
21from pylint.message.message_definition import MessageDefinition
22from pylint.typing import (
23 ExtraMessageOptions,
24 MessageDefinitionTuple,
25 OptionDict,
26 Options,
27 ReportsCallable,
28)
29from pylint.utils import get_rst_section, get_rst_title
31if TYPE_CHECKING:
32 from pylint.lint import PyLinter
35@functools.total_ordering
36class BaseChecker(_ArgumentsProvider):
38 # checker name (you may reuse an existing one)
39 name: str = ""
40 # ordered list of options to control the checker behaviour
41 options: Options = ()
42 # messages issued by this checker
43 msgs: dict[str, MessageDefinitionTuple] = {}
44 # reports issued by this checker
45 reports: tuple[tuple[str, str, ReportsCallable], ...] = ()
46 # mark this checker as enabled or not.
47 enabled: bool = True
49 def __init__(self, linter: PyLinter) -> None:
50 """Checker instances should have the linter as argument."""
51 if getattr(self, "__implements__", None):
52 warnings.warn(
53 "Using the __implements__ inheritance pattern for BaseChecker is no "
54 "longer supported. Child classes should only inherit BaseChecker or any "
55 "of the other checker types from pylint.checkers.",
56 DeprecationWarning,
57 )
58 if self.name is not None:
59 self.name = self.name.lower()
60 self.linter = linter
62 _ArgumentsProvider.__init__(self, linter)
64 def __gt__(self, other: Any) -> bool:
65 """Sorting of checkers."""
66 if not isinstance(other, BaseChecker):
67 return False
68 if self.name == MAIN_CHECKER_NAME:
69 return False
70 if other.name == MAIN_CHECKER_NAME:
71 return True
72 if type(self).__module__.startswith("pylint.checkers") and not type(
73 other
74 ).__module__.startswith("pylint.checkers"):
75 return False
76 return self.name > other.name
78 def __eq__(self, other: Any) -> bool:
79 """Permit to assert Checkers are equal."""
80 if not isinstance(other, BaseChecker):
81 return False
82 return f"{self.name}{self.msgs}" == f"{other.name}{other.msgs}"
84 def __hash__(self) -> int:
85 """Make Checker hashable."""
86 return hash(f"{self.name}{self.msgs}")
88 def __repr__(self) -> str:
89 status = "Checker" if self.enabled else "Disabled checker"
90 msgs = "', '".join(self.msgs.keys())
91 return f"{status} '{self.name}' (responsible for '{msgs}')"
93 def __str__(self) -> str:
94 """This might be incomplete because multiple classes inheriting BaseChecker
95 can have the same name.
97 See: MessageHandlerMixIn.get_full_documentation()
98 """
99 with warnings.catch_warnings():
100 warnings.filterwarnings("ignore", category=DeprecationWarning)
101 return self.get_full_documentation(
102 msgs=self.msgs, options=self.options_and_values(), reports=self.reports
103 )
105 def get_full_documentation(
106 self,
107 msgs: dict[str, MessageDefinitionTuple],
108 options: Iterator[tuple[str, OptionDict, Any]],
109 reports: tuple[tuple[str, str, ReportsCallable], ...],
110 doc: str | None = None,
111 module: str | None = None,
112 ) -> str:
113 result = ""
114 checker_title = f"{self.name.replace('_', ' ').title()} checker"
115 if module:
116 # Provide anchor to link against
117 result += f".. _{module}:\n\n"
118 result += f"{get_rst_title(checker_title, '~')}\n"
119 if module:
120 result += f"This checker is provided by ``{module}``.\n"
121 result += f"Verbatim name of the checker is ``{self.name}``.\n\n"
122 if doc:
123 # Provide anchor to link against
124 result += get_rst_title(f"{checker_title} Documentation", "^")
125 result += f"{cleandoc(doc)}\n\n"
126 # options might be an empty generator and not be False when cast to boolean
127 options_list = list(options)
128 if options_list:
129 result += get_rst_title(f"{checker_title} Options", "^")
130 result += f"{get_rst_section(None, options_list)}\n"
131 if msgs:
132 result += get_rst_title(f"{checker_title} Messages", "^")
133 for msgid, msg in sorted(
134 msgs.items(), key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1])
135 ):
136 msg_def = self.create_message_definition_from_tuple(msgid, msg)
137 result += f"{msg_def.format_help(checkerref=False)}\n"
138 result += "\n"
139 if reports:
140 result += get_rst_title(f"{checker_title} Reports", "^")
141 for report in reports:
142 result += (
143 ":%s: %s\n" % report[:2] # pylint: disable=consider-using-f-string
144 )
145 result += "\n"
146 result += "\n"
147 return result
149 def add_message(
150 self,
151 msgid: str,
152 line: int | None = None,
153 node: nodes.NodeNG | None = None,
154 args: Any = None,
155 confidence: Confidence | None = None,
156 col_offset: int | None = None,
157 end_lineno: int | None = None,
158 end_col_offset: int | None = None,
159 ) -> None:
160 self.linter.add_message(
161 msgid, line, node, args, confidence, col_offset, end_lineno, end_col_offset
162 )
164 def check_consistency(self) -> None:
165 """Check the consistency of msgid.
167 msg ids for a checker should be a string of len 4, where the two first
168 characters are the checker id and the two last the msg id in this
169 checker.
171 :raises InvalidMessageError: If the checker id in the messages are not
172 always the same.
173 """
174 checker_id = None
175 existing_ids = []
176 for message in self.messages:
177 if checker_id is not None and checker_id != message.msgid[1:3]:
178 error_msg = "Inconsistent checker part in message id "
179 error_msg += f"'{message.msgid}' (expected 'x{checker_id}xx' "
180 error_msg += f"because we already had {existing_ids})."
181 raise InvalidMessageError(error_msg)
182 checker_id = message.msgid[1:3]
183 existing_ids.append(message.msgid)
185 def create_message_definition_from_tuple(
186 self, msgid: str, msg_tuple: MessageDefinitionTuple
187 ) -> MessageDefinition:
188 with warnings.catch_warnings():
189 warnings.filterwarnings("ignore", category=DeprecationWarning)
190 if isinstance(self, (BaseTokenChecker, BaseRawFileChecker)):
191 default_scope = WarningScope.LINE
192 # TODO: 3.0: Remove deprecated if-statement
193 elif implements(self, (IRawChecker, ITokenChecker)):
194 warnings.warn( # pragma: no cover
195 "Checkers should subclass BaseTokenChecker or BaseRawFileChecker"
196 "instead of using the __implements__ mechanism. Use of __implements__"
197 "will no longer be supported in pylint 3.0",
198 DeprecationWarning,
199 )
200 default_scope = WarningScope.LINE # pragma: no cover
201 else:
202 default_scope = WarningScope.NODE
203 options: ExtraMessageOptions = {}
204 if len(msg_tuple) == 4:
205 (msg, symbol, descr, options) = msg_tuple # type: ignore[misc]
206 elif len(msg_tuple) == 3:
207 (msg, symbol, descr) = msg_tuple # type: ignore[misc]
208 else:
209 error_msg = """Messages should have a msgid, a symbol and a description. Something like this :
211"W1234": (
212 "message",
213 "message-symbol",
214 "Message description with detail.",
215 ...
216),
217"""
218 raise InvalidMessageError(error_msg)
219 options.setdefault("scope", default_scope)
220 return MessageDefinition(self, msgid, msg, descr, symbol, **options)
222 @property
223 def messages(self) -> list[MessageDefinition]:
224 return [
225 self.create_message_definition_from_tuple(msgid, msg_tuple)
226 for msgid, msg_tuple in sorted(self.msgs.items())
227 ]
229 def get_message_definition(self, msgid: str) -> MessageDefinition:
230 for message_definition in self.messages:
231 if message_definition.msgid == msgid:
232 return message_definition
233 error_msg = f"MessageDefinition for '{msgid}' does not exists. "
234 error_msg += f"Choose from {[m.msgid for m in self.messages]}."
235 raise InvalidMessageError(error_msg)
237 def open(self) -> None:
238 """Called before visiting project (i.e. set of modules)."""
240 def close(self) -> None:
241 """Called after visiting project (i.e set of modules)."""
243 def get_map_data(self) -> Any:
244 return None
246 # pylint: disable-next=unused-argument
247 def reduce_map_data(self, linter: PyLinter, data: list[Any]) -> None:
248 return None
251class BaseTokenChecker(BaseChecker):
252 """Base class for checkers that want to have access to the token stream."""
254 @abc.abstractmethod
255 def process_tokens(self, tokens: list[TokenInfo]) -> None:
256 """Should be overridden by subclasses."""
257 raise NotImplementedError()
260class BaseRawFileChecker(BaseChecker):
261 """Base class for checkers which need to parse the raw file."""
263 @abc.abstractmethod
264 def process_module(self, node: nodes.Module) -> None:
265 """Process a module.
267 The module's content is accessible via ``astroid.stream``
268 """
269 raise NotImplementedError()