Mon, 22 May 2023 19:53:41 +0200
Code Style Checker
- Added a checker for unused function/method and lambda arguments..
# -*- coding: utf-8 -*- # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a checker for unused arguments, variables, ... . """ import ast import copy import AstUtilities class UnusedChecker: """ Class implementing a checker for unused arguments, variables, ... . """ Codes = [ ## Unused Arguments "U100", "U101", ] 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 ### parameters for unused arguments checks ##self.__ignoreAbstract "IgnoreAbstract": False, ##self.__ignoreOverload "IgnoreOverload": False, ##self.__ignoreOverride "IgnoreOverride": False, ##self.__ignoreStubs "IgnoreStubs": False, ##self.__ignoreVariadicNames "IgnoreVariadicNames": False, ##self.__ignoreLambdas "IgnoreLambdas": False, ##self.__ignoreNestedFunctions "IgnoreNestedFunctions": False, ##self.__ignoreDunderMethods "IgnoreDunderMethods": False, # statistics counters self.counters = {} # collection of detected errors self.errors = [] checkersWithCodes = [ (self.__checkUnusedArguments, ("U100", "U101")), ] 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 # 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 __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 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