Code Style Checker eric7

Thu, 28 Jul 2022 14:19:57 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 28 Jul 2022 14:19:57 +0200
branch
eric7
changeset 9276
e6748a5e24b9
parent 9275
1a7d545d3ef2
child 9277
471c5a263d53

Code Style Checker
- updated the annotations checker to support more cases

docs/changelog file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsCheckerDefaults.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFutureVisitor.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui file | annotate | diff | comparison | revisions
--- a/docs/changelog	Wed Jul 27 18:02:43 2022 +0200
+++ b/docs/changelog	Thu Jul 28 14:19:57 2022 +0200
@@ -5,6 +5,7 @@
 - Code Style Checker
   -- extended the Naming style checker to be more PEP8 compliant
   -- updated imports checker to support banned module patterns
+  -- updated the annotations checker to support more cases
 - MicroPython
   -- added capability to connect to devices for which only the serial port name
      is available
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py	Thu Jul 28 14:19:57 2022 +0200
@@ -7,8 +7,9 @@
 Module implementing a checker for function type annotations.
 """
 
+import ast
+import contextlib
 import copy
-import ast
 import sys
 from functools import lru_cache
 
@@ -40,8 +41,11 @@
         "A206",
         ## Mixed kind of annotations
         "A301",
+        ## Dynamically typed annotations
+        "A401",
         ## Annotations Future
         "A871",
+        "A872",
         ## Annotation Coverage
         "A881",
         ## Annotation Complexity
@@ -101,9 +105,10 @@
                     "A205",
                     "A206",
                     "A301",
+                    "A401",
                 ),
             ),
-            (self.__checkAnnotationsFuture, ("A871",)),
+            (self.__checkAnnotationsFuture, ("A871", "A872")),
             (self.__checkAnnotationsCoverage, ("A881",)),
             (self.__checkAnnotationComplexity, ("A891", "A892")),
         ]
@@ -179,9 +184,8 @@
     #######################################################################
     ## Annotations
     ##
-    ## adapted from: flake8-annotations v2.7.0
+    ## adapted from: flake8-annotations v2.9.0
     #######################################################################
-    # TODO: update to v2.9.0
 
     def __checkFunctionAnnotations(self):
         """
@@ -203,6 +207,9 @@
         mypyInitReturn = self.__args.get(
             "MypyInitReturn", AnnotationsCheckerDefaultArgs["MypyInitReturn"]
         )
+        allowStarArgAny = self.__args.get(
+            "AllowStarArgAny", AnnotationsCheckerDefaultArgs["AllowStarArgAny"]
+        )
 
         # Store decorator lists as sets for easier lookup
         dispatchDecorators = set(
@@ -250,9 +257,11 @@
             has3107Annotation = False
             # PEP 3107 annotations are captured by the return arg
 
+            annotatedArgs = function.getAnnotatedArguments()
+
             # 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():
+            for arg in annotatedArgs:
                 if arg.hasTypeComment:
                     hasTypeComment = True
 
@@ -265,6 +274,19 @@
                     self.__error(function.lineno - 1, function.col_offset, "A301")
                     break
 
+            # Iterate over the annotated args to look for 'typing.Any' annotations
+            # We could combine this with the above loop but I'd rather not add even
+            # more sentinels unless we'd notice a significant enough performance impact
+            for arg in annotatedArgs:
+                if arg.isDynamicallyTyped:
+                    if allowStarArgAny and arg.annotationType in {
+                        AnnotationType.VARARG,
+                        AnnotationType.KWARG,
+                    }:
+                        continue
+
+                    self.__error(function.lineno - 1, function.col_offset, "A401")
+
             # 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.
@@ -294,7 +316,7 @@
                         mypyInitReturn
                         and function.isClassMethod
                         and function.name == "__init__"
-                        and function.getAnnotatedArguments()
+                        and annotatedArgs
                     ):
                         # Skip recording return errors for `__init__` if at
                         # least one argument is annotated
@@ -419,9 +441,8 @@
     #######################################################################
     ## Annotations Coverage
     ##
-    ## adapted from: flake8-annotations-coverage v0.0.5
+    ## adapted from: flake8-annotations-coverage v0.0.6
     #######################################################################
-    # TODO: update to v0.0.6
 
     def __checkAnnotationsCoverage(self):
         """
@@ -446,6 +467,9 @@
         functionDefAnnotationsInfo = [
             self.__hasTypeAnnotations(f) for f in functionDefs
         ]
+        if not bool(functionDefAnnotationsInfo):
+            return
+
         annotationsCoverage = int(
             len(list(filter(None, functionDefAnnotationsInfo)))
             / len(functionDefAnnotationsInfo)
@@ -488,9 +512,8 @@
     #######################################################################
     ## Annotations Complexity
     ##
-    ## adapted from: flake8-annotations-complexity v0.0.6
+    ## adapted from: flake8-annotations-complexity v0.0.7
     #######################################################################
-    # TODO: update to v0.0.7
 
     def __checkAnnotationComplexity(self):
         """
@@ -511,7 +534,7 @@
         ]
         for functionDef in functionDefs:
             typeAnnotations += list(
-                filter(None, [a.annotation for a in functionDef.args.args])
+                filter(None, (a.annotation for a in functionDef.args.args))
             )
             if functionDef.returns:
                 typeAnnotations.append(functionDef.returns)
@@ -558,21 +581,25 @@
                 annotationNode = ast.parse(annotationNode.s).body[0].value
             except (SyntaxError, IndexError):
                 return defaultComplexity
+
+        complexity = defaultComplexity
         if isinstance(annotationNode, ast.Subscript):
             if sys.version_info >= (3, 9):
-                return defaultComplexity + self.__getAnnotationComplexity(
+                complexity = defaultComplexity + self.__getAnnotationComplexity(
                     annotationNode.slice
                 )
             else:
-                return defaultComplexity + self.__getAnnotationComplexity(
+                complexity = defaultComplexity + self.__getAnnotationComplexity(
                     annotationNode.slice.value
                 )
+
         if isinstance(annotationNode, ast.Tuple):
-            return max(
+            complexity = max(
                 (self.__getAnnotationComplexity(n) for n in annotationNode.elts),
                 default=defaultComplexity,
             )
-        return defaultComplexity
+
+        return complexity
 
     def __getAnnotationLength(self, annotationNode):
         """
@@ -584,27 +611,28 @@
         @return annotation length
         @rtype = int
         """
+        annotationLength = 0
         if AstUtilities.isString(annotationNode):
             try:
                 annotationNode = ast.parse(annotationNode.s).body[0].value
             except (SyntaxError, IndexError):
-                return 0
+                return annotationLength
+
         if isinstance(annotationNode, ast.Subscript):
-            try:
-                if sys.version_info >= (3, 9):
-                    return len(annotationNode.slice.elts)
-                else:
-                    return len(annotationNode.slice.value.elts)
-            except AttributeError:
-                return 0
-        return 0
+            with contextlib.suppress(AttributeError):
+                annotationLength = (
+                    len(annotationNode.slice.elts)
+                    if sys.version_info >= (3, 9)
+                    else len(annotationNode.slice.value.elts)
+                )
+
+        return annotationLength
 
     #######################################################################
     ## 'from __future__ import annotations' checck
     ##
-    ## adapted from: flake8-future-annotations v0.0.4
+    ## adapted from: flake8-future-annotations v0.0.5
     #######################################################################
-    # TODO: update to v0.0.5
 
     def __checkAnnotationsFuture(self):
         """
@@ -612,11 +640,19 @@
         """
         from .AnnotationsFutureVisitor import AnnotationsFutureVisitor
 
+        forceFutureAnnotations = self.__args.get(
+            "ForceFutureAnnotations",
+            AnnotationsCheckerDefaultArgs["ForceFutureAnnotations"],
+        )
+
         visitor = AnnotationsFutureVisitor()
         visitor.visit(self.__tree)
 
-        if visitor.importsFutureAnnotations() or not visitor.hasTypingImports():
+        if visitor.importsFutureAnnotations():
             return
 
-        imports = ", ".join(visitor.getTypingImports())
-        self.__error(0, 0, "A871", imports)
+        if visitor.hasTypingImports():
+            imports = ", ".join(visitor.getTypingImports())
+            self.__error(0, 0, "A871", imports)
+        elif forceFutureAnnotations:
+            self.__error(0, 0, "A872")
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsCheckerDefaults.py	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsCheckerDefaults.py	Thu Jul 28 14:19:57 2022 +0200
@@ -14,6 +14,7 @@
     "AllowUntypedDefs": False,
     "AllowUntypedNested": False,
     "MypyInitReturn": False,
+    "AllowStarArgAny": False,
     "DispatchDecorators": ["singledispatch", "singledispatchmethod"],
     "OverloadDecorators": ["overload"],
     # Annotation Coverage
@@ -21,4 +22,6 @@
     # Annotation Complexity
     "MaximumComplexity": 3,
     "MaximumLength": 7,
+    # Annotations Future
+    "ForceFutureAnnotations": False,
 }
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py	Thu Jul 28 14:19:57 2022 +0200
@@ -8,7 +8,7 @@
 """
 
 #
-# adapted from flake8-annotations v2.7.0
+# adapted from flake8-annotations v2.9.0
 #
 
 import enum
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py	Thu Jul 28 14:19:57 2022 +0200
@@ -8,7 +8,7 @@
 """
 
 #
-# The visitor and associated classes are adapted from flake8-annotations v2.7.0
+# The visitor and associated classes are adapted from flake8-annotations v2.9.0
 #
 
 import ast
@@ -35,6 +35,7 @@
         hasTypeAnnotation=False,
         has3107Annotation=False,
         hasTypeComment=False,
+        isDynamicallyTyped=False,
     ):
         """
         Constructor
@@ -56,6 +57,8 @@
         @param hasTypeComment flag indicating the presence of a type comment
             (defaults to False)
         @type bool (optional)
+        @param isDynamicallyTyped flag indicating dynamic typing (defaults to False)
+        @type bool (optional)
         """
         self.argname = argname
         self.lineno = lineno
@@ -64,6 +67,7 @@
         self.hasTypeAnnotation = hasTypeAnnotation
         self.has3107Annotation = has3107Annotation
         self.hasTypeComment = hasTypeComment
+        self.isDynamicallyTyped = isDynamicallyTyped
 
     @classmethod
     def fromNode(cls, node, annotationTypeName):
@@ -89,8 +93,43 @@
             newArg.hasTypeAnnotation = True
             newArg.hasTypeComment = True
 
+            if cls._isAnnotatedAny(node.type_comment):
+                newArg.isDynamicallyTyped = True
+
         return newArg
 
+    @staticmethod
+    def _isAnnotatedAny(argExpr):
+        """
+        Static method to check if the provided expression node is annotated with
+        'typing.Any'.
+
+        Support is provided for the following patterns:
+            * 'from typing import Any; foo: Any'
+            * 'import typing; foo: typing.Any'
+            * 'import typing as <alias>; foo: <alias>.Any'
+
+        Type comments are also supported. Inline type comments are assumed to be
+        passed here as 'str', and function-level type comments are assumed to be
+        passed as 'ast.expr'.
+
+        @param argExpr DESCRIPTION
+        @type ast.expr or str
+        @return flag indicating an annotation with 'typing.Any'
+        @rtype bool
+        """
+        if isinstance(argExpr, ast.Name):
+            if argExpr.id == "Any":
+                return True
+        elif isinstance(argExpr, ast.Attribute):
+            if argExpr.attr == "Any":
+                return True
+        elif isinstance(argExpr, str):  # __IGNORE_WARNING_Y102__
+            if argExpr.split(".", maxsplit=1)[-1] == "Any":
+                return True
+
+        return False
+
 
 class Function:
     """
@@ -319,6 +358,9 @@
             returnArg.has3107Annotation = True
             newFunction.isReturnAnnotated = True
 
+            if Argument._isAnnotatedAny(node.returns):
+                returnArg.isDynamicallyTyped = True
+
         newFunction.args.append(returnArg)
 
         # Type comments in-line with input arguments are handled by the
@@ -426,10 +468,15 @@
                 arg.hasTypeAnnotation = True
                 arg.hasTypeComment = True
 
+                if Argument._isAnnotatedAny(hintComment):
+                    arg.isDynamicallyTyped = True
+
         # Return arg is always last
         funcObj.args[-1].hasTypeAnnotation = True
         funcObj.args[-1].hasTypeComment = True
         funcObj.isReturnAnnotated = True
+        if Argument._isAnnotatedAny(hintTree.returns):
+            arg.isDynamicallyTyped = True
 
         return funcObj
 
@@ -476,9 +523,9 @@
             # Short circuit
             return hintTree
 
-        if funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and len(
-            hintTree.argtypes
-        ) < (len(funcObj.args) - 1):
+        if funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and (
+            len(hintTree.argtypes) < (len(funcObj.args) - 1)
+        ):
             # Subtract 1 to skip return arg
             hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes
 
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFutureVisitor.py	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFutureVisitor.py	Thu Jul 28 14:19:57 2022 +0200
@@ -9,7 +9,7 @@
 
 #
 # The visitor and associated classes are adapted from flake8-future-annotations
-# v0.0.4
+# v0.0.5
 #
 
 import ast
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py	Thu Jul 28 14:19:57 2022 +0200
@@ -49,10 +49,17 @@
         "AnnotationsChecker",
         "PEP 484 disallows both type annotations and type comments",
     ),
+    "A401": QCoreApplication.translate(
+        "AnnotationsChecker",
+        "Dynamically typed expressions (typing.Any) are disallowed",
+    ),
     "A871": QCoreApplication.translate(
         "AnnotationsChecker",
         "missing 'from __future__ import annotations' but imports: {0}",
     ),
+    "A872": QCoreApplication.translate(
+        "AnnotationsChecker", "missing 'from __future__ import annotations'"
+    ),
     "A881": QCoreApplication.translate(
         "AnnotationsChecker", "type annotation coverage of {0}% is too low"
     ),
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Thu Jul 28 14:19:57 2022 +0200
@@ -539,6 +539,18 @@
                         ],
                     }
                 )
+            if "AllowStarArgAny" not in self.__data["AnnotationsChecker"]:
+                # AllowStarArgAny is the sentinel for the second extension
+                self.__data["AnnotationsChecker"].update(
+                    {
+                        "AllowStarArgAny": AnnotationsCheckerDefaultArgs[
+                            "AllowStarArgAny"
+                        ],
+                        "ForceFutureAnnotations": AnnotationsCheckerDefaultArgs[
+                            "ForceFutureAnnotations"
+                        ],
+                    }
+                )
 
         if "SecurityChecker" not in self.__data:
             from .Security.SecurityDefaults import SecurityDefaults
@@ -628,6 +640,12 @@
         self.mypyInitReturnCheckBox.setChecked(
             self.__data["AnnotationsChecker"]["MypyInitReturn"]
         )
+        self.allowStarArgAnyCheckBox.setChecked(
+            self.__data["AnnotationsChecker"]["AllowStarArgAny"]
+        )
+        self.forceFutureAnnotationsCheckBox.setChecked(
+            self.__data["AnnotationsChecker"]["ForceFutureAnnotations"]
+        )
         self.dispatchDecoratorEdit.setText(
             ", ".join(self.__data["AnnotationsChecker"]["DispatchDecorators"])
         )
@@ -805,6 +823,10 @@
                 "AllowUntypedDefs": self.allowUntypedDefsCheckBox.isChecked(),
                 "AllowUntypedNested": self.allowUntypedNestedCheckBox.isChecked(),
                 "MypyInitReturn": self.mypyInitReturnCheckBox.isChecked(),
+                "AllowStarArgAny": self.allowStarArgAnyCheckBox.isChecked(),
+                "ForceFutureAnnotations": (
+                    self.forceFutureAnnotationsCheckBox.isChecked()
+                ),
                 "DispatchDecorators": [
                     d.strip() for d in self.dispatchDecoratorEdit.text().split(",")
                 ],
@@ -1246,6 +1268,10 @@
                     "AllowUntypedDefs": self.allowUntypedDefsCheckBox.isChecked(),
                     "AllowUntypedNested": self.allowUntypedNestedCheckBox.isChecked(),
                     "MypyInitReturn": self.mypyInitReturnCheckBox.isChecked(),
+                    "AllowStarArgAny": self.allowStarArgAnyCheckBox.isChecked(),
+                    "ForceFutureAnnotations": (
+                        self.forceFutureAnnotationsCheckBox.isChecked()
+                    ),
                     "DispatchDecorators": [
                         d.strip() for d in self.dispatchDecoratorEdit.text().split(",")
                     ],
@@ -1640,6 +1666,22 @@
                 )
             )
         )
+        self.allowStarArgAnyCheckBox.setChecked(
+            Preferences.toBool(
+                Preferences.getSettings().value(
+                    "PEP8/AllowStarArgAny",
+                    AnnotationsCheckerDefaultArgs["AllowStarArgAny"],
+                )
+            )
+        )
+        self.forceFutureAnnotationsCheckBox.setChecked(
+            Preferences.toBool(
+                Preferences.getSettings().value(
+                    "PEP8/ForceFutureAnnotations",
+                    AnnotationsCheckerDefaultArgs["ForceFutureAnnotations"],
+                )
+            )
+        )
         self.dispatchDecoratorEdit.setText(
             ", ".join(
                 Preferences.toList(
@@ -1873,6 +1915,13 @@
             "PEP8/MypyInitReturn", self.mypyInitReturnCheckBox.isChecked()
         )
         Preferences.getSettings().setValue(
+            "PEP8/AllowStarArgAny", self.allowStarArgAnyCheckBox.isChecked()
+        )
+        Preferences.getSettings().setValue(
+            "PEP8/ForceFutureAnnotations",
+            self.forceFutureAnnotationsCheckBox.isChecked(),
+        )
+        Preferences.getSettings().setValue(
             "PEP8/DispatchDecorators",
             [d.strip() for d in self.dispatchDecoratorEdit.text().split(",")],
         )
@@ -2014,6 +2063,13 @@
             "PEP8/MypyInitReturn", AnnotationsCheckerDefaultArgs["MypyInitReturn"]
         )
         Preferences.getSettings().setValue(
+            "PEP8/AllowStarArgAny", AnnotationsCheckerDefaultArgs["AllowStarArgAny"]
+        )
+        Preferences.getSettings().setValue(
+            "PEP8/ForceFutureAnnotations",
+            AnnotationsCheckerDefaultArgs["ForceFutureAnnotations"],
+        )
+        Preferences.getSettings().setValue(
             "PEP8/DispatchDecorators",
             AnnotationsCheckerDefaultArgs["DispatchDecorators"],
         )
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui	Wed Jul 27 18:02:43 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.ui	Thu Jul 28 14:19:57 2022 +0200
@@ -977,7 +977,7 @@
                   </property>
                  </widget>
                 </item>
-                <item row="2" column="0" colspan="2">
+                <item row="2" column="0">
                  <widget class="QCheckBox" name="mypyInitReturnCheckBox">
                   <property name="toolTip">
                    <string>Select to not report unhinted '__init__' return</string>
@@ -987,6 +987,26 @@
                   </property>
                  </widget>
                 </item>
+                <item row="2" column="1">
+                 <widget class="QCheckBox" name="allowStarArgAnyCheckBox">
+                  <property name="toolTip">
+                   <string>Allow dynamically typed *args and **kwargs</string>
+                  </property>
+                  <property name="text">
+                   <string>Allow dynamically typed * Arguments</string>
+                  </property>
+                 </widget>
+                </item>
+                <item row="3" column="0">
+                 <widget class="QCheckBox" name="forceFutureAnnotationsCheckBox">
+                  <property name="toolTip">
+                   <string>Enforce the presence of a 'from __future__ import annotations' statement</string>
+                  </property>
+                  <property name="text">
+                   <string>Enforce '__future__' statement</string>
+                  </property>
+                 </widget>
+                </item>
                </layout>
               </item>
               <item>
@@ -1796,10 +1816,12 @@
   <tabstop>maxAnnotationsComplexitySpinBox</tabstop>
   <tabstop>maxAnnotationsLengthSpinBox</tabstop>
   <tabstop>suppressNoneReturningCheckBox</tabstop>
+  <tabstop>suppressDummyArgsCheckBox</tabstop>
   <tabstop>allowUntypedDefsCheckBox</tabstop>
+  <tabstop>allowUntypedNestedCheckBox</tabstop>
   <tabstop>mypyInitReturnCheckBox</tabstop>
-  <tabstop>suppressDummyArgsCheckBox</tabstop>
-  <tabstop>allowUntypedNestedCheckBox</tabstop>
+  <tabstop>allowStarArgAnyCheckBox</tabstop>
+  <tabstop>forceFutureAnnotationsCheckBox</tabstop>
   <tabstop>dispatchDecoratorEdit</tabstop>
   <tabstop>overloadDecoratorEdit</tabstop>
   <tabstop>tmpDirectoriesEdit</tabstop>

eric ide

mercurial