--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Unused/UnusedChecker.py Mon May 22 19:53:41 2023 +0200 @@ -0,0 +1,410 @@ +# -*- 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