eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py

changeset 7782
976d3b19ba7f
parent 7781
607a6098cb44
child 7783
36f66ce496bd
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py	Mon Oct 12 19:28:42 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,465 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2019 - 2020 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing a checker for function type annotations.
-"""
-
-import sys
-import ast
-
-import AstUtilities
-
-
-class AnnotationsChecker(object):
-    """
-    Class implementing a checker for function type annotations.
-    """
-    Codes = [
-        ## Function Annotations
-        "A001", "A002", "A003",
-        
-        ## Method Annotations
-        "A101", "A102",
-        
-        ## 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,
-                 args):
-        """
-        Constructor
-        
-        @param source source code to be checked
-        @type list of str
-        @param filename name of the source file
-        @type str
-        @param select list of selected codes
-        @type list of str
-        @param ignore list of codes to be ignored
-        @type list of str
-        @param expected list of expected codes
-        @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)
-        self.__expected = expected[:]
-        self.__repeat = repeat
-        self.__filename = filename
-        self.__source = source[:]
-        self.__args = args
-
-        # statistics counters
-        self.counters = {}
-        
-        # collection of detected errors
-        self.errors = []
-        
-        checkersWithCodes = [
-            (
-                self.__checkFunctionAnnotations,
-                ("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))
-                    for code in codes):
-                self.__checkers.append(checker)
-    
-    def __ignoreCode(self, code):
-        """
-        Private method to check if the message code should be ignored.
-
-        @param code message code to check for
-        @type str
-        @return flag indicating to ignore the given code
-        @rtype bool
-        """
-        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
-        @type int
-        @param code message code
-        @type str
-        @param args arguments for the message
-        @type list
-        """
-        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(
-                {
-                    "file": self.__filename,
-                    "line": lineNumber + 1,
-                    "offset": offset,
-                    "code": code,
-                    "args": args,
-                }
-            )
-    
-    def __reportInvalidSyntax(self):
-        """
-        Private method to report a syntax error.
-        """
-        exc_type, exc = sys.exc_info()[:2]
-        if len(exc.args) > 1:
-            offset = exc.args[1]
-            if len(offset) > 2:
-                offset = offset[1:3]
-        else:
-            offset = (1, 0)
-        self.__error(offset[0] - 1, offset[1] or 0,
-                     'A999', exc_type.__name__, exc.args[0])
-    
-    def __generateTree(self):
-        """
-        Private method to generate an AST for our source.
-        
-        @return generated AST
-        @rtype ast.Module
-        """
-        source = "".join(self.__source)
-        return compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST)
-    
-    def run(self):
-        """
-        Public method to check the given source against annotation issues.
-        """
-        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
-        
-        try:
-            self.__tree = self.__generateTree()
-        except (SyntaxError, TypeError):
-            self.__reportInvalidSyntax()
-            return
-        
-        for check in self.__checkers:
-            check()
-    
-    def __checkFunctionAnnotations(self):
-        """
-        Private method to check for function annotation issues.
-        """
-        visitor = FunctionVisitor(self.__source)
-        visitor.visit(self.__tree)
-        for issue in visitor.issues:
-            node = issue[0]
-            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):
-    """
-    Class implementing a node visitor to check function annotations.
-    
-    Note: this class is modelled after flake8-annotations checker.
-    """
-    def __init__(self, sourceLines):
-        """
-        Constructor
-        
-        @param sourceLines lines of source code
-        @type list of str
-        """
-        super(FunctionVisitor, self).__init__()
-        
-        self.__sourceLines = sourceLines
-        
-        self.issues = []
-    
-    def visit_FunctionDef(self, node):
-        """
-        Public method to handle a function or method definition.
-        
-        @param node reference to the node to be processed
-        @type ast.FunctionDef
-        """
-        self.__checkFunctionNode(node)
-        self.generic_visit(node)
-    
-    def visit_AsyncFunctionDef(self, node):
-        """
-        Public method to handle an async function or method definition.
-        
-        @param node reference to the node to be processed
-        @type ast.AsyncFunctionDef
-        """
-        self.__checkFunctionNode(node)
-        self.generic_visit(node)
-    
-    def visit_ClassDef(self, node):
-        """
-        Public method to handle class definitions.
-        
-        @param node reference to the node to be processed
-        @type ast.ClassDef
-        """
-        methodNodes = [
-            childNode for childNode in node.body
-            if isinstance(childNode, (ast.FunctionDef, ast.AsyncFunctionDef))
-        ]
-        for methodNode in methodNodes:
-            self.__checkFunctionNode(methodNode, classMethod=True)
-    
-    def __checkFunctionNode(self, node, classMethod=False):
-        """
-        Private method to check an individual function definition node.
-        
-        @param node reference to the node to be processed
-        @type ast.FunctionDef or ast.AsyncFunctionDef
-        @param classMethod flag indicating a class method
-        @type bool
-        """
-        if node.name.startswith("__") and node.name.endswith("__"):
-            visibilityType = "special"
-        elif node.name.startswith("__"):
-            visibilityType = "private"
-        elif node.name.startswith("_"):
-            visibilityType = "protected"
-        else:
-            visibilityType = "public"
-        
-        if classMethod:
-            decorators = [
-                decorator.id for decorator in node.decorator_list
-                if isinstance(decorator, ast.Name)
-            ]
-            if "classmethod" in decorators:
-                classMethodType = "decorator"
-            elif "staticmethod" in decorators:
-                classMethodType = "staticmethod"
-            else:
-                classMethodType = ""
-        else:
-            classMethodType = "function"
-        
-        # check argument annotations
-        for argType in ("args", "vararg", "kwonlyargs", "kwarg"):
-            args = node.args.__getattribute__(argType)
-            if args:
-                if not isinstance(args, list):
-                    args = [args]
-                
-                for arg in args:
-                    if not arg.annotation:
-                        self.__classifyArgumentError(
-                            arg, argType, classMethodType)
-        
-        # check function return annotation
-        if not node.returns:
-            lineno = node.lineno
-            colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1
-            self.__classifyReturnError(classMethodType, visibilityType,
-                                       lineno, colOffset)
-    
-    def __classifyReturnError(self, methodType, visibilityType, lineno,
-                              colOffset):
-        """
-        Private method to classify and record a return annotation issue.
-        
-        @param methodType type of method/function the argument belongs to
-        @type str
-        @param visibilityType visibility of the function
-        @type str
-        @param lineno line number
-        @type int
-        @param colOffset column number
-        @type int
-        """
-        # create a dummy AST node to report line and column
-        node = ast.AST()
-        node.lineno = lineno
-        node.col_offset = colOffset
-        
-        # now classify the issue
-        if methodType == "classmethod":
-            self.issues.append((node, "A206"))
-        elif methodType == "staticmethod":
-            self.issues.append((node, "A205"))
-        elif visibilityType == "special":
-            self.issues.append((node, "A204"))
-        elif visibilityType == "private":
-            self.issues.append((node, "A203"))
-        elif visibilityType == "protected":
-            self.issues.append((node, "A202"))
-        else:
-            self.issues.append((node, "A201"))
-    
-    def __classifyArgumentError(self, argNode, argType, methodType):
-        """
-        Private method to classify and record an argument annotation issue.
-        
-        @param argNode reference to the argument node
-        @type ast.arguments
-        @param argType type of the argument node
-        @type str
-        @param methodType type of method/function the argument belongs to
-        @type str
-        """
-        # check class method issues
-        if methodType != "function":
-            if argNode.arg in ("cls", "self"):
-                if methodType == "classmethod":
-                    self.issues.append((argNode, "A102"))
-                    return
-                elif methodType != "staticmethod":
-                    self.issues.append((argNode, "A101"))
-                    return
-        
-        # check all other arguments
-        if argType == "kwarg":
-            self.issues.append((argNode, "A003", argNode.arg))
-        elif argType == "vararg":
-            self.issues.append((argNode, "A002", argNode.arg))
-        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 AstUtilities.isString(annotationNode):
-        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