Thu, 30 Nov 2023 17:48:55 +0100
Fixed some code style and formatting issues as well as an implementation issue.
# -*- coding: utf-8 -*- # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a node visitor to check logging formatting issues. """ import ast import contextlib _LoggingLevels = { "debug", "critical", "error", "exception", "info", "warn", "warning", } # default LogRecord attributes that shouldn't be overwritten by extra dict _ReservedAttrs = { "args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "taskName", "thread", "threadName", } ####################################################################### ## LoggingFormatVisitor ## ## adapted from: flake8-logging-format v0.9.0 ## ## Original: Copyright (c) 2017 Globality Engineering ####################################################################### class LoggingFormatVisitor(ast.NodeVisitor): """ Class implementing a node visitor to check logging formatting issues. """ def __init__(self, errorCallback): """ Constructor @param errorCallback callback function to register an error @type func """ super().__init__() self.__currentLoggingCall = None self.__currentLoggingArgument = None self.__currentLoggingLevel = None self.__currentExtraKeyword = None self.__currentExceptNames = [] self.__error = errorCallback def __withinLoggingStatement(self): """ Private method to check, if we are inside a logging statement. @return flag indicating we are inside a logging statement @rtype bool """ return self.__currentLoggingCall is not None def __withinLoggingArgument(self): """ Private method to check, if we are inside a logging argument. @return flag indicating we are inside a logging argument @rtype bool """ return self.__currentLoggingArgument is not None def __withinExtraKeyword(self, node): """ Private method to check, if we are inside the extra keyword. @param node reference to the node to be checked @type ast.keyword @return flag indicating we are inside the extra keyword @rtype bool """ return ( self.__currentExtraKeyword is not None and self.__currentExtraKeyword != node ) def __getExceptHandlerName(self, node): """ Private method to get the exception name from an ExceptHandler node. @param node reference to the node to be checked @type ast.ExceptHandler @return exception name @rtype str """ name = node.name if not name: return None return name def __getIdAttr(self, value): """ Private method to check if value has id attribute and return it. @param value value to get id from @type ast.Name @return ID of value @rtype str """ """Check if value has id attribute and return it. :param value: The value to get id from. :return: The value.id. """ if not hasattr(value, "id") and hasattr(value, "value"): value = value.value return value.id def __detectLoggingLevel(self, node): """ Private method to decide whether an AST Call is a logging call. @param node reference to the node to be processed @type ast.Call @return logging level @rtype str or None """ with contextlib.suppress(AttributeError): if self.__getIdAttr(node.func.value) in ("parser", "warnings"): return None if node.func.attr in _LoggingLevels: return node.func.attr return None def __isFormatCall(self, node): """ Private method to check if a function call uses format. @param node reference to the node to be processed @type ast.Call @return flag indicating the function call uses format @rtype bool """ try: return node.func.attr == "format" except AttributeError: return False def __shouldCheckExtraFieldClash(self, node): """ Private method to check, if the extra field clash check should be done. @param node reference to the node to be processed @type ast.Dict @return flag indicating to perform the check @rtype bool """ return all( ( self.__withinLoggingStatement(), self.__withinExtraKeyword(node), ) ) def __shouldCheckExtraException(self, node): """ Private method to check, if the check for extra exceptions should be done. @param node reference to the node to be processed @type ast.Dict @return flag indicating to perform the check @rtype bool """ return all( ( self.__withinLoggingStatement(), self.__withinExtraKeyword(node), len(self.__currentExceptNames) > 0, ) ) def __isBareException(self, node): """ Private method to check, if the node is a bare exception name from an except block. @param node reference to the node to be processed @type ast.AST @return flag indicating a bare exception @rtype TYPE """ return isinstance(node, ast.Name) and node.id in self.__currentExceptNames def __isStrException(self, node): """ Private method to check if the node is the expression str(e) or unicode(e), where e is an exception name from an except block. @param node reference to the node to be processed @type ast.AST @return flag indicating a string exception @rtype TYPE """ return ( isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ("str", "unicode") and node.args and self.__isBareException(node.args[0]) ) def __checkExceptionArg(self, node): """ Private method to check an exception argument. @param node reference to the node to be processed @type ast.AST """ if self.__isBareException(node) or self.__isStrException(node): self.__error(node.lineno - 1, node.col_offset, "L130") def __checkExcInfo(self, node): """ Private method to check, if the exc_info keyword is used with logging.error or logging.exception. @param node reference to the node to be processed @type ast.AST """ if self.__currentLoggingLevel not in ("error", "exception"): return for kw in node.keywords: if kw.arg == "exc_info": if self.__currentLoggingLevel == "error": violation = "L131" else: violation = "L132" self.__error(node.lineno - 1, node.col_offset, violation) def visit_Call(self, node): """ Public method to handle a function call. Every logging statement and string format is expected to be a function call. @param node reference to the node to be processed @type ast.Call """ # we are in a logging statement if ( self.__withinLoggingStatement() and self.__withinLoggingArgument() and self.__isFormatCall(node) ): self.__error(node.lineno - 1, node.col_offset, "L101") super().generic_visit(node) return loggingLevel = self.__detectLoggingLevel(node) if loggingLevel and self.__currentLoggingLevel is None: self.__currentLoggingLevel = loggingLevel # we are in some other statement if loggingLevel is None: super().generic_visit(node) return # we are entering a new logging statement self.__currentLoggingCall = node if loggingLevel == "warn": self.__error(node.lineno - 1, node.col_offset, "L110") self.__checkExcInfo(node) for index, child in enumerate(ast.iter_child_nodes(node)): if index == 1: self.__currentLoggingArgument = child if index >= 1: self.__checkExceptionArg(child) if index > 1 and isinstance(child, ast.keyword) and child.arg == "extra": self.__currentExtraKeyword = child super().visit(child) self.__currentLoggingArgument = None self.__currentExtraKeyword = None self.__currentLoggingCall = None self.__currentLoggingLevel = None def visit_BinOp(self, node): """ Public method to handle binary operations while processing the first logging argument. @param node reference to the node to be processed @type ast.BinOp """ if self.__withinLoggingStatement() and self.__withinLoggingArgument(): # handle percent format if isinstance(node.op, ast.Mod): self.__error(node.lineno - 1, node.col_offset, "L102") # handle string concat if isinstance(node.op, ast.Add): self.__error(node.lineno - 1, node.col_offset, "L103") super().generic_visit(node) def visit_Dict(self, node): """ Public method to handle dict arguments. @param node reference to the node to be processed @type ast.Dict """ if self.__shouldCheckExtraFieldClash(node): for key in node.keys: # key can be None if the dict uses double star syntax if key is not None and key.s in _ReservedAttrs: self.__error(node.lineno - 1, node.col_offset, "L121", key.s) if self.__shouldCheckExtraException(node): for value in node.values: self.__checkExceptionArg(value) super().generic_visit(node) def visit_JoinedStr(self, node): """ Public method to handle f-string arguments. @param node reference to the node to be processed @type ast.JoinedStr """ if ( self.__withinLoggingStatement() and any(isinstance(i, ast.FormattedValue) for i in node.values) and self.__withinLoggingArgument() ): self.__error(node.lineno - 1, node.col_offset, "L104") super().generic_visit(node) def visit_ExceptHandler(self, node): """ Public method to handle an exception handler. @param node reference to the node to be processed @type ast.ExceptHandler """ name = self.__getExceptHandlerName(node) if not name: super().generic_visit(node) return self.__currentExceptNames.append(name) super().generic_visit(node) self.__currentExceptNames.pop()