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): """