src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/BugBearVisitor.py

branch
eric7
changeset 11150
73d80859079c
equal deleted inserted replaced
11149:fc45672fae42 11150:73d80859079c
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)

eric ide

mercurial