|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2025 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a visitor to check for various potential issues. |
|
8 """ |
|
9 |
|
10 import ast |
|
11 import builtins |
|
12 import contextlib |
|
13 import itertools |
|
14 import math |
|
15 import re |
|
16 |
|
17 from collections import Counter, namedtuple |
|
18 from dataclasses import dataclass |
|
19 from keyword import iskeyword |
|
20 |
|
21 import AstUtilities |
|
22 |
|
23 ####################################################################### |
|
24 ## adapted from: flake8-bugbear v24.12.12 |
|
25 ## |
|
26 ## Original: Copyright (c) 2016 Ćukasz Langa |
|
27 ####################################################################### |
|
28 |
|
29 BugbearMutableLiterals = ("Dict", "List", "Set") |
|
30 BugbearMutableComprehensions = ("ListComp", "DictComp", "SetComp") |
|
31 BugbearMutableCalls = ( |
|
32 "Counter", |
|
33 "OrderedDict", |
|
34 "collections.Counter", |
|
35 "collections.OrderedDict", |
|
36 "collections.defaultdict", |
|
37 "collections.deque", |
|
38 "defaultdict", |
|
39 "deque", |
|
40 "dict", |
|
41 "list", |
|
42 "set", |
|
43 ) |
|
44 BugbearImmutableCalls = ( |
|
45 "tuple", |
|
46 "frozenset", |
|
47 "types.MappingProxyType", |
|
48 "MappingProxyType", |
|
49 "re.compile", |
|
50 "operator.attrgetter", |
|
51 "operator.itemgetter", |
|
52 "operator.methodcaller", |
|
53 "attrgetter", |
|
54 "itemgetter", |
|
55 "methodcaller", |
|
56 ) |
|
57 |
|
58 |
|
59 BugBearContext = namedtuple("BugBearContext", ["node", "stack"]) |
|
60 |
|
61 |
|
62 def composeCallPath(node): |
|
63 """ |
|
64 Generator function to assemble the call path of a given node. |
|
65 |
|
66 @param node node to assemble call path for |
|
67 @type ast.Node |
|
68 @yield call path components |
|
69 @ytype str |
|
70 """ |
|
71 if isinstance(node, ast.Attribute): |
|
72 yield from composeCallPath(node.value) |
|
73 yield node.attr |
|
74 elif isinstance(node, ast.Call): |
|
75 yield from composeCallPath(node.func) |
|
76 elif isinstance(node, ast.Name): |
|
77 yield node.id |
|
78 |
|
79 |
|
80 @dataclass |
|
81 class M540CaughtException: |
|
82 """ |
|
83 Class to hold the data for a caught exception. |
|
84 """ |
|
85 |
|
86 name: str |
|
87 hasNote: bool |
|
88 |
|
89 |
|
90 class M541UnhandledKeyType: |
|
91 """ |
|
92 Class to hold a dictionary key of a type that we do not check for duplicates. |
|
93 """ |
|
94 |
|
95 |
|
96 class M541VariableKeyType: |
|
97 """ |
|
98 Class to hold the name of a variable key type. |
|
99 """ |
|
100 |
|
101 def __init__(self, name): |
|
102 """ |
|
103 Constructor |
|
104 |
|
105 @param name name of the variable key type |
|
106 @type str |
|
107 """ |
|
108 self.name = name |
|
109 |
|
110 |
|
111 class BugBearVisitor(ast.NodeVisitor): |
|
112 """ |
|
113 Class implementing a node visitor to check for various topics. |
|
114 """ |
|
115 |
|
116 CONTEXTFUL_NODES = ( |
|
117 ast.Module, |
|
118 ast.ClassDef, |
|
119 ast.AsyncFunctionDef, |
|
120 ast.FunctionDef, |
|
121 ast.Lambda, |
|
122 ast.ListComp, |
|
123 ast.SetComp, |
|
124 ast.DictComp, |
|
125 ast.GeneratorExp, |
|
126 ) |
|
127 |
|
128 FUNCTION_NODES = ( |
|
129 ast.AsyncFunctionDef, |
|
130 ast.FunctionDef, |
|
131 ast.Lambda, |
|
132 ) |
|
133 |
|
134 NodeWindowSize = 4 |
|
135 |
|
136 def __init__(self): |
|
137 """ |
|
138 Constructor |
|
139 """ |
|
140 super().__init__() |
|
141 |
|
142 self.nodeWindow = [] |
|
143 self.violations = [] |
|
144 self.contexts = [] |
|
145 |
|
146 self.__M523Seen = set() |
|
147 self.__M505Imports = set() |
|
148 self.__M540CaughtException = None |
|
149 |
|
150 self.__inTryStar = "" |
|
151 |
|
152 @property |
|
153 def nodeStack(self): |
|
154 """ |
|
155 Public method to get a reference to the most recent node stack. |
|
156 |
|
157 @return reference to the most recent node stack |
|
158 @rtype list |
|
159 """ |
|
160 if len(self.contexts) == 0: |
|
161 return [] |
|
162 |
|
163 context, stack = self.contexts[-1] |
|
164 return stack |
|
165 |
|
166 def __isIdentifier(self, arg): |
|
167 """ |
|
168 Private method to check if arg is a valid identifier. |
|
169 |
|
170 See https://docs.python.org/2/reference/lexical_analysis.html#identifiers |
|
171 |
|
172 @param arg reference to an argument node |
|
173 @type ast.Node |
|
174 @return flag indicating a valid identifier |
|
175 @rtype TYPE |
|
176 """ |
|
177 if not AstUtilities.isString(arg): |
|
178 return False |
|
179 |
|
180 return ( |
|
181 re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", AstUtilities.getValue(arg)) |
|
182 is not None |
|
183 ) |
|
184 |
|
185 def toNameStr(self, node): |
|
186 """ |
|
187 Public method to turn Name and Attribute nodes to strings, handling any |
|
188 depth of attribute accesses. |
|
189 |
|
190 |
|
191 @param node reference to the node |
|
192 @type ast.Name or ast.Attribute |
|
193 @return string representation |
|
194 @rtype str |
|
195 """ |
|
196 if isinstance(node, ast.Name): |
|
197 return node.id |
|
198 elif isinstance(node, ast.Call): |
|
199 return self.toNameStr(node.func) |
|
200 elif isinstance(node, ast.Attribute): |
|
201 inner = self.toNameStr(node.value) |
|
202 if inner is None: |
|
203 return None |
|
204 return f"{inner}.{node.attr}" |
|
205 else: |
|
206 return None |
|
207 |
|
208 def __typesafeIssubclass(self, obj, classOrTuple): |
|
209 """ |
|
210 Private method implementing a type safe issubclass() function. |
|
211 |
|
212 @param obj reference to the object to be tested |
|
213 @type Any |
|
214 @param classOrTuple type to check against |
|
215 @type type |
|
216 @return flag indicating a subclass |
|
217 @rtype bool |
|
218 """ |
|
219 try: |
|
220 return issubclass(obj, classOrTuple) |
|
221 except TypeError: |
|
222 # User code specifies a type that is not a type in our current run. |
|
223 # Might be their error, might be a difference in our environments. |
|
224 # We don't know so we ignore this. |
|
225 return False |
|
226 |
|
227 def __getAssignedNames(self, loopNode): |
|
228 """ |
|
229 Private method to get the names of a for loop. |
|
230 |
|
231 @param loopNode reference to the node to be processed |
|
232 @type ast.For |
|
233 @yield DESCRIPTION |
|
234 @ytype TYPE |
|
235 """ |
|
236 loopTargets = (ast.For, ast.AsyncFor, ast.comprehension) |
|
237 for node in self.__childrenInScope(loopNode): |
|
238 if isinstance(node, (ast.Assign)): |
|
239 for child in node.targets: |
|
240 yield from self.__namesFromAssignments(child) |
|
241 if isinstance(node, loopTargets + (ast.AnnAssign, ast.AugAssign)): |
|
242 yield from self.__namesFromAssignments(node.target) |
|
243 |
|
244 def __namesFromAssignments(self, assignTarget): |
|
245 """ |
|
246 Private method to get names of an assignment. |
|
247 |
|
248 @param assignTarget reference to the node to be processed |
|
249 @type ast.Node |
|
250 @yield name of the assignment |
|
251 @ytype str |
|
252 """ |
|
253 if isinstance(assignTarget, ast.Name): |
|
254 yield assignTarget.id |
|
255 elif isinstance(assignTarget, ast.Starred): |
|
256 yield from self.__namesFromAssignments(assignTarget.value) |
|
257 elif isinstance(assignTarget, (ast.List, ast.Tuple)): |
|
258 for child in assignTarget.elts: |
|
259 yield from self.__namesFromAssignments(child) |
|
260 |
|
261 def __childrenInScope(self, node): |
|
262 """ |
|
263 Private method to get all child nodes in the given scope. |
|
264 |
|
265 @param node reference to the node to be processed |
|
266 @type ast.Node |
|
267 @yield reference to a child node |
|
268 @ytype ast.Node |
|
269 """ |
|
270 yield node |
|
271 if not isinstance(node, BugBearVisitor.FUNCTION_NODES): |
|
272 for child in ast.iter_child_nodes(node): |
|
273 yield from self.__childrenInScope(child) |
|
274 |
|
275 def __flattenExcepthandler(self, node): |
|
276 """ |
|
277 Private method to flatten the list of exceptions handled by an except handler. |
|
278 |
|
279 @param node reference to the node to be processed |
|
280 @type ast.Node |
|
281 @yield reference to the exception type node |
|
282 @ytype ast.Node |
|
283 """ |
|
284 if not isinstance(node, ast.Tuple): |
|
285 yield node |
|
286 return |
|
287 |
|
288 exprList = node.elts.copy() |
|
289 while len(exprList): |
|
290 expr = exprList.pop(0) |
|
291 if isinstance(expr, ast.Starred) and isinstance( |
|
292 expr.value, (ast.List, ast.Tuple) |
|
293 ): |
|
294 exprList.extend(expr.value.elts) |
|
295 continue |
|
296 yield expr |
|
297 |
|
298 def __checkRedundantExcepthandlers(self, names, node, inTryStar): |
|
299 """ |
|
300 Private method to check for redundant exception types in an exception handler. |
|
301 |
|
302 @param names list of exception types to be checked |
|
303 @type list of ast.Name |
|
304 @param node reference to the exception handler node |
|
305 @type ast.ExceptionHandler |
|
306 @param inTryStar character indicating an 'except*' handler |
|
307 @type str |
|
308 @return tuple containing the error data |
|
309 @rtype tuple of (ast.Node, str, str, str, str) |
|
310 """ |
|
311 redundantExceptions = { |
|
312 "OSError": { |
|
313 # All of these are actually aliases of OSError since Python 3.3 |
|
314 "IOError", |
|
315 "EnvironmentError", |
|
316 "WindowsError", |
|
317 "mmap.error", |
|
318 "socket.error", |
|
319 "select.error", |
|
320 }, |
|
321 "ValueError": { |
|
322 "binascii.Error", |
|
323 }, |
|
324 } |
|
325 |
|
326 # See if any of the given exception names could be removed, e.g. from: |
|
327 # (MyError, MyError) # duplicate names # noqa: M-891 |
|
328 # (MyError, BaseException) # everything derives from the Base # noqa: M-891 |
|
329 # (Exception, TypeError) # builtins where one subclasses another |
|
330 # noqa: M-891 |
|
331 # (IOError, OSError) # IOError is an alias of OSError since Python3.3 |
|
332 # noqa: M-891 |
|
333 # but note that other cases are impractical to handle from the AST. |
|
334 # We expect this is mostly useful for users who do not have the |
|
335 # builtin exception hierarchy memorised, and include a 'shadowed' |
|
336 # subtype without realising that it's redundant. |
|
337 good = sorted(set(names), key=names.index) |
|
338 if "BaseException" in good: |
|
339 good = ["BaseException"] |
|
340 # Remove redundant exceptions that the automatic system either handles |
|
341 # poorly (usually aliases) or can't be checked (e.g. it's not an |
|
342 # built-in exception). |
|
343 for primary, equivalents in redundantExceptions.items(): |
|
344 if primary in good: |
|
345 good = [g for g in good if g not in equivalents] |
|
346 |
|
347 for name, other in itertools.permutations(tuple(good), 2): |
|
348 if ( |
|
349 self.__typesafeIssubclass( |
|
350 getattr(builtins, name, type), getattr(builtins, other, ()) |
|
351 ) |
|
352 and name in good |
|
353 ): |
|
354 good.remove(name) |
|
355 if good != names: |
|
356 desc = good[0] if len(good) == 1 else "({0})".format(", ".join(good)) |
|
357 as_ = " as " + node.name if node.name is not None else "" |
|
358 return (node, "M-514", ", ".join(names), as_, desc, inTryStar) |
|
359 |
|
360 return None |
|
361 |
|
362 def __walkList(self, nodes): |
|
363 """ |
|
364 Private method to walk a given list of nodes. |
|
365 |
|
366 @param nodes list of nodes to walk |
|
367 @type list of ast.Node |
|
368 @yield node references as determined by the ast.walk() function |
|
369 @ytype ast.Node |
|
370 """ |
|
371 for node in nodes: |
|
372 yield from ast.walk(node) |
|
373 |
|
374 def __getNamesFromTuple(self, node): |
|
375 """ |
|
376 Private method to get the names from an ast.Tuple node. |
|
377 |
|
378 @param node ast node to be processed |
|
379 @type ast.Tuple |
|
380 @yield names |
|
381 @ytype str |
|
382 """ |
|
383 for dim in node.elts: |
|
384 if isinstance(dim, ast.Name): |
|
385 yield dim.id |
|
386 elif isinstance(dim, ast.Tuple): |
|
387 yield from self.__getNamesFromTuple(dim) |
|
388 |
|
389 def __getDictCompLoopAndNamedExprVarNames(self, node): |
|
390 """ |
|
391 Private method to get the names of comprehension loop variables. |
|
392 |
|
393 @param node ast node to be processed |
|
394 @type ast.DictComp |
|
395 @yield loop variable names |
|
396 @ytype str |
|
397 """ |
|
398 finder = NamedExprFinder() |
|
399 for gen in node.generators: |
|
400 if isinstance(gen.target, ast.Name): |
|
401 yield gen.target.id |
|
402 elif isinstance(gen.target, ast.Tuple): |
|
403 yield from self.__getNamesFromTuple(gen.target) |
|
404 |
|
405 finder.visit(gen.ifs) |
|
406 |
|
407 yield from finder.getNames().keys() |
|
408 |
|
409 def __inClassInit(self): |
|
410 """ |
|
411 Private method to check, if we are inside an '__init__' method. |
|
412 |
|
413 @return flag indicating being within the '__init__' method |
|
414 @rtype bool |
|
415 """ |
|
416 return ( |
|
417 len(self.contexts) >= 2 |
|
418 and isinstance(self.contexts[-2].node, ast.ClassDef) |
|
419 and isinstance(self.contexts[-1].node, ast.FunctionDef) |
|
420 and self.contexts[-1].node.name == "__init__" |
|
421 ) |
|
422 |
|
423 def visit_Return(self, node): |
|
424 """ |
|
425 Public method to handle 'Return' nodes. |
|
426 |
|
427 @param node reference to the node to be processed |
|
428 @type ast.Return |
|
429 """ |
|
430 if self.__inClassInit() and node.value is not None: |
|
431 self.violations.append((node, "M-537")) |
|
432 |
|
433 self.generic_visit(node) |
|
434 |
|
435 def visit_Yield(self, node): |
|
436 """ |
|
437 Public method to handle 'Yield' nodes. |
|
438 |
|
439 @param node reference to the node to be processed |
|
440 @type ast.Yield |
|
441 """ |
|
442 if self.__inClassInit(): |
|
443 self.violations.append((node, "M-537")) |
|
444 |
|
445 self.generic_visit(node) |
|
446 |
|
447 def visit_YieldFrom(self, node) -> None: |
|
448 """ |
|
449 Public method to handle 'YieldFrom' nodes. |
|
450 |
|
451 @param node reference to the node to be processed |
|
452 @type ast.YieldFrom |
|
453 """ |
|
454 if self.__inClassInit(): |
|
455 self.violations.append((node, "M-537")) |
|
456 |
|
457 self.generic_visit(node) |
|
458 |
|
459 def visit(self, node): |
|
460 """ |
|
461 Public method to traverse a given AST node. |
|
462 |
|
463 @param node AST node to be traversed |
|
464 @type ast.Node |
|
465 """ |
|
466 isContextful = isinstance(node, BugBearVisitor.CONTEXTFUL_NODES) |
|
467 |
|
468 if isContextful: |
|
469 context = BugBearContext(node, []) |
|
470 self.contexts.append(context) |
|
471 |
|
472 self.nodeStack.append(node) |
|
473 self.nodeWindow.append(node) |
|
474 self.nodeWindow = self.nodeWindow[-BugBearVisitor.NodeWindowSize :] |
|
475 |
|
476 super().visit(node) |
|
477 |
|
478 self.nodeStack.pop() |
|
479 |
|
480 if isContextful: |
|
481 self.contexts.pop() |
|
482 |
|
483 self.__checkForM518(node) |
|
484 |
|
485 def visit_ExceptHandler(self, node): |
|
486 """ |
|
487 Public method to handle exception handlers. |
|
488 |
|
489 @param node reference to the node to be processed |
|
490 @type ast.ExceptHandler |
|
491 """ |
|
492 if node.type is None: |
|
493 # bare except is handled by pycodestyle already |
|
494 self.generic_visit(node) |
|
495 return |
|
496 |
|
497 oldM540CaughtException = self.__M540CaughtException |
|
498 if node.name is None: |
|
499 self.__M540CaughtException = None |
|
500 else: |
|
501 self.__M540CaughtException = M540CaughtException(node.name, False) |
|
502 |
|
503 names = self.__checkForM513_M514_M529_M530(node) |
|
504 |
|
505 if "BaseException" in names and not ExceptBaseExceptionVisitor(node).reRaised(): |
|
506 self.violations.append((node, "M-536")) |
|
507 |
|
508 self.generic_visit(node) |
|
509 |
|
510 if ( |
|
511 self.__M540CaughtException is not None |
|
512 and self.__M540CaughtException.hasNote |
|
513 ): |
|
514 self.violations.append((node, "M-540")) |
|
515 self.__M540CaughtException = oldM540CaughtException |
|
516 |
|
517 def visit_UAdd(self, node): |
|
518 """ |
|
519 Public method to handle unary additions. |
|
520 |
|
521 @param node reference to the node to be processed |
|
522 @type ast.UAdd |
|
523 """ |
|
524 trailingNodes = list(map(type, self.nodeWindow[-4:])) |
|
525 if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]: |
|
526 originator = self.nodeWindow[-4] |
|
527 self.violations.append((originator, "M-502")) |
|
528 |
|
529 self.generic_visit(node) |
|
530 |
|
531 def visit_Call(self, node): |
|
532 """ |
|
533 Public method to handle a function call. |
|
534 |
|
535 @param node reference to the node to be processed |
|
536 @type ast.Call |
|
537 """ |
|
538 isM540AddNote = False |
|
539 |
|
540 if isinstance(node.func, ast.Attribute): |
|
541 self.__checkForM505(node) |
|
542 isM540AddNote = self.__checkForM540AddNote(node.func) |
|
543 else: |
|
544 with contextlib.suppress(AttributeError, IndexError): |
|
545 # bad super() call |
|
546 if isinstance(node.func, ast.Name) and node.func.id == "super": |
|
547 args = node.args |
|
548 if ( |
|
549 len(args) == 2 |
|
550 and isinstance(args[0], ast.Attribute) |
|
551 and isinstance(args[0].value, ast.Name) |
|
552 and args[0].value.id == "self" |
|
553 and args[0].attr == "__class__" |
|
554 ): |
|
555 self.violations.append((node, "M-582")) |
|
556 |
|
557 # bad getattr and setattr |
|
558 if ( |
|
559 node.func.id in ("getattr", "hasattr") |
|
560 and node.args[1].value == "__call__" |
|
561 ): |
|
562 self.violations.append((node, "M-504")) |
|
563 if ( |
|
564 node.func.id == "getattr" |
|
565 and len(node.args) == 2 |
|
566 and self.__isIdentifier(node.args[1]) |
|
567 and iskeyword(AstUtilities.getValue(node.args[1])) |
|
568 ): |
|
569 self.violations.append((node, "M-509")) |
|
570 elif ( |
|
571 node.func.id == "setattr" |
|
572 and len(node.args) == 3 |
|
573 and self.__isIdentifier(node.args[1]) |
|
574 and iskeyword(AstUtilities.getValue(node.args[1])) |
|
575 ): |
|
576 self.violations.append((node, "M-510")) |
|
577 |
|
578 self.__checkForM526(node) |
|
579 |
|
580 self.__checkForM528(node) |
|
581 self.__checkForM534(node) |
|
582 self.__checkForM539(node) |
|
583 |
|
584 # no need for copying, if used in nested calls it will be set to None |
|
585 currentM540CaughtException = self.__M540CaughtException |
|
586 if not isM540AddNote: |
|
587 self.__checkForM540Usage(node.args) |
|
588 self.__checkForM540Usage(node.keywords) |
|
589 |
|
590 self.generic_visit(node) |
|
591 |
|
592 if isM540AddNote: |
|
593 # Avoid nested calls within the parameter list using the variable itself. |
|
594 # e.g. `e.add_note(str(e))` |
|
595 self.__M540CaughtException = currentM540CaughtException |
|
596 |
|
597 def visit_Module(self, node): |
|
598 """ |
|
599 Public method to handle a module node. |
|
600 |
|
601 @param node reference to the node to be processed |
|
602 @type ast.Module |
|
603 """ |
|
604 self.generic_visit(node) |
|
605 |
|
606 def visit_Assign(self, node): |
|
607 """ |
|
608 Public method to handle assignments. |
|
609 |
|
610 @param node reference to the node to be processed |
|
611 @type ast.Assign |
|
612 """ |
|
613 self.__checkForM540Usage(node.value) |
|
614 if len(node.targets) == 1: |
|
615 target = node.targets[0] |
|
616 if ( |
|
617 isinstance(target, ast.Attribute) |
|
618 and isinstance(target.value, ast.Name) |
|
619 and (target.value.id, target.attr) == ("os", "environ") |
|
620 ): |
|
621 self.violations.append((node, "M-503")) |
|
622 |
|
623 self.generic_visit(node) |
|
624 |
|
625 def visit_For(self, node): |
|
626 """ |
|
627 Public method to handle 'for' statements. |
|
628 |
|
629 @param node reference to the node to be processed |
|
630 @type ast.For |
|
631 """ |
|
632 self.__checkForM507(node) |
|
633 self.__checkForM520(node) |
|
634 self.__checkForM523(node) |
|
635 self.__checkForM531(node) |
|
636 self.__checkForM569(node) |
|
637 |
|
638 self.generic_visit(node) |
|
639 |
|
640 def visit_AsyncFor(self, node): |
|
641 """ |
|
642 Public method to handle 'for' statements. |
|
643 |
|
644 @param node reference to the node to be processed |
|
645 @type ast.AsyncFor |
|
646 """ |
|
647 self.__checkForM507(node) |
|
648 self.__checkForM520(node) |
|
649 self.__checkForM523(node) |
|
650 self.__checkForM531(node) |
|
651 |
|
652 self.generic_visit(node) |
|
653 |
|
654 def visit_While(self, node): |
|
655 """ |
|
656 Public method to handle 'while' statements. |
|
657 |
|
658 @param node reference to the node to be processed |
|
659 @type ast.While |
|
660 """ |
|
661 self.__checkForM523(node) |
|
662 |
|
663 self.generic_visit(node) |
|
664 |
|
665 def visit_ListComp(self, node): |
|
666 """ |
|
667 Public method to handle list comprehensions. |
|
668 |
|
669 @param node reference to the node to be processed |
|
670 @type ast.ListComp |
|
671 """ |
|
672 self.__checkForM523(node) |
|
673 |
|
674 self.generic_visit(node) |
|
675 |
|
676 def visit_SetComp(self, node): |
|
677 """ |
|
678 Public method to handle set comprehensions. |
|
679 |
|
680 @param node reference to the node to be processed |
|
681 @type ast.SetComp |
|
682 """ |
|
683 self.__checkForM523(node) |
|
684 |
|
685 self.generic_visit(node) |
|
686 |
|
687 def visit_DictComp(self, node): |
|
688 """ |
|
689 Public method to handle dictionary comprehensions. |
|
690 |
|
691 @param node reference to the node to be processed |
|
692 @type ast.DictComp |
|
693 """ |
|
694 self.__checkForM523(node) |
|
695 self.__checkForM535(node) |
|
696 |
|
697 self.generic_visit(node) |
|
698 |
|
699 def visit_GeneratorExp(self, node): |
|
700 """ |
|
701 Public method to handle generator expressions. |
|
702 |
|
703 @param node reference to the node to be processed |
|
704 @type ast.GeneratorExp |
|
705 """ |
|
706 self.__checkForM523(node) |
|
707 |
|
708 self.generic_visit(node) |
|
709 |
|
710 def visit_Assert(self, node): |
|
711 """ |
|
712 Public method to handle 'assert' statements. |
|
713 |
|
714 @param node reference to the node to be processed |
|
715 @type ast.Assert |
|
716 """ |
|
717 if ( |
|
718 AstUtilities.isNameConstant(node.test) |
|
719 and AstUtilities.getValue(node.test) is False |
|
720 ): |
|
721 self.violations.append((node, "M-511")) |
|
722 |
|
723 self.generic_visit(node) |
|
724 |
|
725 def visit_AsyncFunctionDef(self, node): |
|
726 """ |
|
727 Public method to handle async function definitions. |
|
728 |
|
729 @param node reference to the node to be processed |
|
730 @type ast.AsyncFunctionDef |
|
731 """ |
|
732 self.__checkForM506_M508(node) |
|
733 |
|
734 self.generic_visit(node) |
|
735 |
|
736 def visit_FunctionDef(self, node): |
|
737 """ |
|
738 Public method to handle function definitions. |
|
739 |
|
740 @param node reference to the node to be processed |
|
741 @type ast.FunctionDef |
|
742 """ |
|
743 self.__checkForM506_M508(node) |
|
744 self.__checkForM519(node) |
|
745 self.__checkForM521(node) |
|
746 |
|
747 self.generic_visit(node) |
|
748 |
|
749 def visit_ClassDef(self, node): |
|
750 """ |
|
751 Public method to handle class definitions. |
|
752 |
|
753 @param node reference to the node to be processed |
|
754 @type ast.ClassDef |
|
755 """ |
|
756 self.__checkForM521(node) |
|
757 self.__checkForM524_M527(node) |
|
758 |
|
759 self.generic_visit(node) |
|
760 |
|
761 def visit_Try(self, node): |
|
762 """ |
|
763 Public method to handle 'try' statements. |
|
764 |
|
765 @param node reference to the node to be processed |
|
766 @type ast.Try |
|
767 """ |
|
768 self.__checkForM512(node) |
|
769 self.__checkForM525(node) |
|
770 |
|
771 self.generic_visit(node) |
|
772 |
|
773 def visit_TryStar(self, node): |
|
774 """ |
|
775 Public method to handle 'except*' statements. |
|
776 |
|
777 @param node reference to the node to be processed |
|
778 @type ast.TryStar |
|
779 """ |
|
780 outerTryStar = self.__inTryStar |
|
781 self.__inTryStar = "*" |
|
782 self.visit_Try(node) |
|
783 self.__inTryStar = outerTryStar |
|
784 |
|
785 def visit_Compare(self, node): |
|
786 """ |
|
787 Public method to handle comparison statements. |
|
788 |
|
789 @param node reference to the node to be processed |
|
790 @type ast.Compare |
|
791 """ |
|
792 self.__checkForM515(node) |
|
793 |
|
794 self.generic_visit(node) |
|
795 |
|
796 def visit_Raise(self, node): |
|
797 """ |
|
798 Public method to handle 'raise' statements. |
|
799 |
|
800 @param node reference to the node to be processed |
|
801 @type ast.Raise |
|
802 """ |
|
803 if node.exc is None: |
|
804 self.__M540CaughtException = None |
|
805 else: |
|
806 self.__checkForM540Usage(node.exc) |
|
807 self.__checkForM540Usage(node.cause) |
|
808 self.__checkForM516(node) |
|
809 |
|
810 self.generic_visit(node) |
|
811 |
|
812 def visit_With(self, node): |
|
813 """ |
|
814 Public method to handle 'with' statements. |
|
815 |
|
816 @param node reference to the node to be processed |
|
817 @type ast.With |
|
818 """ |
|
819 self.__checkForM517(node) |
|
820 self.__checkForM522(node) |
|
821 |
|
822 self.generic_visit(node) |
|
823 |
|
824 def visit_JoinedStr(self, node): |
|
825 """ |
|
826 Public method to handle f-string arguments. |
|
827 |
|
828 @param node reference to the node to be processed |
|
829 @type ast.JoinedStr |
|
830 """ |
|
831 for value in node.values: |
|
832 if isinstance(value, ast.FormattedValue): |
|
833 return |
|
834 |
|
835 self.violations.append((node, "M-581")) |
|
836 |
|
837 def visit_AnnAssign(self, node): |
|
838 """ |
|
839 Public method to check annotated assign statements. |
|
840 |
|
841 @param node reference to the node to be processed |
|
842 @type ast.AnnAssign |
|
843 """ |
|
844 self.__checkForM532(node) |
|
845 self.__checkForM540Usage(node.value) |
|
846 |
|
847 self.generic_visit(node) |
|
848 |
|
849 def visit_Import(self, node): |
|
850 """ |
|
851 Public method to check imports. |
|
852 |
|
853 @param node reference to the node to be processed |
|
854 @type ast.Import |
|
855 """ |
|
856 self.__checkForM505(node) |
|
857 |
|
858 self.generic_visit(node) |
|
859 |
|
860 def visit_ImportFrom(self, node): |
|
861 """ |
|
862 Public method to check from imports. |
|
863 |
|
864 @param node reference to the node to be processed |
|
865 @type ast.Import |
|
866 """ |
|
867 self.visit_Import(node) |
|
868 |
|
869 def visit_Set(self, node): |
|
870 """ |
|
871 Public method to check a set. |
|
872 |
|
873 @param node reference to the node to be processed |
|
874 @type ast.Set |
|
875 """ |
|
876 self.__checkForM533(node) |
|
877 |
|
878 self.generic_visit(node) |
|
879 |
|
880 def visit_Dict(self, node): |
|
881 """ |
|
882 Public method to check a dictionary. |
|
883 |
|
884 @param node reference to the node to be processed |
|
885 @type ast.Dict |
|
886 """ |
|
887 self.__checkForM541(node) |
|
888 |
|
889 self.generic_visit(node) |
|
890 |
|
891 def __checkForM505(self, node): |
|
892 """ |
|
893 Private method to check the use of *strip(). |
|
894 |
|
895 @param node reference to the node to be processed |
|
896 @type ast.Call |
|
897 """ |
|
898 if isinstance(node, ast.Import): |
|
899 for name in node.names: |
|
900 self.__M505Imports.add(name.asname or name.name) |
|
901 elif isinstance(node, ast.ImportFrom): |
|
902 for name in node.names: |
|
903 self.__M505Imports.add(f"{node.module}.{name.name or name.asname}") |
|
904 elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): |
|
905 if node.func.attr not in ("lstrip", "rstrip", "strip"): |
|
906 return # method name doesn't match |
|
907 |
|
908 if ( |
|
909 isinstance(node.func.value, ast.Name) |
|
910 and node.func.value.id in self.__M505Imports |
|
911 ): |
|
912 return # method is being run on an imported module |
|
913 |
|
914 if len(node.args) != 1 or not AstUtilities.isString(node.args[0]): |
|
915 return # used arguments don't match the builtin strip |
|
916 |
|
917 value = AstUtilities.getValue(node.args[0]) |
|
918 if len(value) == 1: |
|
919 return # stripping just one character |
|
920 |
|
921 if len(value) == len(set(value)): |
|
922 return # no characters appear more than once |
|
923 |
|
924 self.violations.append((node, "M-505")) |
|
925 |
|
926 def __checkForM506_M508(self, node): |
|
927 """ |
|
928 Private method to check the use of mutable literals, comprehensions and calls. |
|
929 |
|
930 @param node reference to the node to be processed |
|
931 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
932 """ |
|
933 visitor = FunctionDefDefaultsVisitor("M-506", "M-508") |
|
934 visitor.visit(node.args.defaults + node.args.kw_defaults) |
|
935 self.violations.extend(visitor.errors) |
|
936 |
|
937 def __checkForM507(self, node): |
|
938 """ |
|
939 Private method to check for unused loop variables. |
|
940 |
|
941 @param node reference to the node to be processed |
|
942 @type ast.For or ast.AsyncFor |
|
943 """ |
|
944 targets = NameFinder() |
|
945 targets.visit(node.target) |
|
946 ctrlNames = set(filter(lambda s: not s.startswith("_"), targets.getNames())) |
|
947 body = NameFinder() |
|
948 for expr in node.body: |
|
949 body.visit(expr) |
|
950 usedNames = set(body.getNames()) |
|
951 for name in sorted(ctrlNames - usedNames): |
|
952 n = targets.getNames()[name][0] |
|
953 self.violations.append((n, "M-507", name)) |
|
954 |
|
955 def __checkForM512(self, node): |
|
956 """ |
|
957 Private method to check for return/continue/break inside finally blocks. |
|
958 |
|
959 @param node reference to the node to be processed |
|
960 @type ast.Try |
|
961 """ |
|
962 |
|
963 def _loop(node, badNodeTypes): |
|
964 if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)): |
|
965 return |
|
966 |
|
967 if isinstance(node, (ast.While, ast.For)): |
|
968 badNodeTypes = (ast.Return,) |
|
969 |
|
970 elif isinstance(node, badNodeTypes): |
|
971 self.violations.append((node, "M-512", self.__inTryStar)) |
|
972 |
|
973 for child in ast.iter_child_nodes(node): |
|
974 _loop(child, badNodeTypes) |
|
975 |
|
976 for child in node.finalbody: |
|
977 _loop(child, (ast.Return, ast.Continue, ast.Break)) |
|
978 |
|
979 def __checkForM513_M514_M529_M530(self, node): |
|
980 """ |
|
981 Private method to check various exception handler situations. |
|
982 |
|
983 @param node reference to the node to be processed |
|
984 @type ast.ExceptHandler |
|
985 @return list of exception handler names |
|
986 @rtype list of str |
|
987 """ |
|
988 handlers = self.__flattenExcepthandler(node.type) |
|
989 names = [] |
|
990 badHandlers = [] |
|
991 ignoredHandlers = [] |
|
992 |
|
993 for handler in handlers: |
|
994 if isinstance(handler, (ast.Name, ast.Attribute)): |
|
995 name = self.toNameStr(handler) |
|
996 if name is None: |
|
997 ignoredHandlers.append(handler) |
|
998 else: |
|
999 names.append(name) |
|
1000 elif isinstance(handler, (ast.Call, ast.Starred)): |
|
1001 ignoredHandlers.append(handler) |
|
1002 else: |
|
1003 badHandlers.append(handler) |
|
1004 if badHandlers: |
|
1005 self.violations.append((node, "M-530")) |
|
1006 if len(names) == 0 and not badHandlers and not ignoredHandlers: |
|
1007 self.violations.append((node, "M-529", self.__inTryStar)) |
|
1008 elif ( |
|
1009 len(names) == 1 |
|
1010 and not badHandlers |
|
1011 and not ignoredHandlers |
|
1012 and isinstance(node.type, ast.Tuple) |
|
1013 ): |
|
1014 self.violations.append((node, "M-513", *names, self.__inTryStar)) |
|
1015 else: |
|
1016 maybeError = self.__checkRedundantExcepthandlers( |
|
1017 names, node, self.__inTryStar |
|
1018 ) |
|
1019 if maybeError is not None: |
|
1020 self.violations.append(maybeError) |
|
1021 return names |
|
1022 |
|
1023 def __checkForM515(self, node): |
|
1024 """ |
|
1025 Private method to check for pointless comparisons. |
|
1026 |
|
1027 @param node reference to the node to be processed |
|
1028 @type ast.Compare |
|
1029 """ |
|
1030 if isinstance(self.nodeStack[-2], ast.Expr): |
|
1031 self.violations.append((node, "M-515")) |
|
1032 |
|
1033 def __checkForM516(self, node): |
|
1034 """ |
|
1035 Private method to check for raising a literal instead of an exception. |
|
1036 |
|
1037 @param node reference to the node to be processed |
|
1038 @type ast.Raise |
|
1039 """ |
|
1040 if ( |
|
1041 AstUtilities.isNameConstant(node.exc) |
|
1042 or AstUtilities.isNumber(node.exc) |
|
1043 or AstUtilities.isString(node.exc) |
|
1044 ): |
|
1045 self.violations.append((node, "M-516")) |
|
1046 |
|
1047 def __checkForM517(self, node): |
|
1048 """ |
|
1049 Private method to check for use of the evil syntax |
|
1050 'with assertRaises(Exception): or 'with pytest.raises(Exception):'. |
|
1051 |
|
1052 @param node reference to the node to be processed |
|
1053 @type ast.With |
|
1054 """ |
|
1055 item = node.items[0] |
|
1056 itemContext = item.context_expr |
|
1057 if ( |
|
1058 hasattr(itemContext, "func") |
|
1059 and ( |
|
1060 ( |
|
1061 isinstance(itemContext.func, ast.Attribute) |
|
1062 and ( |
|
1063 itemContext.func.attr == "assertRaises" |
|
1064 or ( |
|
1065 itemContext.func.attr == "raises" |
|
1066 and isinstance(itemContext.func.value, ast.Name) |
|
1067 and itemContext.func.value.id == "pytest" |
|
1068 and "match" not in (kwd.arg for kwd in itemContext.keywords) |
|
1069 ) |
|
1070 ) |
|
1071 ) |
|
1072 or ( |
|
1073 isinstance(itemContext.func, ast.Name) |
|
1074 and itemContext.func.id == "raises" |
|
1075 and isinstance(itemContext.func.ctx, ast.Load) |
|
1076 and "pytest.raises" in self.__M505Imports |
|
1077 and "match" not in (kwd.arg for kwd in itemContext.keywords) |
|
1078 ) |
|
1079 ) |
|
1080 and len(itemContext.args) == 1 |
|
1081 and isinstance(itemContext.args[0], ast.Name) |
|
1082 and itemContext.args[0].id in ("Exception", "BaseException") |
|
1083 and not item.optional_vars |
|
1084 ): |
|
1085 self.violations.append((node, "M-517")) |
|
1086 |
|
1087 def __checkForM518(self, node): |
|
1088 """ |
|
1089 Private method to check for useless expressions. |
|
1090 |
|
1091 @param node reference to the node to be processed |
|
1092 @type ast.FunctionDef |
|
1093 """ |
|
1094 if not isinstance(node, ast.Expr): |
|
1095 return |
|
1096 |
|
1097 if isinstance( |
|
1098 node.value, |
|
1099 (ast.List, ast.Set, ast.Dict, ast.Tuple), |
|
1100 ) or ( |
|
1101 isinstance(node.value, ast.Constant) |
|
1102 and ( |
|
1103 isinstance( |
|
1104 node.value.value, |
|
1105 (int, float, complex, bytes, bool), |
|
1106 ) |
|
1107 or node.value.value is None |
|
1108 ) |
|
1109 ): |
|
1110 self.violations.append((node, "M-518", node.value.__class__.__name__)) |
|
1111 |
|
1112 def __checkForM519(self, node): |
|
1113 """ |
|
1114 Private method to check for use of 'functools.lru_cache' or 'functools.cache'. |
|
1115 |
|
1116 @param node reference to the node to be processed |
|
1117 @type ast.FunctionDef |
|
1118 """ |
|
1119 caches = { |
|
1120 "functools.cache", |
|
1121 "functools.lru_cache", |
|
1122 "cache", |
|
1123 "lru_cache", |
|
1124 } |
|
1125 |
|
1126 if ( |
|
1127 len(node.decorator_list) == 0 |
|
1128 or len(self.contexts) < 2 |
|
1129 or not isinstance(self.contexts[-2].node, ast.ClassDef) |
|
1130 ): |
|
1131 return |
|
1132 |
|
1133 # Preserve decorator order so we can get the lineno from the decorator node |
|
1134 # rather than the function node (this location definition changes in Python 3.8) |
|
1135 resolvedDecorators = ( |
|
1136 ".".join(composeCallPath(decorator)) for decorator in node.decorator_list |
|
1137 ) |
|
1138 for idx, decorator in enumerate(resolvedDecorators): |
|
1139 if decorator in {"classmethod", "staticmethod"}: |
|
1140 return |
|
1141 |
|
1142 if decorator in caches: |
|
1143 self.violations.append((node.decorator_list[idx], "M-519")) |
|
1144 return |
|
1145 |
|
1146 def __checkForM520(self, node): |
|
1147 """ |
|
1148 Private method to check for a loop that modifies its iterable. |
|
1149 |
|
1150 @param node reference to the node to be processed |
|
1151 @type ast.For or ast.AsyncFor |
|
1152 """ |
|
1153 targets = NameFinder() |
|
1154 targets.visit(node.target) |
|
1155 ctrlNames = set(targets.getNames()) |
|
1156 |
|
1157 iterset = M520NameFinder() |
|
1158 iterset.visit(node.iter) |
|
1159 itersetNames = set(iterset.getNames()) |
|
1160 |
|
1161 for name in sorted(ctrlNames): |
|
1162 if name in itersetNames: |
|
1163 n = targets.getNames()[name][0] |
|
1164 self.violations.append((n, "M-520")) |
|
1165 |
|
1166 def __checkForM521(self, node): |
|
1167 """ |
|
1168 Private method to check for use of an f-string as docstring. |
|
1169 |
|
1170 @param node reference to the node to be processed |
|
1171 @type ast.FunctionDef or ast.ClassDef |
|
1172 """ |
|
1173 if ( |
|
1174 node.body |
|
1175 and isinstance(node.body[0], ast.Expr) |
|
1176 and isinstance(node.body[0].value, ast.JoinedStr) |
|
1177 ): |
|
1178 self.violations.append((node.body[0].value, "M-521")) |
|
1179 |
|
1180 def __checkForM522(self, node): |
|
1181 """ |
|
1182 Private method to check for use of an f-string as docstring. |
|
1183 |
|
1184 @param node reference to the node to be processed |
|
1185 @type ast.With |
|
1186 """ |
|
1187 item = node.items[0] |
|
1188 itemContext = item.context_expr |
|
1189 if ( |
|
1190 hasattr(itemContext, "func") |
|
1191 and hasattr(itemContext.func, "value") |
|
1192 and hasattr(itemContext.func.value, "id") |
|
1193 and itemContext.func.value.id == "contextlib" |
|
1194 and hasattr(itemContext.func, "attr") |
|
1195 and itemContext.func.attr == "suppress" |
|
1196 and len(itemContext.args) == 0 |
|
1197 ): |
|
1198 self.violations.append((node, "M-522")) |
|
1199 |
|
1200 def __checkForM523(self, loopNode): |
|
1201 """ |
|
1202 Private method to check that functions (including lambdas) do not use loop |
|
1203 variables. |
|
1204 |
|
1205 @param loopNode reference to the node to be processed |
|
1206 @type ast.For, ast.AsyncFor, ast.While, ast.ListComp, ast.SetComp,ast.DictComp, |
|
1207 or ast.GeneratorExp |
|
1208 """ |
|
1209 safe_functions = [] |
|
1210 suspiciousVariables = [] |
|
1211 for node in ast.walk(loopNode): |
|
1212 # check if function is immediately consumed to avoid false alarm |
|
1213 if isinstance(node, ast.Call): |
|
1214 # check for filter&reduce |
|
1215 if ( |
|
1216 isinstance(node.func, ast.Name) |
|
1217 and node.func.id in ("filter", "reduce", "map") |
|
1218 ) or ( |
|
1219 isinstance(node.func, ast.Attribute) |
|
1220 and node.func.attr == "reduce" |
|
1221 and isinstance(node.func.value, ast.Name) |
|
1222 and node.func.value.id == "functools" |
|
1223 ): |
|
1224 for arg in node.args: |
|
1225 if isinstance(arg, BugBearVisitor.FUNCTION_NODES): |
|
1226 safe_functions.append(arg) |
|
1227 |
|
1228 # check for key= |
|
1229 for keyword in node.keywords: |
|
1230 if keyword.arg == "key" and isinstance( |
|
1231 keyword.value, BugBearVisitor.FUNCTION_NODES |
|
1232 ): |
|
1233 safe_functions.append(keyword.value) |
|
1234 |
|
1235 # mark `return lambda: x` as safe |
|
1236 # does not (currently) check inner lambdas in a returned expression |
|
1237 # e.g. `return (lambda: x, ) |
|
1238 if isinstance(node, ast.Return) and isinstance( |
|
1239 node.value, BugBearVisitor.FUNCTION_NODES |
|
1240 ): |
|
1241 safe_functions.append(node.value) |
|
1242 |
|
1243 # find unsafe functions |
|
1244 if ( |
|
1245 isinstance(node, BugBearVisitor.FUNCTION_NODES) |
|
1246 and node not in safe_functions |
|
1247 ): |
|
1248 argnames = { |
|
1249 arg.arg for arg in ast.walk(node.args) if isinstance(arg, ast.arg) |
|
1250 } |
|
1251 if isinstance(node, ast.Lambda): |
|
1252 bodyNodes = ast.walk(node.body) |
|
1253 else: |
|
1254 bodyNodes = itertools.chain.from_iterable(map(ast.walk, node.body)) |
|
1255 errors = [] |
|
1256 for name in bodyNodes: |
|
1257 if isinstance(name, ast.Name) and name.id not in argnames: |
|
1258 if isinstance(name.ctx, ast.Load): |
|
1259 errors.append((name.lineno, name.col_offset, name.id, name)) |
|
1260 elif isinstance(name.ctx, ast.Store): |
|
1261 argnames.add(name.id) |
|
1262 for err in errors: |
|
1263 if err[2] not in argnames and err not in self.__M523Seen: |
|
1264 self.__M523Seen.add(err) # dedupe across nested loops |
|
1265 suspiciousVariables.append(err) |
|
1266 |
|
1267 if suspiciousVariables: |
|
1268 reassignedInLoop = set(self.__getAssignedNames(loopNode)) |
|
1269 |
|
1270 for err in sorted(suspiciousVariables): |
|
1271 if reassignedInLoop.issuperset(err[2]): |
|
1272 self.violations.append((err[3], "M-523", err[2])) |
|
1273 |
|
1274 def __checkForM524_M527(self, node): |
|
1275 """ |
|
1276 Private method to check for inheritance from abstract classes in abc and lack of |
|
1277 any methods decorated with abstract*. |
|
1278 |
|
1279 @param node reference to the node to be processed |
|
1280 @type ast.ClassDef |
|
1281 """ # __IGNORE_WARNING_D-234r__ |
|
1282 |
|
1283 def isAbcClass(value, name="ABC"): |
|
1284 if isinstance(value, ast.keyword): |
|
1285 return value.arg == "metaclass" and isAbcClass(value.value, "ABCMeta") |
|
1286 |
|
1287 # class foo(ABC) |
|
1288 # class foo(abc.ABC) |
|
1289 return (isinstance(value, ast.Name) and value.id == name) or ( |
|
1290 isinstance(value, ast.Attribute) |
|
1291 and value.attr == name |
|
1292 and isinstance(value.value, ast.Name) |
|
1293 and value.value.id == "abc" |
|
1294 ) |
|
1295 |
|
1296 def isAbstractDecorator(expr): |
|
1297 return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or ( |
|
1298 isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract" |
|
1299 ) |
|
1300 |
|
1301 def isOverload(expr): |
|
1302 return (isinstance(expr, ast.Name) and expr.id == "overload") or ( |
|
1303 isinstance(expr, ast.Attribute) and expr.attr == "overload" |
|
1304 ) |
|
1305 |
|
1306 def emptyBody(body): |
|
1307 def isStrOrEllipsis(node): |
|
1308 return isinstance(node, ast.Constant) and ( |
|
1309 node.value is Ellipsis or isinstance(node.value, str) |
|
1310 ) |
|
1311 |
|
1312 # Function body consist solely of `pass`, `...`, and/or (doc)string literals |
|
1313 return all( |
|
1314 isinstance(stmt, ast.Pass) |
|
1315 or (isinstance(stmt, ast.Expr) and isStrOrEllipsis(stmt.value)) |
|
1316 for stmt in body |
|
1317 ) |
|
1318 |
|
1319 # don't check multiple inheritance |
|
1320 if len(node.bases) + len(node.keywords) > 1: |
|
1321 return |
|
1322 |
|
1323 # only check abstract classes |
|
1324 if not any(map(isAbcClass, (*node.bases, *node.keywords))): |
|
1325 return |
|
1326 |
|
1327 hasMethod = False |
|
1328 hasAbstractMethod = False |
|
1329 |
|
1330 if not any(map(isAbcClass, (*node.bases, *node.keywords))): |
|
1331 return |
|
1332 |
|
1333 for stmt in node.body: |
|
1334 # Ignore abc's that declares a class attribute that must be set |
|
1335 if isinstance(stmt, ast.AnnAssign) and stmt.value is None: |
|
1336 hasAbstractMethod = True |
|
1337 continue |
|
1338 |
|
1339 # only check function defs |
|
1340 if not isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): |
|
1341 continue |
|
1342 hasMethod = True |
|
1343 |
|
1344 hasAbstractDecorator = any(map(isAbstractDecorator, stmt.decorator_list)) |
|
1345 |
|
1346 hasAbstractMethod |= hasAbstractDecorator |
|
1347 |
|
1348 if ( |
|
1349 not hasAbstractDecorator |
|
1350 and emptyBody(stmt.body) |
|
1351 and not any(map(isOverload, stmt.decorator_list)) |
|
1352 ): |
|
1353 self.violations.append((stmt, "M-527", stmt.name)) |
|
1354 |
|
1355 if hasMethod and not hasAbstractMethod: |
|
1356 self.violations.append((node, "M-524", node.name)) |
|
1357 |
|
1358 def __checkForM525(self, node): |
|
1359 """ |
|
1360 Private method to check for exceptions being handled multiple times. |
|
1361 |
|
1362 @param node reference to the node to be processed |
|
1363 @type ast.Try |
|
1364 """ |
|
1365 seen = [] |
|
1366 |
|
1367 for handler in node.handlers: |
|
1368 if isinstance(handler.type, (ast.Name, ast.Attribute)): |
|
1369 name = ".".join(composeCallPath(handler.type)) |
|
1370 seen.append(name) |
|
1371 elif isinstance(handler.type, ast.Tuple): |
|
1372 # to avoid checking the same as M514, remove duplicates per except |
|
1373 uniques = set() |
|
1374 for entry in handler.type.elts: |
|
1375 name = ".".join(composeCallPath(entry)) |
|
1376 uniques.add(name) |
|
1377 seen.extend(uniques) |
|
1378 |
|
1379 # sort to have a deterministic output |
|
1380 duplicates = sorted({x for x in seen if seen.count(x) > 1}) |
|
1381 for duplicate in duplicates: |
|
1382 self.violations.append((node, "M-525", duplicate, self.__inTryStar)) |
|
1383 |
|
1384 def __checkForM526(self, node): |
|
1385 """ |
|
1386 Private method to check for Star-arg unpacking after keyword argument. |
|
1387 |
|
1388 @param node reference to the node to be processed |
|
1389 @type ast.Call |
|
1390 """ |
|
1391 if not node.keywords: |
|
1392 return |
|
1393 |
|
1394 starreds = [arg for arg in node.args if isinstance(arg, ast.Starred)] |
|
1395 if not starreds: |
|
1396 return |
|
1397 |
|
1398 firstKeyword = node.keywords[0].value |
|
1399 for starred in starreds: |
|
1400 if (starred.lineno, starred.col_offset) > ( |
|
1401 firstKeyword.lineno, |
|
1402 firstKeyword.col_offset, |
|
1403 ): |
|
1404 self.violations.append((node, "M-526")) |
|
1405 |
|
1406 def __checkForM528(self, node): |
|
1407 """ |
|
1408 Private method to check for warn without stacklevel. |
|
1409 |
|
1410 @param node reference to the node to be processed |
|
1411 @type ast.Call |
|
1412 """ |
|
1413 if ( |
|
1414 isinstance(node.func, ast.Attribute) |
|
1415 and node.func.attr == "warn" |
|
1416 and isinstance(node.func.value, ast.Name) |
|
1417 and node.func.value.id == "warnings" |
|
1418 and not any(kw.arg == "stacklevel" for kw in node.keywords) |
|
1419 and len(node.args) < 3 |
|
1420 and not any(isinstance(a, ast.Starred) for a in node.args) |
|
1421 and not any(kw.arg is None for kw in node.keywords) |
|
1422 ): |
|
1423 self.violations.append((node, "M-528")) |
|
1424 |
|
1425 def __checkForM531(self, loopNode): |
|
1426 """ |
|
1427 Private method to check that 'itertools.groupby' isn't iterated over more than |
|
1428 once. |
|
1429 |
|
1430 A warning is emitted when the generator returned by 'groupby()' is used |
|
1431 more than once inside a loop body or when it's used in a nested loop. |
|
1432 |
|
1433 @param loopNode reference to the node to be processed |
|
1434 @type ast.For or ast.AsyncFor |
|
1435 """ |
|
1436 # for <loop_node.target> in <loop_node.iter>: ... |
|
1437 if isinstance(loopNode.iter, ast.Call): |
|
1438 node = loopNode.iter |
|
1439 if (isinstance(node.func, ast.Name) and node.func.id in ("groupby",)) or ( |
|
1440 isinstance(node.func, ast.Attribute) |
|
1441 and node.func.attr == "groupby" |
|
1442 and isinstance(node.func.value, ast.Name) |
|
1443 and node.func.value.id == "itertools" |
|
1444 ): |
|
1445 # We have an invocation of groupby which is a simple unpacking |
|
1446 if isinstance(loopNode.target, ast.Tuple) and isinstance( |
|
1447 loopNode.target.elts[1], ast.Name |
|
1448 ): |
|
1449 groupName = loopNode.target.elts[1].id |
|
1450 else: |
|
1451 # Ignore any 'groupby()' invocation that isn't unpacked |
|
1452 return |
|
1453 |
|
1454 numUsages = 0 |
|
1455 for node in self.__walkList(loopNode.body): |
|
1456 # Handled nested loops |
|
1457 if isinstance(node, ast.For): |
|
1458 for nestedNode in self.__walkList(node.body): |
|
1459 if ( |
|
1460 isinstance(nestedNode, ast.Name) |
|
1461 and nestedNode.id == groupName |
|
1462 ): |
|
1463 self.violations.append((nestedNode, "M-531")) |
|
1464 |
|
1465 # Handle multiple uses |
|
1466 if isinstance(node, ast.Name) and node.id == groupName: |
|
1467 numUsages += 1 |
|
1468 if numUsages > 1: |
|
1469 self.violations.append((nestedNode, "M-531")) |
|
1470 |
|
1471 def __checkForM532(self, node): |
|
1472 """ |
|
1473 Private method to check for possible unintentional typing annotation. |
|
1474 |
|
1475 @param node reference to the node to be processed |
|
1476 @type ast.AnnAssign |
|
1477 """ |
|
1478 if ( |
|
1479 node.value is None |
|
1480 and hasattr(node.target, "value") |
|
1481 and isinstance(node.target.value, ast.Name) |
|
1482 and ( |
|
1483 isinstance(node.target, ast.Subscript) |
|
1484 or ( |
|
1485 isinstance(node.target, ast.Attribute) |
|
1486 and node.target.value.id != "self" |
|
1487 ) |
|
1488 ) |
|
1489 ): |
|
1490 self.violations.append((node, "M-532")) |
|
1491 |
|
1492 def __checkForM533(self, node): |
|
1493 """ |
|
1494 Private method to check a set for duplicate items. |
|
1495 |
|
1496 @param node reference to the node to be processed |
|
1497 @type ast.Set |
|
1498 """ |
|
1499 seen = set() |
|
1500 for elt in node.elts: |
|
1501 if not isinstance(elt, ast.Constant): |
|
1502 continue |
|
1503 if elt.value in seen: |
|
1504 self.violations.append((node, "M-533", repr(elt.value))) |
|
1505 else: |
|
1506 seen.add(elt.value) |
|
1507 |
|
1508 def __checkForM534(self, node): |
|
1509 """ |
|
1510 Private method to check that re.sub/subn/split arguments flags/count/maxsplit |
|
1511 are passed as keyword arguments. |
|
1512 |
|
1513 @param node reference to the node to be processed |
|
1514 @type ast.Call |
|
1515 """ |
|
1516 if not isinstance(node.func, ast.Attribute): |
|
1517 return |
|
1518 func = node.func |
|
1519 if not isinstance(func.value, ast.Name) or func.value.id != "re": |
|
1520 return |
|
1521 |
|
1522 def check(numArgs, paramName): |
|
1523 if len(node.args) > numArgs: |
|
1524 arg = node.args[numArgs] |
|
1525 self.violations.append((arg, "M-534", func.attr, paramName)) |
|
1526 |
|
1527 if func.attr in ("sub", "subn"): |
|
1528 check(3, "count") |
|
1529 elif func.attr == "split": |
|
1530 check(2, "maxsplit") |
|
1531 |
|
1532 def __checkForM535(self, node): |
|
1533 """ |
|
1534 Private method to check that a static key isn't used in a dict comprehension. |
|
1535 |
|
1536 Record a warning if a likely unchanging key is used - either a constant, |
|
1537 or a variable that isn't coming from the generator expression. |
|
1538 |
|
1539 @param node reference to the node to be processed |
|
1540 @type ast.DictComp |
|
1541 """ |
|
1542 if isinstance(node.key, ast.Constant): |
|
1543 self.violations.append((node, "M-535", node.key.value)) |
|
1544 elif isinstance( |
|
1545 node.key, ast.Name |
|
1546 ) and node.key.id not in self.__getDictCompLoopAndNamedExprVarNames(node): |
|
1547 self.violations.append((node, "M-535", node.key.id)) |
|
1548 |
|
1549 def __checkForM539(self, node): |
|
1550 """ |
|
1551 Private method to check for correct ContextVar usage. |
|
1552 |
|
1553 @param node reference to the node to be processed |
|
1554 @type ast.Call |
|
1555 """ |
|
1556 if not ( |
|
1557 (isinstance(node.func, ast.Name) and node.func.id == "ContextVar") |
|
1558 or ( |
|
1559 isinstance(node.func, ast.Attribute) |
|
1560 and node.func.attr == "ContextVar" |
|
1561 and isinstance(node.func.value, ast.Name) |
|
1562 and node.func.value.id == "contextvars" |
|
1563 ) |
|
1564 ): |
|
1565 return |
|
1566 |
|
1567 # ContextVar only takes one kw currently, but better safe than sorry |
|
1568 for kw in node.keywords: |
|
1569 if kw.arg == "default": |
|
1570 break |
|
1571 else: |
|
1572 return |
|
1573 |
|
1574 visitor = FunctionDefDefaultsVisitor("M-539", "M-539") |
|
1575 visitor.visit(kw.value) |
|
1576 self.violations.extend(visitor.errors) |
|
1577 |
|
1578 def __checkForM540AddNote(self, node): |
|
1579 """ |
|
1580 Private method to check add_note usage. |
|
1581 |
|
1582 @param node reference to the node to be processed |
|
1583 @type ast.Attribute |
|
1584 @return flag |
|
1585 @rtype bool |
|
1586 """ |
|
1587 if ( |
|
1588 node.attr == "add_note" |
|
1589 and isinstance(node.value, ast.Name) |
|
1590 and self.__M540CaughtException |
|
1591 and node.value.id == self.__M540CaughtException.name |
|
1592 ): |
|
1593 self.__M540CaughtException.hasNote = True |
|
1594 return True |
|
1595 |
|
1596 return False |
|
1597 |
|
1598 def __checkForM540Usage(self, node): |
|
1599 """ |
|
1600 Private method to check the usage of exceptions with added note. |
|
1601 |
|
1602 @param node reference to the node to be processed |
|
1603 @type ast.expr or None |
|
1604 """ # noqa: D-234y |
|
1605 |
|
1606 def superwalk(node: ast.AST | list[ast.AST]): |
|
1607 """ |
|
1608 Function to walk an AST node or a list of AST nodes. |
|
1609 |
|
1610 @param node reference to the node or a list of nodes to be processed |
|
1611 @type ast.AST or list[ast.AST] |
|
1612 @yield next node to be processed |
|
1613 @ytype ast.AST |
|
1614 """ |
|
1615 if isinstance(node, list): |
|
1616 for n in node: |
|
1617 yield from ast.walk(n) |
|
1618 else: |
|
1619 yield from ast.walk(node) |
|
1620 |
|
1621 if not self.__M540CaughtException or node is None: |
|
1622 return |
|
1623 |
|
1624 for n in superwalk(node): |
|
1625 if isinstance(n, ast.Name) and n.id == self.__M540CaughtException.name: |
|
1626 self.__M540CaughtException = None |
|
1627 break |
|
1628 |
|
1629 def __checkForM541(self, node): |
|
1630 """ |
|
1631 Private method to check for duplicate key value pairs in a dictionary literal. |
|
1632 |
|
1633 @param node reference to the node to be processed |
|
1634 @type ast.Dict |
|
1635 """ # noqa: D-234r |
|
1636 |
|
1637 def convertToValue(item): |
|
1638 """ |
|
1639 Function to extract the value of a given item. |
|
1640 |
|
1641 @param item node to extract value from |
|
1642 @type ast.Ast |
|
1643 @return value of the node |
|
1644 @rtype Any |
|
1645 """ |
|
1646 if isinstance(item, ast.Constant): |
|
1647 return item.value |
|
1648 elif isinstance(item, ast.Tuple): |
|
1649 return tuple(convertToValue(i) for i in item.elts) |
|
1650 elif isinstance(item, ast.Name): |
|
1651 return M541VariableKeyType(item.id) |
|
1652 else: |
|
1653 return M541UnhandledKeyType() |
|
1654 |
|
1655 keys = [convertToValue(key) for key in node.keys] |
|
1656 keyCounts = Counter(keys) |
|
1657 duplicateKeys = [key for key, count in keyCounts.items() if count > 1] |
|
1658 for key in duplicateKeys: |
|
1659 keyIndices = [i for i, iKey in enumerate(keys) if iKey == key] |
|
1660 seen = set() |
|
1661 for index in keyIndices: |
|
1662 value = convertToValue(node.values[index]) |
|
1663 if value in seen: |
|
1664 keyNode = node.keys[index] |
|
1665 self.violations.append((keyNode, "M-541")) |
|
1666 seen.add(value) |
|
1667 |
|
1668 def __checkForM569(self, node): |
|
1669 """ |
|
1670 Private method to check for changes to a loop's mutable iterable. |
|
1671 |
|
1672 @param node loop node to be checked |
|
1673 @type ast.For |
|
1674 """ |
|
1675 if isinstance(node.iter, ast.Name): |
|
1676 name = self.toNameStr(node.iter) |
|
1677 elif isinstance(node.iter, ast.Attribute): |
|
1678 name = self.toNameStr(node.iter) |
|
1679 else: |
|
1680 return |
|
1681 checker = M569Checker(name, self) |
|
1682 checker.visit(node.body) |
|
1683 for mutation in checker.mutations: |
|
1684 self.violations.append((mutation, "M-569")) |
|
1685 |
|
1686 |
|
1687 class M569Checker(ast.NodeVisitor): |
|
1688 """ |
|
1689 Class traversing a 'for' loop body to check for modifications to a loop's |
|
1690 mutable iterable. |
|
1691 """ |
|
1692 |
|
1693 # https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types |
|
1694 MUTATING_FUNCTIONS = ( |
|
1695 "append", |
|
1696 "sort", |
|
1697 "reverse", |
|
1698 "remove", |
|
1699 "clear", |
|
1700 "extend", |
|
1701 "insert", |
|
1702 "pop", |
|
1703 "popitem", |
|
1704 ) |
|
1705 |
|
1706 def __init__(self, name, bugbear): |
|
1707 """ |
|
1708 Constructor |
|
1709 |
|
1710 @param name name of the iterator |
|
1711 @type str |
|
1712 @param bugbear reference to the bugbear visitor |
|
1713 @type BugBearVisitor |
|
1714 """ |
|
1715 self.__name = name |
|
1716 self.__bb = bugbear |
|
1717 self.mutations = [] |
|
1718 |
|
1719 def visit_Delete(self, node): |
|
1720 """ |
|
1721 Public method handling 'Delete' nodes. |
|
1722 |
|
1723 @param node reference to the node to be processed |
|
1724 @type ast.Delete |
|
1725 """ |
|
1726 for target in node.targets: |
|
1727 if isinstance(target, ast.Subscript): |
|
1728 name = self.__bb.toNameStr(target.value) |
|
1729 elif isinstance(target, (ast.Attribute, ast.Name)): |
|
1730 name = self.__bb.toNameStr(target) |
|
1731 else: |
|
1732 name = "" # fallback |
|
1733 self.generic_visit(target) |
|
1734 |
|
1735 if name == self.__name: |
|
1736 self.mutations.append(node) |
|
1737 |
|
1738 def visit_Call(self, node): |
|
1739 """ |
|
1740 Public method handling 'Call' nodes. |
|
1741 |
|
1742 @param node reference to the node to be processed |
|
1743 @type ast.Call |
|
1744 """ |
|
1745 if isinstance(node.func, ast.Attribute): |
|
1746 name = self.__bb.toNameStr(node.func.value) |
|
1747 functionObject = name |
|
1748 functionName = node.func.attr |
|
1749 |
|
1750 if ( |
|
1751 functionObject == self.__name |
|
1752 and functionName in self.MUTATING_FUNCTIONS |
|
1753 ): |
|
1754 self.mutations.append(node) |
|
1755 |
|
1756 self.generic_visit(node) |
|
1757 |
|
1758 def visit(self, node): |
|
1759 """ |
|
1760 Public method to inspect an ast node. |
|
1761 |
|
1762 Like super-visit but supports iteration over lists. |
|
1763 |
|
1764 @param node AST node to be traversed |
|
1765 @type TYPE |
|
1766 @return reference to the last processed node |
|
1767 @rtype ast.Node |
|
1768 """ |
|
1769 if not isinstance(node, list): |
|
1770 return super().visit(node) |
|
1771 |
|
1772 for elem in node: |
|
1773 super().visit(elem) |
|
1774 return node |
|
1775 |
|
1776 |
|
1777 class NamedExprFinder(ast.NodeVisitor): |
|
1778 """ |
|
1779 Class to extract names defined through an ast.NamedExpr. |
|
1780 """ |
|
1781 |
|
1782 def __init__(self): |
|
1783 """ |
|
1784 Constructor |
|
1785 """ |
|
1786 super().__init__() |
|
1787 |
|
1788 self.__names = {} |
|
1789 |
|
1790 def visit_NamedExpr(self, node: ast.NamedExpr): |
|
1791 """ |
|
1792 Public method handling 'NamedExpr' nodes. |
|
1793 |
|
1794 @param node reference to the node to be processed |
|
1795 @type ast.NamedExpr |
|
1796 """ |
|
1797 self.__names.setdefault(node.target.id, []).append(node.target) |
|
1798 |
|
1799 self.generic_visit(node) |
|
1800 |
|
1801 def visit(self, node): |
|
1802 """ |
|
1803 Public method to traverse a given AST node. |
|
1804 |
|
1805 Like super-visit but supports iteration over lists. |
|
1806 |
|
1807 @param node AST node to be traversed |
|
1808 @type TYPE |
|
1809 @return reference to the last processed node |
|
1810 @rtype ast.Node |
|
1811 """ |
|
1812 if not isinstance(node, list): |
|
1813 super().visit(node) |
|
1814 |
|
1815 for elem in node: |
|
1816 super().visit(elem) |
|
1817 |
|
1818 return node |
|
1819 |
|
1820 def getNames(self): |
|
1821 """ |
|
1822 Public method to return the extracted names and Name nodes. |
|
1823 |
|
1824 @return dictionary containing the names as keys and the list of nodes |
|
1825 @rtype dict |
|
1826 """ |
|
1827 return self.__names |
|
1828 |
|
1829 |
|
1830 class ExceptBaseExceptionVisitor(ast.NodeVisitor): |
|
1831 """ |
|
1832 Class to determine, if a 'BaseException' is re-raised. |
|
1833 """ |
|
1834 |
|
1835 def __init__(self, exceptNode): |
|
1836 """ |
|
1837 Constructor |
|
1838 |
|
1839 @param exceptNode exception node to be inspected |
|
1840 @type ast.ExceptHandler |
|
1841 """ |
|
1842 super().__init__() |
|
1843 self.__root = exceptNode |
|
1844 self.__reRaised = False |
|
1845 |
|
1846 def reRaised(self) -> bool: |
|
1847 """ |
|
1848 Public method to check, if the exception is re-raised. |
|
1849 |
|
1850 @return flag indicating a re-raised exception |
|
1851 @rtype bool |
|
1852 """ |
|
1853 self.visit(self.__root) |
|
1854 return self.__reRaised |
|
1855 |
|
1856 def visit_Raise(self, node): |
|
1857 """ |
|
1858 Public method to handle 'Raise' nodes. |
|
1859 |
|
1860 If we find a corresponding `raise` or `raise e` where e was from |
|
1861 `except BaseException as e:` then we mark re_raised as True and can |
|
1862 stop scanning. |
|
1863 |
|
1864 @param node reference to the node to be processed |
|
1865 @type ast.Raise |
|
1866 """ |
|
1867 if node.exc is None or ( |
|
1868 isinstance(node.exc, ast.Name) and node.exc.id == self.__root.name |
|
1869 ): |
|
1870 self.__reRaised = True |
|
1871 return |
|
1872 |
|
1873 super().generic_visit(node) |
|
1874 |
|
1875 def visit_ExceptHandler(self, node: ast.ExceptHandler): |
|
1876 """ |
|
1877 Public method to handle 'ExceptHandler' nodes. |
|
1878 |
|
1879 @param node reference to the node to be processed |
|
1880 @type ast.ExceptHandler |
|
1881 """ |
|
1882 if node is not self.__root: |
|
1883 return # entered a nested except - stop searching |
|
1884 |
|
1885 super().generic_visit(node) |
|
1886 |
|
1887 |
|
1888 class FunctionDefDefaultsVisitor(ast.NodeVisitor): |
|
1889 """ |
|
1890 Class used by M506, M508 and M539. |
|
1891 """ |
|
1892 |
|
1893 def __init__( |
|
1894 self, |
|
1895 errorCodeCalls, # M506 or M539 |
|
1896 errorCodeLiterals, # M508 or M539 |
|
1897 ): |
|
1898 """ |
|
1899 Constructor |
|
1900 |
|
1901 @param errorCodeCalls error code for ast.Call nodes |
|
1902 @type str |
|
1903 @param errorCodeLiterals error code for literal nodes |
|
1904 @type str |
|
1905 """ |
|
1906 self.__errorCodeCalls = errorCodeCalls |
|
1907 self.__errorCodeLiterals = errorCodeLiterals |
|
1908 for nodeType in BugbearMutableLiterals + BugbearMutableComprehensions: |
|
1909 setattr( |
|
1910 self, f"visit_{nodeType}", self.__visitMutableLiteralOrComprehension |
|
1911 ) |
|
1912 self.errors = [] |
|
1913 self.__argDepth = 0 |
|
1914 |
|
1915 super().__init__() |
|
1916 |
|
1917 def __visitMutableLiteralOrComprehension(self, node): |
|
1918 """ |
|
1919 Private method to flag mutable literals and comprehensions. |
|
1920 |
|
1921 @param node AST node to be processed |
|
1922 @type ast.Dict, ast.List, ast.Set, ast.ListComp, ast.DictComp or ast.SetComp |
|
1923 """ |
|
1924 # Flag M506 if mutable literal/comprehension is not nested. |
|
1925 # We only flag these at the top level of the expression as we |
|
1926 # cannot easily guarantee that nested mutable structures are not |
|
1927 # made immutable by outer operations, so we prefer no false positives. |
|
1928 # e.g. |
|
1929 # >>> def this_is_fine(a=frozenset({"a", "b", "c"})): ... |
|
1930 # |
|
1931 # >>> def this_is_not_fine_but_hard_to_detect(a=(lambda x: x)([1, 2, 3])) |
|
1932 # |
|
1933 # We do still search for cases of B008 within mutable structures though. |
|
1934 if self.__argDepth == 1: |
|
1935 self.errors.append((node, self.__errorCodeCalls)) |
|
1936 |
|
1937 # Check for nested functions. |
|
1938 self.generic_visit(node) |
|
1939 |
|
1940 def visit_Call(self, node): |
|
1941 """ |
|
1942 Public method to process Call nodes. |
|
1943 |
|
1944 @param node AST node to be processed |
|
1945 @type ast.Call |
|
1946 """ |
|
1947 callPath = ".".join(composeCallPath(node.func)) |
|
1948 if callPath in BugbearMutableCalls: |
|
1949 self.errors.append((node, self.__errorCodeCalls)) |
|
1950 self.generic_visit(node) |
|
1951 return |
|
1952 |
|
1953 if callPath in BugbearImmutableCalls: |
|
1954 self.generic_visit(node) |
|
1955 return |
|
1956 |
|
1957 # Check if function call is actually a float infinity/NaN literal |
|
1958 if callPath == "float" and len(node.args) == 1: |
|
1959 try: |
|
1960 value = float(ast.literal_eval(node.args[0])) |
|
1961 except Exception: # secok |
|
1962 pass |
|
1963 else: |
|
1964 if math.isfinite(value): |
|
1965 self.errors.append((node, self.__errorCodeLiterals)) |
|
1966 else: |
|
1967 self.errors.append((node, self.__errorCodeLiterals)) |
|
1968 |
|
1969 # Check for nested functions. |
|
1970 self.generic_visit(node) |
|
1971 |
|
1972 def visit_Lambda(self, node): |
|
1973 """ |
|
1974 Public method to process Lambda nodes. |
|
1975 |
|
1976 @param node AST node to be processed |
|
1977 @type ast.Lambda |
|
1978 """ |
|
1979 # Don't recurse into lambda expressions |
|
1980 # as they are evaluated at call time. |
|
1981 pass |
|
1982 |
|
1983 def visit(self, node): |
|
1984 """ |
|
1985 Public method to traverse an AST node or a list of AST nodes. |
|
1986 |
|
1987 This is an extended method that can also handle a list of AST nodes. |
|
1988 |
|
1989 @param node AST node or list of AST nodes to be processed |
|
1990 @type ast.AST or list of ast.AST |
|
1991 """ |
|
1992 self.__argDepth += 1 |
|
1993 if isinstance(node, list): |
|
1994 for elem in node: |
|
1995 if elem is not None: |
|
1996 super().visit(elem) |
|
1997 else: |
|
1998 super().visit(node) |
|
1999 self.__argDepth -= 1 |
|
2000 |
|
2001 |
|
2002 class NameFinder(ast.NodeVisitor): |
|
2003 """ |
|
2004 Class to extract a name out of a tree of nodes. |
|
2005 """ |
|
2006 |
|
2007 def __init__(self): |
|
2008 """ |
|
2009 Constructor |
|
2010 """ |
|
2011 super().__init__() |
|
2012 |
|
2013 self.__names = {} |
|
2014 |
|
2015 def visit_Name(self, node): |
|
2016 """ |
|
2017 Public method to handle 'Name' nodes. |
|
2018 |
|
2019 @param node reference to the node to be processed |
|
2020 @type ast.Name |
|
2021 """ |
|
2022 self.__names.setdefault(node.id, []).append(node) |
|
2023 |
|
2024 def visit(self, node): |
|
2025 """ |
|
2026 Public method to traverse a given AST node. |
|
2027 |
|
2028 @param node AST node to be traversed |
|
2029 @type ast.Node |
|
2030 @return reference to the last processed node |
|
2031 @rtype ast.Node |
|
2032 """ |
|
2033 if isinstance(node, list): |
|
2034 for elem in node: |
|
2035 super().visit(elem) |
|
2036 return node |
|
2037 else: |
|
2038 return super().visit(node) |
|
2039 |
|
2040 def getNames(self): |
|
2041 """ |
|
2042 Public method to return the extracted names and Name nodes. |
|
2043 |
|
2044 @return dictionary containing the names as keys and the list of nodes |
|
2045 @rtype dict |
|
2046 """ |
|
2047 return self.__names |
|
2048 |
|
2049 |
|
2050 class M520NameFinder(NameFinder): |
|
2051 """ |
|
2052 Class to extract a name out of a tree of nodes ignoring names defined within the |
|
2053 local scope of a comprehension. |
|
2054 """ |
|
2055 |
|
2056 def visit_GeneratorExp(self, node): |
|
2057 """ |
|
2058 Public method to handle a generator expressions. |
|
2059 |
|
2060 @param node reference to the node to be processed |
|
2061 @type ast.GeneratorExp |
|
2062 """ |
|
2063 self.visit(node.generators) |
|
2064 |
|
2065 def visit_ListComp(self, node): |
|
2066 """ |
|
2067 Public method to handle a list comprehension. |
|
2068 |
|
2069 @param node reference to the node to be processed |
|
2070 @type TYPE |
|
2071 """ |
|
2072 self.visit(node.generators) |
|
2073 |
|
2074 def visit_DictComp(self, node): |
|
2075 """ |
|
2076 Public method to handle a dictionary comprehension. |
|
2077 |
|
2078 @param node reference to the node to be processed |
|
2079 @type TYPE |
|
2080 """ |
|
2081 self.visit(node.generators) |
|
2082 |
|
2083 def visit_comprehension(self, node): |
|
2084 """ |
|
2085 Public method to handle the 'for' of a comprehension. |
|
2086 |
|
2087 @param node reference to the node to be processed |
|
2088 @type ast.comprehension |
|
2089 """ |
|
2090 self.visit(node.iter) |
|
2091 |
|
2092 def visit_Lambda(self, node): |
|
2093 """ |
|
2094 Public method to handle a Lambda function. |
|
2095 |
|
2096 @param node reference to the node to be processed |
|
2097 @type ast.Lambda |
|
2098 """ |
|
2099 self.visit(node.body) |
|
2100 for lambdaArg in node.args.args: |
|
2101 self.getNames().pop(lambdaArg.arg, None) |