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

branch
eric7
changeset 9595
2bd590c40309
parent 9482
a2bc06a54d9d
child 9653
e67609152c5e
diff -r bd9550caf22f -r 2bd590c40309 src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Dec 08 16:03:38 2022 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Dec 08 18:03:42 2022 +0100
@@ -126,6 +126,8 @@
         "M523",
         "M524",
         "M525",
+        "M526",
+        "M527",
         ## Bugbear++
         "M581",
         "M582",
@@ -309,6 +311,8 @@
                     "M523",
                     "M524",
                     "M525",
+                    "M526",
+                    "M527",
                     "M581",
                     "M582",
                 ),
@@ -1528,7 +1532,7 @@
     """
 
     #
-    # This class was implemented along the BugBear flake8 extension (v 22.9.11).
+    # This class was implemented along the BugBear flake8 extension (v 22.12.6).
     # Original: Copyright (c) 2016 Ɓukasz Langa
     #
 
@@ -1854,6 +1858,8 @@
                 ):
                     self.violations.append((node, "M510"))
 
+            self.__checkForM526(node)
+
         self.generic_visit(node)
 
     def visit_Assign(self, node):
@@ -1992,7 +1998,7 @@
         """
         self.__checkForM518(node)
         self.__checkForM521(node)
-        self.__checkForM524(node)
+        self.__checkForM524AndM527(node)
 
         self.generic_visit(node)
 
@@ -2295,9 +2301,45 @@
         @type ast.For, ast.AsyncFor, ast.While, ast.ListComp, ast.SetComp,ast.DictComp,
             or ast.GeneratorExp
         """
+        safe_functions = []
         suspiciousVariables = []
         for node in ast.walk(loopNode):
-            if isinstance(node, BugBearVisitor.FUNCTION_NODES):
+            # check if function is immediately consumed to avoid false alarm
+            if isinstance(node, ast.Call):
+                # check for filter&reduce
+                if (
+                    isinstance(node.func, ast.Name)
+                    and node.func.id in ("filter", "reduce", "map")
+                ) or (
+                    isinstance(node.func, ast.Attribute)
+                    and node.func.attr == "reduce"
+                    and isinstance(node.func.value, ast.Name)
+                    and node.func.value.id == "functools"
+                ):
+                    for arg in node.args:
+                        if isinstance(arg, BugBearVisitor.FUNCTION_NODES):
+                            safe_functions.append(arg)
+
+                # check for key=
+                for keyword in node.keywords:
+                    if keyword.arg == "key" and isinstance(
+                        keyword.value, BugBearVisitor.FUNCTION_NODES
+                    ):
+                        safe_functions.append(keyword.value)
+
+            # mark `return lambda: x` as safe
+            # does not (currently) check inner lambdas in a returned expression
+            # e.g. `return (lambda: x, )
+            if isinstance(node, ast.Return) and isinstance(
+                node.value, BugBearVisitor.FUNCTION_NODES
+            ):
+                safe_functions.append(node.value)
+
+            # find unsafe functions
+            if (
+                isinstance(node, BugBearVisitor.FUNCTION_NODES)
+                and node not in safe_functions
+            ):
                 argnames = {
                     arg.arg for arg in ast.walk(node.args) if isinstance(arg, ast.arg)
                 }
@@ -2305,16 +2347,17 @@
                     bodyNodes = ast.walk(node.body)
                 else:
                     bodyNodes = itertools.chain.from_iterable(map(ast.walk, node.body))
+                errors = []
                 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 isinstance(name, ast.Name) and name.id not in argnames:
+                        if isinstance(name.ctx, ast.Load):
+                            errors.append((name.lineno, name.col_offset, name.id, name))
+                        elif isinstance(name.ctx, ast.Store):
+                            argnames.add(name.id)
+                for err in errors:
+                    if err[2] not in argnames and err not in self.__M523Seen:
+                        self.__M523Seen.add(err)  # dedupe across nested loops
+                        suspiciousVariables.append(err)
 
         if suspiciousVariables:
             reassignedInLoop = set(self.__getAssignedNames(loopNode))
@@ -2323,7 +2366,7 @@
             if reassignedInLoop.issuperset(err[2]):
                 self.violations.append((err[3], "M523", err[2]))
 
-    def __checkForM524(self, node):
+    def __checkForM524AndM527(self, node):
         """
         Private method to check for inheritance from abstract classes in abc and lack of
         any methods decorated with abstract*.
@@ -2332,14 +2375,15 @@
         @type ast.ClassDef
         """  # __IGNORE_WARNING_D234r__
 
-        def isAbcClass(value):
+        def isAbcClass(value, name="ABC"):
             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 (
+                return value.arg == "metaclass" and isAbcClass(value.value, "ABCMeta")
+
+            # class foo(ABC)
+            # class foo(abc.ABC)
+            return (isinstance(value, ast.Name) and value.id == name) or (
                 isinstance(value, ast.Attribute)
-                and value.attr in abcNames
+                and value.attr == name
                 and isinstance(value.value, ast.Name)
                 and value.value.id == "abc"
             )
@@ -2349,16 +2393,62 @@
                 isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract"
             )
 
+        def isOverload(expr):
+            return (isinstance(expr, ast.Name) and expr.id == "overload") or (
+                isinstance(expr, ast.Attribute) and expr.attr == "overload"
+            )
+
+        def emptyBody(body):
+            def isStrOrEllipsis(node):
+                # ast.Ellipsis and ast.Str used in python<3.8
+                return isinstance(node, (ast.Ellipsis, ast.Str)) or (
+                    isinstance(node, ast.Constant)
+                    and (node.value is Ellipsis or isinstance(node.value, str))
+                )
+
+            # Function body consist solely of `pass`, `...`, and/or (doc)string literals
+            return all(
+                isinstance(stmt, ast.Pass)
+                or (isinstance(stmt, ast.Expr) and isStrOrEllipsis(stmt.value))
+                for stmt in body
+            )
+
+        # don't check multiple inheritance
+        if len(node.bases) + len(node.keywords) > 1:
+            return
+
+        # only check abstract classes
+        if not any(map(isAbcClass, (*node.bases, *node.keywords))):
+            return
+
+        hasAbstractMethod = False
+
         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)
+            # Ignore abc's that declares a class attribute that must be set
+            if isinstance(stmt, (ast.AnnAssign, ast.Assign)):
+                hasAbstractMethod = True
+                continue
+
+            # only check function defs
+            if not isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
+                continue
+
+            hasAbstractDecorator = any(map(isAbstractDecorator, stmt.decorator_list))
+
+            hasAbstractMethod |= hasAbstractDecorator
+
+            if (
+                not hasAbstractDecorator
+                and emptyBody(stmt.body)
+                and not any(map(isOverload, stmt.decorator_list))
             ):
-                return
-
-        self.violations.append((node, "M524", node.name))
+                self.violations.append((stmt, "M527", stmt.name))
+
+        if not hasAbstractMethod:
+            self.violations.append((node, "M524", node.name))
 
     def __checkForM525(self, node):
         """
@@ -2386,6 +2476,28 @@
         for duplicate in duplicates:
             self.violations.append((node, "M525", duplicate))
 
+    def __checkForM526(self, node):
+        """
+        Private method to check for Star-arg unpacking after keyword argument.
+
+        @param node reference to the node to be processed
+        @type ast.Call
+        """
+        if not node.keywords:
+            return
+
+        starreds = [arg for arg in node.args if isinstance(arg, ast.Starred)]
+        if not starreds:
+            return
+
+        firstKeyword = node.keywords[0].value
+        for starred in starreds:
+            if (starred.lineno, starred.col_offset) > (
+                firstKeyword.lineno,
+                firstKeyword.col_offset,
+            ):
+                self.violations.append((node, "M526"))
+
 
 class NameFinder(ast.NodeVisitor):
     """

eric ide

mercurial