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

branch
eric7
changeset 11138
1f743bad6fd3
parent 11134
3243a66db84a
child 11140
b823386f7591
diff -r a90284948331 -r 1f743bad6fd3 src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Sun Feb 16 14:56:07 2025 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Sun Feb 16 15:05:39 2025 +0100
@@ -17,7 +17,7 @@
 import sys
 import tokenize
 
-from collections import defaultdict, namedtuple
+from collections import Counter, defaultdict, namedtuple
 from dataclasses import dataclass
 from keyword import iskeyword
 from string import Formatter
@@ -207,6 +207,7 @@
         "M537",
         "M539",
         "M540",
+        "M541",
         ## Bugbear, opininonated
         "M569",
         ## Bugbear++
@@ -420,6 +421,7 @@
                     "M537",
                     "M539",
                     "M540",
+                    "M541",
                     "M569",
                     "M581",
                     "M582",
@@ -1639,7 +1641,7 @@
 #######################################################################
 ## BugBearVisitor
 ##
-## adapted from: flake8-bugbear v24.8.19
+## adapted from: flake8-bugbear v24.12.12
 ##
 ## Original: Copyright (c) 2016 Ɓukasz Langa
 #######################################################################
@@ -1657,6 +1659,27 @@
     hasNote: bool
 
 
+class M541UnhandledKeyType:
+    """
+    Class to hold a dictionary key of a type that we do not check for duplicates.
+    """
+
+
+class M541VariableKeyType:
+    """
+    Class to hold the name of a variable key type.
+    """
+
+    def __init__(self, name):
+        """
+        Constructor
+
+        @param name name of the variable key type
+        @type str
+        """
+        self.name = name
+
+
 class BugBearVisitor(ast.NodeVisitor):
     """
     Class implementing a node visitor to check for various topics.
@@ -1696,6 +1719,8 @@
         self.__M505Imports = set()
         self.__M540CaughtException = None
 
+        self.__inTryStar = ""
+
     @property
     def nodeStack(self):
         """
@@ -1842,7 +1867,7 @@
                 continue
             yield expr
 
-    def __checkRedundantExcepthandlers(self, names, node):
+    def __checkRedundantExcepthandlers(self, names, node, inTryStar):
         """
         Private method to check for redundant exception types in an exception handler.
 
@@ -1850,6 +1875,8 @@
         @type list of ast.Name
         @param node reference to the exception handler node
         @type ast.ExceptionHandler
+        @param inTryStar character indicating an 'except*' handler
+        @type str
         @return tuple containing the error data
         @rtype tuple of (ast.Node, str, str, str, str)
         """
@@ -1898,7 +1925,7 @@
         if good != names:
             desc = good[0] if len(good) == 1 else "({0})".format(", ".join(good))
             as_ = " as " + node.name if node.name is not None else ""
-            return (node, "M514", ", ".join(names), as_, desc)
+            return (node, "M514", ", ".join(names), as_, desc, inTryStar)
 
         return None
 
@@ -2043,7 +2070,7 @@
         else:
             self.__M540CaughtException = M540CaughtException(node.name, False)
 
-        names = self.__checkForM513_M529_M530(node)
+        names = self.__checkForM513_M514_M529_M530(node)
 
         if "BaseException" in names and not ExceptBaseExceptionVisitor(node).reRaised():
             self.violations.append((node, "M536"))
@@ -2303,7 +2330,7 @@
 
     def visit_Try(self, node):
         """
-        Public method to handle 'try' statements'.
+        Public method to handle 'try' statements.
 
         @param node reference to the node to be processed
         @type ast.Try
@@ -2313,6 +2340,18 @@
 
         self.generic_visit(node)
 
+    def visit_TryStar(self, node):
+        """
+        Public method to handle 'except*' statements.
+
+        @param node reference to the node to be processed
+        @type ast.TryStar
+        """
+        outerTryStar = self.__inTryStar
+        self.__inTryStar = "*"
+        self.visit_Try(node)
+        self.__inTryStar = outerTryStar
+
     def visit_Compare(self, node):
         """
         Public method to handle comparison statements.
@@ -2408,6 +2447,17 @@
 
         self.generic_visit(node)
 
+    def visit_Dict(self, node):
+        """
+        Public method to check a dictionary.
+
+        @param node reference to the node to be processed
+        @type ast.Dict
+        """
+        self.__checkForM541(node)
+
+        self.generic_visit(node)
+
     def __checkForM505(self, node):
         """
         Private method to check the use of *strip().
@@ -2488,7 +2538,7 @@
                 badNodeTypes = (ast.Return,)
 
             elif isinstance(node, badNodeTypes):
-                self.violations.append((node, "M512"))
+                self.violations.append((node, "M512", self.__inTryStar))
 
             for child in ast.iter_child_nodes(node):
                 _loop(child, badNodeTypes)
@@ -2496,7 +2546,7 @@
         for child in node.finalbody:
             _loop(child, (ast.Return, ast.Continue, ast.Break))
 
-    def __checkForM513_M529_M530(self, node):
+    def __checkForM513_M514_M529_M530(self, node):
         """
         Private method to check various exception handler situations.
 
@@ -2524,16 +2574,18 @@
         if badHandlers:
             self.violations.append((node, "M530"))
         if len(names) == 0 and not badHandlers and not ignoredHandlers:
-            self.violations.append((node, "M529"))
+            self.violations.append((node, "M529", self.__inTryStar))
         elif (
             len(names) == 1
             and not badHandlers
             and not ignoredHandlers
             and isinstance(node.type, ast.Tuple)
         ):
-            self.violations.append((node, "M513", *names))
+            self.violations.append((node, "M513", *names, self.__inTryStar))
         else:
-            maybeError = self.__checkRedundantExcepthandlers(names, node)
+            maybeError = self.__checkRedundantExcepthandlers(
+                names, node, self.__inTryStar
+            )
             if maybeError is not None:
                 self.violations.append(maybeError)
         return names
@@ -2850,7 +2902,7 @@
 
         for stmt in node.body:
             # Ignore abc's that declares a class attribute that must be set
-            if isinstance(stmt, (ast.AnnAssign, ast.Assign)):
+            if isinstance(stmt, ast.AnnAssign) and stmt.value is None:
                 hasAbstractMethod = True
                 continue
 
@@ -2897,7 +2949,7 @@
         # 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))
+            self.violations.append((node, "M525", duplicate, self.__inTryStar))
 
     def __checkForM526(self, node):
         """
@@ -2935,6 +2987,8 @@
             and node.func.value.id == "warnings"
             and not any(kw.arg == "stacklevel" for kw in node.keywords)
             and len(node.args) < 3
+            and not any(isinstance(a, ast.Starred) for a in node.args)
+            and not any(kw.arg is None for kw in node.keywords)
         ):
             self.violations.append((node, "M528"))
 
@@ -3142,6 +3196,45 @@
                 self.__M540CaughtException = None
                 break
 
+    def __checkForM541(self, node):
+        """
+        Private method to check for duplicate key value pairs in a dictionary literal.
+
+        @param node reference to the node to be processed
+        @type ast.Dict
+        """  # noqa: D234r
+
+        def convertToValue(item):
+            """
+            Function to extract the value of a given item.
+
+            @param item node to extract value from
+            @type ast.Ast
+            @return value of the node
+            @rtype Any
+            """
+            if isinstance(item, ast.Constant):
+                return item.value
+            elif isinstance(item, ast.Tuple):
+                return tuple(convertToValue(i) for i in item.elts)
+            elif isinstance(item, ast.Name):
+                return M541VariableKeyType(item.id)
+            else:
+                return M541UnhandledKeyType()
+
+        keys = [convertToValue(key) for key in node.keys]
+        keyCounts = Counter(keys)
+        duplicateKeys = [key for key, count in keyCounts.items() if count > 1]
+        for key in duplicateKeys:
+            keyIndices = [i for i, iKey in enumerate(keys) if iKey == key]
+            seen = set()
+            for index in keyIndices:
+                value = convertToValue(node.values[index])
+                if value in seen:
+                    keyNode = node.keys[index]
+                    self.violations.append((keyNode, "M541"))
+                seen.add(value)
+
     def __checkForM569(self, node):
         """
         Private method to check for changes to a loop's mutable iterable.

eric ide

mercurial