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

branch
eric7
changeset 10510
fa7b8ebfbe13
parent 10439
21c28b0f9e41
child 10516
72baef0baa76
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Jan 18 09:25:11 2024 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Jan 18 13:10:08 2024 +0100
@@ -167,6 +167,9 @@
         "M533",
         "M534",
         "M535",
+        "M536",
+        "M537",
+        "M538",
         ## Bugbear++
         "M581",
         "M582",
@@ -374,6 +377,9 @@
                     "M533",
                     "M534",
                     "M535",
+                    "M536",
+                    "M537",
+                    "M538",
                     "M581",
                     "M582",
                 ),
@@ -1651,7 +1657,7 @@
 #######################################################################
 ## BugBearVisitor
 ##
-## adapted from: flake8-bugbear v23.11.26
+## adapted from: flake8-bugbear v24.1.17
 ##
 ## Original: Copyright (c) 2016 Ɓukasz Langa
 #######################################################################
@@ -1747,9 +1753,9 @@
         elif isinstance(node, ast.Name):
             yield node.id
 
-    def __toNameStr(self, node):
-        """
-        Private method to turn Name and Attribute nodes to strings, handling any
+    def toNameStr(self, node):
+        """
+        Public method to turn Name and Attribute nodes to strings, handling any
         depth of attribute accesses.
 
 
@@ -1762,12 +1768,12 @@
             return node.id
 
         if isinstance(node, ast.Call):
-            return self.__toNameStr(node.func)
+            return self.toNameStr(node.func)
 
         try:
-            return self.__toNameStr(node.value) + "." + node.attr
+            return self.toNameStr(node.value) + "." + node.attr
         except AttributeError:
-            return self.__toNameStr(node.value)
+            return self.toNameStr(node.value)
 
     def __typesafeIssubclass(self, obj, classOrTuple):
         """
@@ -1946,7 +1952,7 @@
             elif isinstance(dim, ast.Tuple):
                 yield from self.__getNamesFromTuple(dim)
 
-    def __getDictCompLoopVarNames(self, node):
+    def __getDictCompLoopAndNamedExprVarNames(self, node):
         """
         Private method to get the names of comprehension loop variables.
 
@@ -1955,12 +1961,67 @@
         @yield loop variable names
         @ytype str
         """
+        finder = NamedExprFinder()
         for gen in node.generators:
             if isinstance(gen.target, ast.Name):
                 yield gen.target.id
             elif isinstance(gen.target, ast.Tuple):
                 yield from self.__getNamesFromTuple(gen.target)
 
+            finder.visit(gen.ifs)
+
+        yield from finder.getNames().keys()
+
+    def __inClassInit(self):
+        """
+        Private method to check, if we are inside an '__init__' method.
+
+        @return flag indicating being within the '__init__' method
+        @rtype bool
+        """
+        return (
+            len(self.contexts) >= 2
+            and isinstance(self.contexts[-2].node, ast.ClassDef)
+            and isinstance(self.contexts[-1].node, ast.FunctionDef)
+            and self.contexts[-1].node.name == "__init__"
+        )
+
+    def visit_Return(self, node):
+        """
+        Public method to handle 'Return' nodes.
+
+        @param node reference to the node to be processed
+        @type ast.Return
+        """
+        if self.__inClassInit() and node.value is not None:
+            self.violations.append((node, "M537"))
+
+        self.generic_visit(node)
+
+    def visit_Yield(self, node):
+        """
+        Public method to handle 'Yield' nodes.
+
+        @param node reference to the node to be processed
+        @type ast.Yield
+        """
+        if self.__inClassInit():
+            self.violations.append((node, "M537"))
+
+        self.generic_visit(node)
+
+    def visit_YieldFrom(self, node) -> None:
+        """
+        Public method to handle 'YieldFrom' nodes.
+
+        @param node reference to the node to be processed
+        @type ast.YieldFrom
+        """
+        if self.__inClassInit():
+            self.violations.append((node, "M537"))
+
+        self.generic_visit(node)
+
     def visit(self, node):
         """
         Public method to traverse a given AST node.
@@ -1985,6 +2046,8 @@
         if isContextful:
             self.contexts.pop()
 
+        self.__checkForM518(node)
+
     def visit_ExceptHandler(self, node):
         """
         Public method to handle exception handlers.
@@ -2003,7 +2066,7 @@
             ignoredHandlers = []
             for handler in handlers:
                 if isinstance(handler, (ast.Name, ast.Attribute)):
-                    name = self.__toNameStr(handler)
+                    name = self.toNameStr(handler)
                     if name is None:
                         ignoredHandlers.append(handler)
                     else:
@@ -2027,6 +2090,11 @@
                 maybeError = self.__checkRedundantExcepthandlers(names, node)
                 if maybeError is not None:
                     self.violations.append(maybeError)
+            if (
+                "BaseException" in names
+                and not ExceptBaseExceptionVisitor(node).reRaised()
+            ):
+                self.violations.append((node, "M536"))
 
         self.generic_visit(node)
 
@@ -2102,8 +2170,6 @@
         @param node reference to the node to be processed
         @type ast.Module
         """
-        self.__checkForM518(node)
-
         self.generic_visit(node)
 
     def visit_Assign(self, node):
@@ -2135,6 +2201,7 @@
         self.__checkForM520(node)
         self.__checkForM523(node)
         self.__checkForM531(node)
+        self.__checkForM538(node)
 
         self.generic_visit(node)
 
@@ -2230,7 +2297,6 @@
         @param node reference to the node to be processed
         @type ast.FunctionDef
         """
-        self.__checkForM518(node)
         self.__checkForM519(node)
         self.__checkForM521(node)
 
@@ -2243,7 +2309,6 @@
         @param node reference to the node to be processed
         @type ast.ClassDef
         """
-        self.__checkForM518(node)
         self.__checkForM521(node)
         self.__checkForM524AndM527(node)
 
@@ -2472,7 +2537,7 @@
                             itemContext.func.attr == "raises"
                             and isinstance(itemContext.func.value, ast.Name)
                             and itemContext.func.value.id == "pytest"
-                            and "match" not in [kwd.arg for kwd in itemContext.keywords]
+                            and "match" not in (kwd.arg for kwd in itemContext.keywords)
                         )
                     )
                 )
@@ -2481,12 +2546,12 @@
                     and itemContext.func.id == "raises"
                     and isinstance(itemContext.func.ctx, ast.Load)
                     and "pytest.raises" in self.__M505Imports
-                    and "match" not in [kwd.arg for kwd in itemContext.keywords]
+                    and "match" not in (kwd.arg for kwd in itemContext.keywords)
                 )
             )
             and len(itemContext.args) == 1
             and isinstance(itemContext.args[0], ast.Name)
-            and itemContext.args[0].id == "Exception"
+            and itemContext.args[0].id in ("Exception", "BaseException")
             and not item.optional_vars
         ):
             self.violations.append((node, "M517"))
@@ -2498,24 +2563,23 @@
         @param node reference to the node to be processed
         @type ast.FunctionDef
         """
-        for subnode in node.body:
-            if not isinstance(subnode, ast.Expr):
-                continue
-
-            if isinstance(
-                subnode.value,
-                (ast.List, ast.Set, ast.Dict),
-            ) or (
-                isinstance(subnode.value, ast.Constant)
-                and (
-                    isinstance(
-                        subnode.value.value,
-                        (int, float, complex, bytes, bool),
-                    )
-                    or subnode.value.value is None
+        if not isinstance(node, ast.Expr):
+            return
+
+        if isinstance(
+            node.value,
+            (ast.List, ast.Set, ast.Dict, ast.Tuple),
+        ) or (
+            isinstance(node.value, ast.Constant)
+            and (
+                isinstance(
+                    node.value.value,
+                    (int, float, complex, bytes, bool),
                 )
-            ):
-                self.violations.append((subnode, "M518"))
+                or node.value.value is None
+            )
+        ):
+            self.violations.append((node, "M518", node.value.__class__.__name__))
 
     def __checkForM519(self, node):
         """
@@ -2934,7 +2998,7 @@
         elif node.func.attr == "split":
             check(2, "maxsplit")
 
-    def __checkForM535(self, node: ast.DictComp):
+    def __checkForM535(self, node):
         """
         Private method to check that a static key isn't used in a dict comprehension.
 
@@ -2944,18 +3008,179 @@
         @param node reference to the node to be processed
         @type ast.DictComp
         """
-        """Check that a static key isn't used in a dict comprehension.
-
-        Emit a warning if a likely unchanging key is used - either a constant,
-        or a variable that isn't coming from the generator expression.
-        """
         if isinstance(node.key, ast.Constant):
             self.violations.append((node, "M535", node.key.value))
         elif isinstance(
             node.key, ast.Name
-        ) and node.key.id not in self.__getDictCompLoopVarNames(node):
+        ) and node.key.id not in self.__getDictCompLoopAndNamedExprVarNames(node):
             self.violations.append((node, "M535", node.key.id))
 
+    def __checkForM538(self, node):
+        """
+        Private method to check for changes to a loop's mutable iterable.
+
+        @param node loop node to be checked
+        @type ast.For
+        """
+        if isinstance(node.iter, ast.Name):
+            name = self.toNameStr(node.iter)
+        elif isinstance(node.iter, ast.Attribute):
+            name = self.toNameStr(node.iter)
+        else:
+            return
+        checker = M538Checker(name, self)
+        checker.visit(node.body)
+        for mutation in checker.mutations:
+            self.violations.append((mutation, "M538"))
+
+
+class M538Checker(ast.NodeVisitor):
+    """
+    Class traversing a 'for' loop body to check for modifications to a loop's
+    mutable iterable.
+    """
+
+    # https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types
+    MUTATING_FUNCTIONS = (
+        "append",
+        "sort",
+        "reverse",
+        "remove",
+        "clear",
+        "extend",
+        "insert",
+        "pop",
+        "popitem",
+    )
+
+    def __init__(self, name, bugbear):
+        """
+        Constructor
+
+        @param name name of the iterator
+        @type str
+        @param bugbear reference to the bugbear visitor
+        @type BugBearVisitor
+        """
+        self.__name = name
+        self.__bb = bugbear
+        self.mutations = []
+
+    def visit_Delete(self, node):
+        """
+        Public method handling 'Delete' nodes.
+
+        @param node reference to the node to be processed
+        @type ast.Delete
+        """
+        for target in node.targets:
+            if isinstance(target, ast.Subscript):
+                name = self.__bb.toNameStr(target.value)
+            elif isinstance(target, (ast.Attribute, ast.Name)):
+                name = self.__bb.toNameStr(target)
+            else:
+                name = ""  # fallback
+                self.generic_visit(target)
+
+            if name == self.__name:
+                self.mutations.append(node)
+
+    def visit_Call(self, node):
+        """
+        Public method handling 'Call' nodes.
+
+        @param node reference to the node to be processed
+        @type ast.Call
+        """
+        if isinstance(node.func, ast.Attribute):
+            name = self.__bb.toNameStr(node.func.value)
+            functionObject = name
+            functionName = node.func.attr
+
+            if (
+                functionObject == self.__name
+                and functionName in self.MUTATING_FUNCTIONS
+            ):
+                self.mutations.append(node)
+
+        self.generic_visit(node)
+
+    def visit(self, node):
+        """
+        Public method to inspect an ast node.
+
+        Like super-visit but supports iteration over lists.
+
+        @param node AST node to be traversed
+        @type TYPE
+        @return reference to the last processed node
+        @rtype ast.Node
+        """
+        if not isinstance(node, list):
+            return super().visit(node)
+
+        for elem in node:
+            super().visit(elem)
+        return node
+
+
+class ExceptBaseExceptionVisitor(ast.NodeVisitor):
+    """
+    Class to determine, if a 'BaseException' is re-raised.
+    """
+
+    def __init__(self, exceptNode):
+        """
+        Constructor
+
+        @param exceptNode exception node to be inspected
+        @type ast.ExceptHandler
+        """
+        super().__init__()
+        self.__root = exceptNode
+        self.__reRaised = False
+
+    def reRaised(self) -> bool:
+        """
+        Public method to check, if the exception is re-raised.
+
+        @return flag indicating a re-raised exception
+        @rtype bool
+        """
+        self.visit(self.__root)
+        return self.__reRaised
+
+    def visit_Raise(self, node):
+        """
+        Public method to handle 'Raise' nodes.
+
+        If we find a corresponding `raise` or `raise e` where e was from
+        `except BaseException as e:` then we mark re_raised as True and can
+        stop scanning.
+
+        @param node reference to the node to be processed
+        @type ast.Raise
+        """
+        if node.exc is None or (
+            isinstance(node.exc, ast.Name) and node.exc.id == self.__root.name
+        ):
+            self.__reRaised = True
+            return
+
+        super().generic_visit(node)
+
+    def visit_ExceptHandler(self, node: ast.ExceptHandler):
+        """
+        Public method to handle 'ExceptHandler' nodes.
+
+        @param node reference to the node to be processed
+        @type ast.ExceptHandler
+        """
+        if node is not self.__root:
+            return  # entered a nested except - stop searching
+
+        super().generic_visit(node)
+
 
 class NameFinder(ast.NodeVisitor):
     """
@@ -3005,6 +3230,59 @@
         return self.__names
 
 
+class NamedExprFinder(ast.NodeVisitor):
+    """
+    Class to extract names defined through an ast.NamedExpr.
+    """
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        super().__init__()
+
+        self.__names = {}
+
+    def visit_NamedExpr(self, node: ast.NamedExpr):
+        """
+        Public method handling 'NamedExpr' nodes.
+
+        @param node reference to the node to be processed
+        @type ast.NamedExpr
+        """
+        self.__names.setdefault(node.target.id, []).append(node.target)
+
+        self.generic_visit(node)
+
+    def visit(self, node):
+        """
+        Public method to traverse a given AST node.
+
+        Like super-visit but supports iteration over lists.
+
+        @param node AST node to be traversed
+        @type TYPE
+        @return reference to the last processed node
+        @rtype ast.Node
+        """
+        if not isinstance(node, list):
+            super().visit(node)
+
+        for elem in node:
+            super().visit(elem)
+
+        return node
+
+    def getNames(self):
+        """
+        Public method to return the extracted names and Name nodes.
+
+        @return dictionary containing the names as keys and the list of nodes
+        @rtype dict
+        """
+        return self.__names
+
+
 class M520NameFinder(NameFinder):
     """
     Class to extract a name out of a tree of nodes ignoring names defined within the

eric ide

mercurial