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

branch
eric7
changeset 9327
2b768afcaee1
parent 9274
86fab0c74430
child 9473
3f23dbf37dbe
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Wed Sep 14 11:07:55 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Sep 15 10:09:53 2022 +0200
@@ -7,15 +7,17 @@
 Module implementing a checker for miscellaneous checks.
 """
 
+import ast
+import builtins
+import contextlib
+import copy
+import itertools
+import re
 import sys
-import ast
-import re
-import itertools
+import tokenize
+from collections import defaultdict, namedtuple
+from keyword import iskeyword
 from string import Formatter
-from collections import defaultdict
-import tokenize
-import copy
-import contextlib
 
 import AstUtilities
 
@@ -106,17 +108,27 @@
         "M503",
         "M504",
         "M505",
-        "M506",
         "M507",
-        "M508",
         "M509",
+        "M510",
         "M511",
         "M512",
         "M513",
+        "M514",
+        "M515",
+        "M516",
+        "M517",
+        "M518",
+        "M519",
+        "M520",
         "M521",
         "M522",
         "M523",
         "M524",
+        "M525",
+        ## Bugbear++
+        "M581",
+        "M582",
         ## Format Strings
         "M601",
         "M611",
@@ -281,17 +293,26 @@
                     "M503",
                     "M504",
                     "M505",
-                    "M506",
                     "M507",
-                    "M508",
                     "M509",
+                    "M510",
                     "M511",
                     "M512",
                     "M513",
+                    "M514",
+                    "M515",
+                    "M516",
+                    "M517",
+                    "M518",
+                    "M519",
+                    "M520",
                     "M521",
                     "M522",
                     "M523",
                     "M524",
+                    "M525",
+                    "M581",
+                    "M582",
                 ),
             ),
             (self.__checkPep3101, ("M601",)),
@@ -996,6 +1017,9 @@
             ast.Dict,
             ast.List,
             ast.Set,
+            ast.DictComp,
+            ast.ListComp,
+            ast.SetComp,
         )
         mutableCalls = (
             "Counter",
@@ -1013,6 +1037,15 @@
         immutableCalls = (
             "tuple",
             "frozenset",
+            "types.MappingProxyType",
+            "MappingProxyType",
+            "re.compile",
+            "operator.attrgetter",
+            "operator.itemgetter",
+            "operator.methodcaller",
+            "attrgetter",
+            "itemgetter",
+            "methodcaller",
         )
         functionDefs = [ast.FunctionDef]
         with contextlib.suppress(AttributeError):
@@ -1488,16 +1521,36 @@
             super().generic_visit(node)
 
 
+BugBearContext = namedtuple("BugBearContext", ["node", "stack"])
+
+
 class BugBearVisitor(ast.NodeVisitor):
     """
     Class implementing a node visitor to check for various topics.
     """
 
     #
-    # This class was implemented along the BugBear flake8 extension (v 19.3.0).
+    # This class was implemented along the BugBear flake8 extension (v 22.9.11).
     # Original: Copyright (c) 2016 Ɓukasz Langa
     #
-    # TODO: update to v22.7.1
+
+    CONTEXTFUL_NODES = (
+        ast.Module,
+        ast.ClassDef,
+        ast.AsyncFunctionDef,
+        ast.FunctionDef,
+        ast.Lambda,
+        ast.ListComp,
+        ast.SetComp,
+        ast.DictComp,
+        ast.GeneratorExp,
+    )
+
+    FUNCTION_NODES = (
+        ast.AsyncFunctionDef,
+        ast.FunctionDef,
+        ast.Lambda,
+    )
 
     NodeWindowSize = 4
 
@@ -1507,9 +1560,150 @@
         """
         super().__init__()
 
-        self.__nodeStack = []
-        self.__nodeWindow = []
+        self.nodeWindow = []
         self.violations = []
+        self.contexts = []
+
+        self.__M523Seen = set()
+
+    @property
+    def nodeStack(self):
+        """
+        Public method to get a reference to the most recent node stack.
+
+        @return reference to the most recent node stack
+        @rtype list
+        """
+        if len(self.contexts) == 0:
+            return []
+
+        context, stack = self.contexts[-1]
+        return stack
+
+    def __isIdentifier(self, arg):
+        """
+        Private method to check if arg is a valid identifier.
+
+        See https://docs.python.org/2/reference/lexical_analysis.html#identifiers
+
+        @param arg reference to an argument node
+        @type ast.Node
+        @return flag indicating a valid identifier
+        @rtype TYPE
+        """
+        if not AstUtilities.isString(arg):
+            return False
+
+        return (
+            re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", AstUtilities.getValue(arg))
+            is not None
+        )
+
+    def __composeCallPath(self, node):
+        """
+        Private method get the individual elements of the call path of a node.
+
+        @param node reference to the node
+        @type ast.Node
+        @yield one element of the call path
+        @ytype ast.Node
+        """
+        if isinstance(node, ast.Attribute):
+            yield from self.__composeCallPath(node.value)
+            yield node.attr
+        elif isinstance(node, ast.Call):
+            yield from self.__composeCallPath(node.func)
+        elif isinstance(node, ast.Name):
+            yield node.id
+
+    def __toNameStr(self, node):
+        """
+        Private method to turn Name and Attribute nodes to strings, handling any
+        depth of attribute accesses.
+
+
+        @param node reference to the node
+        @type ast.Name or ast.Attribute
+        @return string representation
+        @rtype str
+        """
+        if isinstance(node, ast.Name):
+            return node.id
+
+        if isinstance(node, ast.Call):
+            return self.__toNameStr(node.func)
+
+        try:
+            return self.__toNameStr(node.value) + "." + node.attr
+        except AttributeError:
+            return self.__toNameStr(node.value)
+
+    def __typesafeIssubclass(self, obj, classOrTuple):
+        """
+        Private method implementing a type safe issubclass() function.
+
+        @param obj reference to the object to be tested
+        @type any
+        @param classOrTuple type to check against
+        @type type
+        @return flag indicating a subclass
+        @rtype bool
+        """
+        try:
+            return issubclass(obj, classOrTuple)
+        except TypeError:
+            # User code specifies a type that is not a type in our current run.
+            # Might be their error, might be a difference in our environments.
+            # We don't know so we ignore this.
+            return False
+
+    def __getAssignedNames(self, loopNode):
+        """
+        Private method to get the names of a for loop.
+
+        @param loopNode reference to the node to be processed
+        @type ast.For
+        @yield DESCRIPTION
+        @ytype TYPE
+        """
+        loopTargets = (ast.For, ast.AsyncFor, ast.comprehension)
+        for node in self.__childrenInScope(loopNode):
+            if isinstance(node, (ast.Assign)):
+                for child in node.targets:
+                    yield from self.__namesFromAssignments(child)
+            if isinstance(node, loopTargets + (ast.AnnAssign, ast.AugAssign)):
+                yield from self.__namesFromAssignments(node.target)
+
+    def __namesFromAssignments(self, assignTarget):
+        """
+        Private method to get names of an assignment.
+
+        @param assignTarget reference to the node to be processed
+        @type ast.Node
+        @yield name of the assignment
+        @ytype str
+        """
+        if isinstance(assignTarget, ast.Name):
+            yield assignTarget.id
+        elif isinstance(assignTarget, ast.Starred):
+            yield from self.__namesFromAssignments(assignTarget.value)
+        elif isinstance(assignTarget, (ast.List, ast.Tuple)):
+            for child in assignTarget.elts:
+                yield from self.__namesFromAssignments(child)
+
+    def __childrenInScope(self, node):
+        """
+        Private method to get all child nodes in the given scope.
+
+        @param node reference to the node to be processed
+        @type ast.Node
+        @yield reference to a child node
+        @ytype ast.Node
+        """
+        yield node
+        if not isinstance(node, BugBearVisitor.FUNCTION_NODES):
+            for child in ast.iter_child_nodes(node):
+                yield from self.__childrenInScope(child)
 
     def visit(self, node):
         """
@@ -1518,13 +1712,91 @@
         @param node AST node to be traversed
         @type ast.Node
         """
-        self.__nodeStack.append(node)
-        self.__nodeWindow.append(node)
-        self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize :]
+        isContextful = isinstance(node, BugBearVisitor.CONTEXTFUL_NODES)
+
+        if isContextful:
+            context = BugBearContext(node, [])
+            self.contexts.append(context)
+
+        self.nodeStack.append(node)
+        self.nodeWindow.append(node)
+        self.nodeWindow = self.nodeWindow[-BugBearVisitor.NodeWindowSize :]
 
         super().visit(node)
 
-        self.__nodeStack.pop()
+        self.nodeStack.pop()
+
+        if isContextful:
+            self.contexts.pop()
+
+    def visit_ExceptHandler(self, node):
+        """
+        Public method to handle exception handlers.
+
+        @param node reference to the node to be processed
+        @type ast.ExceptHandler
+        """
+        redundantExceptions = {
+            "OSError": {
+                # All of these are actually aliases of OSError since Python 3.3
+                "IOError",
+                "EnvironmentError",
+                "WindowsError",
+                "mmap.error",
+                "socket.error",
+                "select.error",
+            },
+            "ValueError": {
+                "binascii.Error",
+            },
+        }
+
+        if node.type is None:
+            # bare except is handled by pycodestyle already
+            pass
+
+        elif isinstance(node.type, ast.Tuple):
+            names = [self.__toNameStr(e) for e in node.type.elts]
+            as_ = " as " + node.name if node.name is not None else ""
+            if len(names) == 0:
+                self.violations.append((node, "M501", as_))
+            elif len(names) == 1:
+                self.violations.append((node, "M513", *names))
+            else:
+                # See if any of the given exception names could be removed, e.g. from:
+                #  (MyError, MyError)  # duplicate names
+                #  (MyError, BaseException)  # everything derives from the Base
+                #  (Exception, TypeError)  # builtins where one subclasses another
+                #  (IOError, OSError)  # IOError is an alias of OSError since Python3.3
+                # but note that other cases are impractical to handle from the AST.
+                # We expect this is mostly useful for users who do not have the
+                # builtin exception hierarchy memorised, and include a 'shadowed'
+                # subtype without realising that it's redundant.
+                good = sorted(set(names), key=names.index)
+                if "BaseException" in good:
+                    good = ["BaseException"]
+                # Remove redundant exceptions that the automatic system either handles
+                # poorly (usually aliases) or can't be checked (e.g. it's not an
+                # built-in exception).
+                for primary, equivalents in redundantExceptions.items():
+                    if primary in good:
+                        good = [g for g in good if g not in equivalents]
+
+                for name, other in itertools.permutations(tuple(good), 2):
+                    if (
+                        self.__typesafeIssubclass(
+                            getattr(builtins, name, type), getattr(builtins, other, ())
+                        )
+                        and name in good
+                    ):
+                        good.remove(name)
+                if good != names:
+                    desc = (
+                        good[0] if len(good) == 1 else "({0})".format(", ".join(good))
+                    )
+                    self.violations.append((node, "M514", ", ".join(names), as_, desc))
+
+        self.generic_visit(node)
 
     def visit_UAdd(self, node):
         """
@@ -1533,10 +1805,10 @@
         @param node reference to the node to be processed
         @type ast.UAdd
         """
-        trailingNodes = list(map(type, self.__nodeWindow[-4:]))
+        trailingNodes = list(map(type, self.nodeWindow[-4:]))
         if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]:
-            originator = self.__nodeWindow[-4]
-            self.violations.append((originator, "M501"))
+            originator = self.nodeWindow[-4]
+            self.violations.append((originator, "M502"))
 
         self.generic_visit(node)
 
@@ -1547,22 +1819,8 @@
         @param node reference to the node to be processed
         @type ast.Call
         """
-        validPaths = ("six", "future.utils", "builtins")
-        methodsDict = {
-            "M521": ("iterkeys", "itervalues", "iteritems", "iterlists"),
-            "M522": ("viewkeys", "viewvalues", "viewitems", "viewlists"),
-            "M523": ("next",),
-        }
-
         if isinstance(node.func, ast.Attribute):
-            for code, methods in methodsDict.items():
-                if node.func.attr in methods:
-                    callPath = ".".join(composeCallPath(node.func.value))
-                    if callPath not in validPaths:
-                        self.violations.append((node, code))
-                    break
-            else:
-                self.__checkForM502(node)
+            self.__checkForM505(node)
         else:
             with contextlib.suppress(AttributeError, IndexError):
                 # bad super() call
@@ -1575,47 +1833,30 @@
                         and args[0].value.id == "self"
                         and args[0].attr == "__class__"
                     ):
-                        self.violations.append((node, "M509"))
+                        self.violations.append((node, "M582"))
 
                 # bad getattr and setattr
                 if (
                     node.func.id in ("getattr", "hasattr")
                     and node.args[1].s == "__call__"
                 ):
-                    self.violations.append((node, "M511"))
+                    self.violations.append((node, "M504"))
                 if (
                     node.func.id == "getattr"
                     and len(node.args) == 2
-                    and AstUtilities.isString(node.args[1])
+                    and self.__isIdentifier(node.args[1])
+                    and iskeyword(AstUtilities.getValue(node.args[1]))
                 ):
-                    self.violations.append((node, "M512"))
+                    self.violations.append((node, "M509"))
                 elif (
                     node.func.id == "setattr"
                     and len(node.args) == 3
-                    and AstUtilities.isString(node.args[1])
+                    and self.__isIdentifier(node.args[1])
+                    and iskeyword(AstUtilities.getValue(node.args[1]))
                 ):
-                    self.violations.append((node, "M513"))
-
-            self.generic_visit(node)
-
-    def visit_Attribute(self, node):
-        """
-        Public method to handle attributes.
-
-        @param node reference to the node to be processed
-        @type ast.Attribute
-        """
-        callPath = list(composeCallPath(node))
-
-        if ".".join(callPath) == "sys.maxint":
-            self.violations.append((node, "M504"))
-
-        elif len(callPath) == 2 and callPath[1] == "message":
-            name = callPath[0]
-            for elem in reversed(self.__nodeStack[:-1]):
-                if isinstance(elem, ast.ExceptHandler) and elem.name == name:
-                    self.violations.append((node, "M505"))
-                    break
+                    self.violations.append((node, "M510"))
+
+        self.generic_visit(node)
 
     def visit_Assign(self, node):
         """
@@ -1624,21 +1865,14 @@
         @param node reference to the node to be processed
         @type ast.Assign
         """
-        if isinstance(self.__nodeStack[-2], ast.ClassDef):
-            # By using 'hasattr' below we're ignoring starred arguments, slices
-            # and tuples for simplicity.
-            assignTargets = {t.id for t in node.targets if hasattr(t, "id")}
-            if "__metaclass__" in assignTargets:
-                self.violations.append((node, "M524"))
-
-        elif len(node.targets) == 1:
+        if len(node.targets) == 1:
             target = node.targets[0]
             if (
                 isinstance(target, ast.Attribute)
                 and isinstance(target.value, ast.Name)
                 and (target.value.id, target.attr) == ("os", "environ")
             ):
-                self.violations.append((node, "M506"))
+                self.violations.append((node, "M503"))
 
         self.generic_visit(node)
 
@@ -1650,6 +1884,8 @@
         @type ast.For
         """
         self.__checkForM507(node)
+        self.__checkForM520(node)
+        self.__checkForM523(node)
 
         self.generic_visit(node)
 
@@ -1661,6 +1897,63 @@
         @type ast.AsyncFor
         """
         self.__checkForM507(node)
+        self.__checkForM520(node)
+        self.__checkForM523(node)
+
+        self.generic_visit(node)
+
+    def visit_While(self, node):
+        """
+        Public method to handle 'while' statements.
+
+        @param node reference to the node to be processed
+        @type ast.While
+        """
+        self.__checkForM523(node)
+
+        self.generic_visit(node)
+
+    def visit_ListComp(self, node):
+        """
+        Public method to handle list comprehensions.
+
+        @param node reference to the node to be processed
+        @type ast.ListComp
+        """
+        self.__checkForM523(node)
+
+        self.generic_visit(node)
+
+    def visit_SetComp(self, node):
+        """
+        Public method to handle set comprehensions.
+
+        @param node reference to the node to be processed
+        @type ast.SetComp
+        """
+        self.__checkForM523(node)
+
+        self.generic_visit(node)
+
+    def visit_DictComp(self, node):
+        """
+        Public method to handle dictionary comprehensions.
+
+        @param node reference to the node to be processed
+        @type ast.DictComp
+        """
+        self.__checkForM523(node)
+
+        self.generic_visit(node)
+
+    def visit_GeneratorExp(self, node):
+        """
+        Public method to handle generator expressions.
+
+        @param node reference to the node to be processed
+        @type ast.GeneratorExp
+        """
+        self.__checkForM523(node)
 
         self.generic_visit(node)
 
@@ -1675,7 +1968,79 @@
             AstUtilities.isNameConstant(node.test)
             and AstUtilities.getValue(node.test) is False
         ):
-            self.violations.append((node, "M503"))
+            self.violations.append((node, "M511"))
+
+        self.generic_visit(node)
+
+    def visit_FunctionDef(self, node):
+        """
+        Public method to handle function definitions.
+
+        @param node reference to the node to be processed
+        @type ast.FunctionDef
+        """
+        self.__checkForM518(node)
+        self.__checkForM519(node)
+        self.__checkForM521(node)
+
+        self.generic_visit(node)
+
+    def visit_ClassDef(self, node):
+        """
+        Public method to handle class definitions.
+
+        @param node reference to the node to be processed
+        @type ast.ClassDef
+        """
+        self.__checkForM518(node)
+        self.__checkForM521(node)
+        self.__checkForM524(node)
+
+        self.generic_visit(node)
+
+    def visit_Try(self, node):
+        """
+        Public method to handle 'try' statements'.
+
+        @param node reference to the node to be processed
+        @type ast.Try
+        """
+        self.__checkForM512(node)
+        self.__checkForM525(node)
+
+        self.generic_visit(node)
+
+    def visit_Compare(self, node):
+        """
+        Public method to handle comparison statements.
+
+        @param node reference to the node to be processed
+        @type ast.Compare
+        """
+        self.__checkForM515(node)
+
+        self.generic_visit(node)
+
+    def visit_Raise(self, node):
+        """
+        Public method to handle 'raise' statements.
+
+        @param node reference to the node to be processed
+        @type ast.Raise
+        """
+        self.__checkForM516(node)
+
+        self.generic_visit(node)
+
+    def visit_With(self, node):
+        """
+        Public method to handle 'with' statements.
+
+        @param node reference to the node to be processed
+        @type ast.With
+        """
+        self.__checkForM517(node)
+        self.__checkForM522(node)
 
         self.generic_visit(node)
 
@@ -1690,9 +2055,9 @@
             if isinstance(value, ast.FormattedValue):
                 return
 
-        self.violations.append((node, "M508"))
-
-    def __checkForM502(self, node):
+        self.violations.append((node, "M581"))
+
+    def __checkForM505(self, node):
         """
         Private method to check the use of *strip().
 
@@ -1712,14 +2077,14 @@
         if len(s) == len(set(s)):
             return  # no characters appear more than once
 
-        self.violations.append((node, "M502"))
+        self.violations.append((node, "M505"))
 
     def __checkForM507(self, node):
         """
         Private method to check for unused loop variables.
 
         @param node reference to the node to be processed
-        @type ast.For
+        @type ast.For or ast.AsyncFor
         """
         targets = NameFinder()
         targets.visit(node.target)
@@ -1732,6 +2097,297 @@
             n = targets.getNames()[name][0]
             self.violations.append((n, "M507", name))
 
+    def __checkForM512(self, node):
+        """
+        Private method to check for return/continue/break inside finally blocks.
+
+        @param node reference to the node to be processed
+        @type ast.Try
+        """
+
+        def _loop(node, badNodeTypes):
+            if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)):
+                return
+
+            if isinstance(node, (ast.While, ast.For)):
+                badNodeTypes = (ast.Return,)
+
+            elif isinstance(node, badNodeTypes):
+                self.violations.append((node, "M512"))
+
+            for child in ast.iter_child_nodes(node):
+                _loop(child, badNodeTypes)
+
+        for child in node.finalbody:
+            _loop(child, (ast.Return, ast.Continue, ast.Break))
+
+    def __checkForM515(self, node):
+        """
+        Private method to check for pointless comparisons.
+
+        @param node reference to the node to be processed
+        @type ast.Compare
+        """
+        if isinstance(self.nodeStack[-2], ast.Expr):
+            self.violations.append((node, "M515"))
+
+    def __checkForM516(self, node):
+        """
+        Private method to check for raising a literal instead of an exception.
+
+        @param node reference to the node to be processed
+        @type ast.Raise
+        """
+        if (
+            AstUtilities.isNameConstant(node.exc)
+            or AstUtilities.isNumber(node.exc)
+            or AstUtilities.isString(node.exc)
+        ):
+            self.violations.append((node, "M516"))
+
+    def __checkForM517(self, node):
+        """
+        Private method to check for use of the evil syntax
+        'with assertRaises(Exception):.
+
+        @param node reference to the node to be processed
+        @type ast.With
+        """
+        item = node.items[0]
+        itemContext = item.context_expr
+        if (
+            hasattr(itemContext, "func")
+            and hasattr(itemContext.func, "attr")
+            and itemContext.func.attr == "assertRaises"
+            and len(itemContext.args) == 1
+            and isinstance(itemContext.args[0], ast.Name)
+            and itemContext.args[0].id == "Exception"
+            and not item.optional_vars
+        ):
+            self.violations.append((node, "M517"))
+
+    def __checkForM518(self, node):
+        """
+        Private method to check for useless expressions.
+
+        @param node reference to the node to be processed
+        @type ast.FunctionDef
+        """
+        subnodeClasses = (
+            (
+                ast.Constant,
+                ast.List,
+                ast.Set,
+                ast.Dict,
+            )
+            if sys.version_info >= (3, 8, 0)
+            else (
+                ast.Num,
+                ast.Bytes,
+                ast.NameConstant,
+                ast.List,
+                ast.Set,
+                ast.Dict,
+            )
+        )
+        for subnode in node.body:
+            if not isinstance(subnode, ast.Expr):
+                continue
+
+            if isinstance(subnode.value, subnodeClasses) and not AstUtilities.isString(
+                subnode.value
+            ):
+                self.violations.append((subnode, "M518"))
+
+    def __checkForM519(self, node):
+        """
+        Private method to check for use of 'functools.lru_cache' or 'functools.cache'.
+
+        @param node reference to the node to be processed
+        @type ast.FunctionDef
+        """
+        caches = {
+            "functools.cache",
+            "functools.lru_cache",
+            "cache",
+            "lru_cache",
+        }
+
+        if (
+            len(node.decorator_list) == 0
+            or len(self.contexts) < 2
+            or not isinstance(self.contexts[-2].node, ast.ClassDef)
+        ):
+            return
+
+        # Preserve decorator order so we can get the lineno from the decorator node
+        # rather than the function node (this location definition changes in Python 3.8)
+        resolvedDecorators = (
+            ".".join(self.__composeCallPath(decorator))
+            for decorator in node.decorator_list
+        )
+        for idx, decorator in enumerate(resolvedDecorators):
+            if decorator in {"classmethod", "staticmethod"}:
+                return
+
+            if decorator in caches:
+                self.violations.append((node.decorator_list[idx], "M519"))
+                return
+
+    def __checkForM520(self, node):
+        """
+        Private method to check for a loop that modifies its iterable.
+
+        @param node reference to the node to be processed
+        @type ast.For or ast.AsyncFor
+        """
+        targets = NameFinder()
+        targets.visit(node.target)
+        ctrlNames = set(targets.getNames())
+
+        iterset = M520NameFinder()
+        iterset.visit(node.iter)
+        itersetNames = set(iterset.getNames())
+
+        for name in sorted(ctrlNames):
+            if name in itersetNames:
+                n = targets.getNames()[name][0]
+                self.violations.append((n, "M520"))
+
+    def __checkForM521(self, node):
+        """
+        Private method to check for use of an f-string as docstring.
+
+        @param node reference to the node to be processed
+        @type ast.FunctionDef or ast.ClassDef
+        """
+        if (
+            node.body
+            and isinstance(node.body[0], ast.Expr)
+            and isinstance(node.body[0].value, ast.JoinedStr)
+        ):
+            self.violations.append((node.body[0].value, "M521"))
+
+    def __checkForM522(self, node):
+        """
+        Private method to check for use of an f-string as docstring.
+
+        @param node reference to the node to be processed
+        @type ast.With
+        """
+        item = node.items[0]
+        itemContext = item.context_expr
+        if (
+            hasattr(itemContext, "func")
+            and hasattr(itemContext.func, "value")
+            and hasattr(itemContext.func.value, "id")
+            and itemContext.func.value.id == "contextlib"
+            and hasattr(itemContext.func, "attr")
+            and itemContext.func.attr == "suppress"
+            and len(itemContext.args) == 0
+        ):
+            self.violations.append((node, "M522"))
+
+    def __checkForM523(self, loopNode):
+        """
+        Private method to check that functions (including lambdas) do not use loop
+        variables.
+
+        @param loopNode reference to the node to be processed
+        @type ast.For, ast.AsyncFor, ast.While, ast.ListComp, ast.SetComp,ast.DictComp,
+            or ast.GeneratorExp
+        """
+        suspiciousVariables = []
+        for node in ast.walk(loopNode):
+            if isinstance(node, BugBearVisitor.FUNCTION_NODES):
+                argnames = {
+                    arg.arg for arg in ast.walk(node.args) if isinstance(arg, ast.arg)
+                }
+                if isinstance(node, ast.Lambda):
+                    bodyNodes = ast.walk(node.body)
+                else:
+                    bodyNodes = itertools.chain.from_iterable(map(ast.walk, node.body))
+                for name in bodyNodes:
+                    if (
+                        isinstance(name, ast.Name)
+                        and name.id not in argnames
+                        and isinstance(name.ctx, ast.Load)
+                    ):
+                        err = (name.lineno, name.col_offset, name.id, name)
+                        if err not in self.__M523Seen:
+                            self.__M523Seen.add(err)  # dedupe across nested loops
+                            suspiciousVariables.append(err)
+
+        if suspiciousVariables:
+            reassignedInLoop = set(self.__getAssignedNames(loopNode))
+
+        for err in sorted(suspiciousVariables):
+            if reassignedInLoop.issuperset(err[2]):
+                self.violations.append((err[3], "M523", err[2]))
+
+    def __checkForM524(self, node):
+        """
+        Private method to check for inheritance from abstract classes in abc and lack of
+        any methods decorated with abstract*.
+
+        @param node reference to the node to be processed
+        @type ast.ClassDef
+        """  # __IGNORE_WARNING_D234r__
+
+        def isAbcClass(value):
+            if isinstance(value, ast.keyword):
+                return value.arg == "metaclass" and isAbcClass(value.value)
+
+            abcNames = ("ABC", "ABCMeta")
+            return (isinstance(value, ast.Name) and value.id in abcNames) or (
+                isinstance(value, ast.Attribute)
+                and value.attr in abcNames
+                and isinstance(value.value, ast.Name)
+                and value.value.id == "abc"
+            )
+
+        def isAbstractDecorator(expr):
+            return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or (
+                isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract"
+            )
+
+        if not any(map(isAbcClass, (*node.bases, *node.keywords))):
+            return
+
+        for stmt in node.body:
+            if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)) and any(
+                map(isAbstractDecorator, stmt.decorator_list)
+            ):
+                return
+
+        self.violations.append((node, "M524", node.name))
+
+    def __checkForM525(self, node):
+        """
+        Private method to check for exceptions being handled multiple times.
+
+        @param node reference to the node to be processed
+        @type ast.Try
+        """
+        seen = []
+
+        for handler in node.handlers:
+            if isinstance(handler.type, (ast.Name, ast.Attribute)):
+                name = ".".join(self.__composeCallPath(handler.type))
+                seen.append(name)
+            elif isinstance(handler.type, ast.Tuple):
+                # to avoid checking the same as M514, remove duplicates per except
+                uniques = set()
+                for entry in handler.type.elts:
+                    name = ".".join(self.__composeCallPath(entry))
+                    uniques.add(name)
+                seen.extend(uniques)
+
+        # sort to have a deterministic output
+        duplicates = sorted({x for x in seen if seen.count(x) > 1})
+        for duplicate in duplicates:
+            self.violations.append((node, "M525", duplicate))
+
 
 class NameFinder(ast.NodeVisitor):
     """
@@ -1761,12 +2417,15 @@
 
         @param node AST node to be traversed
         @type ast.Node
+        @return reference to the last processed node
+        @rtype ast.Node
         """
         if isinstance(node, list):
             for elem in node:
                 super().visit(elem)
+            return node
         else:
-            super().visit(node)
+            return super().visit(node)
 
     def getNames(self):
         """
@@ -1778,6 +2437,60 @@
         return self.__names
 
 
+class M520NameFinder(NameFinder):
+    """
+    Class to extract a name out of a tree of nodes ignoring names defined within the
+    local scope of a comprehension.
+    """
+
+    def visit_GeneratorExp(self, node):
+        """
+        Public method to handle a generator expressions.
+
+        @param node reference to the node to be processed
+        @type ast.GeneratorExp
+        """
+        self.visit(node.generators)
+
+    def visit_ListComp(self, node):
+        """
+        Public method  to handle a list comprehension.
+
+        @param node reference to the node to be processed
+        @type TYPE
+        """
+        self.visit(node.generators)
+
+    def visit_DictComp(self, node):
+        """
+        Public method  to handle a dictionary comprehension.
+
+        @param node reference to the node to be processed
+        @type TYPE
+        """
+        self.visit(node.generators)
+
+    def visit_comprehension(self, node):
+        """
+        Public method  to handle the 'for' of a comprehension.
+
+        @param node reference to the node to be processed
+        @type ast.comprehension
+        """
+        self.visit(node.iter)
+
+    def visit_Lambda(self, node):
+        """
+        Public method  to handle a Lambda function.
+
+        @param node reference to the node to be processed
+        @type ast.Lambda
+        """
+        self.visit(node.body)
+        for lambdaArg in node.args.args:
+            self.getNames().pop(lambdaArg.arg, None)
+
+
 class ReturnVisitor(ast.NodeVisitor):
     """
     Class implementing a node visitor to check return statements.

eric ide

mercurial