--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py Wed Sep 14 11:07:55 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py Thu Sep 15 10:09:53 2022 +0200 @@ -7,15 +7,17 @@ Module implementing a checker for miscellaneous checks. """ +import ast +import builtins +import contextlib +import copy +import itertools +import re import sys -import ast -import re -import itertools +import tokenize +from collections import defaultdict, namedtuple +from keyword import iskeyword from string import Formatter -from collections import defaultdict -import tokenize -import copy -import contextlib import AstUtilities @@ -106,17 +108,27 @@ "M503", "M504", "M505", - "M506", "M507", - "M508", "M509", + "M510", "M511", "M512", "M513", + "M514", + "M515", + "M516", + "M517", + "M518", + "M519", + "M520", "M521", "M522", "M523", "M524", + "M525", + ## Bugbear++ + "M581", + "M582", ## Format Strings "M601", "M611", @@ -281,17 +293,26 @@ "M503", "M504", "M505", - "M506", "M507", - "M508", "M509", + "M510", "M511", "M512", "M513", + "M514", + "M515", + "M516", + "M517", + "M518", + "M519", + "M520", "M521", "M522", "M523", "M524", + "M525", + "M581", + "M582", ), ), (self.__checkPep3101, ("M601",)), @@ -996,6 +1017,9 @@ ast.Dict, ast.List, ast.Set, + ast.DictComp, + ast.ListComp, + ast.SetComp, ) mutableCalls = ( "Counter", @@ -1013,6 +1037,15 @@ immutableCalls = ( "tuple", "frozenset", + "types.MappingProxyType", + "MappingProxyType", + "re.compile", + "operator.attrgetter", + "operator.itemgetter", + "operator.methodcaller", + "attrgetter", + "itemgetter", + "methodcaller", ) functionDefs = [ast.FunctionDef] with contextlib.suppress(AttributeError): @@ -1488,16 +1521,36 @@ super().generic_visit(node) +BugBearContext = namedtuple("BugBearContext", ["node", "stack"]) + + class BugBearVisitor(ast.NodeVisitor): """ Class implementing a node visitor to check for various topics. """ # - # This class was implemented along the BugBear flake8 extension (v 19.3.0). + # This class was implemented along the BugBear flake8 extension (v 22.9.11). # Original: Copyright (c) 2016 Ćukasz Langa # - # TODO: update to v22.7.1 + + CONTEXTFUL_NODES = ( + ast.Module, + ast.ClassDef, + ast.AsyncFunctionDef, + ast.FunctionDef, + ast.Lambda, + ast.ListComp, + ast.SetComp, + ast.DictComp, + ast.GeneratorExp, + ) + + FUNCTION_NODES = ( + ast.AsyncFunctionDef, + ast.FunctionDef, + ast.Lambda, + ) NodeWindowSize = 4 @@ -1507,9 +1560,150 @@ """ super().__init__() - self.__nodeStack = [] - self.__nodeWindow = [] + self.nodeWindow = [] self.violations = [] + self.contexts = [] + + self.__M523Seen = set() + + @property + def nodeStack(self): + """ + Public method to get a reference to the most recent node stack. + + @return reference to the most recent node stack + @rtype list + """ + if len(self.contexts) == 0: + return [] + + context, stack = self.contexts[-1] + return stack + + def __isIdentifier(self, arg): + """ + Private method to check if arg is a valid identifier. + + See https://docs.python.org/2/reference/lexical_analysis.html#identifiers + + @param arg reference to an argument node + @type ast.Node + @return flag indicating a valid identifier + @rtype TYPE + """ + if not AstUtilities.isString(arg): + return False + + return ( + re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", AstUtilities.getValue(arg)) + is not None + ) + + def __composeCallPath(self, node): + """ + Private method get the individual elements of the call path of a node. + + @param node reference to the node + @type ast.Node + @yield one element of the call path + @ytype ast.Node + """ + if isinstance(node, ast.Attribute): + yield from self.__composeCallPath(node.value) + yield node.attr + elif isinstance(node, ast.Call): + yield from self.__composeCallPath(node.func) + elif isinstance(node, ast.Name): + yield node.id + + def __toNameStr(self, node): + """ + Private method to turn Name and Attribute nodes to strings, handling any + depth of attribute accesses. + + + @param node reference to the node + @type ast.Name or ast.Attribute + @return string representation + @rtype str + """ + if isinstance(node, ast.Name): + return node.id + + if isinstance(node, ast.Call): + return self.__toNameStr(node.func) + + try: + return self.__toNameStr(node.value) + "." + node.attr + except AttributeError: + return self.__toNameStr(node.value) + + def __typesafeIssubclass(self, obj, classOrTuple): + """ + Private method implementing a type safe issubclass() function. + + @param obj reference to the object to be tested + @type any + @param classOrTuple type to check against + @type type + @return flag indicating a subclass + @rtype bool + """ + try: + return issubclass(obj, classOrTuple) + except TypeError: + # User code specifies a type that is not a type in our current run. + # Might be their error, might be a difference in our environments. + # We don't know so we ignore this. + return False + + def __getAssignedNames(self, loopNode): + """ + Private method to get the names of a for loop. + + @param loopNode reference to the node to be processed + @type ast.For + @yield DESCRIPTION + @ytype TYPE + """ + loopTargets = (ast.For, ast.AsyncFor, ast.comprehension) + for node in self.__childrenInScope(loopNode): + if isinstance(node, (ast.Assign)): + for child in node.targets: + yield from self.__namesFromAssignments(child) + if isinstance(node, loopTargets + (ast.AnnAssign, ast.AugAssign)): + yield from self.__namesFromAssignments(node.target) + + def __namesFromAssignments(self, assignTarget): + """ + Private method to get names of an assignment. + + @param assignTarget reference to the node to be processed + @type ast.Node + @yield name of the assignment + @ytype str + """ + if isinstance(assignTarget, ast.Name): + yield assignTarget.id + elif isinstance(assignTarget, ast.Starred): + yield from self.__namesFromAssignments(assignTarget.value) + elif isinstance(assignTarget, (ast.List, ast.Tuple)): + for child in assignTarget.elts: + yield from self.__namesFromAssignments(child) + + def __childrenInScope(self, node): + """ + Private method to get all child nodes in the given scope. + + @param node reference to the node to be processed + @type ast.Node + @yield reference to a child node + @ytype ast.Node + """ + yield node + if not isinstance(node, BugBearVisitor.FUNCTION_NODES): + for child in ast.iter_child_nodes(node): + yield from self.__childrenInScope(child) def visit(self, node): """ @@ -1518,13 +1712,91 @@ @param node AST node to be traversed @type ast.Node """ - self.__nodeStack.append(node) - self.__nodeWindow.append(node) - self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize :] + isContextful = isinstance(node, BugBearVisitor.CONTEXTFUL_NODES) + + if isContextful: + context = BugBearContext(node, []) + self.contexts.append(context) + + self.nodeStack.append(node) + self.nodeWindow.append(node) + self.nodeWindow = self.nodeWindow[-BugBearVisitor.NodeWindowSize :] super().visit(node) - self.__nodeStack.pop() + self.nodeStack.pop() + + if isContextful: + self.contexts.pop() + + def visit_ExceptHandler(self, node): + """ + Public method to handle exception handlers. + + @param node reference to the node to be processed + @type ast.ExceptHandler + """ + redundantExceptions = { + "OSError": { + # All of these are actually aliases of OSError since Python 3.3 + "IOError", + "EnvironmentError", + "WindowsError", + "mmap.error", + "socket.error", + "select.error", + }, + "ValueError": { + "binascii.Error", + }, + } + + if node.type is None: + # bare except is handled by pycodestyle already + pass + + elif isinstance(node.type, ast.Tuple): + names = [self.__toNameStr(e) for e in node.type.elts] + as_ = " as " + node.name if node.name is not None else "" + if len(names) == 0: + self.violations.append((node, "M501", as_)) + elif len(names) == 1: + self.violations.append((node, "M513", *names)) + else: + # See if any of the given exception names could be removed, e.g. from: + # (MyError, MyError) # duplicate names + # (MyError, BaseException) # everything derives from the Base + # (Exception, TypeError) # builtins where one subclasses another + # (IOError, OSError) # IOError is an alias of OSError since Python3.3 + # but note that other cases are impractical to handle from the AST. + # We expect this is mostly useful for users who do not have the + # builtin exception hierarchy memorised, and include a 'shadowed' + # subtype without realising that it's redundant. + good = sorted(set(names), key=names.index) + if "BaseException" in good: + good = ["BaseException"] + # Remove redundant exceptions that the automatic system either handles + # poorly (usually aliases) or can't be checked (e.g. it's not an + # built-in exception). + for primary, equivalents in redundantExceptions.items(): + if primary in good: + good = [g for g in good if g not in equivalents] + + for name, other in itertools.permutations(tuple(good), 2): + if ( + self.__typesafeIssubclass( + getattr(builtins, name, type), getattr(builtins, other, ()) + ) + and name in good + ): + good.remove(name) + if good != names: + desc = ( + good[0] if len(good) == 1 else "({0})".format(", ".join(good)) + ) + self.violations.append((node, "M514", ", ".join(names), as_, desc)) + + self.generic_visit(node) def visit_UAdd(self, node): """ @@ -1533,10 +1805,10 @@ @param node reference to the node to be processed @type ast.UAdd """ - trailingNodes = list(map(type, self.__nodeWindow[-4:])) + trailingNodes = list(map(type, self.nodeWindow[-4:])) if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]: - originator = self.__nodeWindow[-4] - self.violations.append((originator, "M501")) + originator = self.nodeWindow[-4] + self.violations.append((originator, "M502")) self.generic_visit(node) @@ -1547,22 +1819,8 @@ @param node reference to the node to be processed @type ast.Call """ - validPaths = ("six", "future.utils", "builtins") - methodsDict = { - "M521": ("iterkeys", "itervalues", "iteritems", "iterlists"), - "M522": ("viewkeys", "viewvalues", "viewitems", "viewlists"), - "M523": ("next",), - } - if isinstance(node.func, ast.Attribute): - for code, methods in methodsDict.items(): - if node.func.attr in methods: - callPath = ".".join(composeCallPath(node.func.value)) - if callPath not in validPaths: - self.violations.append((node, code)) - break - else: - self.__checkForM502(node) + self.__checkForM505(node) else: with contextlib.suppress(AttributeError, IndexError): # bad super() call @@ -1575,47 +1833,30 @@ and args[0].value.id == "self" and args[0].attr == "__class__" ): - self.violations.append((node, "M509")) + self.violations.append((node, "M582")) # bad getattr and setattr if ( node.func.id in ("getattr", "hasattr") and node.args[1].s == "__call__" ): - self.violations.append((node, "M511")) + self.violations.append((node, "M504")) if ( node.func.id == "getattr" and len(node.args) == 2 - and AstUtilities.isString(node.args[1]) + and self.__isIdentifier(node.args[1]) + and iskeyword(AstUtilities.getValue(node.args[1])) ): - self.violations.append((node, "M512")) + self.violations.append((node, "M509")) elif ( node.func.id == "setattr" and len(node.args) == 3 - and AstUtilities.isString(node.args[1]) + and self.__isIdentifier(node.args[1]) + and iskeyword(AstUtilities.getValue(node.args[1])) ): - self.violations.append((node, "M513")) - - self.generic_visit(node) - - def visit_Attribute(self, node): - """ - Public method to handle attributes. - - @param node reference to the node to be processed - @type ast.Attribute - """ - callPath = list(composeCallPath(node)) - - if ".".join(callPath) == "sys.maxint": - self.violations.append((node, "M504")) - - elif len(callPath) == 2 and callPath[1] == "message": - name = callPath[0] - for elem in reversed(self.__nodeStack[:-1]): - if isinstance(elem, ast.ExceptHandler) and elem.name == name: - self.violations.append((node, "M505")) - break + self.violations.append((node, "M510")) + + self.generic_visit(node) def visit_Assign(self, node): """ @@ -1624,21 +1865,14 @@ @param node reference to the node to be processed @type ast.Assign """ - if isinstance(self.__nodeStack[-2], ast.ClassDef): - # By using 'hasattr' below we're ignoring starred arguments, slices - # and tuples for simplicity. - assignTargets = {t.id for t in node.targets if hasattr(t, "id")} - if "__metaclass__" in assignTargets: - self.violations.append((node, "M524")) - - elif len(node.targets) == 1: + if len(node.targets) == 1: target = node.targets[0] if ( isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and (target.value.id, target.attr) == ("os", "environ") ): - self.violations.append((node, "M506")) + self.violations.append((node, "M503")) self.generic_visit(node) @@ -1650,6 +1884,8 @@ @type ast.For """ self.__checkForM507(node) + self.__checkForM520(node) + self.__checkForM523(node) self.generic_visit(node) @@ -1661,6 +1897,63 @@ @type ast.AsyncFor """ self.__checkForM507(node) + self.__checkForM520(node) + self.__checkForM523(node) + + self.generic_visit(node) + + def visit_While(self, node): + """ + Public method to handle 'while' statements. + + @param node reference to the node to be processed + @type ast.While + """ + self.__checkForM523(node) + + self.generic_visit(node) + + def visit_ListComp(self, node): + """ + Public method to handle list comprehensions. + + @param node reference to the node to be processed + @type ast.ListComp + """ + self.__checkForM523(node) + + self.generic_visit(node) + + def visit_SetComp(self, node): + """ + Public method to handle set comprehensions. + + @param node reference to the node to be processed + @type ast.SetComp + """ + self.__checkForM523(node) + + self.generic_visit(node) + + def visit_DictComp(self, node): + """ + Public method to handle dictionary comprehensions. + + @param node reference to the node to be processed + @type ast.DictComp + """ + self.__checkForM523(node) + + self.generic_visit(node) + + def visit_GeneratorExp(self, node): + """ + Public method to handle generator expressions. + + @param node reference to the node to be processed + @type ast.GeneratorExp + """ + self.__checkForM523(node) self.generic_visit(node) @@ -1675,7 +1968,79 @@ AstUtilities.isNameConstant(node.test) and AstUtilities.getValue(node.test) is False ): - self.violations.append((node, "M503")) + self.violations.append((node, "M511")) + + self.generic_visit(node) + + def visit_FunctionDef(self, node): + """ + Public method to handle function definitions. + + @param node reference to the node to be processed + @type ast.FunctionDef + """ + self.__checkForM518(node) + self.__checkForM519(node) + self.__checkForM521(node) + + self.generic_visit(node) + + def visit_ClassDef(self, node): + """ + Public method to handle class definitions. + + @param node reference to the node to be processed + @type ast.ClassDef + """ + self.__checkForM518(node) + self.__checkForM521(node) + self.__checkForM524(node) + + self.generic_visit(node) + + def visit_Try(self, node): + """ + Public method to handle 'try' statements'. + + @param node reference to the node to be processed + @type ast.Try + """ + self.__checkForM512(node) + self.__checkForM525(node) + + self.generic_visit(node) + + def visit_Compare(self, node): + """ + Public method to handle comparison statements. + + @param node reference to the node to be processed + @type ast.Compare + """ + self.__checkForM515(node) + + self.generic_visit(node) + + def visit_Raise(self, node): + """ + Public method to handle 'raise' statements. + + @param node reference to the node to be processed + @type ast.Raise + """ + self.__checkForM516(node) + + self.generic_visit(node) + + def visit_With(self, node): + """ + Public method to handle 'with' statements. + + @param node reference to the node to be processed + @type ast.With + """ + self.__checkForM517(node) + self.__checkForM522(node) self.generic_visit(node) @@ -1690,9 +2055,9 @@ if isinstance(value, ast.FormattedValue): return - self.violations.append((node, "M508")) - - def __checkForM502(self, node): + self.violations.append((node, "M581")) + + def __checkForM505(self, node): """ Private method to check the use of *strip(). @@ -1712,14 +2077,14 @@ if len(s) == len(set(s)): return # no characters appear more than once - self.violations.append((node, "M502")) + self.violations.append((node, "M505")) def __checkForM507(self, node): """ Private method to check for unused loop variables. @param node reference to the node to be processed - @type ast.For + @type ast.For or ast.AsyncFor """ targets = NameFinder() targets.visit(node.target) @@ -1732,6 +2097,297 @@ n = targets.getNames()[name][0] self.violations.append((n, "M507", name)) + def __checkForM512(self, node): + """ + Private method to check for return/continue/break inside finally blocks. + + @param node reference to the node to be processed + @type ast.Try + """ + + def _loop(node, badNodeTypes): + if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)): + return + + if isinstance(node, (ast.While, ast.For)): + badNodeTypes = (ast.Return,) + + elif isinstance(node, badNodeTypes): + self.violations.append((node, "M512")) + + for child in ast.iter_child_nodes(node): + _loop(child, badNodeTypes) + + for child in node.finalbody: + _loop(child, (ast.Return, ast.Continue, ast.Break)) + + def __checkForM515(self, node): + """ + Private method to check for pointless comparisons. + + @param node reference to the node to be processed + @type ast.Compare + """ + if isinstance(self.nodeStack[-2], ast.Expr): + self.violations.append((node, "M515")) + + def __checkForM516(self, node): + """ + Private method to check for raising a literal instead of an exception. + + @param node reference to the node to be processed + @type ast.Raise + """ + if ( + AstUtilities.isNameConstant(node.exc) + or AstUtilities.isNumber(node.exc) + or AstUtilities.isString(node.exc) + ): + self.violations.append((node, "M516")) + + def __checkForM517(self, node): + """ + Private method to check for use of the evil syntax + 'with assertRaises(Exception):. + + @param node reference to the node to be processed + @type ast.With + """ + item = node.items[0] + itemContext = item.context_expr + if ( + hasattr(itemContext, "func") + and hasattr(itemContext.func, "attr") + and itemContext.func.attr == "assertRaises" + and len(itemContext.args) == 1 + and isinstance(itemContext.args[0], ast.Name) + and itemContext.args[0].id == "Exception" + and not item.optional_vars + ): + self.violations.append((node, "M517")) + + def __checkForM518(self, node): + """ + Private method to check for useless expressions. + + @param node reference to the node to be processed + @type ast.FunctionDef + """ + subnodeClasses = ( + ( + ast.Constant, + ast.List, + ast.Set, + ast.Dict, + ) + if sys.version_info >= (3, 8, 0) + else ( + ast.Num, + ast.Bytes, + ast.NameConstant, + ast.List, + ast.Set, + ast.Dict, + ) + ) + for subnode in node.body: + if not isinstance(subnode, ast.Expr): + continue + + if isinstance(subnode.value, subnodeClasses) and not AstUtilities.isString( + subnode.value + ): + self.violations.append((subnode, "M518")) + + def __checkForM519(self, node): + """ + Private method to check for use of 'functools.lru_cache' or 'functools.cache'. + + @param node reference to the node to be processed + @type ast.FunctionDef + """ + caches = { + "functools.cache", + "functools.lru_cache", + "cache", + "lru_cache", + } + + if ( + len(node.decorator_list) == 0 + or len(self.contexts) < 2 + or not isinstance(self.contexts[-2].node, ast.ClassDef) + ): + return + + # Preserve decorator order so we can get the lineno from the decorator node + # rather than the function node (this location definition changes in Python 3.8) + resolvedDecorators = ( + ".".join(self.__composeCallPath(decorator)) + for decorator in node.decorator_list + ) + for idx, decorator in enumerate(resolvedDecorators): + if decorator in {"classmethod", "staticmethod"}: + return + + if decorator in caches: + self.violations.append((node.decorator_list[idx], "M519")) + return + + def __checkForM520(self, node): + """ + Private method to check for a loop that modifies its iterable. + + @param node reference to the node to be processed + @type ast.For or ast.AsyncFor + """ + targets = NameFinder() + targets.visit(node.target) + ctrlNames = set(targets.getNames()) + + iterset = M520NameFinder() + iterset.visit(node.iter) + itersetNames = set(iterset.getNames()) + + for name in sorted(ctrlNames): + if name in itersetNames: + n = targets.getNames()[name][0] + self.violations.append((n, "M520")) + + def __checkForM521(self, node): + """ + Private method to check for use of an f-string as docstring. + + @param node reference to the node to be processed + @type ast.FunctionDef or ast.ClassDef + """ + if ( + node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.JoinedStr) + ): + self.violations.append((node.body[0].value, "M521")) + + def __checkForM522(self, node): + """ + Private method to check for use of an f-string as docstring. + + @param node reference to the node to be processed + @type ast.With + """ + item = node.items[0] + itemContext = item.context_expr + if ( + hasattr(itemContext, "func") + and hasattr(itemContext.func, "value") + and hasattr(itemContext.func.value, "id") + and itemContext.func.value.id == "contextlib" + and hasattr(itemContext.func, "attr") + and itemContext.func.attr == "suppress" + and len(itemContext.args) == 0 + ): + self.violations.append((node, "M522")) + + def __checkForM523(self, loopNode): + """ + Private method to check that functions (including lambdas) do not use loop + variables. + + @param loopNode reference to the node to be processed + @type ast.For, ast.AsyncFor, ast.While, ast.ListComp, ast.SetComp,ast.DictComp, + or ast.GeneratorExp + """ + suspiciousVariables = [] + for node in ast.walk(loopNode): + if isinstance(node, BugBearVisitor.FUNCTION_NODES): + argnames = { + arg.arg for arg in ast.walk(node.args) if isinstance(arg, ast.arg) + } + if isinstance(node, ast.Lambda): + bodyNodes = ast.walk(node.body) + else: + bodyNodes = itertools.chain.from_iterable(map(ast.walk, node.body)) + 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 suspiciousVariables: + reassignedInLoop = set(self.__getAssignedNames(loopNode)) + + for err in sorted(suspiciousVariables): + if reassignedInLoop.issuperset(err[2]): + self.violations.append((err[3], "M523", err[2])) + + def __checkForM524(self, node): + """ + Private method to check for inheritance from abstract classes in abc and lack of + any methods decorated with abstract*. + + @param node reference to the node to be processed + @type ast.ClassDef + """ # __IGNORE_WARNING_D234r__ + + def isAbcClass(value): + 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 ( + isinstance(value, ast.Attribute) + and value.attr in abcNames + and isinstance(value.value, ast.Name) + and value.value.id == "abc" + ) + + def isAbstractDecorator(expr): + return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or ( + isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract" + ) + + 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) + ): + return + + self.violations.append((node, "M524", node.name)) + + def __checkForM525(self, node): + """ + Private method to check for exceptions being handled multiple times. + + @param node reference to the node to be processed + @type ast.Try + """ + seen = [] + + for handler in node.handlers: + if isinstance(handler.type, (ast.Name, ast.Attribute)): + name = ".".join(self.__composeCallPath(handler.type)) + seen.append(name) + elif isinstance(handler.type, ast.Tuple): + # to avoid checking the same as M514, remove duplicates per except + uniques = set() + for entry in handler.type.elts: + name = ".".join(self.__composeCallPath(entry)) + uniques.add(name) + seen.extend(uniques) + + # 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)) + class NameFinder(ast.NodeVisitor): """ @@ -1761,12 +2417,15 @@ @param node AST node to be traversed @type ast.Node + @return reference to the last processed node + @rtype ast.Node """ if isinstance(node, list): for elem in node: super().visit(elem) + return node else: - super().visit(node) + return super().visit(node) def getNames(self): """ @@ -1778,6 +2437,60 @@ return self.__names +class M520NameFinder(NameFinder): + """ + Class to extract a name out of a tree of nodes ignoring names defined within the + local scope of a comprehension. + """ + + def visit_GeneratorExp(self, node): + """ + Public method to handle a generator expressions. + + @param node reference to the node to be processed + @type ast.GeneratorExp + """ + self.visit(node.generators) + + def visit_ListComp(self, node): + """ + Public method to handle a list comprehension. + + @param node reference to the node to be processed + @type TYPE + """ + self.visit(node.generators) + + def visit_DictComp(self, node): + """ + Public method to handle a dictionary comprehension. + + @param node reference to the node to be processed + @type TYPE + """ + self.visit(node.generators) + + def visit_comprehension(self, node): + """ + Public method to handle the 'for' of a comprehension. + + @param node reference to the node to be processed + @type ast.comprehension + """ + self.visit(node.iter) + + def visit_Lambda(self, node): + """ + Public method to handle a Lambda function. + + @param node reference to the node to be processed + @type ast.Lambda + """ + self.visit(node.body) + for lambdaArg in node.args.args: + self.getNames().pop(lambdaArg.arg, None) + + class ReturnVisitor(ast.NodeVisitor): """ Class implementing a node visitor to check return statements.