src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingFormatVisitor.py

branch
eric7
changeset 10362
cfa7034cccf6
child 10363
6244c89dbc3f
equal deleted inserted replaced
10361:e6ff9a4f6ee5 10362:cfa7034cccf6
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a node visitor to check logging formatting issues.
8 """
9
10 import ast
11 import contextlib
12
13
14 _LoggingLevels = {
15 "debug",
16 "critical",
17 "error",
18 "exception",
19 "info",
20 "warn",
21 "warning",
22 }
23
24
25 # default LogRecord attributes that shouldn't be overwritten by extra dict
26 _ReservedAttrs = {
27 "args", "asctime", "created", "exc_info", "exc_text", "filename",
28 "funcName", "levelname", "levelno", "lineno", "module",
29 "msecs", "message", "msg", "name", "pathname", "process",
30 "processName", "relativeCreated", "stack_info", "thread", "threadName"}
31
32 #######################################################################
33 ## LoggingFormatVisitor
34 ##
35 ## adapted from: flake8-logging-format v0.9.0
36 ##
37 ## Original: Copyright (c) 2017 Globality Engineering
38 #######################################################################
39
40 class LoggingFormatVisitor(ast.NodeVisitor):
41 """
42 Class implementing a node visitor to check logging formatting issues.
43 """
44
45 def __init__(self):
46 """
47 Constructor
48 """
49 super().__init__()
50
51 self.__currentLoggingCall = None
52 self.__currentLoggingArgument = None
53 self.__currentLoggingLevel = None
54 self.__currentExtraKeyword = None
55 self.__currentExceptNames = []
56 self.violations = []
57
58 def __withinLoggingStatement(self):
59 """
60 Private method to check, if we are inside a logging statement.
61
62 @return flag indicating we are inside a logging statement
63 @rtype bool
64 """
65 return self.__currentLoggingCall is not None
66
67 def __withinLoggingArgument(self):
68 """
69 Private method to check, if we are inside a logging argument.
70
71 @return flag indicating we are inside a logging argument
72 @rtype bool
73 """
74 return self.__currentLoggingArgument is not None
75
76 def __withinExtraKeyword(self, node):
77 """
78 Private method to check, if we are inside the extra keyword.
79
80 @param node reference to the node to be checked
81 @type ast.keyword
82 @return flag indicating we are inside the extra keyword
83 @rtype bool
84 """
85 return (
86 self.__currentExtraKeyword is not None
87 and self.__currentExtraKeyword != node
88 )
89
90 def __getExceptHandlerName(self, node):
91 """
92 Private method to get the exception name from an ExceptHandler node.
93
94 @param node reference to the node to be checked
95 @type ast.ExceptHandler
96 @return exception name
97 @rtype str
98 """
99 name = node.name
100 if not name:
101 return None
102
103 return name
104
105 def __getIdAttr(self, value):
106 """
107 Private method to check if value has id attribute and return it.
108
109 @param value value to get id from
110 @type ast.Name
111 @return ID of value
112 @rtype str
113 """
114 """Check if value has id attribute and return it.
115
116 :param value: The value to get id from.
117 :return: The value.id.
118 """
119 if not hasattr(value, "id") and hasattr(value, "value"):
120 value = value.value
121
122 return value.id
123
124 def __detectLoggingLevel(self, node):
125 """
126 Private method to decide whether an AST Call is a logging call.
127
128 @param node reference to the node to be processed
129 @type ast.Call
130 @return logging level
131 @rtype str or None
132 """
133 with contextlib.suppress(AttributeError):
134 if self.__getIdAttr(node.func.value) in ("parser", "warnings"):
135 return None
136
137 if node.func.attr in _LoggingLevels:
138 return node.func.attr
139
140 return None
141
142 def __isFormatCall(self, node):
143 """
144 Private method to check if a function call uses format.
145
146 @param node reference to the node to be processed
147 @type ast.Call
148 @return flag indicating the function call uses format
149 @rtype bool
150 """
151 try:
152 return node.func.attr == "format"
153 except AttributeError:
154 return False
155
156
157 def __shouldCheckExtraFieldClash(self, node):
158 """
159 Private method to check, if the extra field clash check should be done.
160
161 @param node reference to the node to be processed
162 @type ast.Dict
163 @return flag indicating to perform the check
164 @rtype bool
165 """
166 return all(
167 (
168 self.__withinLoggingStatement(),
169 self.__withinExtraKeyword(node),
170 )
171 )
172
173 def __shouldCheckExtraException(self, node):
174 """
175 Private method to check, if the check for extra exceptions should be done.
176
177 c @type ast.Dict
178 @return flag indicating to perform the check
179 @rtype bool
180 """
181 return all(
182 (
183 self.__withinLoggingStatement(),
184 self.__withinExtraKeyword(node),
185 len(self.__currentExceptNames) > 0,
186 )
187 )
188
189 def __isBareException(self, node):
190 """
191 Private method to check, if the node is a bare exception name from an except
192 block.
193
194 @param node reference to the node to be processed
195 @type ast.AST
196 @return flag indicating a bare exception
197 @rtype TYPE
198 """
199 return isinstance(node, ast.Name) and node.id in self.__currentExceptNames
200
201 def __isStrException(self, node):
202 """
203 Private method to check if the node is the expression str(e) or unicode(e),
204 where e is an exception name from an except block.
205
206 @param node reference to the node to be processed
207 @type ast.AST
208 @return flag indicating a string exception
209 @rtype TYPE
210 """
211 return (
212 isinstance(node, ast.Call)
213 and isinstance(node.func, ast.Name)
214 and node.func.id in ('str', 'unicode')
215 and node.args
216 and self.__isBareException(node.args[0])
217 )
218
219 def __checkExceptionArg(self, node):
220 """
221 Private method to check an exception argument.
222
223 @param node reference to the node to be processed
224 @type ast.AST
225 """
226 if self.__isBareException(node) or self.__isStrException(node):
227 self.violations.append((node, "L130"))
228
229 def __checkExcInfo(self, node):
230 """
231 Private method to check, if the exc_info keyword is used with logging.error or
232 logging.exception.
233
234 @param node reference to the node to be processed
235 @type ast.AST
236 """
237 if self.__currentLoggingLevel not in ('error', 'exception'):
238 return
239
240 for kw in node.keywords:
241 if kw.arg == 'exc_info':
242 if self.__currentLoggingLevel == 'error':
243 violation = "L131"
244 else:
245 violation = "L132"
246 self.violations.append((node, violation))
247
248 def visit_Call(self, node):
249 """
250 Public method to handle a function call.
251
252 Every logging statement and string format is expected to be a function
253 call.
254
255 @param node reference to the node to be processed
256 @type ast.Call
257 """
258 # we are in a logging statement
259 if (
260 self.__withinLoggingStatement()
261 and self.__withinLoggingArgument()
262 and self.__isFormatCall(node)
263 ):
264 self.violations.append((node, "L101"))
265 super().generic_visit(node)
266 return
267
268 loggingLevel = self.__detectLoggingLevel(node)
269
270 if loggingLevel and self.__currentLoggingLevel is None:
271 self.__currentLoggingLevel = loggingLevel
272
273 # we are in some other statement
274 if loggingLevel is None:
275 super().generic_visit(node)
276 return
277
278 # we are entering a new logging statement
279 self.__currentLoggingCall = node
280
281 if loggingLevel == "warn":
282 self.violations.append((node, "L110"))
283
284 self.__checkExcInfo(node)
285
286 for index, child in enumerate(ast.iter_child_nodes(node)):
287 if index == 1:
288 self.__currentLoggingArgument = child
289 if index >= 1:
290 self.__checkExceptionArg(child)
291 if index > 1 and isinstance(child, ast.keyword) and child.arg == "extra":
292 self.__currentExtraKeyword = child
293
294 super().visit(child)
295
296 self.__currentLoggingArgument = None
297 self.__currentExtraKeyword = None
298
299 self.__currentLoggingCall = None
300 self.__currentLoggingLevel = None
301
302 def visit_BinOp(self, node):
303 """
304 Public method to handle binary operations while processing the first
305 logging argument.
306
307 @param node reference to the node to be processed
308 @type ast.BinOp
309 """
310 if self.__withinLoggingStatement() and self.__withinLoggingArgument():
311 # handle percent format
312 if isinstance(node.op, ast.Mod):
313 self.violations.append((node, "L102"))
314
315 # handle string concat
316 if isinstance(node.op, ast.Add):
317 self.violations.append((node, "L103"))
318
319 super().generic_visit(node)
320
321 def visit_Dict(self, node):
322 """
323 Public method to handle dict arguments.
324
325 @param node reference to the node to be processed
326 @type ast.Dict
327 """
328 if self.__shouldCheckExtraFieldClash(node):
329 for key in node.keys:
330 # key can be None if the dict uses double star syntax
331 if key is not None and key.s in _ReservedAttrs:
332 self.violations.append((node, "L121", key.s))
333
334 if self.__shouldCheckExtraException(node):
335 for value in node.values:
336 self.__checkExceptionArg(value)
337
338 super().generic_visit(node)
339
340 def visit_JoinedStr(self, node):
341 """
342 Public method to handle f-string arguments.
343
344 @param node reference to the node to be processed
345 @type ast.JoinedStr
346 """
347 if (
348 self.__withinLoggingStatement()
349 and any(isinstance(i, ast.FormattedValue) for i in node.values)
350 and self.__withinLoggingArgument()
351 ):
352 self.violations.append((node, "L104"))
353
354 super().generic_visit(node)
355
356
357 def visit_ExceptHandler(self, node):
358 """
359 Public method to handle an exception handler.
360
361 @param node reference to the node to be processed
362 @type ast.ExceptHandler
363 """
364 name = self.__getExceptHandlerName(node)
365 if not name:
366 super().generic_visit(node)
367 return
368
369 self.__currentExceptNames.append(name)
370
371 super().generic_visit(node)
372
373 self.__currentExceptNames.pop()

eric ide

mercurial