--- /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()