Code Style Checker eric7

Thu, 30 Nov 2023 16:39:46 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 30 Nov 2023 16:39:46 +0100
branch
eric7
changeset 10362
cfa7034cccf6
parent 10361
e6ff9a4f6ee5
child 10363
6244c89dbc3f

Code Style Checker
- Updated the logging checker to support more cases (flake8_logging_format 0.9.0).

docs/ThirdParty.md file | annotate | diff | comparison | revisions
docs/changelog.md file | annotate | diff | comparison | revisions
eric7.epj file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerUtilities.py 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/__init__.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/translations.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/translations.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/translations.py file | annotate | diff | comparison | revisions
--- a/docs/ThirdParty.md	Thu Nov 30 11:59:40 2023 +0100
+++ b/docs/ThirdParty.md	Thu Nov 30 16:39:46 2023 +0100
@@ -28,6 +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-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/docs/changelog.md	Thu Nov 30 11:59:40 2023 +0100
+++ b/docs/changelog.md	Thu Nov 30 16:39:46 2023 +0100
@@ -4,6 +4,7 @@
 - bug fixes
 - Code Style Checker
     - Updated these checkers to support more cases.
+        - Logging
         - Miscellaneous
         - Simplify
 - Virtual Environments
--- a/eric7.epj	Thu Nov 30 11:59:40 2023 +0100
+++ b/eric7.epj	Thu Nov 30 16:39:46 2023 +0100
@@ -66,9 +66,9 @@
         "CopyrightAuthor": "",
         "CopyrightMinFileSize": 0,
         "DocstringType": "eric_black",
-        "EnabledCheckerCategories": "C, D, E, I, M, NO, N, Y, U, W",
+        "EnabledCheckerCategories": "C, D, E, I, L, M, NO, N, Y, U, W",
         "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py,*/ast_unparse.py,*/piplicenses.py,*/pipdeptree.py,*/MCUScripts/*,*/MicroPython/Tools/*",
-        "ExcludeMessages": "C101,E203,E265,E266,E305,E402,M201,M701,M702,M811,M834,M852,N802,N803,N807,N808,N821,W293,W503,Y119,Y401,Y402",
+        "ExcludeMessages": "C101,E203,E265,E266,E305,E402,M201,M701,M702,M811,M834,N802,N803,N807,N808,N821,W293,W503,Y119,Y401,Y402",
         "FixCodes": "",
         "FixIssues": false,
         "FutureChecker": "",
@@ -1484,6 +1484,10 @@
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/LocalImportVisitor.py",
       "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/__init__.py",
+      "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/translations.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousDefaults.py",
       "src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/__init__.py",
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py	Thu Nov 30 11:59:40 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py	Thu Nov 30 16:39:46 2023 +0100
@@ -21,6 +21,7 @@
 from Complexity.ComplexityChecker import ComplexityChecker
 from DocStyle.DocStyleChecker import DocStyleChecker
 from Imports.ImportsChecker import ImportsChecker
+from Logging.LoggingChecker import LoggingChecker
 from Miscellaneous.MiscellaneousChecker import MiscellaneousChecker
 from NameOrder.NameOrderChecker import NameOrderChecker
 from Naming.NamingStyleChecker import NamingStyleChecker
@@ -582,6 +583,21 @@
             stats.update(asyncChecker.counters)
             errors += asyncChecker.errors
 
+            # checking logging statements
+            loggingChecker = LoggingChecker(
+                source,
+                filename,
+                tree,
+                select,
+                ignore,
+                [],
+                repeatMessages,
+                {},  # no arguments yet
+            )
+            loggingChecker.run()
+            stats.update(loggingChecker.counters)
+            errors += loggingChecker.errors
+
         elif syntaxError:
             errors = [syntaxError]
             stats.update(syntaxStats)
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Thu Nov 30 11:59:40 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Thu Nov 30 16:39:46 2023 +0100
@@ -87,6 +87,7 @@
         "D": QCoreApplication.translate("CheckerCategories", "Documentation"),
         "E": QCoreApplication.translate("CheckerCategories", "Errors"),
         "I": QCoreApplication.translate("CheckerCategories", "Imports"),
+        "L": QCoreApplication.translate("CheckerCategories", "Logging"),
         "M": QCoreApplication.translate("CheckerCategories", "Miscellaneous"),
         "N": QCoreApplication.translate("CheckerCategories", "Naming"),
         "NO": QCoreApplication.translate("CheckerCategories", "Name Order"),
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerUtilities.py	Thu Nov 30 11:59:40 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerUtilities.py	Thu Nov 30 16:39:46 2023 +0100
@@ -40,6 +40,8 @@
             itm.setIcon(column, EricPixmapCache.getIcon("docstringError"))
         elif messageCategory == "I":
             itm.setIcon(column, EricPixmapCache.getIcon("imports"))
+        elif messageCategory == "L":
+            itm.setIcon(column, EricPixmapCache.getIcon("logViewer"))
         elif messageCategory == "NO":
             itm.setIcon(column, EricPixmapCache.getIcon("nameOrderError"))
         elif messageCategory == "P":
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/LoggingChecker.py	Thu Nov 30 16:39:46 2023 +0100
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a checker for logging related issues.
+"""
+
+import copy
+
+
+class LoggingChecker:
+    """
+    Class implementing a checker for logging related issues.
+    """
+
+    Codes = [
+        ## Logging format
+        "L101",
+        "L102",
+        "L103",
+        "L104",
+        "L110",
+    ]
+
+    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.__checkLoggingFormat, ("L101", "L102", "L103", "L104", "L110")),
+        ]
+
+        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()
+
+    def __checkLoggingFormat(self):
+        """
+        Private method to check logging statements.
+        """
+        from .LoggingFormatVisitor import LoggingFormatVisitor
+
+        visitor = LoggingFormatVisitor()
+        visitor.visit(self.__tree)
+        for node, reason in visitor.violations:
+            self.__error(node.lineno - 1, node.col_offset, reason)
--- /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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/__init__.py	Thu Nov 30 16:39:46 2023 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the logging style checker.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Logging/translations.py	Thu Nov 30 16:39:46 2023 +0100
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+
+"""
+Module implementing message translations for the code style plugin messages
+(logging part).
+"""
+
+from PyQt6.QtCore import QCoreApplication
+
+_loggingMessages = {
+    ## Logging format
+    "L101": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement uses string.format()",
+    ),
+    "L102": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement uses '%'",  # __IGNORE_WARNING_M601__
+    ),
+    "L103": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement uses '+'",
+    ),
+    "L104": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement uses f-string",
+    ),
+    "L110": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement uses 'warn' instead of 'warning'",
+    ),
+    "L121": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement uses an extra field that clashes with a LogRecord field: {0}"
+    ),
+    "L130": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement uses exception in arguments"
+    ),
+    "L131": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging: .exception(...) should be used instead of .error(..., exc_info=True)"
+    ),
+    "L132": QCoreApplication.translate(
+        "LoggingChecker",
+        "logging statement has redundant exc_info"
+    ),
+}
+
+_loggingMessagesSampleArgs = {
+    "L121": ["pathname"]
+}
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Nov 30 11:59:40 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Nov 30 16:39:46 2023 +0100
@@ -182,12 +182,6 @@
         "M625",
         "M631",
         "M632",
-        ## Logging
-        "M651",
-        "M652",
-        "M653",
-        "M654",
-        "M655",
         ## Future statements
         "M701",
         "M702",
@@ -400,7 +394,6 @@
                     "M632",
                 ),
             ),
-            (self.__checkLogging, ("M651", "M652", "M653", "M654", "M655")),
             (self.__checkFuture, ("M701", "M702")),
             (self.__checkGettext, ("M711",)),
             (self.__checkPrintStatements, ("M801",)),
@@ -1278,15 +1271,6 @@
                             key1.value,
                         )
 
-    def __checkLogging(self):
-        """
-        Private method to check logging statements.
-        """
-        visitor = LoggingVisitor()
-        visitor.visit(self.__tree)
-        for node, reason in visitor.violations:
-            self.__error(node.lineno - 1, node.col_offset, reason)
-
     def __checkGettext(self):
         """
         Private method to check the 'gettext' import statement.
@@ -1437,7 +1421,7 @@
                     self.__error(node.lineno - 1, node.col_offset, "M217", node.name)
 
     #######################################################################
-    ## The following method check for implicitly concatenated strings
+    ## The following methods check for implicitly concatenated strings.
     ##
     ## These methods are adapted from: flake8-implicit-str-concat v0.4.0
     ## Original: Copyright (c) 2023 Dylan Turner
@@ -1660,182 +1644,6 @@
         super().generic_visit(node)
 
 
-class LoggingVisitor(ast.NodeVisitor):
-    """
-    Class implementing a node visitor to check logging statements.
-    """
-
-    LoggingLevels = {
-        "debug",
-        "critical",
-        "error",
-        "info",
-        "warn",
-        "warning",
-    }
-
-    def __init__(self):
-        """
-        Constructor
-        """
-        super().__init__()
-
-        self.__currentLoggingCall = None
-        self.__currentLoggingArgument = None
-        self.__currentLoggingLevel = None
-        self.__currentExtraKeyword = None
-        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 __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 node.func.value.id == "warnings":
-                return None
-
-            if node.func.attr in LoggingVisitor.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 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, "M651"))
-            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, "M655"))
-
-        for index, child in enumerate(ast.iter_child_nodes(node)):
-            if index == 1:
-                self.__currentLoggingArgument = 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, "M652"))
-
-            # handle string concat
-            if isinstance(node.op, ast.Add):
-                self.violations.append((node, "M653"))
-
-        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, "M654"))
-
-            super().generic_visit(node)
-
-
 #######################################################################
 ## BugBearVisitor
 ##
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/translations.py	Thu Nov 30 11:59:40 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/translations.py	Thu Nov 30 16:39:46 2023 +0100
@@ -510,27 +510,6 @@
         "MiscellaneousChecker",
         "format call provides unused keyword ({0})",
     ),
-    ## Logging
-    "M651": QCoreApplication.translate(
-        "MiscellaneousChecker",
-        "logging statement uses string.format()",
-    ),
-    "M652": QCoreApplication.translate(
-        "MiscellaneousChecker",
-        "logging statement uses '%'",  # __IGNORE_WARNING_M601__
-    ),
-    "M653": QCoreApplication.translate(
-        "MiscellaneousChecker",
-        "logging statement uses '+'",
-    ),
-    "M654": QCoreApplication.translate(
-        "MiscellaneousChecker",
-        "logging statement uses f-string",
-    ),
-    "M655": QCoreApplication.translate(
-        "MiscellaneousChecker",
-        "logging statement uses 'warn' instead of 'warning'",
-    ),
     ## Future statements
     "M701": QCoreApplication.translate(
         "MiscellaneousChecker",
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/translations.py	Thu Nov 30 11:59:40 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/translations.py	Thu Nov 30 16:39:46 2023 +0100
@@ -20,6 +20,7 @@
 from .Complexity.translations import _complexityMessages, _complexityMessagesSampleArgs
 from .DocStyle.translations import _docStyleMessages, _docStyleMessagesSampleArgs
 from .Imports.translations import _importsMessages, _importsMessagesSampleArgs
+from .Logging.translations import _loggingMessages, _loggingMessagesSampleArgs
 from .Miscellaneous.translations import (
     _miscellaneousMessages,
     _miscellaneousMessagesSampleArgs,
@@ -430,6 +431,7 @@
     "D": _docStyleMessages,
     "E": _pycodestyleErrorMessages,
     "I": _importsMessages,
+    "L": _loggingMessages,
     "M": _miscellaneousMessages,
     "N": _namingStyleMessages,
     "NO": _nameOrderMessages,
@@ -448,6 +450,7 @@
     "D": _docStyleMessagesSampleArgs,
     "E": _pycodestyleErrorMessagesSampleArgs,
     "I": _importsMessagesSampleArgs,
+    "L": _loggingMessagesSampleArgs,
     "M": _miscellaneousMessagesSampleArgs,
     "NO": _nameOrderMessagesSampleArgs,
     "S": _securityMessagesSampleArgs,

eric ide

mercurial