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

branch
eric7
changeset 10362
cfa7034cccf6
child 10363
6244c89dbc3f
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingFormatVisitor.py	Thu Nov 30 16:39:46 2023 +0100
@@ -0,0 +1,373 @@
+# -*- 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", "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):
+        """
+        Constructor
+        """
+        super().__init__()
+
+        self.__currentLoggingCall = None
+        self.__currentLoggingArgument = None
+        self.__currentLoggingLevel = None
+        self.__currentExtraKeyword = None
+        self.__currentExceptNames = []
+        self.violations = []
+
+    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.
+
+c        @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.violations.append((node, "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.violations.append((node, 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.violations.append((node, "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.violations.append((node, "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.violations.append((node, "L102"))
+
+            # handle string concat
+            if isinstance(node.op, ast.Add):
+                self.violations.append((node, "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.violations.append((node, "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.violations.append((node, "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