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.