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

branch
eric7
changeset 11150
73d80859079c
parent 11148
15e30f0c76a8
diff -r fc45672fae42 -r 73d80859079c src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py
--- 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)

eric ide

mercurial