Fri, 01 Sep 2023 15:54:43 +0200
Updated some codestyle checkers for imports and too complex annotations.
# -*- coding: utf-8 -*- # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a checker for unused arguments, variables, ... . """ import ast import collections import copy import AstUtilities class UnusedChecker: """ Class implementing a checker for unused arguments, variables, ... . """ Codes = [ ## Unused Arguments "U100", "U101", ## Unused Globals "U200", ] def __init__(self, source, filename, tree, select, ignore, expected, repeat, args): """ Constructor @param source source code to be checked @type list of str @param filename name of the source file @type str @param tree AST tree of the source code @type ast.Module @param select list of selected codes @type list of str @param ignore list of codes to be ignored @type list of str @param expected list of expected codes @type list of str @param repeat flag indicating to report each occurrence of a code @type bool @param args dictionary of arguments for the various checks @type dict """ self.__select = tuple(select) self.__ignore = ("",) if select else tuple(ignore) self.__expected = expected[:] self.__repeat = repeat self.__filename = filename self.__source = source[:] self.__tree = copy.deepcopy(tree) self.__args = args # statistics counters self.counters = {} # collection of detected errors self.errors = [] checkersWithCodes = [ (self.__checkUnusedArguments, ("U100", "U101")), (self.__checkUnusedGlobals, ("U200",)), ] 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.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() ####################################################################### ## Unused Arguments ## ## adapted from: flake8-unused-arguments v0.0.13 ####################################################################### def __checkUnusedArguments(self): """ Private method to check function and method definitions for unused arguments. """ finder = FunctionFinder(self.__args["IgnoreNestedFunctions"]) finder.visit(self.__tree) for functionNode in finder.functionNodes(): decoratorNames = set(self.__getDecoratorNames(functionNode)) # ignore overload functions, it's not a surprise when they're empty if self.__args["IgnoreOverload"] and "overload" in decoratorNames: continue # ignore overridden functions if self.__args["IgnoreOverride"] and "override" in decoratorNames: continue # ignore abstractmethods, it's not a surprise when they're empty if self.__args["IgnoreAbstract"] and "abstractmethod" in decoratorNames: continue # ignore Qt slot methods if self.__args["IgnoreSlotMethods"] and ( "pyqtSlot" in decoratorNames or "Slot" in decoratorNames ): continue if self.__args["IgnoreEventHandlerMethods"] and self.__isEventHandlerMethod( functionNode ): continue # ignore stub functions if self.__args["IgnoreStubs"] and self.__isStubFunction(functionNode): continue # ignore lambdas if self.__args["IgnoreLambdas"] and isinstance(functionNode, ast.Lambda): continue # ignore __double_underscore_methods__() if self.__args["IgnoreDunderMethods"] and self.__isDunderMethod( functionNode ): continue for i, argument in self.__getUnusedArguments(functionNode): name = argument.arg if self.__args["IgnoreVariadicNames"]: if ( functionNode.args.vararg and functionNode.args.vararg.arg == name ): continue if functionNode.args.kwarg and functionNode.args.kwarg.arg == name: continue # ignore self or whatever the first argument is for a classmethod if i == 0 and ( name in ("self", "cls") or "classmethod" in decoratorNames ): continue lineNumber = argument.lineno offset = argument.col_offset errorCode = "U101" if name.startswith("_") else "U100" self.__error(lineNumber - 1, offset, errorCode, name) def __getDecoratorNames(self, functionNode): """ Private method to yield the decorator names of the function. @param functionNode reference to the node defining the function or lambda @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda @yield decorator name @ytype str """ if isinstance(functionNode, ast.Lambda): return for decorator in functionNode.decorator_list: if isinstance(decorator, ast.Name): yield decorator.id elif isinstance(decorator, ast.Attribute): yield decorator.attr elif isinstance(decorator, ast.Call): if isinstance(decorator.func, ast.Name): yield decorator.func.id else: yield decorator.func.attr def __isStubFunction(self, functionNode): """ Private method to check, if the given function node defines a stub function. @param functionNode reference to the node defining the function or lambda @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda @return flag indicating a stub function @rtype bool """ if isinstance(functionNode, ast.Lambda): return AstUtilities.isEllipsis(functionNode.body) statement = functionNode.body[0] if isinstance(statement, ast.Expr) and AstUtilities.isString(statement.value): if len(functionNode.body) > 1: # first statement is a docstring, let's skip it statement = functionNode.body[1] else: # it's a function with only a docstring, that's a stub return True if isinstance(statement, ast.Pass): return True if isinstance(statement, ast.Expr) and AstUtilities.isEllipsis(statement.value): return True if isinstance(statement, ast.Raise): # like 'raise NotImplementedError()' if ( isinstance(statement.exc, ast.Call) and hasattr(statement.exc.func, "id") and statement.exc.func.id == "NotImplementedError" ): return True # like 'raise NotImplementedError' elif ( isinstance(statement.exc, ast.Name) and hasattr(statement.exc, "id") and statement.exc.id == "NotImplementedError" ): return True return False def __isDunderMethod(self, functionNode): """ Private method to check, if the function node defines a special function. @param functionNode reference to the node defining the function or lambda @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda @return flag indicating a special function @rtype bool """ if isinstance(functionNode, ast.Lambda): return False if not hasattr(functionNode, "name"): return False name = functionNode.name return len(name) > 4 and name.startswith("__") and name.endswith("__") def __isEventHandlerMethod(self, functionNode): """ Private method to check, if the function node defines a Qt event handler. Qt event handler methods are assumed to end with 'Event' or have the name 'event' or 'eventFilter'. Only standard methodes (i.e. ast.FunctionDef) are assumed to be potential event handlers. @param functionNode reference to the node defining the function or lambda @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda @return flag indicating a Qt event handler method @rtype bool """ if isinstance(functionNode, (ast.Lambda, ast.AsyncFunctionDef)): return False if not hasattr(functionNode, "name"): return False name = functionNode.name return name.endswith("Event") or name in ("event", "eventFilter") def __getUnusedArguments(self, functionNode): """ Private method to get a list of unused arguments of the given function. @param functionNode reference to the node defining the function or lambda @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda @return list of tuples of the argument position and the argument @rtype list of tuples of (int, ast.arg) """ arguments = list(enumerate(self.__getArguments(functionNode))) class NameFinder(ast.NodeVisitor): """ Class to find the used argument names. """ def visit_Name(self, name): """ Public method to check a Name node. @param name reference to the name node to be checked @type ast.Name """ nonlocal arguments if isinstance(name.ctx, ast.Store): return arguments = [ (argIndex, arg) for argIndex, arg in arguments if arg.arg != name.id ] NameFinder().visit(functionNode) return arguments def __getArguments(self, functionNode): """ Private method to get all argument names of the given function. @param functionNode reference to the node defining the function or lambda @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda @return list of argument names @rtype list of ast.arg """ args = functionNode.args orderedArguments = [] # plain old args orderedArguments.extend(args.args) # *arg name if args.vararg is not None: orderedArguments.append(args.vararg) # *, key, word, only, args orderedArguments.extend(args.kwonlyargs) # **kwarg name if args.kwarg is not None: orderedArguments.append(args.kwarg) return orderedArguments ####################################################################### ## Unused Globals ## ## adapted from: flake8-unused-globals v0.1.10 ####################################################################### def __checkUnusedGlobals(self): """ Private method to check for unused global variables. """ errors = {} loadCounter = GlobalVariableLoadCounter() loadCounter.visit(self.__tree) globalVariables = self.__extractGlobalVariables() for varId, loads in loadCounter.getLoads(): if varId in globalVariables and loads == 0: storeInfo = loadCounter.getStoreInfo(varId) errorInfo = (storeInfo.lineno - 1, storeInfo.offset, "U200", varId) errors[varId] = errorInfo for node in self.__tree.body[::-1]: if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name) and target.id in errors: errors.pop(target.id) elif ( isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id in errors ): errors.pop(node.target.id) else: break if self.__args["IgnoreDunderGlobals"]: # eliminate some special cases for name in list(errors.keys()): if name.startswith("__") and name.endswith("__"): errors.pop(name) for varId in errors: self.__error(*errors[varId]) def __extractGlobalVariables(self): """ Private method to get the names of all global variables. @return set containing the defined global variable names @rtype set of str """ variables = set() for assignment in self.__tree.body: if isinstance(assignment, ast.Assign): for target in assignment.targets: if isinstance(target, ast.Name): variables.add(target.id) elif isinstance(assignment, ast.AnnAssign) and isinstance( assignment.target, ast.Name ): variables.add(assignment.target.id) return variables ####################################################################### ## Class used by 'Unused Arguments' ## ## adapted from: flake8-unused-arguments v0.0.13 ####################################################################### class FunctionFinder(ast.NodeVisitor): """ Class to find all defined functions and methods. """ def __init__(self, onlyTopLevel=False): """ Constructor @param onlyTopLevel flag indicating to search for top level functions only (defaults to False) @type bool (optional) """ super().__init__() self.__functions = [] self.__onlyTopLevel = onlyTopLevel def functionNodes(self): """ Public method to get the list of detected functions and lambdas. @return list of detected functions and lambdas @rtype list of ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda """ return self.__functions def __visitFunctionTypes(self, functionNode): """ Private method to handle an AST node defining a function or lambda. @param functionNode reference to the node defining a function or lambda @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda """ self.__functions.append(functionNode) if not self.__onlyTopLevel: if isinstance(functionNode, ast.Lambda): self.visit(functionNode.body) else: for obj in functionNode.body: self.visit(obj) visit_AsyncFunctionDef = visit_FunctionDef = visit_Lambda = __visitFunctionTypes ####################################################################### ## Class used by 'Unused Globals' ## ## adapted from: flake8-unused-globals v0.1.9 ####################################################################### GlobalVariableStoreInfo = collections.namedtuple( "GlobalVariableStoreInfo", ["lineno", "offset"] ) class GlobalVariableLoadCounter(ast.NodeVisitor): """ Class to find all defined global variables and count their usages. """ def __init__(self): """ Constructor """ super().__init__() self.__loads = {} self.__storeInfo = {} def visit_Name(self, nameNode): """ Public method to record the definition and use of a global variable. @param nameNode reference to the name node to be processed @type ast.Name """ if isinstance(nameNode.ctx, ast.Load) and nameNode.id in self.__loads: self.__loads[nameNode.id] += 1 elif ( isinstance(nameNode.ctx, ast.Store) and nameNode.id not in self.__storeInfo ): self.__loads[nameNode.id] = 0 self.__storeInfo[nameNode.id] = GlobalVariableStoreInfo( lineno=nameNode.lineno, offset=nameNode.col_offset ) def getLoads(self): """ Public method to get an iterator of the detected variable loads. @return DESCRIPTION @rtype TYPE """ return self.__loads.items() def getStoreInfo(self, variableId): """ Public method to get the store info data of a given variable ID. @param variableId variable ID to retrieve the store info for @type str @return named tuple containing the line number and column offset @rtype GlobalVariableStoreInfo """ try: return self.__storeInfo[variableId] except KeyError: return None