diff -r fc45672fae42 -r 73d80859079c src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py --- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py Thu Feb 27 09:22:15 2025 +0100 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py Thu Feb 27 14:42:39 2025 +0100 @@ -12,14 +12,10 @@ import contextlib import copy import itertools -import math import re import sys import tokenize -from collections import Counter, defaultdict, namedtuple -from dataclasses import dataclass -from keyword import iskeyword from string import Formatter try: @@ -37,58 +33,19 @@ import AstUtilities +from CodeStyleTopicChecker import CodeStyleTopicChecker + +from .BugBearVisitor import BugBearVisitor +from .DateTimeVisitor import DateTimeVisitor +from .DefaultMatchCaseVisitor import DefaultMatchCaseVisitor from .eradicate import Eradicator from .MiscellaneousDefaults import MiscellaneousCheckerDefaultArgs - -BugbearMutableLiterals = ("Dict", "List", "Set") -BugbearMutableComprehensions = ("ListComp", "DictComp", "SetComp") -BugbearMutableCalls = ( - "Counter", - "OrderedDict", - "collections.Counter", - "collections.OrderedDict", - "collections.defaultdict", - "collections.deque", - "defaultdict", - "deque", - "dict", - "list", - "set", -) -BugbearImmutableCalls = ( - "tuple", - "frozenset", - "types.MappingProxyType", - "MappingProxyType", - "re.compile", - "operator.attrgetter", - "operator.itemgetter", - "operator.methodcaller", - "attrgetter", - "itemgetter", - "methodcaller", -) +from .ReturnVisitor import ReturnVisitor +from .SysVersionVisitor import SysVersionVisitor +from .TextVisitor import TextVisitor -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 - @ytype str - """ - if isinstance(node, ast.Attribute): - yield from composeCallPath(node.value) - yield node.attr - elif isinstance(node, ast.Call): - yield from composeCallPath(node.func) - elif isinstance(node, ast.Name): - yield node.id - - -class MiscellaneousChecker: +class MiscellaneousChecker(CodeStyleTopicChecker): """ Class implementing a checker for miscellaneous checks. """ @@ -234,7 +191,7 @@ "M-801", ## one element tuple "M-811", - ## return statements + ## return statements # noqa: M-891 "M-831", "M-832", "M-833", @@ -251,6 +208,7 @@ "M-901", "M-902", ] + Category = "M" Formatter = Formatter() FormatFieldRegex = re.compile(r"^((?:\s|.)*?)(\..*|\[.*\])?$") @@ -282,16 +240,19 @@ @param args dictionary of arguments for the miscellaneous checks @type dict """ - self.__select = tuple(select) - self.__ignore = tuple(ignore) - self.__expected = expected[:] - self.__repeat = repeat - self.__filename = filename - self.__source = source[:] - self.__tree = copy.deepcopy(tree) - self.__args = args + super().__init__( + MiscellaneousChecker.Category, + source, + filename, + tree, + select, + ignore, + expected, + repeat, + args, + ) - linesIterator = iter(self.__source) + linesIterator = iter(self.source) self.__tokens = list(tokenize.generate_tokens(lambda: next(linesIterator))) self.__pep3101FormatRegex = re.compile( @@ -302,12 +263,6 @@ self.__eradicator = Eradicator() - # statistics counters - self.counters = {} - - # collection of detected errors - self.errors = [] - checkersWithCodes = [ (self.__checkCoding, ("M-101", "M-102")), (self.__checkCopyright, ("M-111", "M-112")), @@ -466,9 +421,10 @@ (self.__checkCommentedCode, ("M-891",)), (self.__checkDefaultMatchCase, ("M-901", "M-902")), ] + self._initializeCheckers(checkersWithCodes) # the eradicate whitelist - commentedCodeCheckerArgs = self.__args.get( + commentedCodeCheckerArgs = self.args.get( "CommentedCodeChecker", MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"], ) @@ -480,77 +436,6 @@ commentedCodeCheckerWhitelist, extend_default=False ) - self.__checkers = [] - for checker, codes in checkersWithCodes: - 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. - - @param code message code to check for - @type str - @return flag indicating to ignore the given code - @rtype bool - """ - return code in self.__ignore or ( - 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 - @type int - @param code message code - @type str - @param args arguments for the message - @type list - """ - 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( - { - "file": self.__filename, - "line": lineNumber + 1, - "offset": offset, - "code": code, - "args": args, - } - ) - - def run(self): - """ - Public method to check the given source against miscellaneous - conditions. - """ - 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. @@ -558,7 +443,7 @@ @return tuple containing the line number and the coding @rtype tuple of int and str """ - for lineno, line in enumerate(self.__source[:5]): + for lineno, line in enumerate(self.source[:5], start=1): matched = re.search(r"coding[:=]\s*([-\w_.]+)", line, re.IGNORECASE) if matched: return lineno, matched.group(1) @@ -570,28 +455,28 @@ Private method to check the presence of a coding line and valid encodings. """ - if len(self.__source) == 0: + if len(self.source) == 0: return encodings = [ e.lower().strip() - for e in self.__args.get( + 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, "M-102", coding) + self.addError(lineno, 0, "M-102", coding) else: - self.__error(0, 0, "M-101") + self.addError(1, 0, "M-101") def __checkCopyright(self): """ Private method to check the presence of a copyright statement. """ - source = "".join(self.__source) - copyrightArgs = self.__args.get( + source = "".join(self.source) + copyrightArgs = self.args.get( "CopyrightChecker", MiscellaneousCheckerDefaultArgs["CopyrightChecker"] ) copyrightMinFileSize = copyrightArgs.get( @@ -612,7 +497,7 @@ copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"), re.IGNORECASE) if not copyrightRe.search(topOfSource): - self.__error(0, 0, "M-111") + self.addError(1, 0, "M-111") return if copyrightAuthor: @@ -620,14 +505,14 @@ copyrightRegexStr.format(author=copyrightAuthor), re.IGNORECASE ) if not copyrightAuthorRe.search(topOfSource): - self.__error(0, 0, "M-112") + self.addError(1, 0, "M-112") def __checkCommentedCode(self): """ Private method to check for commented code. """ - source = "".join(self.__source) - commentedCodeCheckerArgs = self.__args.get( + source = "".join(self.source) + commentedCodeCheckerArgs = self.args.get( "CommentedCodeChecker", MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"], ) @@ -638,7 +523,7 @@ for markedLine in self.__eradicator.commented_out_code_line_numbers( source, aggressive=aggressive ): - self.__error(markedLine - 1, 0, "M-891") + self.addError(markedLine, 0, "M-891") def __checkLineContinuation(self): """ @@ -646,7 +531,7 @@ """ # generate source lines without comments comments = [tok for tok in self.__tokens if tok[0] == tokenize.COMMENT] - stripped = self.__source[:] + stripped = self.source[:] for comment in comments: lineno = comment[3][0] start = comment[2][1] @@ -661,25 +546,25 @@ if strippedLine.endswith("\\") and not strippedLine.startswith( ("assert", "with") ): - self.__error(lineIndex, len(line), "M-841") + self.addError(lineIndex + 1, len(line), "M-841") def __checkPrintStatements(self): """ Private method to check for print statements. """ - for node in ast.walk(self.__tree): + 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)): - self.__error(node.lineno - 1, node.col_offset, "M-801") + self.addErrorFromNode(node, "M-801") def __checkTuple(self): """ Private method to check for one element tuples. """ - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if isinstance(node, ast.Tuple) and len(node.elts) == 1: - self.__error(node.lineno - 1, node.col_offset, "M-811") + self.addErrorFromNode(node, "M-811") def __checkFuture(self): """ @@ -687,7 +572,7 @@ """ expectedImports = { i.strip() - for i in self.__args.get("FutureChecker", "").split(",") + for i in self.args.get("FutureChecker", "").split(",") if bool(i.strip()) } if len(expectedImports) == 0: @@ -698,7 +583,7 @@ node = None hasCode = False - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if isinstance(node, ast.ImportFrom) and node.module == "__future__": imports |= {name.name for name in node.names} elif isinstance(node, ast.Expr): @@ -714,26 +599,17 @@ if imports < expectedImports: if imports: - self.__error( - node.lineno - 1, - node.col_offset, - "M-701", - ", ".join(expectedImports), - ", ".join(imports), + self.addErrorFromNode( + node, "M-701", ", ".join(expectedImports), ", ".join(imports) ) else: - self.__error( - node.lineno - 1, - node.col_offset, - "M-702", - ", ".join(expectedImports), - ) + self.addErrorFromNode(node, "M-702", ", ".join(expectedImports)) def __checkPep3101(self): """ Private method to check for old style string formatting. """ - for lineno, line in enumerate(self.__source): + for lineno, line in enumerate(self.source, start=1): match = self.__pep3101FormatRegex.search(line) if match: lineLen = len(line) @@ -750,7 +626,7 @@ c = line[pos] if c in "diouxXeEfFgGcrs": formatter += c - self.__error(lineno, formatPos, "M-601", formatter) + self.addError(lineno, formatPos, "M-601", formatter) def __checkFormatString(self): """ @@ -762,7 +638,7 @@ coding = "utf-8" visitor = TextVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) for node in visitor.nodes: text = node.value if isinstance(text, bytes): @@ -773,12 +649,12 @@ fields, implicit, explicit = self.__getFields(text) if implicit: if node in visitor.calls: - self.__error(node.lineno - 1, node.col_offset, "M-611") + self.addErrorFromNode(node, "M-611") else: if node.is_docstring: - self.__error(node.lineno - 1, node.col_offset, "M-612") + self.addErrorFromNode(node, "M-612") else: - self.__error(node.lineno - 1, node.col_offset, "M-613") + self.addErrorFromNode(node, "M-613") if node in visitor.calls: call, strArgs = visitor.calls[node] @@ -798,7 +674,7 @@ else: names.add(fieldMatch.group(1)) - keywords = {keyword.arg for keyword in call.keywords} + keywords = {kw.arg for kw in call.keywords} numArgs = len(call.args) if strArgs: numArgs -= 1 @@ -816,35 +692,31 @@ # parameters but at least check if the args are used if hasKwArgs and not names: # No names but kwargs - self.__error(call.lineno - 1, call.col_offset, "M-623") + self.addErrorFromNode(call, "M-623") if hasStarArgs and not numbers: # No numbers but args - self.__error(call.lineno - 1, call.col_offset, "M-624") + self.addErrorFromNode(call, "M-624") 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, "M-621", number - ) + self.addErrorFromNode(call, "M-621", number) for name in sorted(names): if name not in keywords: - self.__error( - call.lineno - 1, call.col_offset, "M-622", name - ) + self.addErrorFromNode(call, "M-622", name) for arg in range(numArgs): if arg not in numbers: - self.__error(call.lineno - 1, call.col_offset, "M-631", arg) + self.addErrorFromNode(call, "M-631", arg) - for keyword in keywords: - if keyword not in names: - self.__error(call.lineno - 1, call.col_offset, "M-632", keyword) + for kw in keywords: + if kw not in names: + self.addErrorFromNode(call, "M-632", kw) if implicit and explicit: - self.__error(call.lineno - 1, call.col_offset, "M-625") + self.addErrorFromNode(call, "M-625") def __getFields(self, string): """ @@ -887,11 +759,11 @@ with contextlib.suppress(AttributeError): functionDefs.append(ast.AsyncFunctionDef) - ignoreBuiltinAssignments = self.__args.get( + ignoreBuiltinAssignments = self.args.get( "BuiltinsChecker", MiscellaneousCheckerDefaultArgs["BuiltinsChecker"] ) - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if isinstance(node, ast.Assign): # assign statement for element in node.targets: @@ -904,45 +776,33 @@ ): # ignore compatibility assignments continue - self.__error( - element.lineno - 1, element.col_offset, "M-131", element.id - ) + self.addErrorFromNode(element, "M-131", 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 ): - self.__error( - tupleElement.lineno - 1, - tupleElement.col_offset, - "M-131", - tupleElement.id, + self.addErrorFromNode( + tupleElement, "M-131", 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, "M-131", target.id - ) + self.addErrorFromNode(target, "M-131", 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 ): - self.__error( - element.lineno - 1, - element.col_offset, - "M-131", - element.id, - ) + self.addErrorFromNode(element, "M-131", 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, "M-132", arg.arg) + self.addErrorFromNode(arg, "M-132", arg.arg) def __checkComprehensions(self): """ @@ -959,7 +819,7 @@ visitedMapCalls = set() - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): numPositionalArgs = len(node.args) numKeywordArgs = len(node.keywords) @@ -973,7 +833,7 @@ "list": "M-180", "set": "M-181", }[node.func.id] - self.__error(node.lineno - 1, node.col_offset, errorCode) + self.addErrorFromNode(node, errorCode) elif ( numPositionalArgs == 1 @@ -987,7 +847,7 @@ errorCode = "M-182" else: errorCode = "M-184" - self.__error(node.lineno - 1, node.col_offset, errorCode) + self.addErrorFromNode(node, errorCode) elif ( numPositionalArgs == 1 @@ -1000,9 +860,7 @@ "any": "M-199", "all": "M-199", }[node.func.id] - self.__error( - node.lineno - 1, node.col_offset, errorCode, node.func.id - ) + self.addErrorFromNode(node, errorCode, node.func.id) elif numPositionalArgs == 1 and ( isinstance(node.args[0], ast.Tuple) @@ -1014,9 +872,8 @@ "tuple": "M-189a", "list": "M-190a", }[node.func.id] - self.__error( - node.lineno - 1, - node.col_offset, + self.addErrorFromNode( + node, errorCode, type(node.args[0]).__name__.lower(), node.func.id, @@ -1032,12 +889,7 @@ type_ = "dict" else: type_ = "dict comprehension" - self.__error( - node.lineno - 1, - node.col_offset, - "M-198", - type_, - ) + self.addErrorFromNode(node, "M-198", type_) elif ( numPositionalArgs == 1 @@ -1059,9 +911,8 @@ "set": "M-185", "dict": "M-186", }[node.func.id] - self.__error( - node.lineno - 1, - node.col_offset, + self.addErrorFromNode( + node, errorCode, type(node.args[0]).__name__.lower(), node.func.id, @@ -1077,9 +928,7 @@ and numKeywordArgs == 0 and node.func.id in ("tuple", "list") ): - self.__error( - node.lineno - 1, node.col_offset, "M-188", node.func.id - ) + self.addErrorFromNode(node, "M-188", node.func.id) elif ( node.func.id in {"list", "reversed"} @@ -1100,17 +949,12 @@ ) if reverseFlagValue is None: - self.__error( - node.lineno - 1, - node.col_offset, - "M-193a", - node.func.id, - node.args[0].func.id, + self.addErrorFromNode( + node, "M-193a", node.func.id, node.args[0].func.id ) else: - self.__error( - node.lineno - 1, - node.col_offset, + self.addErrorFromNode( + node, "M-193b", node.func.id, node.args[0].func.id, @@ -1118,12 +962,8 @@ ) else: - self.__error( - node.lineno - 1, - node.col_offset, - "M-193c", - node.func.id, - node.args[0].func.id, + self.addErrorFromNode( + node, "M-193c", node.func.id, node.args[0].func.id ) elif ( @@ -1143,12 +983,8 @@ or (node.func.id == "set" and node.args[0].func.id == "set") ) ): - self.__error( - node.lineno - 1, - node.col_offset, - "M-194", - node.args[0].func.id, - node.func.id, + self.addErrorFromNode( + node, "M-194", node.args[0].func.id, node.func.id ) elif ( @@ -1163,9 +999,7 @@ and isinstance(node.args[0].slice.step.operand, ast.Constant) and node.args[0].slice.step.operand.n == 1 ): - self.__error( - node.lineno - 1, node.col_offset, "M-195", node.func.id - ) + self.addErrorFromNode(node, "M-195", node.func.id) elif ( node.func.id == "map" @@ -1173,12 +1007,7 @@ and len(node.args) == 2 and isinstance(node.args[0], ast.Lambda) ): - self.__error( - node.lineno - 1, - node.col_offset, - "M-197", - "generator expression", - ) + self.addErrorFromNode(node, "M-197", "generator expression") elif ( node.func.id in ("list", "set", "dict") @@ -1206,9 +1035,7 @@ if rewriteable: comprehensionType = f"{node.func.id} comprehension" - self.__error( - node.lineno - 1, node.col_offset, "M-197", comprehensionType - ) + self.addErrorFromNode(node, "M-197", comprehensionType) elif isinstance(node, (ast.DictComp, ast.ListComp, ast.SetComp)) and ( len(node.generators) == 1 @@ -1231,12 +1058,7 @@ and isinstance(node.generators[0].target.elts[1], ast.Name) and node.generators[0].target.elts[1].id == node.value.id ): - self.__error( - node.lineno - 1, - node.col_offset, - "M-196", - compType[node.__class__], - ) + self.addErrorFromNode(node, "M-196", compType[node.__class__]) elif ( isinstance(node, ast.DictComp) @@ -1245,12 +1067,7 @@ and isinstance(node.generators[0].target, ast.Name) and node.key.id == node.generators[0].target.id ): - self.__error( - node.lineno - 1, - node.col_offset, - "M-200", - compType[node.__class__], - ) + self.addErrorFromNode(node, "M-200", compType[node.__class__]) def __dictShouldBeChecked(self, node): """ @@ -1265,8 +1082,9 @@ return False if ( - "__IGNORE_WARNING__" in self.__source[node.lineno - 1] - or "__IGNORE_WARNING_M251__" in self.__source[node.lineno - 1] + "__IGNORE_WARNING__" in self.source[node.lineno - 1] + or "__IGNORE_WARNING_M-251__" in self.source[node.lineno - 1] + or "noqa: M-251" in self.source[node.lineno - 1] ): return False @@ -1277,52 +1095,39 @@ """ Private method to check, if dictionary keys appear in sorted order. """ - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node): for key1, key2 in zip(node.keys, node.keys[1:]): if key2.value < key1.value: - self.__error( - key2.lineno - 1, - key2.col_offset, - "M-251", - key2.value, - key1.value, - ) + self.addErrorFromNode(key2, "M-251", key2.value, key1.value) def __checkGettext(self): """ Private method to check the 'gettext' import statement. """ - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if isinstance(node, ast.ImportFrom) and any( name.asname == "_" for name in node.names ): - self.__error( - node.lineno - 1, node.col_offset, "M-711", node.names[0].name - ) + self.addErrorFromNode(node, "M-711", node.names[0].name) def __checkBugBear(self): """ Private method for bugbear checks. """ visitor = BugBearVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) for violation in visitor.violations: - node = violation[0] - reason = violation[1] - params = violation[2:] - self.__error(node.lineno - 1, node.col_offset, reason, *params) + self.addErrorFromNode(*violation) def __checkReturn(self): """ Private method to check return statements. """ visitor = ReturnVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) for violation in visitor.violations: - node = violation[0] - reason = violation[1] - self.__error(node.lineno - 1, node.col_offset, reason) + self.addErrorFromNode(*violation) def __checkDateTime(self): """ @@ -1330,7 +1135,7 @@ """ # step 1: generate an augmented node tree containing parent info # for each child node - tree = copy.deepcopy(self.__tree) + tree = copy.deepcopy(self.tree) for node in ast.walk(tree): for childNode in ast.iter_child_nodes(node): childNode._dtCheckerParent = node @@ -1339,27 +1144,23 @@ visitor = DateTimeVisitor() visitor.visit(tree) for violation in visitor.violations: - node = violation[0] - reason = violation[1] - self.__error(node.lineno - 1, node.col_offset, reason) + self.addErrorFromNode(*violation) def __checkSysVersion(self): """ Private method to check the use of sys.version and sys.version_info. """ visitor = SysVersionVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) for violation in visitor.violations: - node = violation[0] - reason = violation[1] - self.__error(node.lineno - 1, node.col_offset, reason) + self.addErrorFromNode(*violation) def __checkProperties(self): """ Private method to check for issue with property related methods. """ properties = [] - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if isinstance(node, ast.ClassDef): properties.clear() @@ -1371,12 +1172,7 @@ propertyCount += 1 properties.append(node.name) if len(node.args.args) != 1: - self.__error( - node.lineno - 1, - node.col_offset, - "M-260", - len(node.args.args), - ) + self.addErrorFromNode(node, "M-260", len(node.args.args)) if isinstance(decorator, ast.Attribute): # property setter method @@ -1384,27 +1180,16 @@ propertyCount += 1 if node.name != decorator.value.id: if node.name in properties: - self.__error( - node.lineno - 1, - node.col_offset, - "M-265", - node.name, - decorator.value.id, + self.addErrorFromNode( + node, "M-265", node.name, decorator.value.id ) else: - self.__error( - node.lineno - 1, - node.col_offset, - "M-263", - decorator.value.id, - node.name, + self.addErrorFromNode( + node, "M-263", decorator.value.id, node.name ) if len(node.args.args) != 2: - self.__error( - node.lineno - 1, - node.col_offset, - "M-261", - len(node.args.args), + self.addErrorFromNode( + node, "M-261", len(node.args.args) ) # property deleter method @@ -1412,31 +1197,20 @@ propertyCount += 1 if node.name != decorator.value.id: if node.name in properties: - self.__error( - node.lineno - 1, - node.col_offset, - "M-266", - node.name, - decorator.value.id, + self.addErrorFromNode( + node, "M-266", node.name, decorator.value.id ) else: - self.__error( - node.lineno - 1, - node.col_offset, - "M-264", - decorator.value.id, - node.name, + self.addErrorFromNode( + node, "M-264", decorator.value.id, node.name ) if len(node.args.args) != 1: - self.__error( - node.lineno - 1, - node.col_offset, - "M-262", - len(node.args.args), + self.addErrorFromNode( + node, "M-262", len(node.args.args) ) if propertyCount > 1: - self.__error(node.lineno - 1, node.col_offset, "M-267", node.name) + self.addErrorFromNode(node, "M-267", node.name) ####################################################################### ## The following methods check for implicitly concatenated strings. @@ -1509,8 +1283,8 @@ ) for a, b in pairwise(tokensWithoutWhitespace): if self.__isImplicitStringConcat(a, b): - self.__error( - a.end[0] - 1, + self.addError( + a.end[0], a.end[1], "M-851" if a.end[0] == b.start[0] else "M-852", ) @@ -1519,7 +1293,7 @@ """ Private method to check for explicitly concatenated strings. """ - for node in ast.walk(self.__tree): + for node in ast.walk(self.tree): if ( isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add) @@ -1529,7 +1303,7 @@ for operand in (node.left, node.right) ) ): - self.__error(node.lineno - 1, node.col_offset, "M-853") + self.addErrorFromNode(node, "M-853") ################################################################################# ## The following method checks default match cases. @@ -1540,3038 +1314,6 @@ Private method to check the default match case. """ visitor = DefaultMatchCaseVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) for violation in visitor.violations: - node = violation[0] - reason = violation[1] - self.__error(node.lineno - 1, node.col_offset, reason) - - -class TextVisitor(ast.NodeVisitor): - """ - Class implementing a node visitor for bytes and str instances. - - It tries to detect docstrings as string of the first expression of each - module, class or function. - """ - - # modeled after the string format flake8 extension - - def __init__(self): - """ - Constructor - """ - super().__init__() - self.nodes = [] - self.calls = {} - - 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"): - node.is_docstring = False - self.nodes.append(node) - - def visit_Constant(self, node): - """ - Public method to handle constant nodes. - - @param node reference to the bytes node - @type ast.Constant - """ - if AstUtilities.isBaseString(node): - self.__addNode(node) - else: - super().generic_visit(node) - - 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 - """ - # Manually traverse class or function definition - # * Handle decorators normally - # * Use special check for body content - # * Don't handle the rest (e.g. bases) - for decorator in node.decorator_list: - self.visit(decorator) - self.__visitBody(node) - - def __visitBody(self, node): - """ - Private method to traverse the body of the node manually. - - 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[0].value.is_docstring = True - - for subnode in node.body: - self.visit(subnode) - - def visit_Module(self, node): - """ - Public method to handle a module. - - @param node reference to the node to handle - @type ast.Module - """ - self.__visitBody(node) - - def visit_ClassDef(self, node): - """ - Public method to handle a class definition. - - @param node reference to the node to handle - @type ast.ClassDef - """ - # Skipped nodes: ('name', 'bases', 'keywords', 'starargs', 'kwargs') - self.__visitDefinition(node) - - def visit_FunctionDef(self, node): - """ - Public method to handle a function definition. - - @param node reference to the node to handle - @type ast.FunctionDef - """ - # Skipped nodes: ('name', 'args', 'returns') - self.__visitDefinition(node) - - def visit_AsyncFunctionDef(self, node): - """ - Public method to handle an asynchronous function definition. - - @param node reference to the node to handle - @type ast.AsyncFunctionDef - """ - # Skipped nodes: ('name', 'args', 'returns') - self.__visitDefinition(node) - - 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 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]) - ): - self.calls[node.args[0]] = (node, True) - super().generic_visit(node) - - -####################################################################### -## BugBearVisitor -## -## adapted from: flake8-bugbear v24.12.12 -## -## Original: Copyright (c) 2016 Łukasz Langa -####################################################################### - -BugBearContext = namedtuple("BugBearContext", ["node", "stack"]) - - -@dataclass -class M540CaughtException: - """ - Class to hold the data for a caught exception. - """ - - name: str - hasNote: bool - - -class M541UnhandledKeyType: - """ - Class to hold a dictionary key of a type that we do not check for duplicates. - """ - - -class M541VariableKeyType: - """ - Class to hold the name of a variable key type. - """ - - def __init__(self, name): - """ - Constructor - - @param name name of the variable key type - @type str - """ - self.name = name - - -class BugBearVisitor(ast.NodeVisitor): - """ - Class implementing a node visitor to check for various topics. - """ - - 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 - - def __init__(self): - """ - Constructor - """ - super().__init__() - - self.nodeWindow = [] - self.violations = [] - self.contexts = [] - - self.__M523Seen = set() - self.__M505Imports = set() - self.__M540CaughtException = None - - self.__inTryStar = "" - - @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 toNameStr(self, node): - """ - Public 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 - elif isinstance(node, ast.Call): - return self.toNameStr(node.func) - elif isinstance(node, ast.Attribute): - inner = self.toNameStr(node.value) - if inner is None: - return None - return f"{inner}.{node.attr}" - else: - return None - - 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 __flattenExcepthandler(self, node): - """ - Private method to flatten the list of exceptions handled by an except handler. - - @param node reference to the node to be processed - @type ast.Node - @yield reference to the exception type node - @ytype ast.Node - """ - if not isinstance(node, ast.Tuple): - yield node - return - - exprList = node.elts.copy() - while len(exprList): - expr = exprList.pop(0) - if isinstance(expr, ast.Starred) and isinstance( - expr.value, (ast.List, ast.Tuple) - ): - exprList.extend(expr.value.elts) - continue - yield expr - - def __checkRedundantExcepthandlers(self, names, node, inTryStar): - """ - Private method to check for redundant exception types in an exception handler. - - @param names list of exception types to be checked - @type list of ast.Name - @param node reference to the exception handler node - @type ast.ExceptionHandler - @param inTryStar character indicating an 'except*' handler - @type str - @return tuple containing the error data - @rtype tuple of (ast.Node, str, str, str, str) - """ - 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", - }, - } - - # 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)) - as_ = " as " + node.name if node.name is not None else "" - return (node, "M-514", ", ".join(names), as_, desc, inTryStar) - - return None - - def __walkList(self, nodes): - """ - Private method to walk a given list of nodes. - - @param nodes list of nodes to walk - @type list of ast.Node - @yield node references as determined by the ast.walk() function - @ytype ast.Node - """ - for node in nodes: - yield from ast.walk(node) - - def __getNamesFromTuple(self, node): - """ - Private method to get the names from an ast.Tuple node. - - @param node ast node to be processed - @type ast.Tuple - @yield names - @ytype str - """ - for dim in node.elts: - if isinstance(dim, ast.Name): - yield dim.id - elif isinstance(dim, ast.Tuple): - yield from self.__getNamesFromTuple(dim) - - def __getDictCompLoopAndNamedExprVarNames(self, node): - """ - Private method to get the names of comprehension loop variables. - - @param node ast node to be processed - @type ast.DictComp - @yield loop variable names - @ytype str - """ - finder = NamedExprFinder() - for gen in node.generators: - if isinstance(gen.target, ast.Name): - yield gen.target.id - elif isinstance(gen.target, ast.Tuple): - yield from self.__getNamesFromTuple(gen.target) - - finder.visit(gen.ifs) - - yield from finder.getNames().keys() - - def __inClassInit(self): - """ - Private method to check, if we are inside an '__init__' method. - - @return flag indicating being within the '__init__' method - @rtype bool - """ - return ( - len(self.contexts) >= 2 - and isinstance(self.contexts[-2].node, ast.ClassDef) - and isinstance(self.contexts[-1].node, ast.FunctionDef) - and self.contexts[-1].node.name == "__init__" - ) - - def visit_Return(self, node): - """ - Public method to handle 'Return' nodes. - - @param node reference to the node to be processed - @type ast.Return - """ - if self.__inClassInit() and node.value is not None: - self.violations.append((node, "M-537")) - - self.generic_visit(node) - - def visit_Yield(self, node): - """ - Public method to handle 'Yield' nodes. - - @param node reference to the node to be processed - @type ast.Yield - """ - if self.__inClassInit(): - self.violations.append((node, "M-537")) - - self.generic_visit(node) - - def visit_YieldFrom(self, node) -> None: - """ - Public method to handle 'YieldFrom' nodes. - - @param node reference to the node to be processed - @type ast.YieldFrom - """ - if self.__inClassInit(): - self.violations.append((node, "M-537")) - - self.generic_visit(node) - - def visit(self, node): - """ - Public method to traverse a given AST node. - - @param node AST node to be traversed - @type ast.Node - """ - 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() - - if isContextful: - self.contexts.pop() - - self.__checkForM518(node) - - def visit_ExceptHandler(self, node): - """ - Public method to handle exception handlers. - - @param node reference to the node to be processed - @type ast.ExceptHandler - """ - if node.type is None: - # bare except is handled by pycodestyle already - self.generic_visit(node) - return - - oldM540CaughtException = self.__M540CaughtException - if node.name is None: - self.__M540CaughtException = None - else: - self.__M540CaughtException = M540CaughtException(node.name, False) - - names = self.__checkForM513_M514_M529_M530(node) - - if "BaseException" in names and not ExceptBaseExceptionVisitor(node).reRaised(): - self.violations.append((node, "M-536")) - - self.generic_visit(node) - - if ( - self.__M540CaughtException is not None - and self.__M540CaughtException.hasNote - ): - self.violations.append((node, "M-540")) - self.__M540CaughtException = oldM540CaughtException - - def visit_UAdd(self, node): - """ - Public method to handle unary additions. - - @param node reference to the node to be processed - @type ast.UAdd - """ - 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, "M-502")) - - 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 - """ - isM540AddNote = False - - if isinstance(node.func, ast.Attribute): - self.__checkForM505(node) - isM540AddNote = self.__checkForM540AddNote(node.func) - else: - with contextlib.suppress(AttributeError, IndexError): - # bad super() call - 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__" - ): - self.violations.append((node, "M-582")) - - # bad getattr and setattr - if ( - node.func.id in ("getattr", "hasattr") - and node.args[1].value == "__call__" - ): - self.violations.append((node, "M-504")) - if ( - node.func.id == "getattr" - and len(node.args) == 2 - and self.__isIdentifier(node.args[1]) - and iskeyword(AstUtilities.getValue(node.args[1])) - ): - self.violations.append((node, "M-509")) - elif ( - node.func.id == "setattr" - and len(node.args) == 3 - and self.__isIdentifier(node.args[1]) - and iskeyword(AstUtilities.getValue(node.args[1])) - ): - self.violations.append((node, "M-510")) - - self.__checkForM526(node) - - self.__checkForM528(node) - self.__checkForM534(node) - self.__checkForM539(node) - - # no need for copying, if used in nested calls it will be set to None - currentM540CaughtException = self.__M540CaughtException - if not isM540AddNote: - self.__checkForM540Usage(node.args) - self.__checkForM540Usage(node.keywords) - - self.generic_visit(node) - - if isM540AddNote: - # Avoid nested calls within the parameter list using the variable itself. - # e.g. `e.add_note(str(e))` - self.__M540CaughtException = currentM540CaughtException - - def visit_Module(self, node): - """ - Public method to handle a module node. - - @param node reference to the node to be processed - @type ast.Module - """ - self.generic_visit(node) - - def visit_Assign(self, node): - """ - Public method to handle assignments. - - @param node reference to the node to be processed - @type ast.Assign - """ - self.__checkForM540Usage(node.value) - 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, "M-503")) - - 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.__checkForM520(node) - self.__checkForM523(node) - self.__checkForM531(node) - self.__checkForM569(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.__checkForM520(node) - self.__checkForM523(node) - self.__checkForM531(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.__checkForM535(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) - - 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 - ): - self.violations.append((node, "M-511")) - - self.generic_visit(node) - - def visit_AsyncFunctionDef(self, node): - """ - Public method to handle async function definitions. - - @param node reference to the node to be processed - @type ast.AsyncFunctionDef - """ - self.__checkForM506_M508(node) - - 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.__checkForM506_M508(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.__checkForM521(node) - self.__checkForM524_M527(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_TryStar(self, node): - """ - Public method to handle 'except*' statements. - - @param node reference to the node to be processed - @type ast.TryStar - """ - outerTryStar = self.__inTryStar - self.__inTryStar = "*" - self.visit_Try(node) - self.__inTryStar = outerTryStar - - def visit_Compare(self, node): - """ - Public method to handle comparison statements. - - @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 - """ - if node.exc is None: - self.__M540CaughtException = None - else: - self.__checkForM540Usage(node.exc) - self.__checkForM540Usage(node.cause) - 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) - - 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, "M-581")) - - def visit_AnnAssign(self, node): - """ - Public method to check annotated assign statements. - - @param node reference to the node to be processed - @type ast.AnnAssign - """ - self.__checkForM532(node) - self.__checkForM540Usage(node.value) - - self.generic_visit(node) - - def visit_Import(self, node): - """ - Public method to check imports. - - @param node reference to the node to be processed - @type ast.Import - """ - self.__checkForM505(node) - - self.generic_visit(node) - - def visit_ImportFrom(self, node): - """ - Public method to check from imports. - - @param node reference to the node to be processed - @type ast.Import - """ - self.visit_Import(node) - - def visit_Set(self, node): - """ - Public method to check a set. - - @param node reference to the node to be processed - @type ast.Set - """ - self.__checkForM533(node) - - self.generic_visit(node) - - def visit_Dict(self, node): - """ - Public method to check a dictionary. - - @param node reference to the node to be processed - @type ast.Dict - """ - self.__checkForM541(node) - - self.generic_visit(node) - - def __checkForM505(self, node): - """ - Private method to check the use of *strip(). - - @param node reference to the node to be processed - @type ast.Call - """ - if isinstance(node, ast.Import): - for name in node.names: - self.__M505Imports.add(name.asname or name.name) - elif isinstance(node, ast.ImportFrom): - for name in node.names: - self.__M505Imports.add(f"{node.module}.{name.name or name.asname}") - elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): - if node.func.attr not in ("lstrip", "rstrip", "strip"): - return # method name doesn't match - - if ( - isinstance(node.func.value, ast.Name) - and node.func.value.id in self.__M505Imports - ): - return # method is being run on an imported module - - if len(node.args) != 1 or not AstUtilities.isString(node.args[0]): - return # used arguments don't match the builtin strip - - value = AstUtilities.getValue(node.args[0]) - if len(value) == 1: - return # stripping just one character - - if len(value) == len(set(value)): - return # no characters appear more than once - - self.violations.append((node, "M-505")) - - def __checkForM506_M508(self, node): - """ - Private method to check the use of mutable literals, comprehensions and calls. - - @param node reference to the node to be processed - @type ast.AsyncFunctionDef or ast.FunctionDef - """ - visitor = FunctionDefDefaultsVisitor("M-506", "M-508") - visitor.visit(node.args.defaults + node.args.kw_defaults) - self.violations.extend(visitor.errors) - - def __checkForM507(self, node): - """ - Private method to check for unused loop variables. - - @param node reference to the node to be processed - @type ast.For or ast.AsyncFor - """ - targets = NameFinder() - targets.visit(node.target) - ctrlNames = set(filter(lambda s: not s.startswith("_"), targets.getNames())) - body = NameFinder() - for expr in node.body: - body.visit(expr) - usedNames = set(body.getNames()) - for name in sorted(ctrlNames - usedNames): - n = targets.getNames()[name][0] - self.violations.append((n, "M-507", 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, "M-512", self.__inTryStar)) - - 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 __checkForM513_M514_M529_M530(self, node): - """ - Private method to check various exception handler situations. - - @param node reference to the node to be processed - @type ast.ExceptHandler - @return list of exception handler names - @rtype list of str - """ - handlers = self.__flattenExcepthandler(node.type) - names = [] - badHandlers = [] - ignoredHandlers = [] - - for handler in handlers: - if isinstance(handler, (ast.Name, ast.Attribute)): - name = self.toNameStr(handler) - if name is None: - ignoredHandlers.append(handler) - else: - names.append(name) - elif isinstance(handler, (ast.Call, ast.Starred)): - ignoredHandlers.append(handler) - else: - badHandlers.append(handler) - if badHandlers: - self.violations.append((node, "M-530")) - if len(names) == 0 and not badHandlers and not ignoredHandlers: - self.violations.append((node, "M-529", self.__inTryStar)) - elif ( - len(names) == 1 - and not badHandlers - and not ignoredHandlers - and isinstance(node.type, ast.Tuple) - ): - self.violations.append((node, "M-513", *names, self.__inTryStar)) - else: - maybeError = self.__checkRedundantExcepthandlers( - names, node, self.__inTryStar - ) - if maybeError is not None: - self.violations.append(maybeError) - return names - - 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, "M-515")) - - 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, "M-516")) - - def __checkForM517(self, node): - """ - Private method to check for use of the evil syntax - 'with assertRaises(Exception): or 'with pytest.raises(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 ( - ( - isinstance(itemContext.func, ast.Attribute) - and ( - itemContext.func.attr == "assertRaises" - or ( - itemContext.func.attr == "raises" - and isinstance(itemContext.func.value, ast.Name) - and itemContext.func.value.id == "pytest" - and "match" not in (kwd.arg for kwd in itemContext.keywords) - ) - ) - ) - or ( - isinstance(itemContext.func, ast.Name) - and itemContext.func.id == "raises" - and isinstance(itemContext.func.ctx, ast.Load) - and "pytest.raises" in self.__M505Imports - and "match" not in (kwd.arg for kwd in itemContext.keywords) - ) - ) - and len(itemContext.args) == 1 - and isinstance(itemContext.args[0], ast.Name) - and itemContext.args[0].id in ("Exception", "BaseException") - and not item.optional_vars - ): - self.violations.append((node, "M-517")) - - def __checkForM518(self, node): - """ - Private method to check for useless expressions. - - @param node reference to the node to be processed - @type ast.FunctionDef - """ - if not isinstance(node, ast.Expr): - return - - if isinstance( - node.value, - (ast.List, ast.Set, ast.Dict, ast.Tuple), - ) or ( - isinstance(node.value, ast.Constant) - and ( - isinstance( - node.value.value, - (int, float, complex, bytes, bool), - ) - or node.value.value is None - ) - ): - self.violations.append((node, "M-518", node.value.__class__.__name__)) - - 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(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], "M-519")) - 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, "M-520")) - - 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, "M-521")) - - 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, "M-522")) - - 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 - """ - safe_functions = [] - suspiciousVariables = [] - for node in ast.walk(loopNode): - # check if function is immediately consumed to avoid false alarm - if isinstance(node, ast.Call): - # check for filter&reduce - if ( - isinstance(node.func, ast.Name) - and node.func.id in ("filter", "reduce", "map") - ) or ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "reduce" - and isinstance(node.func.value, ast.Name) - and node.func.value.id == "functools" - ): - for arg in node.args: - if isinstance(arg, BugBearVisitor.FUNCTION_NODES): - safe_functions.append(arg) - - # check for key= - for keyword in node.keywords: - if keyword.arg == "key" and isinstance( - keyword.value, BugBearVisitor.FUNCTION_NODES - ): - safe_functions.append(keyword.value) - - # mark `return lambda: x` as safe - # does not (currently) check inner lambdas in a returned expression - # e.g. `return (lambda: x, ) - if isinstance(node, ast.Return) and isinstance( - node.value, BugBearVisitor.FUNCTION_NODES - ): - safe_functions.append(node.value) - - # find unsafe functions - if ( - isinstance(node, BugBearVisitor.FUNCTION_NODES) - and node not in safe_functions - ): - argnames = { - arg.arg for arg in ast.walk(node.args) if isinstance(arg, ast.arg) - } - if isinstance(node, ast.Lambda): - bodyNodes = ast.walk(node.body) - else: - bodyNodes = itertools.chain.from_iterable(map(ast.walk, node.body)) - errors = [] - for name in bodyNodes: - if isinstance(name, ast.Name) and name.id not in argnames: - if isinstance(name.ctx, ast.Load): - errors.append((name.lineno, name.col_offset, name.id, name)) - elif isinstance(name.ctx, ast.Store): - argnames.add(name.id) - for err in errors: - if err[2] not in argnames and err not in self.__M523Seen: - self.__M523Seen.add(err) # dedupe across nested loops - suspiciousVariables.append(err) - - if suspiciousVariables: - reassignedInLoop = set(self.__getAssignedNames(loopNode)) - - for err in sorted(suspiciousVariables): - if reassignedInLoop.issuperset(err[2]): - self.violations.append((err[3], "M-523", err[2])) - - def __checkForM524_M527(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_D-234r__ - - def isAbcClass(value, name="ABC"): - if isinstance(value, ast.keyword): - return value.arg == "metaclass" and isAbcClass(value.value, "ABCMeta") - - # class foo(ABC) - # class foo(abc.ABC) - return (isinstance(value, ast.Name) and value.id == name) or ( - isinstance(value, ast.Attribute) - and value.attr == name - 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" - ) - - def isOverload(expr): - return (isinstance(expr, ast.Name) and expr.id == "overload") or ( - isinstance(expr, ast.Attribute) and expr.attr == "overload" - ) - - def emptyBody(body): - def isStrOrEllipsis(node): - return isinstance(node, ast.Constant) and ( - node.value is Ellipsis or isinstance(node.value, str) - ) - - # Function body consist solely of `pass`, `...`, and/or (doc)string literals - return all( - isinstance(stmt, ast.Pass) - or (isinstance(stmt, ast.Expr) and isStrOrEllipsis(stmt.value)) - for stmt in body - ) - - # don't check multiple inheritance - if len(node.bases) + len(node.keywords) > 1: - return - - # only check abstract classes - if not any(map(isAbcClass, (*node.bases, *node.keywords))): - return - - hasMethod = False - hasAbstractMethod = False - - if not any(map(isAbcClass, (*node.bases, *node.keywords))): - return - - for stmt in node.body: - # Ignore abc's that declares a class attribute that must be set - if isinstance(stmt, ast.AnnAssign) and stmt.value is None: - hasAbstractMethod = True - continue - - # only check function defs - if not isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): - continue - hasMethod = True - - hasAbstractDecorator = any(map(isAbstractDecorator, stmt.decorator_list)) - - hasAbstractMethod |= hasAbstractDecorator - - if ( - not hasAbstractDecorator - and emptyBody(stmt.body) - and not any(map(isOverload, stmt.decorator_list)) - ): - self.violations.append((stmt, "M-527", stmt.name)) - - if hasMethod and not hasAbstractMethod: - self.violations.append((node, "M-524", 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(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(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, "M-525", duplicate, self.__inTryStar)) - - def __checkForM526(self, node): - """ - Private method to check for Star-arg unpacking after keyword argument. - - @param node reference to the node to be processed - @type ast.Call - """ - if not node.keywords: - return - - starreds = [arg for arg in node.args if isinstance(arg, ast.Starred)] - if not starreds: - return - - firstKeyword = node.keywords[0].value - for starred in starreds: - if (starred.lineno, starred.col_offset) > ( - firstKeyword.lineno, - firstKeyword.col_offset, - ): - self.violations.append((node, "M-526")) - - def __checkForM528(self, node): - """ - Private method to check for warn without stacklevel. - - @param node reference to the node to be processed - @type ast.Call - """ - if ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "warn" - and isinstance(node.func.value, ast.Name) - and node.func.value.id == "warnings" - and not any(kw.arg == "stacklevel" for kw in node.keywords) - and len(node.args) < 3 - and not any(isinstance(a, ast.Starred) for a in node.args) - and not any(kw.arg is None for kw in node.keywords) - ): - self.violations.append((node, "M-528")) - - def __checkForM531(self, loopNode): - """ - Private method to check that 'itertools.groupby' isn't iterated over more than - once. - - A warning is emitted when the generator returned by 'groupby()' is used - more than once inside a loop body or when it's used in a nested loop. - - @param loopNode reference to the node to be processed - @type ast.For or ast.AsyncFor - """ - # for <loop_node.target> in <loop_node.iter>: ... - if isinstance(loopNode.iter, ast.Call): - node = loopNode.iter - if (isinstance(node.func, ast.Name) and node.func.id in ("groupby",)) or ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "groupby" - and isinstance(node.func.value, ast.Name) - and node.func.value.id == "itertools" - ): - # We have an invocation of groupby which is a simple unpacking - if isinstance(loopNode.target, ast.Tuple) and isinstance( - loopNode.target.elts[1], ast.Name - ): - groupName = loopNode.target.elts[1].id - else: - # Ignore any 'groupby()' invocation that isn't unpacked - return - - numUsages = 0 - for node in self.__walkList(loopNode.body): - # Handled nested loops - if isinstance(node, ast.For): - for nestedNode in self.__walkList(node.body): - if ( - isinstance(nestedNode, ast.Name) - and nestedNode.id == groupName - ): - self.violations.append((nestedNode, "M-531")) - - # Handle multiple uses - if isinstance(node, ast.Name) and node.id == groupName: - numUsages += 1 - if numUsages > 1: - self.violations.append((nestedNode, "M-531")) - - def __checkForM532(self, node): - """ - Private method to check for possible unintentional typing annotation. - - @param node reference to the node to be processed - @type ast.AnnAssign - """ - if ( - node.value is None - and hasattr(node.target, "value") - and isinstance(node.target.value, ast.Name) - and ( - isinstance(node.target, ast.Subscript) - or ( - isinstance(node.target, ast.Attribute) - and node.target.value.id != "self" - ) - ) - ): - self.violations.append((node, "M-532")) - - def __checkForM533(self, node): - """ - Private method to check a set for duplicate items. - - @param node reference to the node to be processed - @type ast.Set - """ - seen = set() - for elt in node.elts: - if not isinstance(elt, ast.Constant): - continue - if elt.value in seen: - self.violations.append((node, "M-533", repr(elt.value))) - else: - seen.add(elt.value) - - def __checkForM534(self, node): - """ - Private method to check that re.sub/subn/split arguments flags/count/maxsplit - are passed as keyword arguments. - - @param node reference to the node to be processed - @type ast.Call - """ - if not isinstance(node.func, ast.Attribute): - return - func = node.func - if not isinstance(func.value, ast.Name) or func.value.id != "re": - return - - def check(numArgs, paramName): - if len(node.args) > numArgs: - arg = node.args[numArgs] - self.violations.append((arg, "M-534", func.attr, paramName)) - - if func.attr in ("sub", "subn"): - check(3, "count") - elif func.attr == "split": - check(2, "maxsplit") - - def __checkForM535(self, node): - """ - Private method to check that a static key isn't used in a dict comprehension. - - Record a warning if a likely unchanging key is used - either a constant, - or a variable that isn't coming from the generator expression. - - @param node reference to the node to be processed - @type ast.DictComp - """ - if isinstance(node.key, ast.Constant): - self.violations.append((node, "M-535", node.key.value)) - elif isinstance( - node.key, ast.Name - ) and node.key.id not in self.__getDictCompLoopAndNamedExprVarNames(node): - self.violations.append((node, "M-535", node.key.id)) - - def __checkForM539(self, node): - """ - Private method to check for correct ContextVar usage. - - @param node reference to the node to be processed - @type ast.Call - """ - if not ( - (isinstance(node.func, ast.Name) and node.func.id == "ContextVar") - or ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "ContextVar" - and isinstance(node.func.value, ast.Name) - and node.func.value.id == "contextvars" - ) - ): - return - - # ContextVar only takes one kw currently, but better safe than sorry - for kw in node.keywords: - if kw.arg == "default": - break - else: - return - - visitor = FunctionDefDefaultsVisitor("M-539", "M-539") - visitor.visit(kw.value) - self.violations.extend(visitor.errors) - - def __checkForM540AddNote(self, node): - """ - Private method to check add_note usage. - - @param node reference to the node to be processed - @type ast.Attribute - @return flag - @rtype bool - """ - if ( - node.attr == "add_note" - and isinstance(node.value, ast.Name) - and self.__M540CaughtException - and node.value.id == self.__M540CaughtException.name - ): - self.__M540CaughtException.hasNote = True - return True - - return False - - def __checkForM540Usage(self, node): - """ - Private method to check the usage of exceptions with added note. - - @param node reference to the node to be processed - @type ast.expr or None - """ # noqa: D-234y - - def superwalk(node: ast.AST | list[ast.AST]): - """ - Function to walk an AST node or a list of AST nodes. - - @param node reference to the node or a list of nodes to be processed - @type ast.AST or list[ast.AST] - @yield next node to be processed - @ytype ast.AST - """ - if isinstance(node, list): - for n in node: - yield from ast.walk(n) - else: - yield from ast.walk(node) - - if not self.__M540CaughtException or node is None: - return - - for n in superwalk(node): - if isinstance(n, ast.Name) and n.id == self.__M540CaughtException.name: - self.__M540CaughtException = None - break - - def __checkForM541(self, node): - """ - Private method to check for duplicate key value pairs in a dictionary literal. - - @param node reference to the node to be processed - @type ast.Dict - """ # noqa: D-234r - - def convertToValue(item): - """ - Function to extract the value of a given item. - - @param item node to extract value from - @type ast.Ast - @return value of the node - @rtype Any - """ - if isinstance(item, ast.Constant): - return item.value - elif isinstance(item, ast.Tuple): - return tuple(convertToValue(i) for i in item.elts) - elif isinstance(item, ast.Name): - return M541VariableKeyType(item.id) - else: - return M541UnhandledKeyType() - - keys = [convertToValue(key) for key in node.keys] - keyCounts = Counter(keys) - duplicateKeys = [key for key, count in keyCounts.items() if count > 1] - for key in duplicateKeys: - keyIndices = [i for i, iKey in enumerate(keys) if iKey == key] - seen = set() - for index in keyIndices: - value = convertToValue(node.values[index]) - if value in seen: - keyNode = node.keys[index] - self.violations.append((keyNode, "M-541")) - seen.add(value) - - def __checkForM569(self, node): - """ - Private method to check for changes to a loop's mutable iterable. - - @param node loop node to be checked - @type ast.For - """ - if isinstance(node.iter, ast.Name): - name = self.toNameStr(node.iter) - elif isinstance(node.iter, ast.Attribute): - name = self.toNameStr(node.iter) - else: - return - checker = M569Checker(name, self) - checker.visit(node.body) - for mutation in checker.mutations: - self.violations.append((mutation, "M-569")) - - -class M569Checker(ast.NodeVisitor): - """ - Class traversing a 'for' loop body to check for modifications to a loop's - mutable iterable. - """ - - # https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types - MUTATING_FUNCTIONS = ( - "append", - "sort", - "reverse", - "remove", - "clear", - "extend", - "insert", - "pop", - "popitem", - ) - - def __init__(self, name, bugbear): - """ - Constructor - - @param name name of the iterator - @type str - @param bugbear reference to the bugbear visitor - @type BugBearVisitor - """ - self.__name = name - self.__bb = bugbear - self.mutations = [] - - def visit_Delete(self, node): - """ - Public method handling 'Delete' nodes. - - @param node reference to the node to be processed - @type ast.Delete - """ - for target in node.targets: - if isinstance(target, ast.Subscript): - name = self.__bb.toNameStr(target.value) - elif isinstance(target, (ast.Attribute, ast.Name)): - name = self.__bb.toNameStr(target) - else: - name = "" # fallback - self.generic_visit(target) - - if name == self.__name: - self.mutations.append(node) - - def visit_Call(self, node): - """ - Public method handling 'Call' nodes. - - @param node reference to the node to be processed - @type ast.Call - """ - if isinstance(node.func, ast.Attribute): - name = self.__bb.toNameStr(node.func.value) - functionObject = name - functionName = node.func.attr - - if ( - functionObject == self.__name - and functionName in self.MUTATING_FUNCTIONS - ): - self.mutations.append(node) - - self.generic_visit(node) - - def visit(self, node): - """ - Public method to inspect an ast node. - - Like super-visit but supports iteration over lists. - - @param node AST node to be traversed - @type TYPE - @return reference to the last processed node - @rtype ast.Node - """ - if not isinstance(node, list): - return super().visit(node) - - for elem in node: - super().visit(elem) - return node - - -class ExceptBaseExceptionVisitor(ast.NodeVisitor): - """ - Class to determine, if a 'BaseException' is re-raised. - """ - - def __init__(self, exceptNode): - """ - Constructor - - @param exceptNode exception node to be inspected - @type ast.ExceptHandler - """ - super().__init__() - self.__root = exceptNode - self.__reRaised = False - - def reRaised(self) -> bool: - """ - Public method to check, if the exception is re-raised. - - @return flag indicating a re-raised exception - @rtype bool - """ - self.visit(self.__root) - return self.__reRaised - - def visit_Raise(self, node): - """ - Public method to handle 'Raise' nodes. - - If we find a corresponding `raise` or `raise e` where e was from - `except BaseException as e:` then we mark re_raised as True and can - stop scanning. - - @param node reference to the node to be processed - @type ast.Raise - """ - if node.exc is None or ( - isinstance(node.exc, ast.Name) and node.exc.id == self.__root.name - ): - self.__reRaised = True - return - - super().generic_visit(node) - - def visit_ExceptHandler(self, node: ast.ExceptHandler): - """ - Public method to handle 'ExceptHandler' nodes. - - @param node reference to the node to be processed - @type ast.ExceptHandler - """ - if node is not self.__root: - return # entered a nested except - stop searching - - super().generic_visit(node) - - -class NameFinder(ast.NodeVisitor): - """ - 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 - """ - self.__names.setdefault(node.id, []).append(node) - - def visit(self, node): - """ - Public method to traverse a given AST node. - - @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: - return 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 - """ - return self.__names - - -class NamedExprFinder(ast.NodeVisitor): - """ - Class to extract names defined through an ast.NamedExpr. - """ - - def __init__(self): - """ - Constructor - """ - super().__init__() - - self.__names = {} - - def visit_NamedExpr(self, node: ast.NamedExpr): - """ - Public method handling 'NamedExpr' nodes. - - @param node reference to the node to be processed - @type ast.NamedExpr - """ - self.__names.setdefault(node.target.id, []).append(node.target) - - self.generic_visit(node) - - def visit(self, node): - """ - Public method to traverse a given AST node. - - Like super-visit but supports iteration over lists. - - @param node AST node to be traversed - @type TYPE - @return reference to the last processed node - @rtype ast.Node - """ - if not isinstance(node, list): - super().visit(node) - - for elem in node: - super().visit(elem) - - return node - - def getNames(self): - """ - Public method to return the extracted names and Name nodes. - - @return dictionary containing the names as keys and the list of nodes - @rtype dict - """ - return self.__names - - -class FunctionDefDefaultsVisitor(ast.NodeVisitor): - """ - Class used by M506, M508 and M539. - """ - - def __init__( - self, - errorCodeCalls, # M506 or M539 - errorCodeLiterals, # M508 or M539 - ): - """ - Constructor - - @param errorCodeCalls error code for ast.Call nodes - @type str - @param errorCodeLiterals error code for literal nodes - @type str - """ - self.__errorCodeCalls = errorCodeCalls - self.__errorCodeLiterals = errorCodeLiterals - for nodeType in BugbearMutableLiterals + BugbearMutableComprehensions: - setattr( - self, f"visit_{nodeType}", self.__visitMutableLiteralOrComprehension - ) - self.errors = [] - self.__argDepth = 0 - - super().__init__() - - def __visitMutableLiteralOrComprehension(self, node): - """ - Private method to flag mutable literals and comprehensions. - - @param node AST node to be processed - @type ast.Dict, ast.List, ast.Set, ast.ListComp, ast.DictComp or ast.SetComp - """ - # Flag M506 if mutable literal/comprehension is not nested. - # We only flag these at the top level of the expression as we - # cannot easily guarantee that nested mutable structures are not - # made immutable by outer operations, so we prefer no false positives. - # e.g. - # >>> def this_is_fine(a=frozenset({"a", "b", "c"})): ... - # - # >>> def this_is_not_fine_but_hard_to_detect(a=(lambda x: x)([1, 2, 3])) - # - # We do still search for cases of B008 within mutable structures though. - if self.__argDepth == 1: - self.errors.append((node, self.__errorCodeCalls)) - - # Check for nested functions. - self.generic_visit(node) - - def visit_Call(self, node): - """ - Public method to process Call nodes. - - @param node AST node to be processed - @type ast.Call - """ - callPath = ".".join(composeCallPath(node.func)) - if callPath in BugbearMutableCalls: - self.errors.append((node, self.__errorCodeCalls)) - self.generic_visit(node) - return - - if callPath in BugbearImmutableCalls: - self.generic_visit(node) - return - - # Check if function call is actually a float infinity/NaN literal - if callPath == "float" and len(node.args) == 1: - try: - value = float(ast.literal_eval(node.args[0])) - except Exception: # secok - pass - else: - if math.isfinite(value): - self.errors.append((node, self.__errorCodeLiterals)) - else: - self.errors.append((node, self.__errorCodeLiterals)) - - # Check for nested functions. - self.generic_visit(node) - - def visit_Lambda(self, node): - """ - Public method to process Lambda nodes. - - @param node AST node to be processed - @type ast.Lambda - """ - # Don't recurse into lambda expressions - # as they are evaluated at call time. - pass - - def visit(self, node): - """ - Public method to traverse an AST node or a list of AST nodes. - - This is an extended method that can also handle a list of AST nodes. - - @param node AST node or list of AST nodes to be processed - @type ast.AST or list of ast.AST - """ - self.__argDepth += 1 - if isinstance(node, list): - for elem in node: - if elem is not None: - super().visit(elem) - else: - super().visit(node) - self.__argDepth -= 1 - - -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. - """ - - 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.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 - """ - if not self.__stack: - return - - self.generic_visit(node.value) - - target = node.targets[0] - 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 - """ - if isinstance(node, ast.Tuple): - 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 - - 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 - ) - - def __resultExists(self): - """ - Private method to check the existance of a return result. - - @return flag indicating the existence of a return result - @rtype bool - """ - for node in self.returns: - 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. - """ - for node in self.returns: - if not node.value: - self.violations.append((node, "M-832")) - - def __checkUnnecessaryReturnNone(self): - """ - Private method to check for an unnecessary 'return None' statement. - """ - for node in self.returns: - if self.__isNone(node.value): - self.violations.append((node, "M-831")) - - def __checkImplicitReturn(self, node): - """ - Private method to check for an implicit return statement. - - @param node reference to the node to check - @type ast.AST - """ - if isinstance(node, ast.If): - if not node.body or not node.orelse: - self.violations.append((node, "M-833")) - 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, "M-833")) - - 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, "M-834")) - return - - if self.__hasRefsBeforeNextAssign(varname, returnLineno): - return - - self.violations.append((node, "M-834")) - - 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 - @type int - @return flag indicating the existence of references - @rtype bool - """ - 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 modeled 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 - @type str - @return keyword node - @rtype ast.AST - """ - 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" - ) - - # 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" - ) - - if isDateTimeClass: - 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 - ) - - # 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 - ) - - if not (isCase1 or isCase2): - self.violations.append((node, "M-301")) - - 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 - ) - - # 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 - ) - - if not (isCase1 or isCase2): - self.violations.append((node, "M-321")) - - elif node.func.attr == "date": - self.violations.append((node, "M-311")) - - if isDateTimeClass or isDateTimeModuleAndClass: - if node.func.attr == "today": - self.violations.append((node, "M-302")) - - elif node.func.attr == "utcnow": - self.violations.append((node, "M-303")) - - elif node.func.attr == "utcfromtimestamp": - self.violations.append((node, "M-304")) - - 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 - ) - ) - - # 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 - ) - - if not (isCase1 or isCase2): - self.violations.append((node, "M-305")) - - 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 - ) - ) - - # 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 - ) - - if not (isCase1 or isCase2): - self.violations.append((node, "M-306")) - - 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): - 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 - ) - - if not isCase1: - self.violations.append((node, "M-307")) - - elif node.func.attr == "fromordinal": - self.violations.append((node, "M-308")) - - # date.something() - 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" - ) - - if isDateClass or isDateModuleAndClass: - if node.func.attr == "today": - self.violations.append((node, "M-312")) - - elif node.func.attr == "fromtimestamp": - self.violations.append((node, "M-313")) - - elif node.func.attr == "fromordinal": - self.violations.append((node, "M-314")) - - elif node.func.attr == "fromisoformat": - self.violations.append((node, "M-315")) - - self.generic_visit(node) - - -class SysVersionVisitor(ast.NodeVisitor): - """ - Class implementing a node visitor to check the use of sys.version and - sys.version_info. - - Note: This class is modeled after flake8-2020 v1.8.1. - """ - - 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 - @type ast.Node - @return flag indicating a match - @rtype bool - """ - 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" - ): - 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 - @type int - @return flag indicating a match - @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 - ) - - def visit_Subscript(self, node): - """ - Public method to handle a subscript. - - @param node reference to the node to be processed - @type ast.Subscript - """ - if self.__isSysVersionUpperSlice(node, 1): - self.violations.append((node.value, "M-423")) - elif self.__isSysVersionUpperSlice(node, 3): - self.violations.append((node.value, "M-401")) - 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.violations.append((node.value, "M-402")) - 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.violations.append((node.value, "M-421")) - - 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 - ): - self.violations.append((node.left, "M-411")) - 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]) - ): - if len(AstUtilities.getValue(node.comparators[0])) == 1: - errorCode = "M-422" - else: - errorCode = "M-403" - 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]) - ): - self.violations.append((node, "M-413")) - 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]) - ): - self.violations.append((node, "M-414")) - - 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" - ): - self.violations.append((node, "M-412")) - - 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": - self.violations.append((node, "M-412")) - - self.generic_visit(node) - - -class DefaultMatchCaseVisitor(ast.NodeVisitor): - """ - Class implementing a node visitor to check default match cases. - - Note: This class is modeled after flake8-spm v0.0.1. - """ - - def __init__(self): - """ - Constructor - """ - super().__init__() - - self.violations = [] - - def visit_Match(self, node): - """ - Public method to handle Match nodes. - - @param node reference to the node to be processed - @type ast.Match - """ - for badNode, issueCode in self.__badNodes(node): - self.violations.append((badNode, issueCode)) - - self.generic_visit(node) - - def __badNodes(self, node): - """ - Private method to yield bad match nodes. - - @param node reference to the node to be processed - @type ast.Match - @yield tuple containing a reference to bad match case node and the corresponding - issue code - @ytype tyuple of (ast.AST, str) - """ - for case in node.cases: - if self.__emptyMatchDefault(case): - if self.__lastStatementDoesNotRaise(case): - yield self.__findBadNode(case), "M-901" - elif self.__returnPrecedesExceptionRaising(case): - yield self.__findBadNode(case), "M-902" - - def __emptyMatchDefault(self, case): - """ - Private method to check for an empty default match case. - - @param case reference to the node to be processed - @type ast.match_case - @return flag indicating an empty default match case - @rtype bool - """ - pattern = case.pattern - return isinstance(pattern, ast.MatchAs) and ( - pattern.pattern is None - or ( - isinstance(pattern.pattern, ast.MatchAs) - and pattern.pattern.pattern is None - ) - ) - - def __lastStatementDoesNotRaise(self, case): - """ - Private method to check that the last case statement does not raise an - exception. - - @param case reference to the node to be processed - @type ast.match_case - @return flag indicating that the last case statement does not raise an - exception - @rtype bool - """ - return not isinstance(case.body[-1], ast.Raise) - - def __returnPrecedesExceptionRaising(self, case): - """ - Private method to check that no return precedes an exception raising. - - @param case reference to the node to be processed - @type ast.match_case - @return flag indicating that a return precedes an exception raising - @rtype bool - """ - returnIndex = -1 - raiseIndex = -1 - for index, body in enumerate(case.body): - if isinstance(body, ast.Return): - returnIndex = index - elif isinstance(body, ast.Raise): - raiseIndex = index - return returnIndex >= 0 and returnIndex < raiseIndex - - def __findBadNode(self, case) -> ast.AST: - """ - Private method returning a reference to the bad node of a case node. - - @param case reference to the node to be processed - @type ast.match_case - @return reference to the bad node - @rtype ast.AST - """ - for body in case.body: - # Handle special case when return precedes exception raising. - # In this case the bad node is that with the return statement. - if isinstance(body, ast.Return): - return body - - return case.body[-1] - - -# -# eflag: noqa = M-891 + self.addErrorFromNode(*violation)