eric6/Plugins/CheckerPlugins/CodeStyleChecker/DocStyle/DocStyleChecker.py

changeset 7784
3257703e10c5
parent 7639
422fd05e9c91
child 7894
4370a8b30648
child 7924
8a96736d465e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/DocStyle/DocStyleChecker.py	Tue Oct 13 19:02:26 2020 +0200
@@ -0,0 +1,1444 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2013 - 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a checker for documentation string conventions.
+"""
+
+#
+# The routines of the checker class are modeled after the ones found in
+# pep257.py (version 0.2.4).
+#
+
+import tokenize
+import ast
+import sys
+from io import StringIO
+
+try:
+    ast.AsyncFunctionDef    # __IGNORE_EXCEPTION__
+except AttributeError:
+    ast.AsyncFunctionDef = ast.FunctionDef
+
+
+class DocStyleContext(object):
+    """
+    Class implementing the source context.
+    """
+    def __init__(self, source, startLine, contextType):
+        """
+        Constructor
+        
+        @param source source code of the context (list of string or string)
+        @param startLine line number the context starts in the source (integer)
+        @param contextType type of the context object (string)
+        """
+        if isinstance(source, str):
+            self.__source = source.splitlines(True)
+        else:
+            self.__source = source[:]
+        self.__start = startLine
+        self.__indent = ""
+        self.__type = contextType
+        self.__special = ""
+        
+        # ensure first line is left justified
+        if self.__source:
+            self.__indent = self.__source[0].replace(
+                self.__source[0].lstrip(), "")
+            self.__source[0] = self.__source[0].lstrip()
+    
+    def source(self):
+        """
+        Public method to get the source.
+        
+        @return source (list of string)
+        """
+        return self.__source
+    
+    def ssource(self):
+        """
+        Public method to get the joined source lines.
+        
+        @return source (string)
+        """
+        return "".join(self.__source)
+    
+    def start(self):
+        """
+        Public method to get the start line number.
+        
+        @return start line number (integer)
+        """
+        return self.__start
+    
+    def end(self):
+        """
+        Public method to get the end line number.
+        
+        @return end line number (integer)
+        """
+        return self.__start + len(self.__source) - 1
+    
+    def indent(self):
+        """
+        Public method to get the indentation of the first line.
+        
+        @return indentation string (string)
+        """
+        return self.__indent
+    
+    def contextType(self):
+        """
+        Public method to get the context type.
+        
+        @return context type (string)
+        """
+        return self.__type
+    
+    def setSpecial(self, special):
+        """
+        Public method to set a special attribute for the context.
+        
+        @param special attribute string
+        @type str
+        """
+        self.__special = special
+    
+    def special(self):
+        """
+        Public method to get the special context attribute string.
+        
+        @return attribute string
+        @rtype str
+        """
+        return self.__special
+
+
+class DocStyleChecker(object):
+    """
+    Class implementing a checker for documentation string conventions.
+    """
+    Codes = [
+        "D101", "D102", "D103", "D104", "D105",
+        "D111", "D112",
+        "D121", "D122",
+        "D130", "D131", "D132", "D133", "D134",
+        "D141", "D142", "D143", "D144", "D145",
+        
+        "D203", "D205", "D206",
+        "D221", "D222",
+        "D231", "D232", "D234", "D235", "D236", "D237", "D238", "D239",
+        "D242", "D243", "D244", "D245", "D246", "D247",
+        "D250", "D251", "D252", "D253",
+        "D260", "D261", "D262", "D263",
+        
+        "D901",
+    ]
+
+    def __init__(self, source, filename, select, ignore, expected, repeat,
+                 maxLineLength=79, docType="pep257"):
+        """
+        Constructor
+        
+        @param source source code to be checked (list of string)
+        @param filename name of the source file (string)
+        @param select list of selected codes (list of string)
+        @param ignore list of codes to be ignored (list of string)
+        @param expected list of expected codes (list of string)
+        @param repeat flag indicating to report each occurrence of a code
+            (boolean)
+        @keyparam maxLineLength allowed line length (integer)
+        @keyparam docType type of the documentation strings
+            (string, one of 'eric' or 'pep257')
+        """
+        self.__select = tuple(select)
+        self.__ignore = ('',) if select else tuple(ignore)
+        self.__expected = expected[:]
+        self.__repeat = repeat
+        self.__maxLineLength = maxLineLength
+        self.__docType = docType
+        self.__filename = filename
+        self.__source = source[:]
+        
+        # statistics counters
+        self.counters = {}
+        
+        # collection of detected errors
+        self.errors = []
+        
+        self.__lineNumber = 0
+        
+        # caches
+        self.__functionsCache = None
+        self.__classesCache = None
+        self.__methodsCache = None
+        
+        self.__keywords = [
+            'moduleDocstring', 'functionDocstring',
+            'classDocstring', 'methodDocstring',
+            'defDocstring', 'docstring'
+        ]
+        if self.__docType == "pep257":
+            checkersWithCodes = {
+                "moduleDocstring": [
+                    (self.__checkModulesDocstrings, ("D101",)),
+                ],
+                "functionDocstring": [
+                ],
+                "classDocstring": [
+                    (self.__checkClassDocstring, ("D104", "D105")),
+                    (self.__checkBlankBeforeAndAfterClass, ("D142", "D143")),
+                ],
+                "methodDocstring": [
+                ],
+                "defDocstring": [
+                    (self.__checkFunctionDocstring, ("D102", "D103")),
+                    (self.__checkImperativeMood, ("D132",)),
+                    (self.__checkNoSignature, ("D133",)),
+                    (self.__checkReturnType, ("D134",)),
+                    (self.__checkNoBlankLineBefore, ("D141",)),
+                ],
+                "docstring": [
+                    (self.__checkTripleDoubleQuotes, ("D111",)),
+                    (self.__checkBackslashes, ("D112",)),
+                    (self.__checkOneLiner, ("D121",)),
+                    (self.__checkIndent, ("D122",)),
+                    (self.__checkSummary, ("D130",)),
+                    (self.__checkEndsWithPeriod, ("D131",)),
+                    (self.__checkBlankAfterSummary, ("D144",)),
+                    (self.__checkBlankAfterLastParagraph, ("D145",)),
+                ],
+            }
+        elif self.__docType == "eric":
+            checkersWithCodes = {
+                "moduleDocstring": [
+                    (self.__checkModulesDocstrings, ("D101",)),
+                ],
+                "functionDocstring": [
+                ],
+                "classDocstring": [
+                    (self.__checkClassDocstring, ("D104", "D205", "D206")),
+                    (self.__checkEricNoBlankBeforeAndAfterClassOrFunction,
+                     ("D242", "D243")),
+                    (self.__checkEricSignal, ("D260", "D261", "D262", "D263")),
+                ],
+                "methodDocstring": [
+                    (self.__checkEricSummary, ("D232")),
+                ],
+                "defDocstring": [
+                    (self.__checkFunctionDocstring, ("D102", "D203")),
+                    (self.__checkImperativeMood, ("D132",)),
+                    (self.__checkNoSignature, ("D133",)),
+                    (self.__checkEricReturn, ("D234", "D235")),
+                    (self.__checkEricFunctionArguments,
+                     ("D236", "D237", "D238", "D239")),
+                    (self.__checkEricNoBlankBeforeAndAfterClassOrFunction,
+                     ("D244", "D245")),
+                    (self.__checkEricException,
+                     ("D250", "D251", "D252", "D253")),
+                ],
+                "docstring": [
+                    (self.__checkTripleDoubleQuotes, ("D111",)),
+                    (self.__checkBackslashes, ("D112",)),
+                    (self.__checkIndent, ("D122",)),
+                    (self.__checkSummary, ("D130",)),
+                    (self.__checkEricEndsWithPeriod, ("D231",)),
+                    (self.__checkEricBlankAfterSummary, ("D246",)),
+                    (self.__checkEricNBlankAfterLastParagraph, ("D247",)),
+                    (self.__checkEricQuotesOnSeparateLines, ("D222", "D223"))
+                ],
+            }
+        
+        self.__checkers = {}
+        for key, checkers in checkersWithCodes.items():
+            for checker, codes in checkers:
+                if any(not (code and self.__ignoreCode(code))
+                        for code in codes):
+                    if key not in self.__checkers:
+                        self.__checkers[key] = []
+                    self.__checkers[key].append(checker)
+    
+    def __ignoreCode(self, code):
+        """
+        Private method to check if the error code should be ignored.
+
+        @param code message code to check for (string)
+        @return flag indicating to ignore the given code (boolean)
+        """
+        return (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 (integer)
+        @param offset position within line of the issue (integer)
+        @param code message code (string)
+        @param args arguments for the message (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 __reportInvalidSyntax(self):
+        """
+        Private method to report a syntax error.
+        """
+        exc_type, exc = sys.exc_info()[:2]
+        if len(exc.args) > 1:
+            offset = exc.args[1]
+            if len(offset) > 2:
+                offset = offset[1:3]
+        else:
+            offset = (1, 0)
+        self.__error(offset[0] - 1, offset[1] or 0,
+                     'D901', exc_type.__name__, exc.args[0])
+    
+    def __resetReadline(self):
+        """
+        Private method to reset the internal readline function.
+        """
+        self.__lineNumber = 0
+    
+    def __readline(self):
+        """
+        Private method to get the next line from the source.
+        
+        @return next line of source (string)
+        """
+        self.__lineNumber += 1
+        if self.__lineNumber > len(self.__source):
+            return ''
+        return self.__source[self.__lineNumber - 1]
+    
+    def run(self):
+        """
+        Public method to check the given source for violations of doc string
+        conventions.
+        """
+        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
+        
+        source = "".join(self.__source)
+        try:
+            compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST)
+        except (SyntaxError, TypeError):
+            self.__reportInvalidSyntax()
+            return
+        
+        for keyword in self.__keywords:
+            if keyword in self.__checkers:
+                for check in self.__checkers[keyword]:
+                    for context in self.__parseContexts(keyword):
+                        docstring = self.__parseDocstring(context, keyword)
+                        check(docstring, context)
+    
+    def __getSummaryLine(self, docstringContext):
+        """
+        Private method to extract the summary line.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @return summary line (string) and the line it was found on (integer)
+        """
+        lines = docstringContext.source()
+        
+        line = (lines[0]
+                .replace('r"""', "", 1)
+                .replace('u"""', "", 1)
+                .replace('"""', "")
+                .replace("r'''", "", 1)
+                .replace("u'''", "", 1)
+                .replace("'''", "")
+                .strip())
+        
+        if len(lines) == 1 or len(line) > 0:
+            return line, 0
+        return lines[1].strip().replace('"""', "").replace("'''", ""), 1
+    
+    def __getSummaryLines(self, docstringContext):
+        """
+        Private method to extract the summary lines.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @return summary lines (list of string) and the line it was found on
+            (integer)
+        """
+        summaries = []
+        lines = docstringContext.source()
+        
+        line0 = (lines[0]
+                 .replace('r"""', "", 1)
+                 .replace('u"""', "", 1)
+                 .replace('"""', "")
+                 .replace("r'''", "", 1)
+                 .replace("u'''", "", 1)
+                 .replace("'''", "")
+                 .strip())
+        if len(lines) > 1:
+            line1 = lines[1].strip().replace('"""', "").replace("'''", "")
+        else:
+            line1 = ""
+        if len(lines) > 2:
+            line2 = lines[2].strip().replace('"""', "").replace("'''", "")
+        else:
+            line2 = ""
+        if line0:
+            lineno = 0
+            summaries.append(line0)
+            if not line0.endswith(".") and line1:
+                # two line summary
+                summaries.append(line1)
+        elif line1:
+            lineno = 1
+            summaries.append(line1)
+            if not line1.endswith(".") and line2:
+                # two line summary
+                summaries.append(line2)
+        else:
+            lineno = 2
+            summaries.append(line2)
+        return summaries, lineno
+    
+    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 tuple of two list of argument names, one for arguments
+            and one for keyword arguments (tuple of list of string)
+        """
+        arguments = []
+        arguments.extend([arg.arg for arg in node.args.args])
+        if node.args.vararg is not None:
+            if sys.version_info < (3, 4, 0):
+                arguments.append(node.args.vararg)
+            else:
+                arguments.append(node.args.vararg.arg)
+        
+        kwarguments = []
+        kwarguments.extend([arg.arg for arg in node.args.kwonlyargs])
+        if node.args.kwarg is not None:
+            if sys.version_info < (3, 4, 0):
+                kwarguments.append(node.args.kwarg)
+            else:
+                kwarguments.append(node.args.kwarg.arg)
+        return arguments, kwarguments
+    
+    ##################################################################
+    ## Parsing functionality below
+    ##################################################################
+    
+    def __parseModuleDocstring(self, source):
+        """
+        Private method to extract a docstring given a module source.
+        
+        @param source source to parse (list of string)
+        @return context of extracted docstring (DocStyleContext)
+        """
+        for kind, value, (line, _char), _, _ in tokenize.generate_tokens(
+                StringIO("".join(source)).readline):
+            if kind in [tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL]:
+                continue
+            elif kind == tokenize.STRING:  # first STRING should be docstring
+                return DocStyleContext(value, line - 1, "docstring")
+            else:
+                return None
+        
+        return None
+
+    def __parseDocstring(self, context, what=''):
+        """
+        Private method to extract a docstring given `def` or `class` source.
+        
+        @param context context data to get the docstring from (DocStyleContext)
+        @param what string denoting what is being parsed (string)
+        @return context of extracted docstring (DocStyleContext)
+        """
+        moduleDocstring = self.__parseModuleDocstring(context.source())
+        if what.startswith('module') or context.contextType() == "module":
+            return moduleDocstring
+        if moduleDocstring:
+            return moduleDocstring
+        
+        tokenGenerator = tokenize.generate_tokens(
+            StringIO(context.ssource()).readline)
+        try:
+            kind = None
+            while kind != tokenize.INDENT:
+                kind, _, _, _, _ = next(tokenGenerator)
+            kind, value, (line, char), _, _ = next(tokenGenerator)
+            if kind == tokenize.STRING:  # STRING after INDENT is a docstring
+                return DocStyleContext(
+                    value, context.start() + line - 1, "docstring")
+        except StopIteration:
+            pass
+        
+        return None
+    
+    def __parseTopLevel(self, keyword):
+        """
+        Private method to extract top-level functions or classes.
+        
+        @param keyword keyword signaling what to extract (string)
+        @return extracted function or class contexts (list of DocStyleContext)
+        """
+        self.__resetReadline()
+        tokenGenerator = tokenize.generate_tokens(self.__readline)
+        kind, value, char = None, None, None
+        contexts = []
+        try:
+            while True:
+                start, end = None, None
+                while not (kind == tokenize.NAME and
+                           value == keyword and
+                           char == 0):
+                    kind, value, (line, char), _, _ = next(tokenGenerator)
+                start = line - 1, char
+                while not (kind == tokenize.DEDENT and
+                           value == '' and
+                           char == 0):
+                    kind, value, (line, char), _, _ = next(tokenGenerator)
+                end = line - 1, char
+                contexts.append(DocStyleContext(
+                    self.__source[start[0]:end[0]], start[0], keyword))
+        except StopIteration:
+            return contexts
+    
+    def __parseFunctions(self):
+        """
+        Private method to extract top-level functions.
+        
+        @return extracted function contexts (list of DocStyleContext)
+        """
+        if not self.__functionsCache:
+            self.__functionsCache = self.__parseTopLevel('def')
+        return self.__functionsCache
+    
+    def __parseClasses(self):
+        """
+        Private method to extract top-level classes.
+        
+        @return extracted class contexts (list of DocStyleContext)
+        """
+        if not self.__classesCache:
+            self.__classesCache = self.__parseTopLevel('class')
+        return self.__classesCache
+    
+    def __skipIndentedBlock(self, tokenGenerator):
+        """
+        Private method to skip over an indented block of source code.
+        
+        @param tokenGenerator token generator
+        @return last token of the indented block
+        """
+        kind, value, start, end, raw = next(tokenGenerator)
+        while kind != tokenize.INDENT:
+            kind, value, start, end, raw = next(tokenGenerator)
+        indent = 1
+        for kind, value, start, end, raw in tokenGenerator:
+            if kind == tokenize.INDENT:
+                indent += 1
+            elif kind == tokenize.DEDENT:
+                indent -= 1
+            if indent == 0:
+                return kind, value, start, end, raw
+        
+        return None
+    
+    def __parseMethods(self):
+        """
+        Private method to extract methods of all classes.
+        
+        @return extracted method contexts (list of DocStyleContext)
+        """
+        if not self.__methodsCache:
+            contexts = []
+            for classContext in self.__parseClasses():
+                tokenGenerator = tokenize.generate_tokens(
+                    StringIO(classContext.ssource()).readline)
+                kind, value, char = None, None, None
+                try:
+                    while True:
+                        start, end = None, None
+                        while not (kind == tokenize.NAME and value == 'def'):
+                            kind, value, (line, char), _, _ = (
+                                next(tokenGenerator)
+                            )
+                        start = line - 1, char
+                        kind, value, (line, char), _, _ = (
+                            self.__skipIndentedBlock(tokenGenerator)
+                        )
+                        end = line - 1, char
+                        startLine = classContext.start() + start[0]
+                        endLine = classContext.start() + end[0]
+                        context = DocStyleContext(
+                            self.__source[startLine:endLine],
+                            startLine, "def")
+                        if startLine > 0:
+                            if (
+                                self.__source[startLine - 1].strip() ==
+                                "@staticmethod"
+                            ):
+                                context.setSpecial("staticmethod")
+                            elif (
+                                self.__source[startLine - 1].strip() ==
+                                "@classmethod"
+                            ):
+                                context.setSpecial("classmethod")
+                        contexts.append(context)
+                except StopIteration:
+                    pass
+            self.__methodsCache = contexts
+        
+        return self.__methodsCache
+
+    def __parseContexts(self, kind):
+        """
+        Private method to extract a context from the source.
+        
+        @param kind kind of context to extract (string)
+        @return requested contexts (list of DocStyleContext)
+        """
+        if kind == 'moduleDocstring':
+            return [DocStyleContext(self.__source, 0, "module")]
+        if kind == 'functionDocstring':
+            return self.__parseFunctions()
+        if kind == 'classDocstring':
+            return self.__parseClasses()
+        if kind == 'methodDocstring':
+            return self.__parseMethods()
+        if kind == 'defDocstring':
+            return self.__parseFunctions() + self.__parseMethods()
+        if kind == 'docstring':
+            return ([DocStyleContext(self.__source, 0, "module")] +
+                    self.__parseFunctions() +
+                    self.__parseClasses() +
+                    self.__parseMethods())
+        return []       # fall back
+    
+    ##################################################################
+    ## Checking functionality below (PEP-257)
+    ##################################################################
+
+    def __checkModulesDocstrings(self, docstringContext, context):
+        """
+        Private method to check, if the module has a docstring.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            self.__error(context.start(), 0, "D101")
+            return
+        
+        docstring = docstringContext.ssource()
+        if (not docstring or not docstring.strip() or
+                not docstring.strip('\'"')):
+            self.__error(context.start(), 0, "D101")
+        
+        if (
+            self.__docType == "eric" and
+            docstring.strip('\'"').strip() ==
+            "Module documentation goes here."
+        ):
+            self.__error(docstringContext.end(), 0, "D201")
+            return
+    
+    def __checkFunctionDocstring(self, docstringContext, context):
+        """
+        Private method to check, that all public functions and methods
+        have a docstring.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        functionName = context.source()[0].lstrip().split()[1].split("(")[0]
+        if functionName.startswith('_') and not functionName.endswith('__'):
+            if self.__docType == "eric":
+                code = "D203"
+            else:
+                code = "D103"
+        else:
+            code = "D102"
+        
+        if docstringContext is None:
+            self.__error(context.start(), 0, code)
+            return
+        
+        docstring = docstringContext.ssource()
+        if (not docstring or not docstring.strip() or
+                not docstring.strip('\'"')):
+            self.__error(context.start(), 0, code)
+        
+        if (
+            self.__docType == "eric" and
+            docstring.strip('\'"').strip() ==
+            "Function documentation goes here."
+        ):
+            self.__error(docstringContext.end(), 0, "D202")
+            return
+    
+    def __checkClassDocstring(self, docstringContext, context):
+        """
+        Private method to check, that all public functions and methods
+        have a docstring.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        className = context.source()[0].lstrip().split()[1].split("(")[0]
+        if className.startswith('_'):
+            if self.__docType == "eric":
+                code = "D205"
+            else:
+                code = "D105"
+        else:
+            code = "D104"
+        
+        if docstringContext is None:
+            self.__error(context.start(), 0, code)
+            return
+        
+        docstring = docstringContext.ssource()
+        if (not docstring or not docstring.strip() or
+                not docstring.strip('\'"')):
+            self.__error(context.start(), 0, code)
+            return
+        
+        if (
+            self.__docType == "eric" and
+            docstring.strip('\'"').strip() == "Class documentation goes here."
+        ):
+            self.__error(docstringContext.end(), 0, "D206")
+            return
+    
+    def __checkTripleDoubleQuotes(self, docstringContext, context):
+        """
+        Private method to check, that all docstrings are surrounded
+        by triple double quotes.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        docstring = docstringContext.ssource().strip()
+        if not docstring.startswith(('"""', 'r"""', 'u"""')):
+            self.__error(docstringContext.start(), 0, "D111")
+    
+    def __checkBackslashes(self, docstringContext, context):
+        """
+        Private method to check, that all docstrings containing
+        backslashes are surrounded by raw triple double quotes.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        docstring = docstringContext.ssource().strip()
+        if "\\" in docstring and not docstring.startswith('r"""'):
+            self.__error(docstringContext.start(), 0, "D112")
+    
+    def __checkOneLiner(self, docstringContext, context):
+        """
+        Private method to check, that one-liner docstrings fit on
+        one line with quotes.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        lines = docstringContext.source()
+        if len(lines) > 1:
+            nonEmptyLines = [line for line in lines
+                             if line.strip().strip('\'"')]
+            if len(nonEmptyLines) == 1:
+                modLen = len(context.indent() + '"""' +
+                             nonEmptyLines[0].strip() + '"""')
+                if context.contextType() != "module":
+                    modLen += 4
+                if not nonEmptyLines[0].strip().endswith("."):
+                    # account for a trailing dot
+                    modLen += 1
+                if modLen <= self.__maxLineLength:
+                    self.__error(docstringContext.start(), 0, "D121")
+    
+    def __checkIndent(self, docstringContext, context):
+        """
+        Private method to check, that docstrings are properly indented.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        lines = docstringContext.source()
+        if len(lines) == 1:
+            return
+        
+        nonEmptyLines = [line.rstrip() for line in lines[1:] if line.strip()]
+        if not nonEmptyLines:
+            return
+        
+        indent = min(len(line) - len(line.strip()) for line in nonEmptyLines)
+        if context.contextType() == "module":
+            expectedIndent = 0
+        else:
+            expectedIndent = len(context.indent()) + 4
+        if indent != expectedIndent:
+            self.__error(docstringContext.start(), 0, "D122")
+    
+    def __checkSummary(self, docstringContext, context):
+        """
+        Private method to check, that docstring summaries contain some text.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        summary, lineNumber = self.__getSummaryLine(docstringContext)
+        if summary == "":
+            self.__error(docstringContext.start() + lineNumber, 0, "D130")
+    
+    def __checkEndsWithPeriod(self, docstringContext, context):
+        """
+        Private method to check, that docstring summaries end with a period.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        summary, lineNumber = self.__getSummaryLine(docstringContext)
+        if not summary.endswith("."):
+            self.__error(docstringContext.start() + lineNumber, 0, "D131")
+    
+    def __checkImperativeMood(self, docstringContext, context):
+        """
+        Private method to check, that docstring summaries are in
+        imperative mood.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        summary, lineNumber = self.__getSummaryLine(docstringContext)
+        if summary:
+            firstWord = summary.strip().split()[0]
+            if firstWord.endswith("s") and not firstWord.endswith("ss"):
+                self.__error(docstringContext.start() + lineNumber, 0, "D132")
+    
+    def __checkNoSignature(self, docstringContext, context):
+        """
+        Private method to check, that docstring summaries don't repeat
+        the function's signature.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        functionName = context.source()[0].lstrip().split()[1].split("(")[0]
+        summary, lineNumber = self.__getSummaryLine(docstringContext)
+        if (
+            functionName + "(" in summary.replace(" ", "") and
+            not functionName + "()" in summary.replace(" ", "")
+        ):
+            # report only, if it is not an abbreviated form (i.e. function() )
+            self.__error(docstringContext.start() + lineNumber, 0, "D133")
+    
+    def __checkReturnType(self, docstringContext, context):
+        """
+        Private method to check, that docstrings mention the return value type.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        if "return" not in docstringContext.ssource().lower():
+            tokens = list(
+                tokenize.generate_tokens(StringIO(context.ssource()).readline))
+            return_ = [tokens[i + 1][0] for i, token in enumerate(tokens)
+                       if token[1] == "return"]
+            if (set(return_) -
+                    {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
+                    set()):
+                self.__error(docstringContext.end(), 0, "D134")
+    
+    def __checkNoBlankLineBefore(self, docstringContext, context):
+        """
+        Private method to check, that function/method docstrings are not
+        preceded by a blank line.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        contextLines = context.source()
+        cti = 0
+        while (
+            cti < len(contextLines) and
+            not contextLines[cti].strip().startswith(
+                ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''"))
+        ):
+            cti += 1
+        if cti == len(contextLines):
+            return
+        
+        if not contextLines[cti - 1].strip():
+            self.__error(docstringContext.start(), 0, "D141")
+    
+    def __checkBlankBeforeAndAfterClass(self, docstringContext, context):
+        """
+        Private method to check, that class docstrings have one
+        blank line around them.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        contextLines = context.source()
+        cti = 0
+        while (
+            cti < len(contextLines) and
+            not contextLines[cti].strip().startswith(
+                ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''"))
+        ):
+            cti += 1
+        if cti == len(contextLines):
+            return
+        
+        start = cti
+        if contextLines[cti].strip() in (
+                '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"):
+            # it is a multi line docstring
+            cti += 1
+        
+        while (
+            cti < len(contextLines) and
+            not contextLines[cti].strip().endswith(('"""', "'''"))
+        ):
+            cti += 1
+        end = cti
+        if cti >= len(contextLines) - 1:
+            return
+        
+        if contextLines[start - 1].strip():
+            self.__error(docstringContext.start(), 0, "D142")
+        if contextLines[end + 1].strip():
+            self.__error(docstringContext.end(), 0, "D143")
+    
+    def __checkBlankAfterSummary(self, docstringContext, context):
+        """
+        Private method to check, that docstring summaries are followed
+        by a blank line.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        docstrings = docstringContext.source()
+        if len(docstrings) <= 3:
+            # correct/invalid one-liner
+            return
+        
+        summary, lineNumber = self.__getSummaryLine(docstringContext)
+        if len(docstrings) > 2:
+            if docstrings[lineNumber + 1].strip():
+                self.__error(docstringContext.start() + lineNumber, 0, "D144")
+    
+    def __checkBlankAfterLastParagraph(self, docstringContext, context):
+        """
+        Private method to check, that the last paragraph of docstrings is
+        followed by a blank line.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        docstrings = docstringContext.source()
+        if len(docstrings) <= 3:
+            # correct/invalid one-liner
+            return
+        
+        if docstrings[-2].strip():
+            self.__error(docstringContext.end(), 0, "D145")
+    
+    ##################################################################
+    ## Checking functionality below (eric specific ones)
+    ##################################################################
+
+    def __checkEricQuotesOnSeparateLines(self, docstringContext, context):
+        """
+        Private method to check, that leading and trailing quotes are on
+        a line by themselves.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        lines = docstringContext.source()
+        if lines[0].strip().strip('ru"\''):
+            self.__error(docstringContext.start(), 0, "D221")
+        if lines[-1].strip().strip('"\''):
+            self.__error(docstringContext.end(), 0, "D222")
+    
+    def __checkEricEndsWithPeriod(self, docstringContext, context):
+        """
+        Private method to check, that docstring summaries end with a period.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        summaryLines, lineNumber = self.__getSummaryLines(docstringContext)
+        if summaryLines:
+            if summaryLines[-1].lstrip().startswith("@"):
+                summaryLines.pop(-1)
+            summary = " ".join([s.strip() for s in summaryLines if s])
+            if (
+                summary and
+                not summary.endswith(".") and
+                not summary.split(None, 1)[0].lower() == "constructor"
+            ):
+                self.__error(
+                    docstringContext.start() + lineNumber +
+                    len(summaryLines) - 1,
+                    0, "D231")
+    
+    def __checkEricReturn(self, docstringContext, context):
+        """
+        Private method to check, that docstrings contain an &#64;return line
+        if they return anything and don't otherwise.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        tokens = list(
+            tokenize.generate_tokens(StringIO(context.ssource()).readline))
+        return_ = [tokens[i + 1][0] for i, token in enumerate(tokens)
+                   if token[1] in ("return", "yield")]
+        if "@return" not in docstringContext.ssource():
+            if (set(return_) -
+                    {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
+                    set()):
+                self.__error(docstringContext.end(), 0, "D234")
+        else:
+            if (set(return_) -
+                    {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} ==
+                    set()):
+                self.__error(docstringContext.end(), 0, "D235")
+    
+    def __checkEricFunctionArguments(self, docstringContext, context):
+        """
+        Private method to check, that docstrings contain an &#64;param and/or
+        &#64;keyparam line for each argument.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        try:
+            tree = ast.parse(context.ssource())
+        except (SyntaxError, TypeError):
+            return
+        if (isinstance(tree, ast.Module) and len(tree.body) == 1 and
+                isinstance(tree.body[0],
+                           (ast.FunctionDef, ast.AsyncFunctionDef))):
+            functionDef = tree.body[0]
+            argNames, kwNames = self.__getArgNames(functionDef)
+            if "self" in argNames:
+                argNames.remove("self")
+            if "cls" in argNames:
+                argNames.remove("cls")
+            
+            docstring = docstringContext.ssource()
+            if (docstring.count("@param") + docstring.count("@keyparam") <
+                    len(argNames + kwNames)):
+                self.__error(docstringContext.end(), 0, "D236")
+            elif (docstring.count("@param") + docstring.count("@keyparam") >
+                    len(argNames + kwNames)):
+                self.__error(docstringContext.end(), 0, "D237")
+            else:
+                # extract @param and @keyparam from docstring
+                args = []
+                kwargs = []
+                for line in docstringContext.source():
+                    if line.strip().startswith(("@param", "@keyparam")):
+                        paramParts = line.strip().split(None, 2)
+                        if len(paramParts) >= 2:
+                            at, name = paramParts[:2]
+                            if at == "@keyparam":
+                                kwargs.append(name.lstrip("*"))
+                            args.append(name.lstrip("*"))
+                
+                # do the checks
+                for name in kwNames:
+                    if name not in kwargs:
+                        self.__error(docstringContext.end(), 0, "D238")
+                        return
+                if argNames + kwNames != args:
+                    self.__error(docstringContext.end(), 0, "D239")
+    
+    def __checkEricException(self, docstringContext, context):
+        """
+        Private method to check, that docstrings contain an &#64;exception line
+        if they raise an exception and don't otherwise.
+        
+        Note: This method also checks the raised and documented exceptions for
+        completeness (i.e. raised exceptions that are not documented or
+        documented exceptions that are not raised)
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        tokens = list(
+            tokenize.generate_tokens(StringIO(context.ssource()).readline))
+        exceptions = set()
+        raisedExceptions = set()
+        tokensLen = len(tokens)
+        for i, token in enumerate(tokens):
+            if token[1] == "raise":
+                exceptions.add(tokens[i + 1][0])
+                if tokens[i + 1][0] == tokenize.NAME:
+                    if (
+                        tokensLen > (i + 2) and
+                        tokens[i + 2][1] == "."
+                    ):
+                        raisedExceptions.add("{0}.{1}".format(
+                            tokens[i + 1][1], tokens[i + 3][1]))
+                    else:
+                        raisedExceptions.add(tokens[i + 1][1])
+        
+        if (
+            "@exception" not in docstringContext.ssource() and
+            "@throws" not in docstringContext.ssource() and
+            "@raise" not in docstringContext.ssource()
+        ):
+            if (exceptions -
+                    {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
+                    set()):
+                self.__error(docstringContext.end(), 0, "D250")
+        else:
+            if (exceptions -
+                    {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} ==
+                    set()):
+                self.__error(docstringContext.end(), 0, "D251")
+            else:
+                # step 1: extract documented exceptions
+                documentedExceptions = set()
+                for line in docstringContext.source():
+                    line = line.strip()
+                    if line.startswith(("@exception", "@throws", "@raise")):
+                        exceptionTokens = line.split(None, 2)
+                        if len(exceptionTokens) >= 2:
+                            documentedExceptions.add(exceptionTokens[1])
+                
+                # step 2: report undocumented exceptions
+                for exception in raisedExceptions:
+                    if exception not in documentedExceptions:
+                        self.__error(docstringContext.end(), 0, "D252",
+                                     exception)
+                
+                # step 3: report undefined signals
+                for exception in documentedExceptions:
+                    if exception not in raisedExceptions:
+                        self.__error(docstringContext.end(), 0, "D253",
+                                     exception)
+    
+    def __checkEricSignal(self, docstringContext, context):
+        """
+        Private method to check, that docstrings contain an &#64;signal line
+        if they define signals and don't otherwise.
+        
+        Note: This method also checks the defined and documented signals for
+        completeness (i.e. defined signals that are not documented or
+        documented signals that are not defined)
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        tokens = list(
+            tokenize.generate_tokens(StringIO(context.ssource()).readline))
+        definedSignals = set()
+        for i, token in enumerate(tokens):
+            if token[1] in ("pyqtSignal", "Signal"):
+                if tokens[i - 1][1] == "." and tokens[i - 2][1] == "QtCore":
+                    definedSignals.add(tokens[i - 4][1])
+                elif tokens[i - 1][1] == "=":
+                    definedSignals.add(tokens[i - 2][1])
+        
+        if "@signal" not in docstringContext.ssource() and definedSignals:
+            self.__error(docstringContext.end(), 0, "D260")
+        elif "@signal" in docstringContext.ssource():
+            if not definedSignals:
+                self.__error(docstringContext.end(), 0, "D261")
+            else:
+                # step 1: extract documented signals
+                documentedSignals = set()
+                for line in docstringContext.source():
+                    line = line.strip()
+                    if line.startswith("@signal"):
+                        signalTokens = line.split(None, 2)
+                        if len(signalTokens) >= 2:
+                            signal = signalTokens[1]
+                            if "(" in signal:
+                                signal = signal.split("(", 1)[0]
+                            documentedSignals.add(signal)
+                
+                # step 2: report undocumented signals
+                for signal in definedSignals:
+                    if signal not in documentedSignals:
+                        self.__error(docstringContext.end(), 0, "D262", signal)
+                
+                # step 3: report undefined signals
+                for signal in documentedSignals:
+                    if signal not in definedSignals:
+                        self.__error(docstringContext.end(), 0, "D263", signal)
+    
+    def __checkEricBlankAfterSummary(self, docstringContext, context):
+        """
+        Private method to check, that docstring summaries are followed
+        by a blank line.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        docstrings = docstringContext.source()
+        if len(docstrings) <= 3:
+            # correct/invalid one-liner
+            return
+        
+        summaryLines, lineNumber = self.__getSummaryLines(docstringContext)
+        if len(docstrings) - 2 > lineNumber + len(summaryLines) - 1:
+            if docstrings[lineNumber + len(summaryLines)].strip():
+                self.__error(docstringContext.start() + lineNumber, 0, "D246")
+    
+    def __checkEricNoBlankBeforeAndAfterClassOrFunction(
+            self, docstringContext, context):
+        """
+        Private method to check, that class and function/method docstrings
+        have no blank line around them.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        contextLines = context.source()
+        isClassContext = contextLines[0].lstrip().startswith("class ")
+        cti = 0
+        while (
+            cti < len(contextLines) and
+            not contextLines[cti].strip().startswith(
+                ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''"))
+        ):
+            cti += 1
+        if cti == len(contextLines):
+            return
+        
+        start = cti
+        if contextLines[cti].strip() in (
+                '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"):
+            # it is a multi line docstring
+            cti += 1
+        
+        while (
+            cti < len(contextLines) and
+            not contextLines[cti].strip().endswith(('"""', "'''"))
+        ):
+            cti += 1
+        end = cti
+        if cti >= len(contextLines) - 1:
+            return
+        
+        if isClassContext:
+            if not contextLines[start - 1].strip():
+                self.__error(docstringContext.start(), 0, "D242")
+            if not contextLines[end + 1].strip():
+                self.__error(docstringContext.end(), 0, "D243")
+        else:
+            if not contextLines[start - 1].strip():
+                self.__error(docstringContext.start(), 0, "D244")
+            if not contextLines[end + 1].strip():
+                self.__error(docstringContext.end(), 0, "D245")
+    
+    def __checkEricNBlankAfterLastParagraph(self, docstringContext, context):
+        """
+        Private method to check, that the last paragraph of docstrings is
+        not followed by a blank line.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        docstrings = docstringContext.source()
+        if len(docstrings) <= 3:
+            # correct/invalid one-liner
+            return
+        
+        if not docstrings[-2].strip():
+            self.__error(docstringContext.end(), 0, "D247")
+    
+    def __checkEricSummary(self, docstringContext, context):
+        """
+        Private method to check, that method docstring summaries start with
+        specific words.
+        
+        @param docstringContext docstring context (DocStyleContext)
+        @param context context of the docstring (DocStyleContext)
+        """
+        if docstringContext is None:
+            return
+        
+        summary, lineNumber = self.__getSummaryLine(docstringContext)
+        if summary:
+            # check, if the first word is 'Constructor', 'Public',
+            # 'Protected' or 'Private'
+            functionName, arguments = (
+                context.source()[0].lstrip().split()[1].split("(", 1)
+            )
+            firstWord = summary.strip().split(None, 1)[0].lower()
+            if functionName == '__init__':
+                if firstWord != 'constructor':
+                    self.__error(docstringContext.start() + lineNumber, 0,
+                                 "D232", 'constructor')
+            elif (
+                functionName.startswith('__') and
+                functionName.endswith('__')
+            ):
+                if firstWord != 'special':
+                    self.__error(docstringContext.start() + lineNumber, 0,
+                                 "D232", 'special')
+            elif context.special() == "staticmethod":
+                secondWord = summary.strip().split(None, 2)[1].lower()
+                if firstWord != 'static' and secondWord != 'static':
+                    self.__error(docstringContext.start() + lineNumber, 0,
+                                 "D232", 'static')
+                elif secondWord == 'static':
+                    if functionName.startswith(('__', 'on_')):
+                        if firstWord != 'private':
+                            self.__error(docstringContext.start() + lineNumber,
+                                         0, "D232", 'private static')
+                    elif (
+                        functionName.startswith('_') or
+                        functionName.endswith('Event')
+                    ):
+                        if firstWord != 'protected':
+                            self.__error(docstringContext.start() + lineNumber,
+                                         0, "D232", 'protected static')
+                    else:
+                        if firstWord != 'public':
+                            self.__error(docstringContext.start() + lineNumber,
+                                         0, "D232", 'public static')
+            elif (
+                arguments.startswith(('cls,', 'cls)')) or
+                context.special() == "classmethod"
+            ):
+                secondWord = summary.strip().split(None, 2)[1].lower()
+                if firstWord != 'class' and secondWord != 'class':
+                    self.__error(docstringContext.start() + lineNumber, 0,
+                                 "D232", 'class')
+                elif secondWord == 'class':
+                    if functionName.startswith(('__', 'on_')):
+                        if firstWord != 'private':
+                            self.__error(docstringContext.start() + lineNumber,
+                                         0, "D232", 'private class')
+                    elif (
+                        functionName.startswith('_') or
+                        functionName.endswith('Event')
+                    ):
+                        if firstWord != 'protected':
+                            self.__error(docstringContext.start() + lineNumber,
+                                         0, "D232", 'protected class')
+                    else:
+                        if firstWord != 'public':
+                            self.__error(docstringContext.start() + lineNumber,
+                                         0, "D232", 'public class')
+            elif functionName.startswith(('__', 'on_')):
+                if firstWord != 'private':
+                    self.__error(docstringContext.start() + lineNumber, 0,
+                                 "D232", 'private')
+            elif (
+                functionName.startswith('_') or
+                functionName.endswith('Event')
+            ):
+                if firstWord != 'protected':
+                    self.__error(docstringContext.start() + lineNumber, 0,
+                                 "D232", 'protected')
+            else:
+                if firstWord != 'public':
+                    self.__error(docstringContext.start() + lineNumber, 0,
+                                 "D232", 'public')

eric ide

mercurial