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

Tue, 23 May 2023 12:04:07 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 23 May 2023 12:04:07 +0200
branch
eric7
changeset 10054
d7a47f0cff2b
parent 10053
9914b7b4b11c
child 10056
ac1c214e0a05
permissions
-rw-r--r--

Regenerated source documentation.

# -*- 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

            # 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

    #######################################################################
    ## Unused Arguments
    ##
    ## adapted from: flake8-unused-arguments v0.0.13
    #######################################################################

    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

eric ide

mercurial