Code Style Checker eric7

Fri, 01 Dec 2023 16:19:15 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 01 Dec 2023 16:19:15 +0100
branch
eric7
changeset 10367
b189ca1f5d53
parent 10363
6244c89dbc3f
child 10368
8765229e7cc8

Code Style Checker
- Updated the logging checker to support more cases (based on flake8_logging 1.4.0).

docs/ThirdParty.md file | annotate | diff | comparison | revisions
eric7.epj file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingChecker.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingFormatVisitor.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingVisitor.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/translations.py file | annotate | diff | comparison | revisions
--- a/docs/ThirdParty.md	Thu Nov 30 17:48:55 2023 +0100
+++ b/docs/ThirdParty.md	Fri Dec 01 16:19:15 2023 +0100
@@ -28,7 +28,7 @@
 | flake8-future-annotations     |   1.1.0   | MIT License (MIT)                  |
 | flake8-implicit-str-concat    |   0.4.0   | MIT License (MIT)                  |
 | flake8-local-import           |   1.0.6   | MIT License (MIT)                  |
-| flake8-logging-format         |   0.9.0   | UNKNOWN                            |
+| flake8-logging                |   1.4.0   | MIT License (MIT)                  |
 | flake8-pep585                 |   0.1.7   | Mozilla Public License Version 2.0 |
 | flake8-pep604                 |   1.1.0   | MIT License (MIT)                  |
 | flake8-simplify               |   0.21.0  | MIT License (MIT)                  |
--- a/eric7.epj	Thu Nov 30 17:48:55 2023 +0100
+++ b/eric7.epj	Fri Dec 01 16:19:15 2023 +0100
@@ -1485,7 +1485,7 @@
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/__init__.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/translations.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingChecker.py",
-      "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingFormatVisitor.py",
+      "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingVisitor.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/__init__.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/translations.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py",
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingChecker.py	Thu Nov 30 17:48:55 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingChecker.py	Fri Dec 01 16:19:15 2023 +0100
@@ -16,12 +16,21 @@
     """
 
     Codes = [
-        ## Logging format
+        ## Logging
         "L101",
         "L102",
         "L103",
         "L104",
+        "L105",
+        "L106",
+        "L107",
+        "L108",
+        "L109",
         "L110",
+        "L111",
+        "L112",
+        "L113",
+        "L114",
     ]
 
     def __init__(self, source, filename, tree, select, ignore, expected, repeat, args):
@@ -45,8 +54,8 @@
         @param args dictionary of arguments for the various checks
         @type dict
         """
-        self.__select = tuple(select)
-        self.__ignore = ("",) if select else tuple(ignore)
+        self.__select = tuple(select)  # noqa: M188
+        self.__ignore = ("",) if select else tuple(ignore)  # noqa: M188
         self.__expected = expected[:]
         self.__repeat = repeat
         self.__filename = filename
@@ -61,7 +70,25 @@
         self.errors = []
 
         checkersWithCodes = [
-            (self.__checkLoggingFormat, ("L101", "L102", "L103", "L104", "L110")),
+            (
+                self.__checkLogging,
+                (
+                    "L101",
+                    "L102",
+                    "L103",
+                    "L104",
+                    "L105",
+                    "L106",
+                    "L107",
+                    "L108",
+                    "L109",
+                    "L110",
+                    "L111",
+                    "L112",
+                    "L113",
+                    "L114",
+                ),
+            ),
         ]
 
         self.__checkers = []
@@ -133,11 +160,11 @@
         for check in self.__checkers:
             check()
 
-    def __checkLoggingFormat(self):
+    def __checkLogging(self):
         """
         Private method to check logging statements.
         """
-        from .LoggingFormatVisitor import LoggingFormatVisitor
+        from .LoggingVisitor import LoggingVisitor
 
-        visitor = LoggingFormatVisitor(errorCallback=self.__error)
+        visitor = LoggingVisitor(errorCallback=self.__error)
         visitor.visit(self.__tree)
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingFormatVisitor.py	Thu Nov 30 17:48:55 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,396 +0,0 @@
-# -*- 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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingVisitor.py	Fri Dec 01 16:19:15 2023 +0100
@@ -0,0 +1,561 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a node visitor to check for logging issues.
+"""
+
+#######################################################################
+## LoggingVisitor
+##
+## adapted from: flake8-logging v1.4.0
+##
+## Original: Copyright (c) 2023 Adam Johnson
+#######################################################################
+
+import ast
+import re
+import sys
+
+from functools import lru_cache
+from typing import cast
+
+_LoggerMethods = frozenset(
+    (
+        "debug",
+        "info",
+        "warn",
+        "warning",
+        "error",
+        "critical",
+        "log",
+        "exception",
+    )
+)
+
+_LogrecordAttributes = frozenset(
+    (
+        "asctime",
+        "args",
+        "created",
+        "exc_info",
+        "exc_text",
+        "filename",
+        "funcName",
+        "levelname",
+        "levelno",
+        "lineno",
+        "module",
+        "msecs",
+        "msg",
+        "name",
+        "pathname",
+        "process",
+        "processName",
+        "relativeCreated",
+        "stack_info",
+        "taskName",
+        "thread",
+        "threadName",
+    )
+)
+
+
+@lru_cache(maxsize=None)
+def _modposPlaceholderRe():
+    """
+    Function to generate a regular expression object for '%' formatting codes.
+
+    @return regular expression object
+    @rtype re.Pattern
+    """
+    # https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
+    return re.compile(
+        r"""
+            %  # noqa: M601
+            (?P<spec>
+                % |  # raw % character  # noqa: M601
+                (?:
+                    ([-#0 +]+)?  # conversion flags
+                    (?P<minwidth>\d+|\*)?  # minimum field width
+                    (?P<precision>\.\d+|\.\*)?  # precision
+                    [hlL]?  # length modifier
+                    [acdeEfFgGiorsuxX]  # conversion type
+                )
+            )
+        """,
+        re.VERBOSE,
+    )
+
+
+@lru_cache(maxsize=None)
+def _modnamedPlaceholderRe():
+    """
+    Function to generate a regular expression object for '%' formatting codes using
+    names.
+
+    @return regular expression object
+    @rtype re.Pattern
+    """
+    # https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
+    return re.compile(
+        r"""
+            %  # noqa: M601
+            \(
+                (?P<name>.*?)
+            \)
+            ([-#0 +]+)?  # conversion flags
+            (\d+)?  # minimum field width
+            (\.\d+)?  # precision
+            [hlL]?  # length modifier
+            [acdeEfFgGiorsuxX]  # conversion type
+        """,
+        re.VERBOSE,
+    )
+
+
+class LoggingVisitor(ast.NodeVisitor):
+    """
+    Class implementing a node visitor to check for logging issues.
+    """
+
+    GetLoggerNames = frozenset(("__cached__", "__file__"))
+
+    def __init__(self, errorCallback):
+        """
+        Constructor
+
+        @param errorCallback callback function to register an error
+        @type func
+        """
+        super().__init__()
+
+        self.__error = errorCallback
+
+        self.__loggingName = None
+        self.__loggerName = None
+        self.__fromImports = {}
+        self.__stack = []
+
+    def visit(self, node):
+        """
+        Public method to handle ast nodes.
+
+        @param node reference to the node to be processed
+        @type ast.AST
+        """
+        self.__stack.append(node)
+        super().visit(node)
+        self.__stack.pop()
+
+    def visit_Import(self, node):
+        """
+        Public method to handle Import nodes.
+
+        @param node reference to the node to be processed
+        @type ast.Import
+        """
+        for alias in node.names:
+            if alias.name == "logging":
+                self.__loggingName = alias.asname or alias.name
+        self.generic_visit(node)
+
+    def visit_ImportFrom(self, node):
+        """
+        Public method to handle ImportFrom nodes.
+
+        @param node reference to the node to be processed
+        @type ast.ImportFrom
+        """
+        if node.module == "logging":
+            for alias in node.names:
+                if alias.name == "WARN":
+                    if sys.version_info >= (3, 10):
+                        lineno = alias.lineno
+                        colOffset = alias.col_offset
+                    else:
+                        lineno = node.lineno
+                        colOffset = node.col_offset
+                    self.__error(lineno - 1, colOffset, "L109")
+                if not alias.asname:
+                    self.__fromImports[alias.name] = node.module
+
+        self.generic_visit(node)
+
+    def visit_Attribute(self, node):
+        """
+        Public method to handle  Attribute nodes.
+
+        @param node reference to the node to be processed
+        @type ast.Attribute
+        """
+        if (
+            self.__loggingName
+            and isinstance(node.value, ast.Name)
+            and node.value.id == self.__loggingName
+            and node.attr == "WARN"
+        ):
+            self.__error(node.lineno - 1, node.col_offset, "L109")
+
+        self.generic_visit(node)
+
+    def visit_Call(self, node):
+        """
+        Public method to handle Call nodes.
+
+        @param node reference to the node to be processed
+        @type ast.Call
+        """
+        if (
+            (
+                self.__loggingName
+                and isinstance(node.func, ast.Attribute)
+                and node.func.attr == "Logger"
+                and isinstance(node.func.value, ast.Name)
+                and node.func.value.id == self.__loggingName
+            )
+            or (
+                isinstance(node.func, ast.Name)
+                and node.func.id == "Logger"
+                and self.__fromImports.get("Logger") == "logging"
+            )
+        ) and not self.__atModuleLevel():
+            self.__error(node.lineno - 1, node.col_offset, "L101")
+
+        if (
+            self.__loggingName
+            and isinstance(node.func, ast.Attribute)
+            and node.func.attr == "getLogger"
+            and isinstance(node.func.value, ast.Name)
+            and node.func.value.id == self.__loggingName
+        ) or (
+            isinstance(node.func, ast.Name)
+            and node.func.id == "getLogger"
+            and self.__fromImports.get("getLogger") == "logging"
+        ):
+            if (
+                len(self.__stack) >= 2
+                and isinstance(assign := self.__stack[-2], ast.Assign)
+                and len(assign.targets) == 1
+                and isinstance(assign.targets[0], ast.Name)
+                and not self.__atModuleLevel()
+            ):
+                self.__loggerName = assign.targets[0].id
+
+            if (
+                node.args
+                and isinstance(node.args[0], ast.Name)
+                and node.args[0].id in self.GetLoggerNames
+            ):
+                self.__error(node.args[0].lineno - 1, node.args[0].col_offset, "L102")
+
+        if (
+            isinstance(node.func, ast.Attribute)
+            and node.func.attr in _LoggerMethods
+            and isinstance(node.func.value, ast.Name)
+        ) and (
+            (self.__loggingName and node.func.value.id == self.__loggingName)
+            or (self.__loggerName and node.func.value.id == self.__loggerName)
+        ):
+            excHandler = self.__currentExceptHandler()
+
+            # L108
+            if node.func.attr == "warn":
+                self.__error(node.lineno - 1, node.col_offset, "L108")
+
+            # L103
+            extraKeys = []
+            if any((extraNode := kw).arg == "extra" for kw in node.keywords):
+                if isinstance(extraNode.value, ast.Dict):
+                    extraKeys = [
+                        (k.value, k)
+                        for k in extraNode.value.keys
+                        if isinstance(k, ast.Constant)
+                    ]
+                elif (
+                    isinstance(extraNode.value, ast.Call)
+                    and isinstance(extraNode.value.func, ast.Name)
+                    and extraNode.value.func.id == "dict"
+                ):
+                    extraKeys = [
+                        (k.arg, k)
+                        for k in extraNode.value.keywords
+                        if k.arg is not None
+                    ]
+
+            for key, keyNode in extraKeys:
+                if key in _LogrecordAttributes:
+                    if isinstance(keyNode, ast.keyword):
+                        lineno, colOffset = self.__keywordPos(keyNode)
+                    else:
+                        lineno = keyNode.lineno
+                        colOffset = keyNode.col_offset
+                    self.__error(lineno - 1, colOffset, "L103", repr(key))
+
+            if node.func.attr == "exception":
+                # L104
+                if not excHandler:
+                    self.__error(node.lineno - 1, node.col_offset, "L104")
+
+                if any((excInfo := kw).arg == "exc_info" for kw in node.keywords):
+                    # L106
+                    if (
+                        isinstance(excInfo.value, ast.Constant) and excInfo.value.value
+                    ) or (
+                        excHandler
+                        and isinstance(excInfo.value, ast.Name)
+                        and excInfo.value.id == excHandler.name
+                    ):
+                        lineno, colOffset = self.__keywordPos(excInfo)
+                        self.__error(lineno - 1, colOffset, "L106")
+
+                    # L107
+                    elif (
+                        isinstance(excInfo.value, ast.Constant)
+                        and not excInfo.value.value
+                    ):
+                        lineno, colOffset = self.__keywordPos(excInfo)
+                        self.__error(lineno - 1, colOffset, "L107")
+
+            # L105
+            elif node.func.attr == "error" and excHandler is not None:
+                rewritable = False
+                if any((excInfo := kw).arg == "exc_info" for kw in node.keywords):
+                    if isinstance(excInfo.value, ast.Constant) and excInfo.value.value:
+                        rewritable = True
+                    elif (
+                        isinstance(excInfo.value, ast.Name)
+                        and excInfo.value.id == excHandler.name
+                    ):
+                        rewritable = True
+                else:
+                    rewritable = True
+
+                if rewritable:
+                    self.__error(node.lineno - 1, node.col_offset, "L105")
+
+            # L114
+            elif (
+                excHandler is None
+                and any((excInfo := kw).arg == "exc_info" for kw in node.keywords)
+                and isinstance(excInfo.value, ast.Constant)
+                and excInfo.value.value
+            ):
+                lineno, colOffset = self.__keywordPos(excInfo)
+                self.__error(lineno - 1, colOffset, "L114")
+
+            # L110
+            if (
+                node.func.attr == "exception"
+                and len(node.args) >= 1
+                and isinstance(node.args[0], ast.Name)
+                and excHandler is not None
+                and node.args[0].id == excHandler.name
+            ):
+                self.__error(node.args[0].lineno - 1, node.args[0].col_offset, "L110")
+
+            msgArgKwarg = False
+            if node.func.attr == "log" and len(node.args) >= 2:
+                msgArg = node.args[1]
+            elif node.func.attr != "log" and len(node.args) >= 1:
+                msgArg = node.args[0]
+            else:
+                try:
+                    msgArg = [k for k in node.keywords if k.arg == "msg"][0].value
+                    msgArgKwarg = True
+                except IndexError:
+                    msgArg = None
+
+            # L111
+            if isinstance(msgArg, ast.JoinedStr):
+                self.__error(msgArg.lineno - 1, msgArg.col_offset, "L111a")
+            elif (
+                isinstance(msgArg, ast.Call)
+                and isinstance(msgArg.func, ast.Attribute)
+                and isinstance(msgArg.func.value, ast.Constant)
+                and isinstance(msgArg.func.value.value, str)
+                and msgArg.func.attr == "format"
+            ):
+                self.__error(msgArg.lineno - 1, msgArg.col_offset, "L111b")
+            elif (
+                isinstance(msgArg, ast.BinOp)
+                and isinstance(msgArg.op, ast.Mod)
+                and isinstance(msgArg.left, ast.Constant)
+                and isinstance(msgArg.left.value, str)
+            ):
+                self.__error(msgArg.lineno - 1, msgArg.col_offset, "L111c")
+            elif isinstance(msgArg, ast.BinOp) and self.__isAddChainWithNonStr(msgArg):
+                self.__error(msgArg.lineno - 1, msgArg.col_offset, "L111d")
+
+            # L112
+            if (
+                msgArg is not None
+                and not msgArgKwarg
+                and (msg := self.__flattenStrChain(msgArg))
+                and not any(isinstance(arg, ast.Starred) for arg in node.args)
+            ):
+                self.__checkMsgAndArgs(node, msgArg, msg)
+
+        self.generic_visit(node)
+
+    def __checkMsgAndArgs(self, node, msgArg, msg):
+        """
+        Private method to check the message and arguments a given Call node.
+
+        @param node reference to the Call node
+        @type ast.Call
+        @param msgArg message argument nodes
+        @type ast.AST
+        @param msg message
+        @type str
+        """
+        if not isinstance(node.func, ast.Attribute):
+            return
+
+        if (
+            (
+                (node.func.attr != "log" and (dictIdx := 1))
+                or (node.func.attr == "log" and (dictIdx := 2))
+            )
+            and len(node.args) == dictIdx + 1
+            and (dictNode := node.args[dictIdx])
+            and isinstance(dictNode, ast.Dict)
+            and all(
+                isinstance(k, ast.Constant) and isinstance(k.value, str)
+                for k in dictNode.keys
+            )
+            and (
+                modnames := {m["name"] for m in _modnamedPlaceholderRe().finditer(msg)}
+            )
+        ):
+            # L113
+            given = {cast(ast.Constant, k).value for k in dictNode.keys}
+            if missing := modnames - given:
+                self.__error(
+                    msgArg.lineno - 1,
+                    msgArg.col_offset,
+                    "L113a",  # missing keys
+                    ", ".join([repr(k) for k in missing]),
+                )
+
+            if missing := given - modnames:
+                self.__error(
+                    msgArg.lineno - 1,
+                    msgArg.col_offset,
+                    "L113b",  # unreferenced keys
+                    ", ".join([repr(k) for k in missing]),
+                )
+
+            return
+
+        # L112
+        modposCount = sum(
+            1 + (m["minwidth"] == "*") + (m["precision"] == ".*")
+            for m in _modposPlaceholderRe().finditer(msg)
+            if m["spec"] != "%"
+        )
+        argCount = len(node.args) - 1 - (node.func.attr == "log")
+
+        if modposCount > 0 and modposCount != argCount:
+            self.__error(
+                msgArg.lineno - 1,
+                msgArg.col_offset,
+                "L112",
+                modposCount,
+                "'%'",  # noqa: M601
+                argCount,
+            )
+            return
+
+    def __atModuleLevel(self):
+        """
+        Private method to check, if we are on the module level.
+
+        @return flag indicating the module level
+        @rtype bool
+        """
+        return any(
+            isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef))
+            for parent in self.__stack
+        )
+
+    def __currentExceptHandler(self):
+        """
+        Private method to determine the current exception handler node.
+
+        @return reference to the current exception handler node or None
+        @rtype ast.ExceptHandler
+        """
+        for node in reversed(self.__stack):
+            if isinstance(node, ast.ExceptHandler):
+                return node
+            elif isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)):
+                break
+
+        return None
+
+    def __keywordPos(self, node):
+        """
+        Private method determine line number and column offset of a given keyword node.
+
+        @param node reference to the keyword node
+        @type ast.keyword
+        @return tuple containing the line number and the column offset
+        @rtype tuple of (int, int)
+        """
+        if sys.version_info >= (3, 9):
+            return (node.lineno, node.col_offset)
+        else:
+            # Educated guess
+            return (
+                node.value.lineno,
+                max(0, node.value.col_offset - 1 - len(node.arg)),
+            )
+
+    def __isAddChainWithNonStr(self, node):
+        """
+        Private method to check, if the node is an Add with a non string argument.
+
+        @param node reference to the binary operator node
+        @type ast.BinOp
+        @return flag indicating an Add with a non string argument
+        @rtype bool
+        """
+        if not isinstance(node.op, ast.Add):
+            return False
+
+        for side in (node.left, node.right):
+            if isinstance(side, ast.BinOp):
+                if self.__isAddChainWithNonStr(side):
+                    return True
+            elif not (isinstance(side, ast.Constant) and isinstance(side.value, str)):
+                return True
+
+        return False
+
+    def __flattenStrChain(self, node):
+        """
+        Private method to flatten the given string chain.
+
+        @param node reference to the AST node
+        @type ast.AST
+        @return flattened string
+        @rtype str
+        """
+        parts = []
+
+        def visit(node):
+            if isinstance(node, ast.Constant) and isinstance(node.value, str):
+                parts.append(node.value)
+                return True
+            elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
+                return visit(node.left) and visit(node.right)
+            return False
+
+        result = visit(node)
+        if result:
+            return "".join(parts)
+        else:
+            return None
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/translations.py	Thu Nov 30 17:48:55 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/translations.py	Fri Dec 01 16:19:15 2023 +0100
@@ -12,46 +12,85 @@
 from PyQt6.QtCore import QCoreApplication
 
 _loggingMessages = {
-    ## Logging format
+    ## Logging
     "L101": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement uses string.format()",
+        "use logging.getLogger() to instantiate loggers",
     ),
     "L102": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement uses '%'",  # __IGNORE_WARNING_M601__
+        "use '__name__' with getLogger()",
     ),
     "L103": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement uses '+'",
+        "extra key {0} clashes with LogRecord attribute",
     ),
     "L104": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement uses f-string",
+        "avoid exception() outside of exception handlers",
+    ),
+    "L105": QCoreApplication.translate(
+        "LoggingChecker",
+        ".exception(...) should be used instead of .error(..., exc_info=True)",
+    ),
+    "L106": QCoreApplication.translate(
+        "LoggingChecker",
+        "redundant exc_info argument for exception() should be removed",
+    ),
+    "L107": QCoreApplication.translate(
+        "LoggingChecker",
+        "use error() instead of exception() with exc_info=False",
+    ),
+    "L108": QCoreApplication.translate(
+        "LoggingChecker",
+        "warn() is deprecated, use warning() instead",
+    ),
+    "L109": QCoreApplication.translate(
+        "LoggingChecker",
+        "WARN is undocumented, use WARNING instead",
     ),
     "L110": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement uses 'warn' instead of 'warning'",
+        "exception() does not take an exception",
     ),
-    "L121": QCoreApplication.translate(
+    "L111a": QCoreApplication.translate(
+        "LoggingChecker",
+        "avoid pre-formatting log messages using f-string",
+    ),
+    "L111b": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement uses an extra field that clashes with a LogRecord field:"
-        " {0}",
+        "avoid pre-formatting log messages using string.format()",
     ),
-    "L130": QCoreApplication.translate(
+    "L111c": QCoreApplication.translate(
+        "LoggingChecker",
+        "avoid pre-formatting log messages using '%'",  # noqa: M601
+    ),
+    "L111d": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement uses exception in arguments",
+        "avoid pre-formatting log messages using '+'",
     ),
-    "L131": QCoreApplication.translate(
+    "L112": QCoreApplication.translate(
+        "LoggingChecker",
+        "formatting error: {0} {1} placeholder(s) but {2} argument(s)",
+    ),
+    "L113a": QCoreApplication.translate(
         "LoggingChecker",
-        "logging: .exception(...) should be used instead of .error(..., exc_info=True)",
+        "formatting error: missing key(s): {0}",
     ),
-    "L132": QCoreApplication.translate(
+    "L113b": QCoreApplication.translate(
         "LoggingChecker",
-        "logging statement has redundant exc_info",
+        "formatting error: unreferenced key(s): {0}",
+    ),
+    "L114": QCoreApplication.translate(
+        "LoggingChecker",
+        "avoid exc_info=True outside of exception handlers",
     ),
 }
 
 _loggingMessagesSampleArgs = {
-    "L121": ["pathname"],
+    ## Logging
+    "L103": ["'pathname'"],
+    "L112": [3, "'%'", 2],  # noqa: M601
+    "L113a": ["'foo', 'bar'"],
+    "L113b": ["'foo', 'bar'"],
 }

eric ide

mercurial