--- 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")