eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py

changeset 7247
bf9379f964f3
parent 7246
c32a350d2414
child 7360
9190402e4505
diff -r c32a350d2414 -r bf9379f964f3 eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py	Wed Sep 18 20:25:52 2019 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py	Thu Sep 19 19:14:34 2019 +0200
@@ -25,11 +25,18 @@
         ## Return Annotations
         "A201", "A202", "A203", "A204", "A205", "A206",
         
+        ## Annotation Coverage
+        "A881",
+        
+        ## Annotation Complexity
+        "A891",
+        
         ## Syntax Error
         "A999",
     ]
 
-    def __init__(self, source, filename, select, ignore, expected, repeat):
+    def __init__(self, source, filename, select, ignore, expected, repeat,
+                 args):
         """
         Constructor
         
@@ -45,6 +52,8 @@
         @type list of str
         @param repeat flag indicating to report each occurrence of a code
         @type bool
+        @param args dictionary of arguments for the annotation checks
+        @type dict
         """
         self.__select = tuple(select)
         self.__ignore = ('',) if select else tuple(ignore)
@@ -52,6 +61,7 @@
         self.__repeat = repeat
         self.__filename = filename
         self.__source = source[:]
+        self.__args = args
 
         # statistics counters
         self.counters = {}
@@ -65,8 +75,15 @@
                 ("A001", "A002", "A003", "A101", "A102",
                  "A201", "A202", "A203", "A204", "A205", "A206",)
             ),
+            (self.__checkAnnotationsCoverage, ("A881",)),
+            (self.__checkAnnotationComplexity, ("A891",)),
         ]
         
+        self.__defaultArgs = {
+            "MinimumCoverage": 75,      # % of type annotation coverage
+            "MaximumComplexity": 3,
+        }
+        
         self.__checkers = []
         for checker, codes in checkersWithCodes:
             if any(not (code and self.__ignoreCode(code))
@@ -178,6 +195,59 @@
             reason = issue[1]
             params = issue[2:]
             self.__error(node.lineno - 1, node.col_offset, reason, *params)
+    
+    def __checkAnnotationsCoverage(self):
+        """
+        Private method to check for function annotation coverage.
+        """
+        minAnnotationsCoverage = self.__args.get(
+            "MinimumCoverage", self.__defaultArgs["MinimumCoverage"])
+        if minAnnotationsCoverage == 0:
+            # 0 means it is switched off
+            return
+        
+        functionDefs = [
+            f for f in ast.walk(self.__tree)
+            if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef))
+        ]
+        if not functionDefs:
+            # no functions/methods at all
+            return
+        
+        functionDefAnnotationsInfo = [
+            hasTypeAnnotations(f) for f in functionDefs
+        ]
+        annotationsCoverage = int(
+            len(list(filter(None, functionDefAnnotationsInfo))) /
+            len(functionDefAnnotationsInfo) * 100
+        )
+        if annotationsCoverage < minAnnotationsCoverage:
+            self.__error(0, 0, "A881", annotationsCoverage)
+    
+    def __checkAnnotationComplexity(self):
+        """
+        Private method to check the type annotation complexity.
+        """
+        maxAnnotationComplexity = self.__args.get(
+            "MaximumComplexity", self.__defaultArgs["MaximumComplexity"])
+        typeAnnotations = []
+        
+        functionDefs = [
+            f for f in ast.walk(self.__tree)
+            if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef))
+        ]
+        for functionDef in functionDefs:
+            typeAnnotations += list(filter(
+                None, [a.annotation for a in functionDef.args.args]))
+            if functionDef.returns:
+                typeAnnotations.append(functionDef.returns)
+        typeAnnotations += [a.annotation for a in ast.walk(self.__tree)
+                            if isinstance(a, ast.AnnAssign) and a.annotation]
+        for annotation in typeAnnotations:
+            complexity = getAnnotationComplexity(annotation)
+            if complexity > maxAnnotationComplexity:
+                self.__error(annotation.lineno - 1, annotation.col_offset,
+                             "A891", complexity, maxAnnotationComplexity)
 
 
 class FunctionVisitor(ast.NodeVisitor):
@@ -279,8 +349,8 @@
         
         # check function return annotation
         if not node.returns:
-            lineno = node.body[0].lineno
-            colOffset = self.__sourceLines[lineno - 1].find(":") + 1
+            lineno = node.lineno
+            colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1
             self.__classifyReturnError(classMethodType, visibilityType,
                                        lineno, colOffset)
     
@@ -346,3 +416,48 @@
         else:
             # args and kwonlyargs
             self.issues.append((argNode, "A001", argNode.arg))
+
+######################################################################
+## some utility functions below
+######################################################################
+
+
+def hasTypeAnnotations(funcNode):
+    """
+    Function to check for type annotations.
+    
+    @param funcNode reference to the function definition node to be checked
+    @type ast.AsyncFunctionDef or ast.FunctionDef
+    @return flag indicating the presence of type annotations
+    @rtype bool
+    """
+    hasReturnAnnotation = funcNode.returns is not None
+    hasArgsAnnotations = any(a for a in funcNode.args.args
+                             if a.annotation is not None)
+    hasKwargsAnnotations = (funcNode.args and
+                            funcNode.args.kwarg and
+                            funcNode.args.kwarg.annotation is not None)
+    hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs
+                                   if a.annotation is not None)
+    
+    return any((hasReturnAnnotation, hasArgsAnnotations, hasKwargsAnnotations,
+               hasKwonlyargsAnnotations))
+
+
+def getAnnotationComplexity(annotationNode):
+    """
+    Function to determine the annotation complexity.
+    
+    @param annotationNode reference to the node to determine the annotation
+        complexity for
+    @type ast.AST
+    @return annotation complexity
+    @rtype = int
+    """
+    if isinstance(annotationNode, ast.Str):
+        annotationNode = ast.parse(annotationNode.s).body[0].value
+    if isinstance(annotationNode, ast.Subscript):
+        return 1 + getAnnotationComplexity(annotationNode.slice.value)
+    if isinstance(annotationNode, ast.Tuple):
+        return max(getAnnotationComplexity(n) for n in annotationNode.elts)
+    return 1

eric ide

mercurial