--- /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 @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 @param and/or + @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 @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 @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')