--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py Wed Jul 13 14:55:47 2022 +0200 @@ -27,7 +27,7 @@ def composeCallPath(node): """ Generator function to assemble the call path of a given node. - + @param node node to assemble call path for @type ast.Node @yield call path components @@ -44,88 +44,133 @@ """ Class implementing a checker for miscellaneous checks. """ + Codes = [ ## Coding line - "M101", "M102", - + "M101", + "M102", ## Copyright - "M111", "M112", - + "M111", + "M112", ## Shadowed Builtins - "M131", "M132", - + "M131", + "M132", ## Comprehensions - "M181", "M182", "M183", "M184", "M185", "M186", "M187", "M188", + "M181", + "M182", + "M183", + "M184", + "M185", + "M186", + "M187", + "M188", "M189", - "M191", "M192", "M193", "M195", "M196", "M197", "M198", - + "M191", + "M192", + "M193", + "M195", + "M196", + "M197", + "M198", ## Dictionaries with sorted keys "M201", - ## Naive datetime usage - "M301", "M302", "M303", "M304", "M305", "M306", "M307", "M308", - "M311", "M312", "M313", "M314", "M315", + "M301", + "M302", + "M303", + "M304", + "M305", + "M306", + "M307", + "M308", + "M311", + "M312", + "M313", + "M314", + "M315", "M321", - ## sys.version and sys.version_info usage - "M401", "M402", "M403", - "M411", "M412", "M413", "M414", - "M421", "M422", "M423", - + "M401", + "M402", + "M403", + "M411", + "M412", + "M413", + "M414", + "M421", + "M422", + "M423", ## Bugbear - "M501", "M502", "M503", "M504", "M505", "M506", "M507", "M508", + "M501", + "M502", + "M503", + "M504", + "M505", + "M506", + "M507", + "M508", "M509", - "M511", "M512", "M513", - "M521", "M522", "M523", "M524", - + "M511", + "M512", + "M513", + "M521", + "M522", + "M523", + "M524", ## Format Strings "M601", - "M611", "M612", "M613", - "M621", "M622", "M623", "M624", "M625", - "M631", "M632", - + "M611", + "M612", + "M613", + "M621", + "M622", + "M623", + "M624", + "M625", + "M631", + "M632", ## Logging - "M651", "M652", "M653", "M654", "M655", - + "M651", + "M652", + "M653", + "M654", + "M655", ## Future statements - "M701", "M702", - + "M701", + "M702", ## Gettext "M711", - ## print "M801", - ## one element tuple "M811", - ## Mutable Defaults - "M821", "M822", - + "M821", + "M822", ## return statements - "M831", "M832", "M833", "M834", - + "M831", + "M832", + "M833", + "M834", ## line continuation "M841", - ## commented code "M891", ] - + Formatter = Formatter() - FormatFieldRegex = re.compile(r'^((?:\s|.)*?)(\..*|\[.*\])?$') - + FormatFieldRegex = re.compile(r"^((?:\s|.)*?)(\..*|\[.*\])?$") + BuiltinsWhiteList = [ "__name__", "__doc__", "credits", ] - def __init__(self, source, filename, tree, select, ignore, expected, - repeat, args): + def __init__(self, source, filename, tree, select, ignore, expected, repeat, args): """ Constructor - + @param source source code to be checked @type list of str @param filename name of the source file @@ -144,53 +189,127 @@ @type dict """ self.__select = tuple(select) - self.__ignore = ('',) if select else tuple(ignore) + self.__ignore = ("",) if select else tuple(ignore) self.__expected = expected[:] self.__repeat = repeat self.__filename = filename self.__source = source[:] self.__tree = copy.deepcopy(tree) self.__args = args - + self.__pep3101FormatRegex = re.compile( - r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%') - + r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%' + ) + import builtins - self.__builtins = [b for b in dir(builtins) - if b not in self.BuiltinsWhiteList] - + + self.__builtins = [b for b in dir(builtins) if b not in self.BuiltinsWhiteList] + self.__eradicator = Eradicator() - + # statistics counters self.counters = {} - + # collection of detected errors self.errors = [] - + checkersWithCodes = [ (self.__checkCoding, ("M101", "M102")), (self.__checkCopyright, ("M111", "M112")), (self.__checkBuiltins, ("M131", "M132")), - (self.__checkComprehensions, ("M181", "M182", "M183", "M184", - "M185", "M186", "M187", "M188", - "M189", - "M191", "M192", "M193", - "M195", "M196", "M197", "M198")), + ( + self.__checkComprehensions, + ( + "M181", + "M182", + "M183", + "M184", + "M185", + "M186", + "M187", + "M188", + "M189", + "M191", + "M192", + "M193", + "M195", + "M196", + "M197", + "M198", + ), + ), (self.__checkDictWithSortedKeys, ("M201",)), - (self.__checkDateTime, ("M301", "M302", "M303", "M304", "M305", - "M306", "M307", "M308", "M311", "M312", - "M313", "M314", "M315", "M321")), - (self.__checkSysVersion, ("M401", "M402", "M403", - "M411", "M412", "M413", "M414", - "M421", "M422", "M423")), - (self.__checkBugBear, ("M501", "M502", "M503", "M504", "M505", - "M506", "M507", "M508", "M509", - "M511", "M512", "M513", - "M521", "M522", "M523", "M524")), + ( + self.__checkDateTime, + ( + "M301", + "M302", + "M303", + "M304", + "M305", + "M306", + "M307", + "M308", + "M311", + "M312", + "M313", + "M314", + "M315", + "M321", + ), + ), + ( + self.__checkSysVersion, + ( + "M401", + "M402", + "M403", + "M411", + "M412", + "M413", + "M414", + "M421", + "M422", + "M423", + ), + ), + ( + self.__checkBugBear, + ( + "M501", + "M502", + "M503", + "M504", + "M505", + "M506", + "M507", + "M508", + "M509", + "M511", + "M512", + "M513", + "M521", + "M522", + "M523", + "M524", + ), + ), (self.__checkPep3101, ("M601",)), - (self.__checkFormatString, ("M611", "M612", "M613", - "M621", "M622", "M623", "M624", "M625", - "M631", "M632")), + ( + self.__checkFormatString, + ( + "M611", + "M612", + "M613", + "M621", + "M622", + "M623", + "M624", + "M625", + "M631", + "M632", + ), + ), (self.__checkLogging, ("M651", "M652", "M653", "M654", "M655")), (self.__checkFuture, ("M701", "M702")), (self.__checkGettext, ("M711",)), @@ -201,24 +320,25 @@ (self.__checkLineContinuation, ("M841",)), (self.__checkCommentedCode, ("M891",)), ] - + # the eradicate whitelist commentedCodeCheckerArgs = self.__args.get( "CommentedCodeChecker", - MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]) + MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"], + ) commentedCodeCheckerWhitelist = commentedCodeCheckerArgs.get( "WhiteList", - MiscellaneousCheckerDefaultArgs[ - "CommentedCodeChecker"]["WhiteList"]) - self.__eradicator.update_whitelist(commentedCodeCheckerWhitelist, - extend_default=False) - + MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]["WhiteList"], + ) + self.__eradicator.update_whitelist( + commentedCodeCheckerWhitelist, extend_default=False + ) + self.__checkers = [] for checker, codes in checkersWithCodes: - if any(not (code and self.__ignoreCode(code)) - for code in codes): + if any(not (code and self.__ignoreCode(code)) for code in codes): self.__checkers.append(checker) - + def __ignoreCode(self, code): """ Private method to check if the message code should be ignored. @@ -228,13 +348,12 @@ @return flag indicating to ignore the given code @rtype bool """ - return (code.startswith(self.__ignore) and - not code.startswith(self.__select)) - + return code.startswith(self.__ignore) and not code.startswith(self.__select) + def __error(self, lineNumber, offset, code, *args): """ Private method to record an issue. - + @param lineNumber line number of the issue @type int @param offset position within line of the issue @@ -246,16 +365,16 @@ """ if self.__ignoreCode(code): return - + if code in self.counters: self.counters[code] += 1 else: self.counters[code] = 1 - + # Don't care about expected codes if code in self.__expected: return - + if code and (self.counters[code] == 1 or self.__repeat): # record the issue with one based line number self.errors.append( @@ -267,7 +386,7 @@ "args": args, } ) - + def run(self): """ Public method to check the given source against miscellaneous @@ -276,29 +395,28 @@ if not self.__filename: # don't do anything, if essential data is missing return - + if not self.__checkers: # don't do anything, if no codes were selected return - + for check in self.__checkers: check() - + def __getCoding(self): """ Private method to get the defined coding of the source. - + @return tuple containing the line number and the coding @rtype tuple of int and str """ for lineno, line in enumerate(self.__source[:5]): - matched = re.search(r'coding[:=]\s*([-\w_.]+)', - line, re.IGNORECASE) + matched = re.search(r"coding[:=]\s*([-\w_.]+)", line, re.IGNORECASE) if matched: return lineno, matched.group(1) else: return 0, "" - + def __checkCoding(self): """ Private method to check the presence of a coding line and valid @@ -306,55 +424,56 @@ """ if len(self.__source) == 0: return - - encodings = [e.lower().strip() - for e in self.__args.get( - "CodingChecker", - MiscellaneousCheckerDefaultArgs["CodingChecker"]) - .split(",")] + + encodings = [ + e.lower().strip() + for e in self.__args.get( + "CodingChecker", MiscellaneousCheckerDefaultArgs["CodingChecker"] + ).split(",") + ] lineno, coding = self.__getCoding() if coding: if coding.lower() not in encodings: self.__error(lineno, 0, "M102", coding) else: self.__error(0, 0, "M101") - + def __checkCopyright(self): """ Private method to check the presence of a copyright statement. """ source = "".join(self.__source) copyrightArgs = self.__args.get( - "CopyrightChecker", - MiscellaneousCheckerDefaultArgs["CopyrightChecker"]) + "CopyrightChecker", MiscellaneousCheckerDefaultArgs["CopyrightChecker"] + ) copyrightMinFileSize = copyrightArgs.get( "MinFilesize", - MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["MinFilesize"]) + MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["MinFilesize"], + ) copyrightAuthor = copyrightArgs.get( - "Author", - MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["Author"]) + "Author", MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["Author"] + ) copyrightRegexStr = ( r"Copyright\s+(\(C\)\s+)?(\d{{4}}\s+-\s+)?\d{{4}}\s+{author}" ) - + tocheck = max(1024, copyrightMinFileSize) topOfSource = source[:tocheck] if len(topOfSource) < copyrightMinFileSize: return - copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"), - re.IGNORECASE) + copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"), re.IGNORECASE) if not copyrightRe.search(topOfSource): self.__error(0, 0, "M111") return - + if copyrightAuthor: copyrightAuthorRe = re.compile( - copyrightRegexStr.format(author=copyrightAuthor), - re.IGNORECASE) + copyrightRegexStr.format(author=copyrightAuthor), re.IGNORECASE + ) if not copyrightAuthorRe.search(topOfSource): self.__error(0, 0, "M112") - + def __checkCommentedCode(self): """ Private method to check for commented code. @@ -362,15 +481,17 @@ source = "".join(self.__source) commentedCodeCheckerArgs = self.__args.get( "CommentedCodeChecker", - MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]) + MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"], + ) aggressive = commentedCodeCheckerArgs.get( "Aggressive", - MiscellaneousCheckerDefaultArgs[ - "CommentedCodeChecker"]["Aggressive"]) + MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]["Aggressive"], + ) for markedLine in self.__eradicator.commented_out_code_line_numbers( - source, aggressive=aggressive): + source, aggressive=aggressive + ): self.__error(markedLine - 1, 0, "M891") - + def __checkLineContinuation(self): """ Private method to check line continuation using backslash. @@ -387,37 +508,33 @@ content = stripped[lineno - 1] withoutComment = content[:start] + content[stop:] stripped[lineno - 1] = withoutComment.rstrip() - + # perform check with 'cleaned' source for lineIndex, line in enumerate(stripped): strippedLine = line.strip() - if (strippedLine.endswith('\\') and - not strippedLine.startswith(('assert', 'with'))): + if strippedLine.endswith("\\") and not strippedLine.startswith( + ("assert", "with") + ): self.__error(lineIndex, len(line), "M841") - + def __checkPrintStatements(self): """ Private method to check for print statements. """ for node in ast.walk(self.__tree): if ( - (isinstance(node, ast.Call) and - getattr(node.func, 'id', None) == 'print') or - (hasattr(ast, 'Print') and isinstance(node, ast.Print)) - ): + isinstance(node, ast.Call) and getattr(node.func, "id", None) == "print" + ) or (hasattr(ast, "Print") and isinstance(node, ast.Print)): self.__error(node.lineno - 1, node.col_offset, "M801") - + def __checkTuple(self): """ Private method to check for one element tuples. """ for node in ast.walk(self.__tree): - if ( - isinstance(node, ast.Tuple) and - len(node.elts) == 1 - ): + if isinstance(node, ast.Tuple) and len(node.elts) == 1: self.__error(node.lineno - 1, node.col_offset, "M811") - + def __checkFuture(self): """ Private method to check the __future__ imports. @@ -425,27 +542,24 @@ expectedImports = { i.strip() for i in self.__args.get("FutureChecker", "").split(",") - if bool(i.strip())} + if bool(i.strip()) + } if len(expectedImports) == 0: # nothing to check for; disabling the check return - + imports = set() node = None hasCode = False - + for node in ast.walk(self.__tree): - if (isinstance(node, ast.ImportFrom) and - node.module == '__future__'): + if isinstance(node, ast.ImportFrom) and node.module == "__future__": imports |= {name.name for name in node.names} elif isinstance(node, ast.Expr): if not AstUtilities.isString(node.value): hasCode = True break - elif not ( - AstUtilities.isString(node) or - isinstance(node, ast.Module) - ): + elif not (AstUtilities.isString(node) or isinstance(node, ast.Module)): hasCode = True break @@ -454,12 +568,18 @@ if imports < expectedImports: if imports: - self.__error(node.lineno - 1, node.col_offset, "M701", - ", ".join(expectedImports), ", ".join(imports)) + self.__error( + node.lineno - 1, + node.col_offset, + "M701", + ", ".join(expectedImports), + ", ".join(imports), + ) else: - self.__error(node.lineno - 1, node.col_offset, "M702", - ", ".join(expectedImports)) - + self.__error( + node.lineno - 1, node.col_offset, "M702", ", ".join(expectedImports) + ) + def __checkPep3101(self): """ Private method to check for old style string formatting. @@ -468,9 +588,9 @@ match = self.__pep3101FormatRegex.search(line) if match: lineLen = len(line) - pos = line.find('%') + pos = line.find("%") formatPos = pos - formatter = '%' + formatter = "%" if line[pos + 1] == "(": pos = line.find(")", pos) c = line[pos] @@ -482,7 +602,7 @@ if c in "diouxXeEfFgGcrs": formatter += c self.__error(lineno, formatPos, "M601", formatter) - + def __checkFormatString(self): """ Private method to check string format strings. @@ -491,7 +611,7 @@ if not coding: # default to utf-8 coding = "utf-8" - + visitor = TextVisitor() visitor.visit(self.__tree) for node in visitor.nodes: @@ -510,10 +630,10 @@ self.__error(node.lineno - 1, node.col_offset, "M612") else: self.__error(node.lineno - 1, node.col_offset, "M613") - + if node in visitor.calls: call, strArgs = visitor.calls[node] - + numbers = set() names = set() # Determine which fields require a keyword and which an arg @@ -528,20 +648,21 @@ numbers.add(number) else: names.add(fieldMatch.group(1)) - + keywords = {keyword.arg for keyword in call.keywords} numArgs = len(call.args) if strArgs: numArgs -= 1 hasKwArgs = any(kw.arg is None for kw in call.keywords) - hasStarArgs = sum(1 for arg in call.args - if isinstance(arg, ast.Starred)) - + hasStarArgs = sum( + 1 for arg in call.args if isinstance(arg, ast.Starred) + ) + if hasKwArgs: keywords.discard(None) if hasStarArgs: numArgs -= 1 - + # if starargs or kwargs is not None, it can't count the # parameters but at least check if the args are used if hasKwArgs and not names: @@ -550,36 +671,34 @@ if hasStarArgs and not numbers: # No numbers but args self.__error(call.lineno - 1, call.col_offset, "M624") - + if not hasKwArgs and not hasStarArgs: # can actually verify numbers and names for number in sorted(numbers): if number >= numArgs: - self.__error(call.lineno - 1, call.col_offset, - "M621", number) - + self.__error( + call.lineno - 1, call.col_offset, "M621", number + ) + for name in sorted(names): if name not in keywords: - self.__error(call.lineno - 1, call.col_offset, - "M622", name) - + self.__error(call.lineno - 1, call.col_offset, "M622", name) + for arg in range(numArgs): if arg not in numbers: - self.__error(call.lineno - 1, call.col_offset, "M631", - arg) - + self.__error(call.lineno - 1, call.col_offset, "M631", arg) + for keyword in keywords: if keyword not in names: - self.__error(call.lineno - 1, call.col_offset, "M632", - keyword) - + self.__error(call.lineno - 1, call.col_offset, "M632", keyword) + if implicit and explicit: self.__error(call.lineno - 1, call.col_offset, "M625") - + def __getFields(self, string): """ Private method to extract the format field information. - + @param string format string to be parsed @type str @return format field information as a tuple with fields, implicit @@ -592,21 +711,23 @@ explicit = False try: for _literal, field, spec, conv in self.Formatter.parse(string): - if field is not None and (conv is None or conv in 'rsa'): + if field is not None and (conv is None or conv in "rsa"): if not field: field = str(next(cnt)) implicit = True else: explicit = True fields.add(field) - fields.update(parsedSpec[1] - for parsedSpec in self.Formatter.parse(spec) - if parsedSpec[1] is not None) + fields.update( + parsedSpec[1] + for parsedSpec in self.Formatter.parse(spec) + if parsedSpec[1] is not None + ) except ValueError: return set(), False, False else: return fields, implicit, explicit - + def __checkBuiltins(self): """ Private method to check, if built-ins are shadowed. @@ -614,67 +735,64 @@ functionDefs = [ast.FunctionDef] with contextlib.suppress(AttributeError): functionDefs.append(ast.AsyncFunctionDef) - + ignoreBuiltinAssignments = self.__args.get( - "BuiltinsChecker", - MiscellaneousCheckerDefaultArgs["BuiltinsChecker"]) - + "BuiltinsChecker", MiscellaneousCheckerDefaultArgs["BuiltinsChecker"] + ) + for node in ast.walk(self.__tree): if isinstance(node, ast.Assign): # assign statement for element in node.targets: - if ( - isinstance(element, ast.Name) and - element.id in self.__builtins - ): + if isinstance(element, ast.Name) and element.id in self.__builtins: value = node.value if ( - isinstance(value, ast.Name) and - element.id in ignoreBuiltinAssignments and - value.id in ignoreBuiltinAssignments[element.id] + isinstance(value, ast.Name) + and element.id in ignoreBuiltinAssignments + and value.id in ignoreBuiltinAssignments[element.id] ): # ignore compatibility assignments continue - self.__error(element.lineno - 1, element.col_offset, - "M131", element.id) + self.__error( + element.lineno - 1, element.col_offset, "M131", element.id + ) elif isinstance(element, (ast.Tuple, ast.List)): for tupleElement in element.elts: if ( - isinstance(tupleElement, ast.Name) and - tupleElement.id in self.__builtins + isinstance(tupleElement, ast.Name) + and tupleElement.id in self.__builtins ): - self.__error(tupleElement.lineno - 1, - tupleElement.col_offset, - "M131", tupleElement.id) + self.__error( + tupleElement.lineno - 1, + tupleElement.col_offset, + "M131", + tupleElement.id, + ) elif isinstance(node, ast.For): # for loop target = node.target - if ( - isinstance(target, ast.Name) and - target.id in self.__builtins - ): - self.__error(target.lineno - 1, target.col_offset, - "M131", target.id) + if isinstance(target, ast.Name) and target.id in self.__builtins: + self.__error( + target.lineno - 1, target.col_offset, "M131", target.id + ) elif isinstance(target, (ast.Tuple, ast.List)): for element in target.elts: if ( - isinstance(element, ast.Name) and - element.id in self.__builtins + isinstance(element, ast.Name) + and element.id in self.__builtins ): - self.__error(element.lineno - 1, - element.col_offset, - "M131", element.id) - elif any(isinstance(node, functionDef) - for functionDef in functionDefs): + self.__error( + element.lineno - 1, + element.col_offset, + "M131", + element.id, + ) + elif any(isinstance(node, functionDef) for functionDef in functionDefs): # (asynchronous) function definition for arg in node.args.args: - if ( - isinstance(arg, ast.arg) and - arg.arg in self.__builtins - ): - self.__error(arg.lineno - 1, arg.col_offset, - "M132", arg.arg) - + if isinstance(arg, ast.arg) and arg.arg in self.__builtins: + self.__error(arg.lineno - 1, arg.col_offset, "M132", arg.arg) + def __checkComprehensions(self): """ Private method to check some comprehension related things. @@ -683,11 +801,11 @@ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): nArgs = len(node.args) nKwArgs = len(node.keywords) - + if ( - nArgs == 1 and - isinstance(node.args[0], ast.GeneratorExp) and - node.func.id in ('list', 'set') + nArgs == 1 + and isinstance(node.args[0], ast.GeneratorExp) + and node.func.id in ("list", "set") ): errorCode = { "list": "M181", @@ -696,48 +814,51 @@ self.__error(node.lineno - 1, node.col_offset, errorCode) elif ( - nArgs == 1 and - isinstance(node.args[0], - (ast.GeneratorExp, ast.ListComp)) and - isinstance(node.args[0].elt, ast.Tuple) and - len(node.args[0].elt.elts) == 2 and - node.func.id == "dict" + nArgs == 1 + and isinstance(node.args[0], (ast.GeneratorExp, ast.ListComp)) + and isinstance(node.args[0].elt, ast.Tuple) + and len(node.args[0].elt.elts) == 2 + and node.func.id == "dict" ): if isinstance(node.args[0], ast.GeneratorExp): errorCode = "M183" else: errorCode = "M185" self.__error(node.lineno - 1, node.col_offset, errorCode) - + elif ( - nArgs == 1 and - isinstance(node.args[0], ast.ListComp) and - node.func.id in ('list', 'set') + nArgs == 1 + and isinstance(node.args[0], ast.ListComp) + and node.func.id in ("list", "set") ): errorCode = { - 'list': 'M195', - 'set': 'M184', + "list": "M195", + "set": "M184", }[node.func.id] self.__error(node.lineno - 1, node.col_offset, errorCode) - + elif nArgs == 1 and ( - isinstance(node.args[0], ast.Tuple) and - node.func.id == "tuple" or - isinstance(node.args[0], ast.List) and - node.func.id == "list" + isinstance(node.args[0], ast.Tuple) + and node.func.id == "tuple" + or isinstance(node.args[0], ast.List) + and node.func.id == "list" ): errorCode = { - 'tuple': 'M197', - 'list': 'M198', + "tuple": "M197", + "list": "M198", }[node.func.id] - self.__error(node.lineno - 1, node.col_offset, errorCode, - type(node.args[0]).__name__.lower(), - node.func.id) - + self.__error( + node.lineno - 1, + node.col_offset, + errorCode, + type(node.args[0]).__name__.lower(), + node.func.id, + ) + elif ( - nArgs == 1 and - isinstance(node.args[0], (ast.Tuple, ast.List)) and - node.func.id in ("tuple", "list", "set", "dict") + nArgs == 1 + and isinstance(node.args[0], (ast.Tuple, ast.List)) + and node.func.id in ("tuple", "list", "set", "dict") ): errorCode = { "tuple": "M192", @@ -745,29 +866,30 @@ "set": "M191", "dict": "M191", }[node.func.id] - self.__error(node.lineno - 1, node.col_offset, errorCode, - type(node.args[0]).__name__.lower(), - node.func.id) - + self.__error( + node.lineno - 1, + node.col_offset, + errorCode, + type(node.args[0]).__name__.lower(), + node.func.id, + ) + elif ( - nArgs == 0 and - not any(isinstance(a, ast.Starred) for a in node.args) and - not any(k.arg is None for k in node.keywords) and - node.func.id == "dict" + nArgs == 0 + and not any(isinstance(a, ast.Starred) for a in node.args) + and not any(k.arg is None for k in node.keywords) + and node.func.id == "dict" ) or ( - nArgs == 0 and - nKwArgs == 0 and - node.func.id in ("tuple", "list") + nArgs == 0 and nKwArgs == 0 and node.func.id in ("tuple", "list") ): - self.__error(node.lineno - 1, node.col_offset, "M186", - node.func.id) - + self.__error(node.lineno - 1, node.col_offset, "M186", node.func.id) + elif ( - node.func.id in {"list", "reversed"} and - nArgs > 0 and - isinstance(node.args[0], ast.Call) and - isinstance(node.args[0].func, ast.Name) and - node.args[0].func.id == "sorted" + node.func.id in {"list", "reversed"} + and nArgs > 0 + and isinstance(node.args[0], ast.Call) + and isinstance(node.args[0].func, ast.Name) + and node.args[0].func.id == "sorted" ): if node.func.id == "reversed": reverseFlagValue = False @@ -783,64 +905,78 @@ reverseFlagValue = None if reverseFlagValue is None: - self.__error(node.lineno - 1, node.col_offset, - "M187a", node.func.id, - node.args[0].func.id) + self.__error( + node.lineno - 1, + node.col_offset, + "M187a", + node.func.id, + node.args[0].func.id, + ) else: - self.__error(node.lineno - 1, node.col_offset, - "M187b", node.func.id, - node.args[0].func.id, - not reverseFlagValue) + self.__error( + node.lineno - 1, + node.col_offset, + "M187b", + node.func.id, + node.args[0].func.id, + not reverseFlagValue, + ) else: - self.__error(node.lineno - 1, node.col_offset, - "M187c", node.func.id, - node.args[0].func.id) - + self.__error( + node.lineno - 1, + node.col_offset, + "M187c", + node.func.id, + node.args[0].func.id, + ) + elif ( - nArgs > 0 and - isinstance(node.args[0], ast.Call) and - isinstance(node.args[0].func, ast.Name) and - ( + nArgs > 0 + and isinstance(node.args[0], ast.Call) + and isinstance(node.args[0].func, ast.Name) + and ( ( - node.func.id in {"set", "sorted"} and - node.args[0].func.id in { - "list", "reversed", "sorted", "tuple"} - ) or ( - node.func.id in {"list", "tuple"} and - node.args[0].func.id in {"list", "tuple"} - ) or ( - node.func.id == "set" and - node.args[0].func.id == "set" + node.func.id in {"set", "sorted"} + and node.args[0].func.id + in {"list", "reversed", "sorted", "tuple"} ) + or ( + node.func.id in {"list", "tuple"} + and node.args[0].func.id in {"list", "tuple"} + ) + or (node.func.id == "set" and node.args[0].func.id == "set") ) ): - self.__error(node.lineno - 1, node.col_offset, "M188", - node.args[0].func.id, node.func.id) - + self.__error( + node.lineno - 1, + node.col_offset, + "M188", + node.args[0].func.id, + node.func.id, + ) + elif ( - node.func.id in {"reversed", "set", "sorted"} and - nArgs > 0 and - isinstance(node.args[0], ast.Subscript) and - isinstance(node.args[0].slice, ast.Slice) and - node.args[0].slice.lower is None and - node.args[0].slice.upper is None and - isinstance(node.args[0].slice.step, ast.UnaryOp) and - isinstance(node.args[0].slice.step.op, ast.USub) and - isinstance(node.args[0].slice.step.operand, ast.Num) and - node.args[0].slice.step.operand.n == 1 + node.func.id in {"reversed", "set", "sorted"} + and nArgs > 0 + and isinstance(node.args[0], ast.Subscript) + and isinstance(node.args[0].slice, ast.Slice) + and node.args[0].slice.lower is None + and node.args[0].slice.upper is None + and isinstance(node.args[0].slice.step, ast.UnaryOp) + and isinstance(node.args[0].slice.step.op, ast.USub) + and isinstance(node.args[0].slice.step.operand, ast.Num) + and node.args[0].slice.step.operand.n == 1 ): - self.__error(node.lineno - 1, node.col_offset, - "M189", node.func.id) - - elif ( - isinstance(node, (ast.ListComp, ast.SetComp)) and ( - len(node.generators) == 1 and - not node.generators[0].ifs and - not node.generators[0].is_async and ( - isinstance(node.elt, ast.Name) and - isinstance(node.generators[0].target, ast.Name) and - node.elt.id == node.generators[0].target.id - ) + self.__error(node.lineno - 1, node.col_offset, "M189", node.func.id) + + elif isinstance(node, (ast.ListComp, ast.SetComp)) and ( + len(node.generators) == 1 + and not node.generators[0].ifs + and not node.generators[0].is_async + and ( + isinstance(node.elt, ast.Name) + and isinstance(node.generators[0].target, ast.Name) + and node.elt.id == node.generators[0].target.id ) ): compType = { @@ -848,10 +984,9 @@ ast.ListComp: "list", ast.SetComp: "set", }[node.__class__] - - self.__error(node.lineno - 1, node.col_offset, - "M196", compType) - + + self.__error(node.lineno - 1, node.col_offset, "M196", compType) + def __checkMutableDefault(self): """ Private method to check for use of mutable types as default arguments. @@ -882,52 +1017,58 @@ functionDefs = [ast.FunctionDef] with contextlib.suppress(AttributeError): functionDefs.append(ast.AsyncFunctionDef) - + for node in ast.walk(self.__tree): - if any(isinstance(node, functionDef) - for functionDef in functionDefs): + if any(isinstance(node, functionDef) for functionDef in functionDefs): defaults = node.args.defaults[:] with contextlib.suppress(AttributeError): defaults += node.args.kw_defaults[:] for default in defaults: - if any(isinstance(default, mutableType) - for mutableType in mutableTypes): + if any( + isinstance(default, mutableType) for mutableType in mutableTypes + ): typeName = type(default).__name__ if isinstance(default, ast.Call): - callPath = '.'.join(composeCallPath(default.func)) + callPath = ".".join(composeCallPath(default.func)) if callPath in mutableCalls: - self.__error(default.lineno - 1, - default.col_offset, - "M823", callPath + "()") + self.__error( + default.lineno - 1, + default.col_offset, + "M823", + callPath + "()", + ) elif callPath not in immutableCalls: - self.__error(default.lineno - 1, - default.col_offset, - "M822", typeName) + self.__error( + default.lineno - 1, + default.col_offset, + "M822", + typeName, + ) else: - self.__error(default.lineno - 1, - default.col_offset, - "M821", typeName) - + self.__error( + default.lineno - 1, default.col_offset, "M821", typeName + ) + def __dictShouldBeChecked(self, node): """ Private function to test, if the node should be checked. - + @param node reference to the AST node @return flag indicating to check the node @rtype bool """ if not all(AstUtilities.isString(key) for key in node.keys): return False - + if ( - "__IGNORE_WARNING__" in self.__source[node.lineno - 1] or - "__IGNORE_WARNING_M201__" in self.__source[node.lineno - 1] + "__IGNORE_WARNING__" in self.__source[node.lineno - 1] + or "__IGNORE_WARNING_M201__" in self.__source[node.lineno - 1] ): return False - + lineNumbers = [key.lineno for key in node.keys] return len(lineNumbers) == len(set(lineNumbers)) - + def __checkDictWithSortedKeys(self): """ Private method to check, if dictionary keys appear in sorted order. @@ -936,9 +1077,10 @@ if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node): for key1, key2 in zip(node.keys, node.keys[1:]): if key2.s < key1.s: - self.__error(key2.lineno - 1, key2.col_offset, - "M201", key2.s, key1.s) - + self.__error( + key2.lineno - 1, key2.col_offset, "M201", key2.s, key1.s + ) + def __checkLogging(self): """ Private method to check logging statements. @@ -947,19 +1089,19 @@ visitor.visit(self.__tree) for node, reason in visitor.violations: self.__error(node.lineno - 1, node.col_offset, reason) - + def __checkGettext(self): """ Private method to check the 'gettext' import statement. """ for node in ast.walk(self.__tree): - if ( - isinstance(node, ast.ImportFrom) and - any(name.asname == '_' for name in node.names) + if isinstance(node, ast.ImportFrom) and any( + name.asname == "_" for name in node.names ): - self.__error(node.lineno - 1, node.col_offset, "M711", - node.names[0].name) - + self.__error( + node.lineno - 1, node.col_offset, "M711", node.names[0].name + ) + def __checkBugBear(self): """ Private method for bugbear checks. @@ -971,7 +1113,7 @@ reason = violation[1] params = violation[2:] self.__error(node.lineno - 1, node.col_offset, reason, *params) - + def __checkReturn(self): """ Private method to check return statements. @@ -982,7 +1124,7 @@ node = violation[0] reason = violation[1] self.__error(node.lineno - 1, node.col_offset, reason) - + def __checkDateTime(self): """ Private method to check use of naive datetime functions. @@ -993,7 +1135,7 @@ for node in ast.walk(tree): for childNode in ast.iter_child_nodes(node): childNode._dtCheckerParent = node - + # step 2: perform checks and report issues visitor = DateTimeVisitor() visitor.visit(tree) @@ -1001,7 +1143,7 @@ node = violation[0] reason = violation[1] self.__error(node.lineno - 1, node.col_offset, reason) - + def __checkSysVersion(self): """ Private method to check the use of sys.version and sys.version_info. @@ -1021,8 +1163,9 @@ It tries to detect docstrings as string of the first expression of each module, class or function. """ + # modelled after the string format flake8 extension - + def __init__(self): """ Constructor @@ -1034,18 +1177,18 @@ def __addNode(self, node): """ Private method to add a node to our list of nodes. - + @param node reference to the node to add @type ast.AST """ - if not hasattr(node, 'is_docstring'): + if not hasattr(node, "is_docstring"): node.is_docstring = False self.nodes.append(node) def visit_Str(self, node): """ Public method to record a string node. - + @param node reference to the string node @type ast.Str """ @@ -1054,16 +1197,16 @@ def visit_Bytes(self, node): """ Public method to record a bytes node. - + @param node reference to the bytes node @type ast.Bytes """ self.__addNode(node) - + def visit_Constant(self, node): """ Public method to handle constant nodes. - + @param node reference to the bytes node @type ast.Constant """ @@ -1078,7 +1221,7 @@ def __visitDefinition(self, node): """ Private method handling class and function definitions. - + @param node reference to the node to handle @type ast.FunctionDef, ast.AsyncFunctionDef or ast.ClassDef """ @@ -1096,14 +1239,14 @@ If the first node is an expression which contains a string or bytes it marks that as a docstring. - + @param node reference to the node to traverse @type ast.AST """ if ( - node.body and - isinstance(node.body[0], ast.Expr) and - AstUtilities.isBaseString(node.body[0].value) + node.body + and isinstance(node.body[0], ast.Expr) + and AstUtilities.isBaseString(node.body[0].value) ): node.body[0].value.is_docstring = True @@ -1113,7 +1256,7 @@ def visit_Module(self, node): """ Public method to handle a module. - + @param node reference to the node to handle @type ast.Module """ @@ -1122,7 +1265,7 @@ def visit_ClassDef(self, node): """ Public method to handle a class definition. - + @param node reference to the node to handle @type ast.ClassDef """ @@ -1132,7 +1275,7 @@ def visit_FunctionDef(self, node): """ Public method to handle a function definition. - + @param node reference to the node to handle @type ast.FunctionDef """ @@ -1142,7 +1285,7 @@ def visit_AsyncFunctionDef(self, node): """ Public method to handle an asynchronous function definition. - + @param node reference to the node to handle @type ast.AsyncFunctionDef """ @@ -1152,21 +1295,18 @@ def visit_Call(self, node): """ Public method to handle a function call. - + @param node reference to the node to handle @type ast.Call """ - if ( - isinstance(node.func, ast.Attribute) and - node.func.attr == 'format' - ): + if isinstance(node.func, ast.Attribute) and node.func.attr == "format": if AstUtilities.isBaseString(node.func.value): self.calls[node.func.value] = (node, False) elif ( - isinstance(node.func.value, ast.Name) and - node.func.value.id == 'str' and - node.args and - AstUtilities.isBaseString(node.args[0]) + isinstance(node.func.value, ast.Name) + and node.func.value.id == "str" + and node.args + and AstUtilities.isBaseString(node.args[0]) ): self.calls[node.args[0]] = (node, True) super().generic_visit(node) @@ -1176,6 +1316,7 @@ """ Class implementing a node visitor to check logging statements. """ + LoggingLevels = { "debug", "critical", @@ -1184,13 +1325,13 @@ "warn", "warning", } - + def __init__(self): """ Constructor """ super().__init__() - + self.__currentLoggingCall = None self.__currentLoggingArgument = None self.__currentLoggingLevel = None @@ -1200,7 +1341,7 @@ def __withinLoggingStatement(self): """ Private method to check, if we are inside a logging statement. - + @return flag indicating we are inside a logging statement @rtype bool """ @@ -1209,7 +1350,7 @@ def __withinLoggingArgument(self): """ Private method to check, if we are inside a logging argument. - + @return flag indicating we are inside a logging argument @rtype bool """ @@ -1218,21 +1359,21 @@ def __withinExtraKeyword(self, node): """ Private method to check, if we are inside the extra keyword. - + @param node reference to the node to be checked @type ast.keyword @return flag indicating we are inside the extra keyword @rtype bool """ return ( - self.__currentExtraKeyword is not None and - self.__currentExtraKeyword != node + self.__currentExtraKeyword is not None + and self.__currentExtraKeyword != node ) - + def __detectLoggingLevel(self, node): """ Private method to decide whether an AST Call is a logging call. - + @param node reference to the node to be processed @type ast.Call @return logging level @@ -1241,10 +1382,10 @@ with contextlib.suppress(AttributeError): if node.func.value.id == "warnings": return None - + if node.func.attr in LoggingVisitor.LoggingLevels: return node.func.attr - + return None def __isFormatCall(self, node): @@ -1260,66 +1401,62 @@ return node.func.attr == "format" except AttributeError: return False - + def visit_Call(self, node): """ Public method to handle a function call. Every logging statement and string format is expected to be a function call. - + @param node reference to the node to be processed @type ast.Call """ # we are in a logging statement if ( - self.__withinLoggingStatement() and - self.__withinLoggingArgument() and - self.__isFormatCall(node) + self.__withinLoggingStatement() + and self.__withinLoggingArgument() + and self.__isFormatCall(node) ): self.violations.append((node, "M651")) super().generic_visit(node) return - + loggingLevel = self.__detectLoggingLevel(node) - + if loggingLevel and self.__currentLoggingLevel is None: self.__currentLoggingLevel = loggingLevel - + # we are in some other statement if loggingLevel is None: super().generic_visit(node) return - + # we are entering a new logging statement self.__currentLoggingCall = node - + if loggingLevel == "warn": self.violations.append((node, "M655")) - + for index, child in enumerate(ast.iter_child_nodes(node)): if index == 1: self.__currentLoggingArgument = child - if ( - index > 1 and - isinstance(child, ast.keyword) and - child.arg == "extra" - ): + if index > 1 and isinstance(child, ast.keyword) and child.arg == "extra": self.__currentExtraKeyword = child - + super().visit(child) - + self.__currentLoggingArgument = None self.__currentExtraKeyword = None - + self.__currentLoggingCall = None self.__currentLoggingLevel = None - + def visit_BinOp(self, node): """ Public method to handle binary operations while processing the first logging argument. - + @param node reference to the node to be processed @type ast.BinOp """ @@ -1327,27 +1464,27 @@ # handle percent format if isinstance(node.op, ast.Mod): self.violations.append((node, "M652")) - + # handle string concat if isinstance(node.op, ast.Add): self.violations.append((node, "M653")) - + super().generic_visit(node) - + def visit_JoinedStr(self, node): """ Public method to handle f-string arguments. - + @param node reference to the node to be processed @type ast.JoinedStr """ if ( - self.__withinLoggingStatement() and - any(isinstance(i, ast.FormattedValue) for i in node.values) and - self.__withinLoggingArgument() + self.__withinLoggingStatement() + and any(isinstance(i, ast.FormattedValue) for i in node.values) + and self.__withinLoggingArgument() ): self.violations.append((node, "M654")) - + super().generic_visit(node) @@ -1355,42 +1492,43 @@ """ Class implementing a node visitor to check for various topics. """ + # # This class was implemented along the BugBear flake8 extension (v 19.3.0). # Original: Copyright (c) 2016 Łukasz Langa # - + NodeWindowSize = 4 - + def __init__(self): """ Constructor """ super().__init__() - + self.__nodeStack = [] self.__nodeWindow = [] self.violations = [] - + def visit(self, node): """ Public method to traverse a given AST node. - + @param node AST node to be traversed @type ast.Node """ self.__nodeStack.append(node) self.__nodeWindow.append(node) - self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize:] - + self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize :] + super().visit(node) - + self.__nodeStack.pop() - + def visit_UAdd(self, node): """ Public method to handle unary additions. - + @param node reference to the node to be processed @type ast.UAdd """ @@ -1398,13 +1536,13 @@ if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]: originator = self.__nodeWindow[-4] self.violations.append((originator, "M501")) - + self.generic_visit(node) - + def visit_Call(self, node): """ Public method to handle a function call. - + @param node reference to the node to be processed @type ast.Call """ @@ -1414,7 +1552,7 @@ "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: @@ -1430,165 +1568,161 @@ if isinstance(node.func, ast.Name) and node.func.id == "super": args = node.args if ( - len(args) == 2 and - isinstance(args[0], ast.Attribute) and - isinstance(args[0].value, ast.Name) and - args[0].value.id == 'self' and - args[0].attr == '__class__' + len(args) == 2 + and isinstance(args[0], ast.Attribute) + and isinstance(args[0].value, ast.Name) + and args[0].value.id == "self" + and args[0].attr == "__class__" ): self.violations.append((node, "M509")) - + # bad getattr and setattr if ( - node.func.id in ("getattr", "hasattr") and - node.args[1].s == "__call__" + node.func.id in ("getattr", "hasattr") + and node.args[1].s == "__call__" ): self.violations.append((node, "M511")) if ( - node.func.id == "getattr" and - len(node.args) == 2 and - AstUtilities.isString(node.args[1]) + node.func.id == "getattr" + and len(node.args) == 2 + and AstUtilities.isString(node.args[1]) ): self.violations.append((node, "M512")) elif ( - node.func.id == "setattr" and - len(node.args) == 3 and - AstUtilities.isString(node.args[1]) + node.func.id == "setattr" + and len(node.args) == 3 + and AstUtilities.isString(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': + + if ".".join(callPath) == "sys.maxint": self.violations.append((node, "M504")) - - elif ( - len(callPath) == 2 and - callPath[1] == 'message' - ): + + 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 - + def visit_Assign(self, node): """ Public method to handle assignments. - + @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: + 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: target = node.targets[0] if ( - isinstance(target, ast.Attribute) and - isinstance(target.value, ast.Name) and - (target.value.id, target.attr) == ('os', 'environ') + isinstance(target, ast.Attribute) + and isinstance(target.value, ast.Name) + and (target.value.id, target.attr) == ("os", "environ") ): self.violations.append((node, "M506")) - + self.generic_visit(node) - + def visit_For(self, node): """ Public method to handle 'for' statements. - + @param node reference to the node to be processed @type ast.For """ self.__checkForM507(node) - + self.generic_visit(node) - + def visit_AsyncFor(self, node): """ Public method to handle 'for' statements. - + @param node reference to the node to be processed @type ast.AsyncFor """ self.__checkForM507(node) - + self.generic_visit(node) - + def visit_Assert(self, node): """ Public method to handle 'assert' statements. - + @param node reference to the node to be processed @type ast.Assert """ if ( - AstUtilities.isNameConstant(node.test) and - AstUtilities.getValue(node.test) is False + AstUtilities.isNameConstant(node.test) + and AstUtilities.getValue(node.test) is False ): self.violations.append((node, "M503")) - + self.generic_visit(node) - + def visit_JoinedStr(self, node): """ Public method to handle f-string arguments. - + @param node reference to the node to be processed @type ast.JoinedStr """ for value in node.values: if isinstance(value, ast.FormattedValue): return - + self.violations.append((node, "M508")) - + def __checkForM502(self, node): """ Private method to check the use of *strip(). - + @param node reference to the node to be processed @type ast.Call """ if node.func.attr not in ("lstrip", "rstrip", "strip"): - return # method name doesn't match - + return # method name doesn't match + if len(node.args) != 1 or not AstUtilities.isString(node.args[0]): - return # used arguments don't match the builtin strip - + return # used arguments don't match the builtin strip + s = AstUtilities.getValue(node.args[0]) if len(s) == 1: - return # stripping just one character - + return # stripping just one character + if len(s) == len(set(s)): - return # no characters appear more than once + return # no characters appear more than once self.violations.append((node, "M502")) - + def __checkForM507(self, node): """ Private method to check for unused loop variables. - + @param node reference to the node to be processed @type ast.For """ targets = NameFinder() targets.visit(node.target) - ctrlNames = set(filter(lambda s: not s.startswith('_'), - targets.getNames())) + ctrlNames = set(filter(lambda s: not s.startswith("_"), targets.getNames())) body = NameFinder() for expr in node.body: body.visit(expr) @@ -1602,18 +1736,19 @@ """ Class to extract a name out of a tree of nodes. """ + def __init__(self): """ Constructor """ super().__init__() - + self.__names = {} def visit_Name(self, node): """ Public method to handle 'Name' nodes. - + @param node reference to the node to be processed @type ast.Name """ @@ -1622,7 +1757,7 @@ def visit(self, node): """ Public method to traverse a given AST node. - + @param node AST node to be traversed @type ast.Node """ @@ -1631,11 +1766,11 @@ super().visit(elem) else: super().visit(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 """ @@ -1646,140 +1781,143 @@ """ Class implementing a node visitor to check return statements. """ - Assigns = 'assigns' - Refs = 'refs' - Returns = 'returns' - + + Assigns = "assigns" + Refs = "refs" + Returns = "returns" + def __init__(self): """ Constructor """ super().__init__() - + self.__stack = [] self.violations = [] self.__loopCount = 0 - + @property def assigns(self): """ Public method to get the Assign nodes. - + @return dictionary containing the node name as key and line number as value @rtype dict """ return self.__stack[-1][ReturnVisitor.Assigns] - + @property def refs(self): """ Public method to get the References nodes. - + @return dictionary containing the node name as key and line number as value @rtype dict """ return self.__stack[-1][ReturnVisitor.Refs] - + @property def returns(self): """ Public method to get the Return nodes. - + @return dictionary containing the node name as key and line number as value @rtype dict """ return self.__stack[-1][ReturnVisitor.Returns] - + def visit_For(self, node): """ Public method to handle a for loop. - + @param node reference to the for node to handle @type ast.For """ self.__visitLoop(node) - + def visit_AsyncFor(self, node): """ Public method to handle an async for loop. - + @param node reference to the async for node to handle @type ast.AsyncFor """ self.__visitLoop(node) - + def visit_While(self, node): """ Public method to handle a while loop. - + @param node reference to the while node to handle @type ast.While """ self.__visitLoop(node) - + def __visitLoop(self, node): """ Private method to handle loop nodes. - + @param node reference to the loop node to handle @type ast.For, ast.AsyncFor or ast.While """ self.__loopCount += 1 self.generic_visit(node) self.__loopCount -= 1 - + def __visitWithStack(self, node): """ Private method to traverse a given function node using a stack. - + @param node AST node to be traversed @type ast.FunctionDef or ast.AsyncFunctionDef """ - self.__stack.append({ - ReturnVisitor.Assigns: defaultdict(list), - ReturnVisitor.Refs: defaultdict(list), - ReturnVisitor.Returns: [] - }) - + self.__stack.append( + { + ReturnVisitor.Assigns: defaultdict(list), + ReturnVisitor.Refs: defaultdict(list), + ReturnVisitor.Returns: [], + } + ) + self.generic_visit(node) self.__checkFunction(node) self.__stack.pop() - + def visit_FunctionDef(self, node): """ Public method to handle a function definition. - + @param node reference to the node to handle @type ast.FunctionDef """ self.__visitWithStack(node) - + def visit_AsyncFunctionDef(self, node): """ Public method to handle a function definition. - + @param node reference to the node to handle @type ast.AsyncFunctionDef """ self.__visitWithStack(node) - + def visit_Return(self, node): """ Public method to handle a return node. - + @param node reference to the node to handle @type ast.Return """ self.returns.append(node) self.generic_visit(node) - + def visit_Assign(self, node): """ Public method to handle an assign node. - + @param node reference to the node to handle @type ast.Assign """ @@ -1787,31 +1925,28 @@ return self.generic_visit(node.value) - + target = node.targets[0] - if ( - isinstance(target, ast.Tuple) and - not isinstance(node.value, ast.Tuple) - ): + if isinstance(target, ast.Tuple) and not isinstance(node.value, ast.Tuple): # skip unpacking assign return - + self.__visitAssignTarget(target) - + def visit_Name(self, node): """ Public method to handle a name node. - + @param node reference to the node to handle @type ast.Name """ if self.__stack: self.refs[node.id].append(node.lineno) - + def __visitAssignTarget(self, node): """ Private method to handle an assign target node. - + @param node reference to the node to handle @type ast.AST """ @@ -1819,70 +1954,66 @@ for elt in node.elts: self.__visitAssignTarget(elt) return - + if not self.__loopCount and isinstance(node, ast.Name): self.assigns[node.id].append(node.lineno) return - + self.generic_visit(node) - + def __checkFunction(self, node): """ Private method to check a function definition node. - + @param node reference to the node to check @type ast.AsyncFunctionDef or ast.FunctionDef """ if not self.returns or not node.body: return - + if len(node.body) == 1 and isinstance(node.body[-1], ast.Return): # skip functions that consist of `return None` only return - + if not self.__resultExists(): self.__checkUnnecessaryReturnNone() return - + self.__checkImplicitReturnValue() self.__checkImplicitReturn(node.body[-1]) - + for n in self.returns: if n.value: self.__checkUnnecessaryAssign(n.value) - + def __isNone(self, node): """ Private method to check, if a node value is None. - + @param node reference to the node to check @type ast.AST @return flag indicating the node contains a None value @rtype bool """ - return ( - AstUtilities.isNameConstant(node) and - AstUtilities.getValue(node) is None - ) - + return AstUtilities.isNameConstant(node) and AstUtilities.getValue(node) is None + def __isFalse(self, node): """ Private method to check, if a node value is False. - + @param node reference to the node to check @type ast.AST @return flag indicating the node contains a False value @rtype bool """ return ( - AstUtilities.isNameConstant(node) and - AstUtilities.getValue(node) is False + AstUtilities.isNameConstant(node) and AstUtilities.getValue(node) is False ) - + def __resultExists(self): """ Private method to check the existance of a return result. - + @return flag indicating the existence of a return result @rtype bool """ @@ -1890,9 +2021,9 @@ value = node.value if value and not self.__isNone(value): return True - + return False - + def __checkImplicitReturnValue(self): """ Private method to check for implicit return values. @@ -1900,7 +2031,7 @@ for node in self.returns: if not node.value: self.violations.append((node, "M832")) - + def __checkUnnecessaryReturnNone(self): """ Private method to check for an unnecessary 'return None' statement. @@ -1908,11 +2039,11 @@ for node in self.returns: if self.__isNone(node.value): self.violations.append((node, "M831")) - + def __checkImplicitReturn(self, node): """ Private method to check for an implicit return statement. - + @param node reference to the node to check @type ast.AST """ @@ -1920,59 +2051,59 @@ if not node.body or not node.orelse: self.violations.append((node, "M833")) return - + self.__checkImplicitReturn(node.body[-1]) self.__checkImplicitReturn(node.orelse[-1]) return - + if isinstance(node, (ast.For, ast.AsyncFor)) and node.orelse: self.__checkImplicitReturn(node.orelse[-1]) return - + if isinstance(node, (ast.With, ast.AsyncWith)): self.__checkImplicitReturn(node.body[-1]) return - + if isinstance(node, ast.Assert) and self.__isFalse(node.test): return - + try: okNodes = (ast.Return, ast.Raise, ast.While, ast.Try) except AttributeError: okNodes = (ast.Return, ast.Raise, ast.While) if not isinstance(node, okNodes): self.violations.append((node, "M833")) - + def __checkUnnecessaryAssign(self, node): """ Private method to check for an unnecessary assign statement. - + @param node reference to the node to check @type ast.AST """ if not isinstance(node, ast.Name): return - + varname = node.id returnLineno = node.lineno - + if varname not in self.assigns: return - + if varname not in self.refs: self.violations.append((node, "M834")) return - + if self.__hasRefsBeforeNextAssign(varname, returnLineno): return - + self.violations.append((node, "M834")) def __hasRefsBeforeNextAssign(self, varname, returnLineno): """ Private method to check for references before a following assign statement. - + @param varname variable name to check for @type str @param returnLineno line number of the return statement @@ -1982,47 +2113,48 @@ """ beforeAssign = 0 afterAssign = None - + for lineno in sorted(self.assigns[varname]): if lineno > returnLineno: afterAssign = lineno break - + if lineno <= returnLineno: beforeAssign = lineno - + for lineno in self.refs[varname]: if lineno == returnLineno: continue - + if afterAssign: if beforeAssign < lineno <= afterAssign: return True - + elif beforeAssign < lineno: return True - + return False class DateTimeVisitor(ast.NodeVisitor): """ Class implementing a node visitor to check datetime function calls. - + Note: This class is modelled after flake8_datetimez checker. """ + def __init__(self): """ Constructor """ super().__init__() - + self.violations = [] - + def __getFromKeywords(self, keywords, name): """ Private method to get a keyword node given its name. - + @param keywords list of keyword argument nodes @type list of ast.AST @param name name of the keyword node @@ -2033,195 +2165,176 @@ for keyword in keywords: if keyword.arg == name: return keyword - + return None - + def visit_Call(self, node): """ Public method to handle a function call. Every datetime related function call is check for use of the naive variant (i.e. use without TZ info). - + @param node reference to the node to be processed @type ast.Call """ # datetime.something() isDateTimeClass = ( - isinstance(node.func, ast.Attribute) and - isinstance(node.func.value, ast.Name) and - node.func.value.id == 'datetime') - + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "datetime" + ) + # datetime.datetime.something() isDateTimeModuleAndClass = ( - isinstance(node.func, ast.Attribute) and - isinstance(node.func.value, ast.Attribute) and - node.func.value.attr == 'datetime' and - isinstance(node.func.value.value, ast.Name) and - node.func.value.value.id == 'datetime') - + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Attribute) + and node.func.value.attr == "datetime" + and isinstance(node.func.value.value, ast.Name) + and node.func.value.value.id == "datetime" + ) + if isDateTimeClass: - if node.func.attr == 'datetime': + if node.func.attr == "datetime": # datetime.datetime(2000, 1, 1, 0, 0, 0, 0, # datetime.timezone.utc) - isCase1 = ( - len(node.args) >= 8 and - not ( - AstUtilities.isNameConstant(node.args[7]) and - AstUtilities.getValue(node.args[7]) is None - ) + isCase1 = len(node.args) >= 8 and not ( + AstUtilities.isNameConstant(node.args[7]) + and AstUtilities.getValue(node.args[7]) is None ) - + # datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc) - tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo') - isCase2 = ( - tzinfoKeyword is not None and - not ( - AstUtilities.isNameConstant(tzinfoKeyword.value) and - AstUtilities.getValue(tzinfoKeyword.value) is None - ) + tzinfoKeyword = self.__getFromKeywords(node.keywords, "tzinfo") + isCase2 = tzinfoKeyword is not None and not ( + AstUtilities.isNameConstant(tzinfoKeyword.value) + and AstUtilities.getValue(tzinfoKeyword.value) is None ) - + if not (isCase1 or isCase2): self.violations.append((node, "M301")) - - elif node.func.attr == 'time': + + elif node.func.attr == "time": # time(12, 10, 45, 0, datetime.timezone.utc) - isCase1 = ( - len(node.args) >= 5 and - not ( - AstUtilities.isNameConstant(node.args[4]) and - AstUtilities.getValue(node.args[4]) is None - ) + isCase1 = len(node.args) >= 5 and not ( + AstUtilities.isNameConstant(node.args[4]) + and AstUtilities.getValue(node.args[4]) is None ) - + # datetime.time(12, 10, 45, tzinfo=datetime.timezone.utc) - tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo') - isCase2 = ( - tzinfoKeyword is not None and - not ( - AstUtilities.isNameConstant(tzinfoKeyword.value) and - AstUtilities.getValue(tzinfoKeyword.value) is None - ) + tzinfoKeyword = self.__getFromKeywords(node.keywords, "tzinfo") + isCase2 = tzinfoKeyword is not None and not ( + AstUtilities.isNameConstant(tzinfoKeyword.value) + and AstUtilities.getValue(tzinfoKeyword.value) is None ) - + if not (isCase1 or isCase2): self.violations.append((node, "M321")) - - elif node.func.attr == 'date': + + elif node.func.attr == "date": self.violations.append((node, "M311")) - + if isDateTimeClass or isDateTimeModuleAndClass: - if node.func.attr == 'today': + if node.func.attr == "today": self.violations.append((node, "M302")) - - elif node.func.attr == 'utcnow': + + elif node.func.attr == "utcnow": self.violations.append((node, "M303")) - - elif node.func.attr == 'utcfromtimestamp': + + elif node.func.attr == "utcfromtimestamp": self.violations.append((node, "M304")) - - elif node.func.attr in 'now': + + elif node.func.attr in "now": # datetime.now(UTC) isCase1 = ( - len(node.args) == 1 and - len(node.keywords) == 0 and - not ( - AstUtilities.isNameConstant(node.args[0]) and - AstUtilities.getValue(node.args[0]) is None + len(node.args) == 1 + and len(node.keywords) == 0 + and not ( + AstUtilities.isNameConstant(node.args[0]) + and AstUtilities.getValue(node.args[0]) is None ) ) - + # datetime.now(tz=UTC) - tzKeyword = self.__getFromKeywords(node.keywords, 'tz') - isCase2 = ( - tzKeyword is not None and - not ( - AstUtilities.isNameConstant(tzKeyword.value) and - AstUtilities.getValue(tzKeyword.value) is None - ) + tzKeyword = self.__getFromKeywords(node.keywords, "tz") + isCase2 = tzKeyword is not None and not ( + AstUtilities.isNameConstant(tzKeyword.value) + and AstUtilities.getValue(tzKeyword.value) is None ) - + if not (isCase1 or isCase2): self.violations.append((node, "M305")) - - elif node.func.attr == 'fromtimestamp': + + elif node.func.attr == "fromtimestamp": # datetime.fromtimestamp(1234, UTC) isCase1 = ( - len(node.args) == 2 and - len(node.keywords) == 0 and - not ( - AstUtilities.isNameConstant(node.args[1]) and - AstUtilities.getValue(node.args[1]) is None + len(node.args) == 2 + and len(node.keywords) == 0 + and not ( + AstUtilities.isNameConstant(node.args[1]) + and AstUtilities.getValue(node.args[1]) is None ) ) - + # datetime.fromtimestamp(1234, tz=UTC) - tzKeyword = self.__getFromKeywords(node.keywords, 'tz') - isCase2 = ( - tzKeyword is not None and - not ( - AstUtilities.isNameConstant(tzKeyword.value) and - AstUtilities.getValue(tzKeyword.value) is None - ) + tzKeyword = self.__getFromKeywords(node.keywords, "tz") + isCase2 = tzKeyword is not None and not ( + AstUtilities.isNameConstant(tzKeyword.value) + and AstUtilities.getValue(tzKeyword.value) is None ) - + if not (isCase1 or isCase2): self.violations.append((node, "M306")) - - elif node.func.attr == 'strptime': + + elif node.func.attr == "strptime": # datetime.strptime(...).replace(tzinfo=UTC) - parent = getattr(node, '_dtCheckerParent', None) - pparent = getattr(parent, '_dtCheckerParent', None) - if ( - not (isinstance(parent, ast.Attribute) and - parent.attr == 'replace') or - not isinstance(pparent, ast.Call) - ): + parent = getattr(node, "_dtCheckerParent", None) + pparent = getattr(parent, "_dtCheckerParent", None) + if not ( + isinstance(parent, ast.Attribute) and parent.attr == "replace" + ) or not isinstance(pparent, ast.Call): isCase1 = False else: - tzinfoKeyword = self.__getFromKeywords(pparent.keywords, - 'tzinfo') - isCase1 = ( - tzinfoKeyword is not None and - not ( - AstUtilities.isNameConstant( - tzinfoKeyword.value) and - AstUtilities.getValue(tzinfoKeyword.value) is None - ) + tzinfoKeyword = self.__getFromKeywords(pparent.keywords, "tzinfo") + isCase1 = tzinfoKeyword is not None and not ( + AstUtilities.isNameConstant(tzinfoKeyword.value) + and AstUtilities.getValue(tzinfoKeyword.value) is None ) - + if not isCase1: self.violations.append((node, "M307")) - - elif node.func.attr == 'fromordinal': + + elif node.func.attr == "fromordinal": self.violations.append((node, "M308")) - + # date.something() - isDateClass = (isinstance(node.func, ast.Attribute) and - isinstance(node.func.value, ast.Name) and - node.func.value.id == 'date') - + isDateClass = ( + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "date" + ) + # datetime.date.something() - isDateModuleAndClass = (isinstance(node.func, ast.Attribute) and - isinstance(node.func.value, ast.Attribute) and - node.func.value.attr == 'date' and - isinstance(node.func.value.value, ast.Name) and - node.func.value.value.id == 'datetime') - + isDateModuleAndClass = ( + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Attribute) + and node.func.value.attr == "date" + and isinstance(node.func.value.value, ast.Name) + and node.func.value.value.id == "datetime" + ) + if isDateClass or isDateModuleAndClass: - if node.func.attr == 'today': + if node.func.attr == "today": self.violations.append((node, "M312")) - - elif node.func.attr == 'fromtimestamp': + + elif node.func.attr == "fromtimestamp": self.violations.append((node, "M313")) - - elif node.func.attr == 'fromordinal': + + elif node.func.attr == "fromordinal": self.violations.append((node, "M314")) - - elif node.func.attr == 'fromisoformat': + + elif node.func.attr == "fromisoformat": self.violations.append((node, "M315")) - + self.generic_visit(node) @@ -2229,35 +2342,36 @@ """ Class implementing a node visitor to check the use of sys.version and sys.version_info. - + Note: This class is modelled after flake8-2020 checker. """ + def __init__(self): """ Constructor """ super().__init__() - + self.violations = [] self.__fromImports = {} - + def visit_ImportFrom(self, node): """ Public method to handle a from ... import ... statement. - + @param node reference to the node to be processed @type ast.ImportFrom """ for alias in node.names: if node.module is not None and not alias.asname: self.__fromImports[alias.name] = node.module - + self.generic_visit(node) - + def __isSys(self, attr, node): """ Private method to check for a reference to sys attribute. - + @param attr attribute name @type str @param node reference to the node to be checked @@ -2267,22 +2381,23 @@ """ match = False if ( - (isinstance(node, ast.Attribute) and - isinstance(node.value, ast.Name) and - node.value.id == "sys" and - node.attr == attr) or - (isinstance(node, ast.Name) and - node.id == attr and - self.__fromImports.get(node.id) == "sys") + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id == "sys" + and node.attr == attr + ) or ( + isinstance(node, ast.Name) + and node.id == attr + and self.__fromImports.get(node.id) == "sys" ): match = True - + return match - + def __isSysVersionUpperSlice(self, node, n): """ Private method to check the upper slice of sys.version. - + @param node reference to the node to be checked @type ast.Node @param n slice value to check against @@ -2291,18 +2406,18 @@ @rtype bool """ return ( - self.__isSys("version", node.value) and - isinstance(node.slice, ast.Slice) and - node.slice.lower is None and - AstUtilities.isNumber(node.slice.upper) and - AstUtilities.getValue(node.slice.upper) == n and - node.slice.step is None + self.__isSys("version", node.value) + and isinstance(node.slice, ast.Slice) + and node.slice.lower is None + and AstUtilities.isNumber(node.slice.upper) + and AstUtilities.getValue(node.slice.upper) == n + and node.slice.step is None ) - + def visit_Subscript(self, node): """ Public method to handle a subscript. - + @param node reference to the node to be processed @type ast.Subscript """ @@ -2311,46 +2426,46 @@ elif self.__isSysVersionUpperSlice(node, 3): self.violations.append((node.value, "M401")) elif ( - self.__isSys('version', node.value) and - isinstance(node.slice, ast.Index) and - AstUtilities.isNumber(node.slice.value) and - AstUtilities.getValue(node.slice.value) == 2 + self.__isSys("version", node.value) + and isinstance(node.slice, ast.Index) + and AstUtilities.isNumber(node.slice.value) + and AstUtilities.getValue(node.slice.value) == 2 ): self.violations.append((node.value, "M402")) elif ( - self.__isSys('version', node.value) and - isinstance(node.slice, ast.Index) and - AstUtilities.isNumber(node.slice.value) and - AstUtilities.getValue(node.slice.value) == 0 + self.__isSys("version", node.value) + and isinstance(node.slice, ast.Index) + and AstUtilities.isNumber(node.slice.value) + and AstUtilities.getValue(node.slice.value) == 0 ): self.violations.append((node.value, "M421")) self.generic_visit(node) - + def visit_Compare(self, node): """ Public method to handle a comparison. - + @param node reference to the node to be processed @type ast.Compare """ if ( - isinstance(node.left, ast.Subscript) and - self.__isSys('version_info', node.left.value) and - isinstance(node.left.slice, ast.Index) and - AstUtilities.isNumber(node.left.slice.value) and - AstUtilities.getValue(node.left.slice.value) == 0 and - len(node.ops) == 1 and - isinstance(node.ops[0], ast.Eq) and - AstUtilities.isNumber(node.comparators[0]) and - AstUtilities.getValue(node.comparators[0]) == 3 + isinstance(node.left, ast.Subscript) + and self.__isSys("version_info", node.left.value) + and isinstance(node.left.slice, ast.Index) + and AstUtilities.isNumber(node.left.slice.value) + and AstUtilities.getValue(node.left.slice.value) == 0 + and len(node.ops) == 1 + and isinstance(node.ops[0], ast.Eq) + and AstUtilities.isNumber(node.comparators[0]) + and AstUtilities.getValue(node.comparators[0]) == 3 ): self.violations.append((node.left, "M411")) elif ( - self.__isSys('version', node.left) and - len(node.ops) == 1 and - isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and - AstUtilities.isString(node.comparators[0]) + self.__isSys("version", node.left) + and len(node.ops) == 1 + and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) + and AstUtilities.isString(node.comparators[0]) ): if len(AstUtilities.getValue(node.comparators[0])) == 1: errorCode = "M422" @@ -2358,55 +2473,56 @@ errorCode = "M403" self.violations.append((node.left, errorCode)) elif ( - isinstance(node.left, ast.Subscript) and - self.__isSys('version_info', node.left.value) and - isinstance(node.left.slice, ast.Index) and - AstUtilities.isNumber(node.left.slice.value) and - AstUtilities.getValue(node.left.slice.value) == 1 and - len(node.ops) == 1 and - isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and - AstUtilities.isNumber(node.comparators[0]) + isinstance(node.left, ast.Subscript) + and self.__isSys("version_info", node.left.value) + and isinstance(node.left.slice, ast.Index) + and AstUtilities.isNumber(node.left.slice.value) + and AstUtilities.getValue(node.left.slice.value) == 1 + and len(node.ops) == 1 + and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) + and AstUtilities.isNumber(node.comparators[0]) ): self.violations.append((node, "M413")) elif ( - isinstance(node.left, ast.Attribute) and - self.__isSys('version_info', node.left.value) and - node.left.attr == 'minor' and - len(node.ops) == 1 and - isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and - AstUtilities.isNumber(node.comparators[0]) + isinstance(node.left, ast.Attribute) + and self.__isSys("version_info", node.left.value) + and node.left.attr == "minor" + and len(node.ops) == 1 + and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) + and AstUtilities.isNumber(node.comparators[0]) ): self.violations.append((node, "M414")) - + self.generic_visit(node) - + def visit_Attribute(self, node): """ Public method to handle an attribute. - + @param node reference to the node to be processed @type ast.Attribute """ if ( - isinstance(node.value, ast.Name) and - node.value.id == 'six' and - node.attr == 'PY3' + isinstance(node.value, ast.Name) + and node.value.id == "six" + and node.attr == "PY3" ): self.violations.append((node, "M412")) - + self.generic_visit(node) def visit_Name(self, node): """ Public method to handle an name. - + @param node reference to the node to be processed @type ast.Name """ - if node.id == 'PY3' and self.__fromImports.get(node.id) == 'six': + if node.id == "PY3" and self.__fromImports.get(node.id) == "six": self.violations.append((node, "M412")) - + self.generic_visit(node) + # # eflag: noqa = M891