--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py Thu Feb 27 09:22:15 2025 +0100 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py Thu Feb 27 14:42:39 2025 +0100 @@ -9,18 +9,19 @@ import ast import contextlib -import copy import sys from functools import lru_cache import AstUtilities +from CodeStyleTopicChecker import CodeStyleTopicChecker + from .AnnotationsCheckerDefaults import AnnotationsCheckerDefaultArgs from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType -class AnnotationsChecker: +class AnnotationsChecker(CodeStyleTopicChecker): """ Class implementing a checker for function type annotations. """ @@ -58,6 +59,7 @@ ## deprecated 'typing' symbols (PEP 585) "A-911", ] + Category = "A" def __init__(self, source, filename, tree, select, ignore, expected, repeat, args): """ @@ -80,20 +82,17 @@ @param args dictionary of arguments for the annotation checks @type dict """ - self.__select = tuple(select) - self.__ignore = tuple(ignore) - self.__expected = expected[:] - self.__repeat = repeat - self.__filename = filename - self.__source = source[:] - self.__tree = copy.deepcopy(tree) - self.__args = args - - # statistics counters - self.counters = {} - - # collection of detected errors - self.errors = [] + super().__init__( + AnnotationsChecker.Category, + source, + filename, + tree, + select, + ignore, + expected, + repeat, + args, + ) checkersWithCodes = [ ( @@ -120,76 +119,7 @@ (self.__checkAnnotationPep604, ("A-901",)), (self.__checkDeprecatedTypingSymbols, ("A-911",)), ] - - 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 in self.__ignore or ( - 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 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 - - for check in self.__checkers: - check() + self._initializeCheckers(checkersWithCodes) ####################################################################### ## Annotations @@ -205,50 +135,50 @@ # Type ignores are provided by ast at the module level & we'll need them later # when deciding whether or not to emit errors for a given function - typeIgnoreLineno = {ti.lineno for ti in self.__tree.type_ignores} + typeIgnoreLineno = {ti.lineno for ti in self.tree.type_ignores} hasMypyIgnoreErrors = any( - "# mypy: ignore-errors" in line for line in self.__source[:5] + "# mypy: ignore-errors" in line for line in self.source[:5] ) - suppressNoneReturning = self.__args.get( + suppressNoneReturning = self.args.get( "SuppressNoneReturning", AnnotationsCheckerDefaultArgs["SuppressNoneReturning"], ) - suppressDummyArgs = self.__args.get( + suppressDummyArgs = self.args.get( "SuppressDummyArgs", AnnotationsCheckerDefaultArgs["SuppressDummyArgs"] ) - allowUntypedDefs = self.__args.get( + allowUntypedDefs = self.args.get( "AllowUntypedDefs", AnnotationsCheckerDefaultArgs["AllowUntypedDefs"] ) - allowUntypedNested = self.__args.get( + allowUntypedNested = self.args.get( "AllowUntypedNested", AnnotationsCheckerDefaultArgs["AllowUntypedNested"] ) - mypyInitReturn = self.__args.get( + mypyInitReturn = self.args.get( "MypyInitReturn", AnnotationsCheckerDefaultArgs["MypyInitReturn"] ) - allowStarArgAny = self.__args.get( + allowStarArgAny = self.args.get( "AllowStarArgAny", AnnotationsCheckerDefaultArgs["AllowStarArgAny"] ) - respectTypeIgnore = self.__args.get( + respectTypeIgnore = self.args.get( "RespectTypeIgnore", AnnotationsCheckerDefaultArgs["RespectTypeIgnore"] ) # Store decorator lists as sets for easier lookup dispatchDecorators = set( - self.__args.get( + self.args.get( "DispatchDecorators", AnnotationsCheckerDefaultArgs["DispatchDecorators"], ) ) overloadDecorators = set( - self.__args.get( + self.args.get( "OverloadDecorators", AnnotationsCheckerDefaultArgs["OverloadDecorators"], ) ) - visitor = FunctionVisitor(self.__source) - visitor.visit(self.__tree) + 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, @@ -259,7 +189,7 @@ # Iterate over the arguments with missing type hints, by function. for function in visitor.functionDefinitions: if function.hasTypeComment: - self.__error(function.lineno - 1, function.col_offset, "A-402") + self.addErrorFromNode(function, "A-402") if function.isDynamicallyTyped() and ( allowUntypedDefs or (function.isNested and allowUntypedNested) @@ -284,7 +214,7 @@ }: continue - self.__error(function.lineno - 1, function.col_offset, "A-401") + self.addErrorFromNode(function, "A-401") # Before we iterate over the function's missing annotations, check # to see if it's the closing function def in a series of @@ -317,7 +247,7 @@ # Check for type comments here since we're not considering them as # typed args if arg.hasTypeComment: - self.__error(arg.lineno - 1, arg.col_offset, "A-402") + self.addErrorFromNode(arg, "A-402") if arg.argname == "return": # return annotations have multiple possible short-circuit @@ -384,9 +314,9 @@ ) if errorCode in ("A-001", "A-002", "A-003"): - self.__error(arg.lineno - 1, arg.col_offset, errorCode, arg.argname) + self.addErrorFromNode(arg, errorCode, arg.argname) else: - self.__error(arg.lineno - 1, arg.col_offset, errorCode) + self.addErrorFromNode(arg, errorCode) @lru_cache() # __IGNORE_WARNING_M-519__ def __returnErrorClassifier(self, isClassMethod, classDecoratorType, functionType): @@ -467,7 +397,7 @@ """ Private method to check for function annotation coverage. """ - minAnnotationsCoverage = self.__args.get( + minAnnotationsCoverage = self.args.get( "MinimumCoverage", AnnotationsCheckerDefaultArgs["MinimumCoverage"] ) if minAnnotationsCoverage == 0: @@ -476,7 +406,7 @@ functionDefs = [ f - for f in ast.walk(self.__tree) + for f in ast.walk(self.tree) if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) ] if not functionDefs: @@ -495,7 +425,7 @@ * 100 ) if annotationsCoverage < minAnnotationsCoverage: - self.__error(0, 0, "A-881", annotationsCoverage) + self.addError(1, 0, "A-881", annotationsCoverage) def __hasTypeAnnotations(self, funcNode): """ @@ -538,17 +468,17 @@ """ Private method to check the type annotation complexity. """ - maxAnnotationComplexity = self.__args.get( + maxAnnotationComplexity = self.args.get( "MaximumComplexity", AnnotationsCheckerDefaultArgs["MaximumComplexity"] ) - maxAnnotationLength = self.__args.get( + maxAnnotationLength = self.args.get( "MaximumLength", AnnotationsCheckerDefaultArgs["MaximumLength"] ) typeAnnotations = [] functionDefs = [ f - for f in ast.walk(self.__tree) + for f in ast.walk(self.tree) if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) ] for functionDef in functionDefs: @@ -559,28 +489,20 @@ typeAnnotations.append(functionDef.returns) typeAnnotations += [ a.annotation - for a in ast.walk(self.__tree) + 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, - "A-891", - complexity, - maxAnnotationComplexity, + self.addErrorFromNode( + annotation, "A-891", complexity, maxAnnotationComplexity ) annotationLength = self.__getAnnotationLength(annotation) if annotationLength > maxAnnotationLength: - self.__error( - annotation.lineno - 1, - annotation.col_offset, - "A-892", - annotationLength, - maxAnnotationLength, + self.addErrorFromNode( + annotation, "A-892", annotationLength, maxAnnotationLength ) def __getAnnotationComplexity(self, annotationNode, defaultComplexity=1): @@ -663,30 +585,30 @@ # the __future__ typing import is only needed before Python 3.9 return - forceFutureAnnotations = self.__args.get( + forceFutureAnnotations = self.args.get( "ForceFutureAnnotations", AnnotationsCheckerDefaultArgs["ForceFutureAnnotations"], ) - checkFutureAnnotations = self.__args.get( + checkFutureAnnotations = self.args.get( "CheckFutureAnnotations", AnnotationsCheckerDefaultArgs["CheckFutureAnnotations"], ) visitor = AnnotationsFutureVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) if visitor.importsFutureAnnotations(): return if visitor.hasTypingImports(): imports = ", ".join(visitor.getTypingImports()) - self.__error(0, 0, "A-871", imports) + self.addError(1, 0, "A-871", imports) elif forceFutureAnnotations: - self.__error(0, 0, "A-872") + self.addError(1, 0, "A-872") if checkFutureAnnotations and visitor.hasSimplifiedTypes(): simplifiedTypes = ", ".join(sorted(visitor.getSimplifiedTypes())) - self.__error(0, 0, "A-873", simplifiedTypes) + self.addError(1, 0, "A-873", simplifiedTypes) ####################################################################### ## check use of 'typing.Union' (see PEP 604) @@ -705,10 +627,10 @@ return visitor = AnnotationsUnionVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) for node in visitor.getIssues(): - self.__error(node.lineno - 1, node.col_offset, "A-901") + self.addErrorFromNode(node, "A-901") ####################################################################### ## check use of 'typing.Union' (see PEP 604) @@ -728,17 +650,17 @@ if sys.version_info < (3, 9): # py 3.8: only if activated via __future__ import visitor = AnnotationsFutureImportVisitor() - visitor.visit(self.__tree) + visitor.visit(self.tree) if not visitor.futureImportPresent(): return visitor = AnnotationsDeprecationsVisitor( - self.__args.get( + self.args.get( "ExemptedTypingSymbols", AnnotationsCheckerDefaultArgs["ExemptedTypingSymbols"], ) ) - visitor.visit(self.__tree) + visitor.visit(self.tree) for node, (name, replacement) in visitor.getIssues(): - self.__error(node.lineno - 1, node.col_offset, "A-911", name, replacement) + self.addErrorFromNode(node, "A-911", name, replacement)