src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Naming/NamingStyleChecker.py

branch
eric7
changeset 9271
f655c20ff500
parent 9221
bf71ee032bb4
child 9653
e67609152c5e
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Naming/NamingStyleChecker.py	Sun Jul 24 16:41:08 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Naming/NamingStyleChecker.py	Wed Jul 27 15:01:13 2022 +0200
@@ -7,9 +7,9 @@
 Module implementing a checker for naming conventions.
 """
 
+import ast
 import collections
-import ast
-import re
+import functools
 import os
 
 try:
@@ -23,11 +23,6 @@
     Class implementing a checker for naming conventions.
     """
 
-    LowercaseRegex = re.compile(r"[_a-z][_a-z0-9]*$")
-    UppercaseRegexp = re.compile(r"[_A-Z][_A-Z0-9]*$")
-    CamelcaseRegexp = re.compile(r"_?[A-Z][a-zA-Z0-9]*$")
-    MixedcaseRegexp = re.compile(r"_?[a-z][a-zA-Z0-9]*$")
-
     Codes = [
         "N801",
         "N802",
@@ -37,11 +32,16 @@
         "N806",
         "N807",
         "N808",
+        "N809",
         "N811",
         "N812",
         "N813",
         "N814",
+        "N815",
+        "N818",
         "N821",
+        "N822",
+        "N823",
         "N831",
     ]
 
@@ -59,25 +59,42 @@
 
         self.__checkersWithCodes = {
             "classdef": [
-                (self.__checkClassName, ("N801",)),
-                (self.__checkNameToBeAvoided, ("N831",)),
-            ],
-            "functiondef": [
-                (self.__checkFunctionName, ("N802",)),
-                (self.__checkFunctionArgumentNames, ("N803", "N804", "N805", "N806")),
+                (self.__checkClassName, ("N801", "N818")),
                 (self.__checkNameToBeAvoided, ("N831",)),
             ],
-            "assign": [
-                (self.__checkVariablesInFunction, ("N821",)),
-                (self.__checkNameToBeAvoided, ("N831",)),
-            ],
-            "importfrom": [
-                (self.__checkImportAs, ("N811", "N812", "N813", "N814")),
-            ],
             "module": [
                 (self.__checkModule, ("N807", "N808")),
             ],
         }
+        for name in ("functiondef", "asyncfunctiondef"):
+            self.__checkersWithCodes[name] = [
+                (self.__checkFunctionName, ("N802", "N809")),
+                (self.__checkFunctionArgumentNames, ("N803", "N804", "N805", "N806")),
+                (self.__checkNameToBeAvoided, ("N831",)),
+            ]
+        for name in ("assign", "namedexpr", "annassign"):
+            self.__checkersWithCodes[name] = [
+                (self.__checkVariableNames, ("N821",)),
+                (self.__checkNameToBeAvoided, ("N831",)),
+            ]
+        for name in (
+            "with",
+            "asyncwith",
+            "for",
+            "asyncfor",
+            "excepthandler",
+            "generatorexp",
+            "listcomp",
+            "dictcomp",
+            "setcomp",
+        ):
+            self.__checkersWithCodes[name] = [
+                (self.__checkVariableNames, ("N821",)),
+            ]
+        for name in ("import", "importfrom"):
+            self.__checkersWithCodes[name] = [
+                (self.__checkImportAs, ("N811", "N812", "N813", "N814", "N815")),
+            ]
 
         self.__checkers = {}
         for key, checkers in self.__checkersWithCodes.items():
@@ -251,41 +268,80 @@
             name = node.name
             if self.__isNameToBeAvoided(name):
                 yield self.__error(node, "N831")
-                return
 
-        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+        elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
             argNames = self.__getArgNames(node)
             for arg in argNames:
                 if self.__isNameToBeAvoided(arg):
                     yield self.__error(node, "N831")
-                    return
 
-        if isinstance(node, ast.Assign):
-            for target in node.targets:
+        elif isinstance(node, (ast.Assign, ast.NamedExpr, ast.AnnAssign)):
+            if isinstance(node, ast.Assign):
+                targets = node.targets
+            else:
+                targets = [node.target]
+            for target in targets:
                 if isinstance(target, ast.Name):
                     name = target.id
-                    if not name:
-                        return
-
-                    if self.__isNameToBeAvoided(name):
+                    if bool(name) and self.__isNameToBeAvoided(name):
                         yield self.__error(node, "N831")
-                        return
 
                 elif isinstance(target, (ast.Tuple, ast.List)):
                     for element in target.elts:
                         if isinstance(element, ast.Name):
                             name = element.id
-                            if not name:
-                                return
+                            if bool(name) and self.__isNameToBeAvoided(name):
+                                yield self.__error(node, "N831")
+
+    def __getClassdef(self, name, parents):
+        """
+        Private method to extract the class definition.
+
+        @param name name of the class
+        @type str
+        @param parents list of parent nodes
+        @type ast.AST
+        @return node containing the class definition
+        @rtype ast.ClassDef
+        """
+        for parent in parents:
+            for node in parent.body:
+                if isinstance(node, ast.ClassDef) and node.name == name:
+                    return node
+
+        return None
 
-                            if self.__isNameToBeAvoided(name):
-                                yield self.__error(node, "N831")
-                                return
+    def __superClassNames(self, name, parents, names=None):
+        """
+        Private method to extract the names of all super classes.
+
+        @param name name of the class
+        @type str
+        @param parents list of parent nodes
+        @type ast.AST
+        @param names set of collected class names (defaults to None)
+        @type set of str (optional)
+        @return set of class names
+        @rtype set of str
+        """
+        if names is None:
+            # initialize recursive search with empty set
+            names = set()
+
+        classdef = self.__getClassdef(name, parents)
+        if not classdef:
+            return names
+
+        for base in classdef.bases:
+            if isinstance(base, ast.Name) and base.id not in names:
+                names.add(base.id)
+                names.update(self.__superClassNames(base.id, parents, names))
+        return names
 
     def __checkClassName(self, node, parents):
         """
         Private class to check the given node for class name
-        conventions (N801).
+        conventions (N801, N818).
 
         Almost without exception, class names use the CapWords convention.
         Classes for internal use have a leading underscore in addition.
@@ -295,13 +351,19 @@
         @yield tuple giving line number, offset within line and error code
         @ytype tuple of (int, int, str)
         """
-        if not self.CamelcaseRegexp.match(node.name):
+        name = node.name
+        strippedName = name.strip("_")
+        if not strippedName[:1].isupper() or "_" in strippedName:
             yield self.__error(node, "N801")
 
+        superClasses = self.__superClassNames(name, parents)
+        if "Exception" in superClasses and not name.endswith("Error"):
+            yield self.__error(node, "N818")
+
     def __checkFunctionName(self, node, parents):
         """
         Private class to check the given node for function name
-        conventions (N802).
+        conventions (N802, N809).
 
         Function names should be lowercase, with words separated by underscores
         as necessary to improve readability. Functions <b>not</b> being
@@ -316,10 +378,14 @@
         """
         functionType = getattr(node, "function_type", "function")
         name = node.name
-        if (
-            functionType == "function" and "__" in (name[:2], name[-2:])
-        ) or not self.LowercaseRegex.match(name):
+
+        if name in ("__dir__", "__getattr__"):
+            return
+
+        if name.lower() != name:
             yield self.__error(node, "N802")
+        if functionType == "function" and name[:2] == "__" and name[-2:] == "__":
+            yield self.__error(node, "N809")
 
     def __checkFunctionArgumentNames(self, node, parents):
         """
@@ -337,40 +403,39 @@
         """
         if node.args.kwarg is not None:
             kwarg = node.args.kwarg.arg
-            if not self.LowercaseRegex.match(kwarg):
+            if kwarg.lower() != kwarg:
                 yield self.__error(node, "N803")
-                return
 
-        if node.args.vararg is not None:
+        elif node.args.vararg is not None:
             vararg = node.args.vararg.arg
-            if not self.LowercaseRegex.match(vararg):
+            if vararg.lower() != vararg:
                 yield self.__error(node, "N803")
-                return
+
+        else:
+            argNames = self.__getArgNames(node)
+            functionType = getattr(node, "function_type", "function")
 
-        argNames = self.__getArgNames(node)
-        functionType = getattr(node, "function_type", "function")
+            if not argNames:
+                if functionType == "method":
+                    yield self.__error(node, "N805")
+                elif functionType == "classmethod":
+                    yield self.__error(node, "N804")
 
-        if not argNames:
-            if functionType == "method":
+            elif functionType == "method" and argNames[0] != "self":
                 yield self.__error(node, "N805")
-            elif functionType == "classmethod":
+            elif functionType == "classmethod" and argNames[0] != "cls":
                 yield self.__error(node, "N804")
-            return
+            elif functionType == "staticmethod" and argNames[0] in ("cls", "self"):
+                yield self.__error(node, "N806")
+            for arg in argNames:
+                if arg.lower() != arg:
+                    yield self.__error(node, "N803")
+                    break
 
-        if functionType == "method" and argNames[0] != "self":
-            yield self.__error(node, "N805")
-        elif functionType == "classmethod" and argNames[0] != "cls":
-            yield self.__error(node, "N804")
-        elif functionType == "staticmethod" and argNames[0] in ("cls", "self"):
-            yield self.__error(node, "N806")
-        for arg in argNames:
-            if not self.LowercaseRegex.match(arg):
-                yield self.__error(node, "N803")
-                return
-
-    def __checkVariablesInFunction(self, node, parents):
+    def __checkVariableNames(self, node, parents):
         """
-        Private method to check local variables in functions (N821).
+        Private method to check variable names in function, class and global scope
+        (N821, N822, N823).
 
         Local variables in functions should be lowercase.
 
@@ -379,20 +444,156 @@
         @yield tuple giving line number, offset within line and error code
         @ytype tuple of (int, int, str)
         """
+        nodeType = type(node)
+        if nodeType is ast.Assign:
+            if self.__isNamedTupel(node.value):
+                return
+            for target in node.targets:
+                yield from self.__findVariableNameErrors(target, parents)
+
+        elif nodeType in (ast.NamedExpr, ast.AnnAssign):
+            if self.__isNamedTupel(node.value):
+                return
+            yield from self.__findVariableNameErrors(node.target, parents)
+
+        elif nodeType in (ast.With, ast.AsyncWith):
+            for item in node.items:
+                yield from self.__findVariableNameErrors(item.optional_vars, parents)
+
+        elif nodeType in (ast.For, ast.AsyncFor):
+            yield from self.__findVariableNameErrors(node.target, parents)
+
+        elif nodeType is ast.ExceptHandler:
+            if node.name:
+                yield from self.__findVariableNameErrors(node, parents)
+
+        elif nodeType in (ast.GeneratorExp, ast.ListComp, ast.DictComp, ast.SetComp):
+            for gen in node.generators:
+                yield from self.__findVariableNameErrors(gen.target, parents)
+
+    def __findVariableNameErrors(self, assignmentTarget, parents):
+        """
+        Private method to check, if there is a variable name error.
+
+        @param assignmentTarget target node of the assignment
+        @type ast.Name, ast.Tuple, ast.List or ast.ExceptHandler
+        @param parents list of parent nodes
+        @type ast.AST
+        @yield tuple giving line number, offset within line and error code
+        @ytype tuple of (int, int, str)
+        """
         for parentFunc in reversed(parents):
             if isinstance(parentFunc, ast.ClassDef):
-                return
+                checker = self.__classVariableCheck
+                break
             if isinstance(parentFunc, (ast.FunctionDef, ast.AsyncFunctionDef)):
+                checker = functools.partial(self.__functionVariableCheck, parentFunc)
                 break
         else:
-            return
-        for target in node.targets:
-            name = isinstance(target, ast.Name) and target.id
-            if not name or name in parentFunc.global_names:
-                return
+            checker = self.__globalVariableCheck
+        for name in self.__extractNames(assignmentTarget):
+            errorCode = checker(name)
+            if errorCode:
+                yield self.__error(assignmentTarget, errorCode)
+
+    def __extractNames(self, assignmentTarget):
+        """
+        Private method to extract the names from the target node.
+
+        @param assignmentTarget target node of the assignment
+        @type ast.Name, ast.Tuple, ast.List or ast.ExceptHandler
+        @yield name of the variable
+        @ytype str
+        """
+        targetType = type(assignmentTarget)
+        if targetType is ast.Name:
+            yield assignmentTarget.id
+        elif targetType in (ast.Tuple, ast.List):
+            for element in assignmentTarget.elts:
+                elementType = type(element)
+                if elementType is ast.Name:
+                    yield element.id
+                elif elementType in (ast.Tuple, ast.List):
+                    yield from self.__extractNames(element)
+                elif elementType is ast.Starred:  # PEP 3132
+                    yield from self.__extractNames(element.value)
+        elif isinstance(assignmentTarget, ast.ExceptHandler):
+            yield assignmentTarget.name
+
+    def __isMixedCase(self, name):
+        """
+        Private method to check, if the given name is mixed case.
+
+        @param name variable name to be checked
+        @type str
+        @return flag indicating mixed case
+        @rtype bool
+        """
+        return name.lower() != name and name.lstrip("_")[:1].islower()
+
+    def __globalVariableCheck(self, name):
+        """
+        Private method to determine the error code for a variable in global scope.
 
-            if not self.LowercaseRegex.match(name) and name[:1] != "_":
-                yield self.__error(target, "N821")
+        @param name variable name to be checked
+        @type str
+        @return error code or None
+        @rtype str or None
+        """
+        if self.__isMixedCase(name):
+            return "N823"
+
+        return None
+
+    def __classVariableCheck(self, name):
+        """
+        Private method to determine the error code for a variable in class scope.
+
+        @param name variable name to be checked
+        @type str
+        @return error code or None
+        @rtype str or None
+        """
+        if self.__isMixedCase(name):
+            return "N822"
+
+        return None
+
+    def __functionVariableCheck(self, func, varName):
+        """
+        Private method to determine the error code for a variable in class scope.
+
+        @param func reference to the function definition node
+        @type ast.FunctionDef or ast.AsyncFunctionDef
+        @param varName variable name to be checked
+        @type str
+        @return error code or None
+        @rtype str or None
+        """
+        if varName not in func.global_names and varName.lower() != varName:
+            return "N821"
+
+        return None
+
+    def __isNamedTupel(self, nodeValue):
+        """
+        Private method to check, if a node is a named tuple.
+
+        @param nodeValue node to be checked
+        @type ast.AST
+        @return flag indicating a nemd tuple
+        @rtype bool
+        """
+        return isinstance(nodeValue, ast.Call) and (
+            (
+                isinstance(nodeValue.func, ast.Attribute)
+                and nodeValue.func.attr == "namedtuple"
+            )
+            or (
+                isinstance(nodeValue.func, ast.Name)
+                and nodeValue.func.id == "namedtuple"
+            )
+        )
 
     def __checkModule(self, node, parents):
         """
@@ -419,7 +620,7 @@
     def __checkImportAs(self, node, parents):
         """
         Private method to check that imports don't change the
-        naming convention (N811, N812, N813, N814).
+        naming convention (N811, N812, N813, N814, N815).
 
         @param node AST note to check
         @param parents list of parent nodes
@@ -427,16 +628,21 @@
         @ytype tuple of (int, int, str)
         """
         for name in node.names:
-            if not name.asname:
+            asname = name.asname
+            if not asname:
                 continue
 
-            if self.UppercaseRegexp.match(name.name):
-                if not self.UppercaseRegexp.match(name.asname):
+            originalName = name.name
+            if originalName.isupper():
+                if not asname.isupper():
                     yield self.__error(node, "N811")
-            elif self.LowercaseRegex.match(name.name):
-                if not self.LowercaseRegex.match(name.asname):
+            elif originalName.islower():
+                if asname.lower() != asname:
                     yield self.__error(node, "N812")
-            elif self.LowercaseRegex.match(name.asname):
+            elif asname.islower():
                 yield self.__error(node, "N813")
-            elif self.UppercaseRegexp.match(name.asname):
-                yield self.__error(node, "N814")
+            elif asname.isupper():
+                if "".join(filter(str.isupper, originalName)) == asname:
+                    yield self.__error(node, "N815")
+                else:
+                    yield self.__error(node, "N814")

eric ide

mercurial