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

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 

4 

5from __future__ import annotations 

6 

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 

14 

15from astroid import nodes 

16 

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 

30 

31if TYPE_CHECKING: 

32 from pylint.lint import PyLinter 

33 

34 

35@functools.total_ordering 

36class BaseChecker(_ArgumentsProvider): 

37 

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 

48 

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 

61 

62 _ArgumentsProvider.__init__(self, linter) 

63 

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 

77 

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}" 

83 

84 def __hash__(self) -> int: 

85 """Make Checker hashable.""" 

86 return hash(f"{self.name}{self.msgs}") 

87 

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}')" 

92 

93 def __str__(self) -> str: 

94 """This might be incomplete because multiple classes inheriting BaseChecker 

95 can have the same name. 

96 

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 ) 

104 

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 

148 

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 ) 

163 

164 def check_consistency(self) -> None: 

165 """Check the consistency of msgid. 

166 

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. 

170 

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) 

184 

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 : 

210 

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) 

221 

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 ] 

228 

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) 

236 

237 def open(self) -> None: 

238 """Called before visiting project (i.e. set of modules).""" 

239 

240 def close(self) -> None: 

241 """Called after visiting project (i.e set of modules).""" 

242 

243 def get_map_data(self) -> Any: 

244 return None 

245 

246 # pylint: disable-next=unused-argument 

247 def reduce_map_data(self, linter: PyLinter, data: list[Any]) -> None: 

248 return None 

249 

250 

251class BaseTokenChecker(BaseChecker): 

252 """Base class for checkers that want to have access to the token stream.""" 

253 

254 @abc.abstractmethod 

255 def process_tokens(self, tokens: list[TokenInfo]) -> None: 

256 """Should be overridden by subclasses.""" 

257 raise NotImplementedError() 

258 

259 

260class BaseRawFileChecker(BaseChecker): 

261 """Base class for checkers which need to parse the raw file.""" 

262 

263 @abc.abstractmethod 

264 def process_module(self, node: nodes.Module) -> None: 

265 """Process a module. 

266 

267 The module's content is accessible via ``astroid.stream`` 

268 """ 

269 raise NotImplementedError()