Plugins/CheckerPlugins/CodeStyleChecker/MiscellaneousChecker.py

changeset 6180
8d72871c16ba
parent 6178
905ea208884a
child 6182
f293e95b914d
--- a/Plugins/CheckerPlugins/CodeStyleChecker/MiscellaneousChecker.py	Sat Mar 10 14:57:23 2018 +0100
+++ b/Plugins/CheckerPlugins/CodeStyleChecker/MiscellaneousChecker.py	Sun Mar 11 15:27:23 2018 +0100
@@ -32,6 +32,7 @@
         "M611", "M612", "M613",
         "M621", "M622", "M623", "M624", "M625",
         "M631", "M632",
+        "M651", "M652", "M653", "M654", "M655",
         
         "M701", "M702",
         
@@ -108,6 +109,7 @@
             (self.__checkFormatString, ("M611", "M612", "M613",
                                         "M621", "M622", "M623", "M624", "M625",
                                         "M631", "M632")),
+            (self.__checkLogging, ("M651", "M652", "M653", "M654", "M655")),
             (self.__checkFuture, ("M701", "M702")),
             (self.__checkPrintStatements, ("M801",)),
             (self.__checkTuple, ("M811", )),
@@ -644,6 +646,15 @@
                     if key2.s < key1.s:
                         self.__error(key2.lineno - 1, key2.col_offset,
                                      "M201", key2.s, key1.s)
+    
+    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)
 
 
 class TextVisitor(ast.NodeVisitor):
@@ -784,5 +795,177 @@
                 self.calls[node.args[0]] = (node, True)
         super(TextVisitor, self).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(LoggingVisitor, self).__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
+        """
+        try:
+            if node.func.value.id == "warnings":
+                return None
+            
+            if node.func.attr in LoggingVisitor.LoggingLevels:
+                return node.func.attr
+        except AttributeError:
+            pass
+        
+        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():
+            if self.__withinLoggingArgument() and self.__isFormatCall(node):
+                self.violations.append((node, "M651"))
+                super(LoggingVisitor, self).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(LoggingVisitor, self).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(LoggingVisitor, self).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(LoggingVisitor, self).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 sys.version_info >= (3, 6):
+            if self.__withinLoggingStatement():
+                if any(isinstance(i, ast.FormattedValue) for i in node.values):
+                    if self.__withinLoggingArgument():
+                        self.violations.append((node, "M654"))
+                        
+                        super(LoggingVisitor, self).generic_visit(node)
+
 #
 # eflag: noqa = M702

eric ide

mercurial