diff -r 094ab8028423 -r fa7b8ebfbe13 src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py --- 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