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