src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Unused/UnusedChecker.py

Mon, 22 May 2023 19:53:41 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 22 May 2023 19:53:41 +0200
branch
eric7
changeset 10052
041d0785dd42
child 10053
9914b7b4b11c
permissions
-rw-r--r--

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

eric ide

mercurial