src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingFormatVisitor.py

Thu, 30 Nov 2023 17:48:55 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 30 Nov 2023 17:48:55 +0100
branch
eric7
changeset 10363
6244c89dbc3f
parent 10362
cfa7034cccf6
permissions
-rw-r--r--

Fixed some code style and formatting issues as well as an implementation issue.

# -*- coding: utf-8 -*-

# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a node visitor to check logging formatting issues.
"""

import ast
import contextlib

_LoggingLevels = {
    "debug",
    "critical",
    "error",
    "exception",
    "info",
    "warn",
    "warning",
}


# default LogRecord attributes that shouldn't be overwritten by extra dict
_ReservedAttrs = {
    "args",
    "asctime",
    "created",
    "exc_info",
    "exc_text",
    "filename",
    "funcName",
    "levelname",
    "levelno",
    "lineno",
    "module",
    "msecs",
    "message",
    "msg",
    "name",
    "pathname",
    "process",
    "processName",
    "relativeCreated",
    "stack_info",
    "taskName",
    "thread",
    "threadName",
}

#######################################################################
## LoggingFormatVisitor
##
## adapted from: flake8-logging-format v0.9.0
##
## Original: Copyright (c) 2017 Globality Engineering
#######################################################################


class LoggingFormatVisitor(ast.NodeVisitor):
    """
    Class implementing a node visitor to check logging formatting issues.
    """

    def __init__(self, errorCallback):
        """
        Constructor

        @param errorCallback callback function to register an error
        @type func
        """
        super().__init__()

        self.__currentLoggingCall = None
        self.__currentLoggingArgument = None
        self.__currentLoggingLevel = None
        self.__currentExtraKeyword = None
        self.__currentExceptNames = []

        self.__error = errorCallback

    def __withinLoggingStatement(self):
        """
        Private method to check, if we are inside a logging statement.

        @return flag indicating we are inside a logging statement
        @rtype bool
        """
        return self.__currentLoggingCall is not None

    def __withinLoggingArgument(self):
        """
        Private method to check, if we are inside a logging argument.

        @return flag indicating we are inside a logging argument
        @rtype bool
        """
        return self.__currentLoggingArgument is not None

    def __withinExtraKeyword(self, node):
        """
        Private method to check, if we are inside the extra keyword.

        @param node reference to the node to be checked
        @type ast.keyword
        @return flag indicating we are inside the extra keyword
        @rtype bool
        """
        return (
            self.__currentExtraKeyword is not None
            and self.__currentExtraKeyword != node
        )

    def __getExceptHandlerName(self, node):
        """
        Private method to get the exception name from an ExceptHandler node.

        @param node reference to the node to be checked
        @type ast.ExceptHandler
        @return exception name
        @rtype str
        """
        name = node.name
        if not name:
            return None

        return name

    def __getIdAttr(self, value):
        """
        Private method to check if value has id attribute and return it.

        @param value value to get id from
        @type ast.Name
        @return ID of value
        @rtype str
        """
        """Check if value has id attribute and return it.

        :param value: The value to get id from.
        :return: The value.id.
        """
        if not hasattr(value, "id") and hasattr(value, "value"):
            value = value.value

        return value.id

    def __detectLoggingLevel(self, node):
        """
        Private method to decide whether an AST Call is a logging call.

        @param node reference to the node to be processed
        @type ast.Call
        @return logging level
        @rtype str or None
        """
        with contextlib.suppress(AttributeError):
            if self.__getIdAttr(node.func.value) in ("parser", "warnings"):
                return None

            if node.func.attr in _LoggingLevels:
                return node.func.attr

        return None

    def __isFormatCall(self, node):
        """
        Private method to check if a function call uses format.

        @param node reference to the node to be processed
        @type ast.Call
        @return flag indicating the function call uses format
        @rtype bool
        """
        try:
            return node.func.attr == "format"
        except AttributeError:
            return False

    def __shouldCheckExtraFieldClash(self, node):
        """
        Private method to check, if the extra field clash check should be done.

        @param node reference to the node to be processed
        @type ast.Dict
        @return flag indicating to perform the check
        @rtype bool
        """
        return all(
            (
                self.__withinLoggingStatement(),
                self.__withinExtraKeyword(node),
            )
        )

    def __shouldCheckExtraException(self, node):
        """
        Private method to check, if the check for extra exceptions should be done.

        @param node reference to the node to be processed
        @type ast.Dict
        @return flag indicating to perform the check
        @rtype bool
        """
        return all(
            (
                self.__withinLoggingStatement(),
                self.__withinExtraKeyword(node),
                len(self.__currentExceptNames) > 0,
            )
        )

    def __isBareException(self, node):
        """
        Private method to check, if the node is a bare exception name from an except
        block.

        @param node reference to the node to be processed
        @type ast.AST
        @return flag indicating a bare exception
        @rtype TYPE
        """
        return isinstance(node, ast.Name) and node.id in self.__currentExceptNames

    def __isStrException(self, node):
        """
        Private method to check if the node is the expression str(e) or unicode(e),
        where e is an exception name from an except block.

        @param node reference to the node to be processed
        @type ast.AST
        @return flag indicating a string exception
        @rtype TYPE
        """
        return (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id in ("str", "unicode")
            and node.args
            and self.__isBareException(node.args[0])
        )

    def __checkExceptionArg(self, node):
        """
        Private method to check an exception argument.

        @param node reference to the node to be processed
        @type ast.AST
        """
        if self.__isBareException(node) or self.__isStrException(node):
            self.__error(node.lineno - 1, node.col_offset, "L130")

    def __checkExcInfo(self, node):
        """
        Private method to check, if the exc_info keyword is used with logging.error or
        logging.exception.

        @param node reference to the node to be processed
        @type ast.AST
        """
        if self.__currentLoggingLevel not in ("error", "exception"):
            return

        for kw in node.keywords:
            if kw.arg == "exc_info":
                if self.__currentLoggingLevel == "error":
                    violation = "L131"
                else:
                    violation = "L132"
                self.__error(node.lineno - 1, node.col_offset, violation)

    def visit_Call(self, node):
        """
        Public method to handle a function call.

        Every logging statement and string format is expected to be a function
        call.

        @param node reference to the node to be processed
        @type ast.Call
        """
        # we are in a logging statement
        if (
            self.__withinLoggingStatement()
            and self.__withinLoggingArgument()
            and self.__isFormatCall(node)
        ):
            self.__error(node.lineno - 1, node.col_offset, "L101")
            super().generic_visit(node)
            return

        loggingLevel = self.__detectLoggingLevel(node)

        if loggingLevel and self.__currentLoggingLevel is None:
            self.__currentLoggingLevel = loggingLevel

        # we are in some other statement
        if loggingLevel is None:
            super().generic_visit(node)
            return

        # we are entering a new logging statement
        self.__currentLoggingCall = node

        if loggingLevel == "warn":
            self.__error(node.lineno - 1, node.col_offset, "L110")

        self.__checkExcInfo(node)

        for index, child in enumerate(ast.iter_child_nodes(node)):
            if index == 1:
                self.__currentLoggingArgument = child
            if index >= 1:
                self.__checkExceptionArg(child)
            if index > 1 and isinstance(child, ast.keyword) and child.arg == "extra":
                self.__currentExtraKeyword = child

            super().visit(child)

            self.__currentLoggingArgument = None
            self.__currentExtraKeyword = None

        self.__currentLoggingCall = None
        self.__currentLoggingLevel = None

    def visit_BinOp(self, node):
        """
        Public method to handle binary operations while processing the first
        logging argument.

        @param node reference to the node to be processed
        @type ast.BinOp
        """
        if self.__withinLoggingStatement() and self.__withinLoggingArgument():
            # handle percent format
            if isinstance(node.op, ast.Mod):
                self.__error(node.lineno - 1, node.col_offset, "L102")

            # handle string concat
            if isinstance(node.op, ast.Add):
                self.__error(node.lineno - 1, node.col_offset, "L103")

        super().generic_visit(node)

    def visit_Dict(self, node):
        """
        Public method to handle dict arguments.

        @param node reference to the node to be processed
        @type ast.Dict
        """
        if self.__shouldCheckExtraFieldClash(node):
            for key in node.keys:
                # key can be None if the dict uses double star syntax
                if key is not None and key.s in _ReservedAttrs:
                    self.__error(node.lineno - 1, node.col_offset, "L121", key.s)

        if self.__shouldCheckExtraException(node):
            for value in node.values:
                self.__checkExceptionArg(value)

        super().generic_visit(node)

    def visit_JoinedStr(self, node):
        """
        Public method to handle f-string arguments.

        @param node reference to the node to be processed
        @type ast.JoinedStr
        """
        if (
            self.__withinLoggingStatement()
            and any(isinstance(i, ast.FormattedValue) for i in node.values)
            and self.__withinLoggingArgument()
        ):
            self.__error(node.lineno - 1, node.col_offset, "L104")

            super().generic_visit(node)

    def visit_ExceptHandler(self, node):
        """
        Public method to handle an exception handler.

        @param node reference to the node to be processed
        @type ast.ExceptHandler
        """
        name = self.__getExceptHandlerName(node)
        if not name:
            super().generic_visit(node)
            return

        self.__currentExceptNames.append(name)

        super().generic_visit(node)

        self.__currentExceptNames.pop()

eric ide

mercurial