Plugins/CheckerPlugins/CodeStyleChecker/NamingStyleChecker.py

branch
Py2 comp.
changeset 3057
10516539f238
parent 3056
9986ec0e559a
parent 3004
c4bf32c791d0
child 3058
0a02c433f52d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/CheckerPlugins/CodeStyleChecker/NamingStyleChecker.py	Fri Oct 18 23:00:41 2013 +0200
@@ -0,0 +1,464 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2013 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a checker for naming conventions.
+"""
+
+from __future__ import unicode_literals    # __IGNORE_WARNING__
+
+import collections
+import ast
+import re
+import os
+
+from PyQt4.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
+
+
+class NamingStyleChecker(object):
+    """
+    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", "N803", "N804", "N805", "N806", "N807", "N808",
+        "N811", "N812", "N813", "N814", "N821", "N831"
+    ]
+    Messages = {
+        "N801": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "class names should use CapWords convention"),
+        "N802": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "function name should be lowercase"),
+        "N803": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "argument name should be lowercase"),
+        "N804": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "first argument of a class method should be named 'cls'"),
+        "N805": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "first argument of a method should be named 'self'"),
+        "N806": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "first argument of a static method should not be named"
+            " 'self' or 'cls"),
+        "N807": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "module names should be lowercase"),
+        "N808": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "package names should be lowercase"),
+        "N811": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "constant imported as non constant"),
+        "N812": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "lowercase imported as non lowercase"),
+        "N813": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "camelcase imported as lowercase"),
+        "N814": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "camelcase imported as constant"),
+        "N821": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "variable in function should be lowercase"),
+        "N831": QT_TRANSLATE_NOOP("NamingStyleChecker",
+            "names 'l', 'O' and 'I' should be avoided"),
+    }
+    
+    def __init__(self, tree, filename, options):
+        """
+        Constructor (according to 'extended' pep8.py API)
+        
+        @param tree AST tree of the source file
+        @param filename name of the source file (string)
+        @param options options as parsed by pep8.StyleGuide
+        """
+        self.__parents = collections.deque()
+        self.__tree = tree
+        self.__filename = filename
+        
+        self.__checkersWithCodes = {
+            "classdef": [
+                (self.__checkClassName, ("N801",)),
+                (self.__checkNameToBeAvoided, ("N831",)),
+            ],
+            "functiondef": [
+                (self.__checkFuntionName, ("N802",)),
+                (self.__checkFunctionArgumentNames,
+                    ("N803", "N804", "N805", "N806")),
+                (self.__checkNameToBeAvoided, ("N831",)),
+            ],
+            "assign": [
+                (self.__checkVariablesInFunction, ("N821",)),
+                (self.__checkNameToBeAvoided, ("N831",)),
+            ],
+            "importfrom": [
+                (self.__checkImportAs, ("N811", "N812", "N813", "N814")),
+            ],
+            "module": [
+                (self.__checkModule, ("N807", "N808")),
+            ],
+        }
+        
+        self.__checkers = {}
+        for key, checkers in self.__checkersWithCodes.items():
+            for checker, codes in checkers:
+                if any(not (code and options.ignore_code(code))
+                        for code in codes):
+                    if key not in self.__checkers:
+                        self.__checkers[key] = []
+                    self.__checkers[key].append(checker)
+
+    def run(self):
+        """
+        Public method run by the pep8.py checker.
+        
+        @return tuple giving line number, offset within line, code and
+            checker function
+        """
+        if self.__tree and self.__checkers:
+            return self.__visitTree(self.__tree)
+        else:
+            return ()
+    
+    @classmethod
+    def getMessage(cls, code, *args):
+        """
+        Class method to get a translated and formatted message for a
+        given code.
+        
+        @param code message code (string)
+        @param args arguments for a formatted message (list)
+        @return translated and formatted message (string)
+        """
+        if code in cls.Messages:
+            return code + " " + QCoreApplication.translate(
+                "NamingStyleChecker",
+                cls.Messages[code]).format(*args)
+        else:
+            return code + " " + QCoreApplication.translate(
+                "NamingStyleChecker",
+                "no message for this code defined")
+    
+    def __visitTree(self, node):
+        """
+        Private method to scan the given AST tree.
+        
+        @param node AST tree node to scan
+        @return tuple giving line number, offset within line, code and
+            checker function
+        """
+        for error in self.__visitNode(node):
+            yield error
+        self.__parents.append(node)
+        for child in ast.iter_child_nodes(node):
+            for error in self.__visitTree(child):
+                yield error
+        self.__parents.pop()
+    
+    def __visitNode(self, node):
+        """
+        Private method to inspect the given AST node.
+        
+        @param node AST tree node to inspect
+        @return tuple giving line number, offset within line, code and
+            checker function
+        """
+        if isinstance(node, ast.ClassDef):
+            self.__tagClassFunctions(node)
+        elif isinstance(node, ast.FunctionDef):
+            self.__findGlobalDefs(node)
+        
+        checkerName = node.__class__.__name__.lower()
+        if checkerName in self.__checkers:
+            for checker in self.__checkers[checkerName]:
+                for error in checker(node, self.__parents):
+                    yield error + (self.__checkers[checkerName],)
+    
+    def __tagClassFunctions(self, classNode):
+        """
+        Private method to tag functions if they are methods, class methods or
+        static methods.
+        
+        @param classNode AST tree node to tag
+        """
+        # try to find all 'old style decorators' like
+        # m = staticmethod(m)
+        lateDecoration = {}
+        for node in ast.iter_child_nodes(classNode):
+            if not (isinstance(node, ast.Assign) and
+                    isinstance(node.value, ast.Call) and
+                    isinstance(node.value.func, ast.Name)):
+                continue
+            funcName = node.value.func.id
+            if funcName in ("classmethod", "staticmethod"):
+                meth = (len(node.value.args) == 1 and node.value.args[0])
+                if isinstance(meth, ast.Name):
+                    lateDecoration[meth.id] = funcName
+
+        # iterate over all functions and tag them
+        for node in ast.iter_child_nodes(classNode):
+            if not isinstance(node, ast.FunctionDef):
+                continue
+            
+            node.function_type = 'method'
+            if node.name == "__new__":
+                node.function_type = "classmethod"
+            
+            if node.name in lateDecoration:
+                node.function_type = lateDecoration[node.name]
+            elif node.decorator_list:
+                names = [d.id for d in node.decorator_list
+                         if isinstance(d, ast.Name) and
+                         d.id in ("classmethod", "staticmethod")]
+                if names:
+                    node.function_type = names[0]
+
+    def __findGlobalDefs(self, functionNode):
+        """
+        Private method amend a node with global definitions information.
+        
+        @param functionNode AST tree node to amend
+        """
+        globalNames = set()
+        nodesToCheck = collections.deque(ast.iter_child_nodes(functionNode))
+        while nodesToCheck:
+            node = nodesToCheck.pop()
+            if isinstance(node, ast.Global):
+                globalNames.update(node.names)
+
+            if not isinstance(node, (ast.FunctionDef, ast.ClassDef)):
+                nodesToCheck.extend(ast.iter_child_nodes(node))
+        functionNode.global_names = globalNames
+    
+    def __getArgNames(self, node):
+        """
+        Private method to get the argument names of a function node.
+        
+        @param node AST node to extract arguments names from
+        @return list of argument names (list of string)
+        """
+        posArgs = [arg.arg for arg in node.args.args]
+        kwOnly = [arg.arg for arg in node.args.kwonlyargs]
+        return posArgs + kwOnly
+    
+    def __error(self, node, code):
+        """
+        Private method to build the error information.
+        
+        @param node AST node to report an error for
+        @param code error code to report (string)
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        if isinstance(node, ast.Module):
+            lineno = 0
+            offset = 0
+        else:
+            lineno = node.lineno
+            offset = node.col_offset
+            if isinstance(node, ast.ClassDef):
+                lineno += len(node.decorator_list)
+                offset += 6
+            elif isinstance(node, ast.FunctionDef):
+                lineno += len(node.decorator_list)
+                offset += 4
+        return (lineno, offset, code)
+    
+    def __isNameToBeAvoided(self, name):
+        """
+        Private method to check, if the given name should be avoided.
+        
+        @param name name to be checked (string)
+        @return flag indicating to avoid it (boolen)
+        """
+        return name in ("l", "O", "I")
+    
+    def __checkNameToBeAvoided(self, node, parents):
+        """
+        Private class to check the given node for a name to be avoided (N831).
+        
+        @param node AST note to check
+        @param parents list of parent nodes
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
+            name = node.name
+            if self.__isNameToBeAvoided(name):
+                yield self.__error(node, "N831")
+                return
+        
+        if isinstance(node, ast.FunctionDef):
+            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:
+                name = isinstance(target, ast.Name) and target.id
+                if not name:
+                    return
+                
+                if self.__isNameToBeAvoided(name):
+                    yield self.__error(node, "N831")
+                    return
+    
+    def __checkClassName(self, node, parents):
+        """
+        Private class to check the given node for class name
+        conventions (N801).
+        
+        Almost without exception, class names use the CapWords convention.
+        Classes for internal use have a leading underscore in addition.
+        
+        @param node AST note to check
+        @param parents list of parent nodes
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        if not self.CamelcaseRegexp.match(node.name):
+            yield self.__error(node, "N801")
+    
+    def __checkFuntionName(self, node, parents):
+        """
+        Private class to check the given node for function name
+        conventions (N802).
+        
+        Function names should be lowercase, with words separated by underscores
+        as necessary to improve readability. Functions <b>not</b> being
+        methods '__' in front and back are not allowed. Mixed case is allowed
+        only in contexts where that's already the prevailing style
+        (e.g. threading.py), to retain backwards compatibility.
+        
+        @param node AST note to check
+        @param parents list of parent nodes
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        functionType = getattr(node, "function_type", "function")
+        name = node.name
+        if (functionType == "function" and "__" in (name[:2], name[-2:])) or \
+                not self.LowercaseRegex.match(name):
+            yield self.__error(node, "N802")
+    
+    def __checkFunctionArgumentNames(self, node, parents):
+        """
+        Private class to check the argument names of functions
+        (N803, N804, N805, N806).
+        
+        The argument names of a function should be lowercase, with words
+        separated by underscores. A class method should have 'cls' as the
+        first argument. A method should have 'self' as the first argument.
+        
+        @param node AST note to check
+        @param parents list of parent nodes
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        if node.args.kwarg is not None:
+            if not self.LowercaseRegex.match(node.args.kwarg):
+                yield self.__error(node, "N803")
+                return
+        
+        if node.args.vararg is not None:
+            if not self.LowercaseRegex.match(node.args.vararg):
+                yield self.__error(node, "N803")
+                return
+        
+        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")
+            return
+        
+        if functionType == "method":
+            if argNames[0] != "self":
+                yield self.__error(node, "N805")
+        elif functionType == "classmethod":
+            if argNames[0] != "cls":
+                yield self.__error(node, "N804")
+        elif functionType == "staticmethod":
+            if 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):
+        """
+        Private method to check local variables in functions (N821).
+        
+        Local variables in functions should be lowercase.
+        
+        @param node AST note to check
+        @param parents list of parent nodes
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        for parentFunc in reversed(parents):
+            if isinstance(parentFunc, ast.ClassDef):
+                return
+            if isinstance(parentFunc, ast.FunctionDef):
+                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
+            
+            if not self.LowercaseRegex.match(name) and name[:1] != '_':
+                yield self.__error(target, "N821")
+    
+    def __checkModule(self, node, parents):
+        """
+        Private method to check module naming conventions (N807, N808).
+        
+        Module and package names should be lowercase.
+        
+        @param node AST note to check
+        @param parents list of parent nodes
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        if self.__filename:
+            moduleName = os.path.splitext(os.path.basename(self.__filename))[0]
+            if moduleName.lower() != moduleName:
+                yield self.__error(node, "N807")
+            
+            if moduleName == "__init__":
+                # we got a package
+                packageName = \
+                    os.path.split(os.path.dirname(self.__filename))[1]
+                if packageName.lower != packageName:
+                    yield self.__error(node, "N808")
+    
+    def __checkImportAs(self, node, parents):
+        """
+        Private method to check that imports don't change the
+        naming convention (N811, N812, N813, N814).
+        
+        @param node AST note to check
+        @param parents list of parent nodes
+        @return tuple giving line number, offset within line and error code
+            (integer, integer, string)
+        """
+        for name in node.names:
+            if not name.asname:
+                continue
+            
+            if self.UppercaseRegexp.match(name.name):
+                if not self.UppercaseRegexp.match(name.asname):
+                    yield self.__error(node, "N811")
+            elif self.LowercaseRegex.match(name.name):
+                if not self.LowercaseRegex.match(name.asname):
+                    yield self.__error(node, "N812")
+            elif self.LowercaseRegex.match(name.asname):
+                yield self.__error(node, "N813")
+            elif self.UppercaseRegexp.match(name.asname):
+                yield self.__error(node, "N814")

eric ide

mercurial