src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9274
86fab0c74430
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py	Wed Jul 13 11:16:20 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py	Wed Jul 13 14:55:47 2022 +0200
@@ -22,34 +22,37 @@
     """
     Class implementing a checker for function type annotations.
     """
+
     Codes = [
         ## Function Annotations
-        "A001", "A002", "A003",
-        
+        "A001",
+        "A002",
+        "A003",
         ## Method Annotations
-        "A101", "A102",
-        
+        "A101",
+        "A102",
         ## Return Annotations
-        "A201", "A202", "A203", "A204", "A205", "A206",
-        
+        "A201",
+        "A202",
+        "A203",
+        "A204",
+        "A205",
+        "A206",
         ## Mixed kind of annotations
         "A301",
-        
         ## Annotations Future
         "A871",
-        
         ## Annotation Coverage
         "A881",
-        
         ## Annotation Complexity
-        "A891", "A892",
+        "A891",
+        "A892",
     ]
 
-    def __init__(self, source, filename, tree, select, ignore, expected,
-                 repeat, args):
+    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
@@ -68,7 +71,7 @@
         @type dict
         """
         self.__select = tuple(select)
-        self.__ignore = ('',) if select else tuple(ignore)
+        self.__ignore = ("",) if select else tuple(ignore)
         self.__expected = expected[:]
         self.__repeat = repeat
         self.__filename = filename
@@ -78,28 +81,38 @@
 
         # statistics counters
         self.counters = {}
-        
+
         # collection of detected errors
         self.errors = []
-        
+
         checkersWithCodes = [
             (
                 self.__checkFunctionAnnotations,
-                ("A001", "A002", "A003", "A101", "A102",
-                 "A201", "A202", "A203", "A204", "A205", "A206",
-                 "A301", )
+                (
+                    "A001",
+                    "A002",
+                    "A003",
+                    "A101",
+                    "A102",
+                    "A201",
+                    "A202",
+                    "A203",
+                    "A204",
+                    "A205",
+                    "A206",
+                    "A301",
+                ),
             ),
             (self.__checkAnnotationsFuture, ("A871",)),
             (self.__checkAnnotationsCoverage, ("A881",)),
             (self.__checkAnnotationComplexity, ("A891", "A892")),
         ]
-        
+
         self.__checkers = []
         for checker, codes in checkersWithCodes:
-            if any(not (code and self.__ignoreCode(code))
-                    for code in codes):
+            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.
@@ -109,13 +122,12 @@
         @return flag indicating to ignore the given code
         @rtype bool
         """
-        return (code.startswith(self.__ignore) and
-                not code.startswith(self.__select))
-    
+        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
@@ -127,16 +139,16 @@
         """
         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(
@@ -148,7 +160,7 @@
                     "args": args,
                 }
             )
-    
+
     def run(self):
         """
         Public method to check the given source against annotation issues.
@@ -156,97 +168,102 @@
         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()
-    
+
     #######################################################################
     ## Annotations
     ##
     ## adapted from: flake8-annotations v2.7.0
     #######################################################################
-    
+
     def __checkFunctionAnnotations(self):
         """
         Private method to check for function annotation issues.
         """
         suppressNoneReturning = self.__args.get(
             "SuppressNoneReturning",
-            AnnotationsCheckerDefaultArgs["SuppressNoneReturning"])
+            AnnotationsCheckerDefaultArgs["SuppressNoneReturning"],
+        )
         suppressDummyArgs = self.__args.get(
-            "SuppressDummyArgs",
-            AnnotationsCheckerDefaultArgs["SuppressDummyArgs"])
+            "SuppressDummyArgs", AnnotationsCheckerDefaultArgs["SuppressDummyArgs"]
+        )
         allowUntypedDefs = self.__args.get(
-            "AllowUntypedDefs",
-            AnnotationsCheckerDefaultArgs["AllowUntypedDefs"])
+            "AllowUntypedDefs", AnnotationsCheckerDefaultArgs["AllowUntypedDefs"]
+        )
         allowUntypedNested = self.__args.get(
-            "AllowUntypedNested",
-            AnnotationsCheckerDefaultArgs["AllowUntypedNested"])
+            "AllowUntypedNested", AnnotationsCheckerDefaultArgs["AllowUntypedNested"]
+        )
         mypyInitReturn = self.__args.get(
-            "MypyInitReturn",
-            AnnotationsCheckerDefaultArgs["MypyInitReturn"])
-        
+            "MypyInitReturn", AnnotationsCheckerDefaultArgs["MypyInitReturn"]
+        )
+
         # Store decorator lists as sets for easier lookup
-        dispatchDecorators = set(self.__args.get(
-            "DispatchDecorators",
-            AnnotationsCheckerDefaultArgs["DispatchDecorators"]))
-        overloadDecorators = set(self.__args.get(
-            "OverloadDecorators",
-            AnnotationsCheckerDefaultArgs["OverloadDecorators"]))
-        
+        dispatchDecorators = set(
+            self.__args.get(
+                "DispatchDecorators",
+                AnnotationsCheckerDefaultArgs["DispatchDecorators"],
+            )
+        )
+        overloadDecorators = set(
+            self.__args.get(
+                "OverloadDecorators",
+                AnnotationsCheckerDefaultArgs["OverloadDecorators"],
+            )
+        )
+
         from .AnnotationsFunctionVisitor import FunctionVisitor
+
         visitor = FunctionVisitor(self.__source)
         visitor.visit(self.__tree)
-        
+
         # Keep track of the last encountered function decorated by
         # `typing.overload`, if any. Per the `typing` module documentation,
         # a series of overload-decorated definitions must be followed by
         # exactly one non-overload-decorated definition of the same function.
         lastOverloadDecoratedFunctionName = None
-        
+
         # Iterate over the arguments with missing type hints, by function.
         for function in visitor.functionDefinitions:
-            if (
-                function.isDynamicallyTyped() and
-                (allowUntypedDefs or
-                 (function.isNested and allowUntypedNested))
+            if function.isDynamicallyTyped() and (
+                allowUntypedDefs or (function.isNested and allowUntypedNested)
             ):
                 # Skip recording errors from dynamically typed functions
                 # or nested functions
                 continue
-            
+
             # Skip recording errors for configured dispatch functions, such as
             # (by default) `functools.singledispatch` and
             # `functools.singledispatchmethod`
             if function.hasDecorator(dispatchDecorators):
                 continue
-            
+
             # Create sentinels to check for mixed hint styles
             hasTypeComment = function.hasTypeComment
-            
+
             has3107Annotation = False
             # PEP 3107 annotations are captured by the return arg
-            
+
             # Iterate over annotated args to detect mixing of type annotations
             # and type comments. Emit this only once per function definition
             for arg in function.getAnnotatedArguments():
                 if arg.hasTypeComment:
                     hasTypeComment = True
-                
+
                 if arg.has3107Annotation:
                     has3107Annotation = True
-                
+
                 if hasTypeComment and has3107Annotation:
                     # Short-circuit check for mixing of type comments &
                     # 3107-style annotations
-                    self.__error(function.lineno - 1, function.col_offset,
-                                 "A301")
+                    self.__error(function.lineno - 1, function.col_offset, "A301")
                     break
-            
+
             # Before we iterate over the function's missing annotations, check
             # to see if it's the closing function def in a series of
             # `typing.overload` decorated functions.
@@ -257,50 +274,50 @@
             # iteration
             if function.hasDecorator(overloadDecorators):
                 lastOverloadDecoratedFunctionName = function.name
-            
+
             # Record explicit errors for arguments that are missing annotations
             for arg in function.getMissedAnnotations():
                 if arg.argname == "return":
                     # return annotations have multiple possible short-circuit
                     # paths
                     if (
-                        suppressNoneReturning and
-                        not arg.hasTypeAnnotation and
-                        function.hasOnlyNoneReturns
+                        suppressNoneReturning
+                        and not arg.hasTypeAnnotation
+                        and function.hasOnlyNoneReturns
                     ):
                         # Skip recording return errors if the function has only
                         # `None` returns. This includes the case of no returns.
                         continue
-                    
+
                     if (
-                        mypyInitReturn and
-                        function.isClassMethod and
-                        function.name == "__init__" and
-                        function.getAnnotatedArguments()
+                        mypyInitReturn
+                        and function.isClassMethod
+                        and function.name == "__init__"
+                        and function.getAnnotatedArguments()
                     ):
                         # Skip recording return errors for `__init__` if at
                         # least one argument is annotated
                         continue
-                
+
                 # If the `suppressDummyArgs` flag is `True`, skip recording
                 # errors for any arguments named `_`
                 if arg.argname == "_" and suppressDummyArgs:
                     continue
 
                 self.__classifyError(function, arg)
-    
+
     def __classifyError(self, function, arg):
         """
         Private method to classify the missing type annotation based on the
         Function & Argument metadata.
-        
+
         For the currently defined rules & program flow, the assumption can be
         made that an argument passed to this method will match a linting error,
         and will only match a single linting error
-        
+
         This function provides an initial classificaton, then passes relevant
         attributes to cached helper function(s).
-        
+
         @param function reference to the Function object
         @type Function
         @param arg reference to the Argument object
@@ -310,29 +327,30 @@
         # All return "arguments" have an explicitly defined name "return"
         if arg.argname == "return":
             errorCode = self.__returnErrorClassifier(
-                function.isClassMethod, function.classDecoratorType,
-                function.functionType
+                function.isClassMethod,
+                function.classDecoratorType,
+                function.functionType,
             )
         else:
             # Otherwise, classify function argument error
             isFirstArg = arg == function.args[0]
             errorCode = self.__argumentErrorClassifier(
-                function.isClassMethod, isFirstArg,
-                function.classDecoratorType, arg.annotationType,
+                function.isClassMethod,
+                isFirstArg,
+                function.classDecoratorType,
+                arg.annotationType,
             )
-        
+
         if errorCode in ("A001", "A002", "A003"):
-            self.__error(arg.lineno - 1, arg.col_offset, errorCode,
-                         arg.argname)
+            self.__error(arg.lineno - 1, arg.col_offset, errorCode, arg.argname)
         else:
             self.__error(arg.lineno - 1, arg.col_offset, errorCode)
-    
+
     @lru_cache()
-    def __returnErrorClassifier(self, isClassMethod, classDecoratorType,
-                                functionType):
+    def __returnErrorClassifier(self, isClassMethod, classDecoratorType, functionType):
         """
         Private method to classify a return type annotation issue.
-        
+
         @param isClassMethod flag indicating a classmethod type function
         @type bool
         @param classDecoratorType type of class decorator
@@ -358,13 +376,14 @@
             return "A202"
         else:
             return "A201"
-    
+
     @lru_cache()
-    def __argumentErrorClassifier(self, isClassMethod, isFirstArg,
-                                  classDecoratorType, annotationType):
+    def __argumentErrorClassifier(
+        self, isClassMethod, isFirstArg, classDecoratorType, annotationType
+    ):
         """
         Private method to classify an argument type annotation issue.
-        
+
         @param isClassMethod flag indicating a classmethod type function
         @type bool
         @param isFirstArg flag indicating the first argument
@@ -395,106 +414,134 @@
         else:
             # Combine PosOnlyArgs, Args, and KwOnlyArgs
             return "A001"
-    
+
     #######################################################################
     ## Annotations Coverage
     ##
     ## adapted from: flake8-annotations-coverage v0.0.5
     #######################################################################
-    
+
     def __checkAnnotationsCoverage(self):
         """
         Private method to check for function annotation coverage.
         """
         minAnnotationsCoverage = self.__args.get(
-            "MinimumCoverage",
-            AnnotationsCheckerDefaultArgs["MinimumCoverage"])
+            "MinimumCoverage", AnnotationsCheckerDefaultArgs["MinimumCoverage"]
+        )
         if minAnnotationsCoverage == 0:
             # 0 means it is switched off
             return
-        
+
         functionDefs = [
-            f for f in ast.walk(self.__tree)
+            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 = [
             self.__hasTypeAnnotations(f) for f in functionDefs
         ]
         annotationsCoverage = int(
-            len(list(filter(None, functionDefAnnotationsInfo))) /
-            len(functionDefAnnotationsInfo) * 100
+            len(list(filter(None, functionDefAnnotationsInfo)))
+            / len(functionDefAnnotationsInfo)
+            * 100
         )
         if annotationsCoverage < minAnnotationsCoverage:
             self.__error(0, 0, "A881", annotationsCoverage)
-    
+
     def __hasTypeAnnotations(self, funcNode):
         """
         Private method 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))
-    
+        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,
+            )
+        )
+
     #######################################################################
     ## Annotations Complexity
     ##
     ## adapted from: flake8-annotations-complexity v0.0.6
     #######################################################################
-    
+
     def __checkAnnotationComplexity(self):
         """
         Private method to check the type annotation complexity.
         """
         maxAnnotationComplexity = self.__args.get(
-            "MaximumComplexity",
-            AnnotationsCheckerDefaultArgs["MaximumComplexity"])
+            "MaximumComplexity", AnnotationsCheckerDefaultArgs["MaximumComplexity"]
+        )
         maxAnnotationLength = self.__args.get(
-            "MaximumLength", AnnotationsCheckerDefaultArgs["MaximumLength"])
+            "MaximumLength", AnnotationsCheckerDefaultArgs["MaximumLength"]
+        )
         typeAnnotations = []
-        
+
         functionDefs = [
-            f for f in ast.walk(self.__tree)
+            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]))
+            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]
+        typeAnnotations += [
+            a.annotation
+            for a in ast.walk(self.__tree)
+            if isinstance(a, ast.AnnAssign) and a.annotation
+        ]
         for annotation in typeAnnotations:
             complexity = self.__getAnnotationComplexity(annotation)
             if complexity > maxAnnotationComplexity:
-                self.__error(annotation.lineno - 1, annotation.col_offset,
-                             "A891", complexity, maxAnnotationComplexity)
-            
+                self.__error(
+                    annotation.lineno - 1,
+                    annotation.col_offset,
+                    "A891",
+                    complexity,
+                    maxAnnotationComplexity,
+                )
+
             annotationLength = self.__getAnnotationLength(annotation)
             if annotationLength > maxAnnotationLength:
-                self.__error(annotation.lineno - 1, annotation.col_offset,
-                             "A892", annotationLength, maxAnnotationLength)
-    
+                self.__error(
+                    annotation.lineno - 1,
+                    annotation.col_offset,
+                    "A892",
+                    annotationLength,
+                    maxAnnotationLength,
+                )
+
     def __getAnnotationComplexity(self, annotationNode, defaultComplexity=1):
         """
         Private method to determine the annotation complexity.
-        
+
         @param annotationNode reference to the node to determine the annotation
             complexity for
         @type ast.AST
@@ -510,25 +557,24 @@
                 return defaultComplexity
         if isinstance(annotationNode, ast.Subscript):
             if sys.version_info >= (3, 9):
-                return (defaultComplexity +
-                        self.__getAnnotationComplexity(annotationNode.slice))
+                return defaultComplexity + self.__getAnnotationComplexity(
+                    annotationNode.slice
+                )
             else:
-                return (
-                    defaultComplexity +
-                    self.__getAnnotationComplexity(annotationNode.slice.value)
+                return defaultComplexity + self.__getAnnotationComplexity(
+                    annotationNode.slice.value
                 )
         if isinstance(annotationNode, ast.Tuple):
             return max(
-                (self.__getAnnotationComplexity(n)
-                 for n in annotationNode.elts),
-                default=defaultComplexity
+                (self.__getAnnotationComplexity(n) for n in annotationNode.elts),
+                default=defaultComplexity,
             )
         return defaultComplexity
-    
+
     def __getAnnotationLength(self, annotationNode):
         """
         Private method to determine the annotation length.
-        
+
         @param annotationNode reference to the node to determine the annotation
             length for
         @type ast.AST
@@ -549,26 +595,24 @@
             except AttributeError:
                 return 0
         return 0
-    
+
     #######################################################################
     ## 'from __future__ import annotations' checck
     ##
     ## adapted from: flake8-future-annotations v0.0.4
     #######################################################################
-    
+
     def __checkAnnotationsFuture(self):
         """
         Private method to check the use of __future__ and typing imports.
         """
         from .AnnotationsFutureVisitor import AnnotationsFutureVisitor
+
         visitor = AnnotationsFutureVisitor()
         visitor.visit(self.__tree)
-        
-        if (
-            visitor.importsFutureAnnotations() or
-            not visitor.hasTypingImports()
-        ):
+
+        if visitor.importsFutureAnnotations() or not visitor.hasTypingImports():
             return
-        
+
         imports = ", ".join(visitor.getTypingImports())
         self.__error(0, 0, "A871", imports)

eric ide

mercurial