--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/DocStyle/DocStyleChecker.py Thu Feb 27 09:22:15 2025 +0100 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/DocStyle/DocStyleChecker.py Thu Feb 27 14:42:39 2025 +0100 @@ -13,6 +13,7 @@ # import ast +import collections import contextlib import tokenize @@ -23,6 +24,8 @@ except AttributeError: ast.AsyncFunctionDef = ast.FunctionDef +from CodeStyleTopicChecker import CodeStyleTopicChecker + class DocStyleContext: """ @@ -127,7 +130,7 @@ return self.__special -class DocStyleChecker: +class DocStyleChecker(CodeStyleTopicChecker): """ Class implementing a checker for documentation string conventions. """ @@ -189,6 +192,7 @@ "D-272", "D-273", ] + Category = "D" def __init__( self, @@ -221,21 +225,20 @@ @param docType type of the documentation strings (one of 'eric' or 'pep257') @type str """ - self.__select = tuple(select) - self.__ignore = tuple(ignore) - self.__expected = expected[:] - self.__repeat = repeat + super().__init__( + DocStyleChecker.Category, + source, + filename, + None, + select, + ignore, + expected, + 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 @@ -331,63 +334,29 @@ ], } - self.__checkers = {} + self.__checkers = collections.defaultdict(list) 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] = [] + if any( + not (msgCode and self._ignoreCode(msgCode)) for msgCode in codes + ): 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 - @type str - @return flag indicating to ignore the given code - @rtype bool + def addError(self, lineNumber, offset, msgCode, *args): """ - return code in self.__ignore or ( - code.startswith(self.__ignore) and not code.startswith(self.__select) - ) + Public method to record an issue. - def __error(self, lineNumber, offset, code, *args): - """ - Private method to record an issue. - - @param lineNumber line number of the issue + @param lineNumber line number of the issue (zero based) @type int @param offset position within line of the issue @type int - @param code message code + @param msgCode message code @type str @param args arguments for the message @type 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, - } - ) + # call super class method with one based line number + super().addError(lineNumber + 1, offset, msgCode, *args) def __resetReadline(self): """ @@ -403,16 +372,16 @@ @rtype str """ self.__lineNumber += 1 - if self.__lineNumber > len(self.__source): + if self.__lineNumber > len(self.source): return "" - return self.__source[self.__lineNumber - 1] + 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: + if not self.filename: # don't do anything, if essential data is missing return @@ -420,11 +389,11 @@ # don't do anything, if no codes were selected 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) + for key in self.__keywords: + if key in self.__checkers: + for check in self.__checkers[key]: + for context in self.__parseContexts(key): + docstring = self.__parseDocstring(context, key) check(docstring, context) def __getSummaryLine(self, docstringContext): @@ -599,7 +568,7 @@ kind, value, (line, char), _, _ = next(tokenGenerator) end = line - 1, char contexts.append( - DocStyleContext(self.__source[start[0] : end[0]], start[0], keyword) + DocStyleContext(self.source[start[0] : end[0]], start[0], keyword) ) except StopIteration: return contexts @@ -676,12 +645,12 @@ startLine = classContext.start() + start[0] endLine = classContext.start() + end[0] context = DocStyleContext( - self.__source[startLine:endLine], startLine, "def" + self.source[startLine:endLine], startLine, "def" ) if startLine > 0: - if self.__source[startLine - 1].strip() == "@staticmethod": + if self.source[startLine - 1].strip() == "@staticmethod": context.setSpecial("staticmethod") - elif self.__source[startLine - 1].strip() == "@classmethod": + elif self.source[startLine - 1].strip() == "@classmethod": context.setSpecial("classmethod") contexts.append(context) self.__methodsCache = contexts @@ -698,7 +667,7 @@ @rtype list of DocStyleContext """ if kind == "moduleDocstring": - return [DocStyleContext(self.__source, 0, "module")] + return [DocStyleContext(self.source, 0, "module")] if kind == "functionDocstring": return self.__parseFunctions() if kind == "classDocstring": @@ -709,7 +678,7 @@ return self.__parseFunctions() + self.__parseMethods() if kind == "docstring": return ( - [DocStyleContext(self.__source, 0, "module")] + [DocStyleContext(self.source, 0, "module")] + self.__parseFunctions() + self.__parseClasses() + self.__parseMethods() @@ -730,18 +699,18 @@ @type DocStyleContext """ if docstringContext is None: - self.__error(context.start(), 0, "D-101") + self.addError(context.start(), 0, "D-101") return docstring = docstringContext.ssource() if not docstring or not docstring.strip() or not docstring.strip("'\""): - self.__error(context.start(), 0, "D-101") + self.addError(context.start(), 0, "D-101") if ( self.__docType == "eric" and docstring.strip("'\"").strip() == "Module documentation goes here." ): - self.__error(docstringContext.end(), 0, "D-201") + self.addError(docstringContext.end(), 0, "D-201") return def __checkFunctionDocstring(self, docstringContext, context): @@ -764,20 +733,20 @@ code = "D-102" if docstringContext is None: - self.__error(context.start(), 0, code) + self.addError(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) + self.addError(context.start(), 0, code) if self.__docType == "eric": if docstring.strip("'\"").strip() == "Function documentation goes here.": - self.__error(docstringContext.end(), 0, "D-202.1") + self.addError(docstringContext.end(), 0, "D-202.1") return if "DESCRIPTION" in docstring or "TYPE" in docstring: - self.__error(docstringContext.end(), 0, "D-202.2") + self.addError(docstringContext.end(), 0, "D-202.2") return def __checkClassDocstring(self, docstringContext, context): @@ -800,19 +769,19 @@ code = "D-104" if docstringContext is None: - self.__error(context.start(), 0, code) + self.addError(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) + self.addError(context.start(), 0, code) return if ( self.__docType == "eric" and docstring.strip("'\"").strip() == "Class documentation goes here." ): - self.__error(docstringContext.end(), 0, "D-206") + self.addError(docstringContext.end(), 0, "D-206") return def __checkTripleDoubleQuotes(self, docstringContext, _context): @@ -830,7 +799,7 @@ docstring = docstringContext.ssource().strip() if not docstring.startswith(('"""', 'r"""', 'u"""')): - self.__error(docstringContext.start(), 0, "D-111") + self.addError(docstringContext.start(), 0, "D-111") def __checkBackslashes(self, docstringContext, _context): """ @@ -847,7 +816,7 @@ docstring = docstringContext.ssource().strip() if "\\" in docstring and not docstring.startswith('r"""'): - self.__error(docstringContext.start(), 0, "D-112") + self.addError(docstringContext.start(), 0, "D-112") def __checkOneLiner(self, docstringContext, context): """ @@ -875,7 +844,7 @@ # account for a trailing dot modLen += 1 if modLen <= self.__maxLineLength: - self.__error(docstringContext.start(), 0, "D-121") + self.addError(docstringContext.start(), 0, "D-121") def __checkIndent(self, docstringContext, context): """ @@ -902,7 +871,7 @@ 0 if context.contextType() == "module" else len(context.indent()) + 4 ) if indent != expectedIndent: - self.__error(docstringContext.start(), 0, "D-122") + self.addError(docstringContext.start(), 0, "D-122") def __checkSummary(self, docstringContext, _context): """ @@ -918,7 +887,7 @@ summary, lineNumber = self.__getSummaryLine(docstringContext) if summary == "": - self.__error(docstringContext.start() + lineNumber, 0, "D-130") + self.addError(docstringContext.start() + lineNumber, 0, "D-130") def __checkEndsWithPeriod(self, docstringContext, _context): """ @@ -934,7 +903,7 @@ summary, lineNumber = self.__getSummaryLine(docstringContext) if not summary.endswith("."): - self.__error(docstringContext.start() + lineNumber, 0, "D-131") + self.addError(docstringContext.start() + lineNumber, 0, "D-131") def __checkImperativeMood(self, docstringContext, _context): """ @@ -953,7 +922,7 @@ if summary: firstWord = summary.strip().split()[0] if firstWord.endswith("s") and not firstWord.endswith("ss"): - self.__error(docstringContext.start() + lineNumber, 0, "D-132") + self.addError(docstringContext.start() + lineNumber, 0, "D-132") def __checkNoSignature(self, docstringContext, context): """ @@ -974,7 +943,7 @@ " ", "" ) and functionName + "()" not in summary.replace(" ", ""): # report only, if it is not an abbreviated form (i.e. function() ) - self.__error(docstringContext.start() + lineNumber, 0, "D-133") + self.addError(docstringContext.start() + lineNumber, 0, "D-133") def __checkReturnType(self, docstringContext, context): """ @@ -1001,7 +970,7 @@ set(return_) - {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} != set() ): - self.__error(docstringContext.end(), 0, "D-134") + self.addError(docstringContext.end(), 0, "D-134") def __checkNoBlankLineBefore(self, docstringContext, context): """ @@ -1026,7 +995,7 @@ return if not contextLines[cti - 1].strip(): - self.__error(docstringContext.start(), 0, "D-141") + self.addError(docstringContext.start(), 0, "D-141") def __checkBlankBeforeAndAfterClass(self, docstringContext, context): """ @@ -1064,9 +1033,9 @@ return if contextLines[start - 1].strip(): - self.__error(docstringContext.start(), 0, "D-142") + self.addError(docstringContext.start(), 0, "D-142") if contextLines[end + 1].strip(): - self.__error(docstringContext.end(), 0, "D-143") + self.addError(docstringContext.end(), 0, "D-143") def __checkBlankAfterSummary(self, docstringContext, _context): """ @@ -1088,7 +1057,7 @@ summary, lineNumber = self.__getSummaryLine(docstringContext) if len(docstrings) > 2 and docstrings[lineNumber + 1].strip(): - self.__error(docstringContext.start() + lineNumber, 0, "D-144") + self.addError(docstringContext.start() + lineNumber, 0, "D-144") def __checkBlankAfterLastParagraph(self, docstringContext, _context): """ @@ -1109,7 +1078,7 @@ return if docstrings[-2].strip(): - self.__error(docstringContext.end(), 0, "D-145") + self.addError(docstringContext.end(), 0, "D-145") ################################################################## ## Checking functionality below (eric specific ones) @@ -1130,9 +1099,9 @@ lines = docstringContext.source() if lines[0].strip().strip("ru\"'"): - self.__error(docstringContext.start(), 0, "D-221") + self.addError(docstringContext.start(), 0, "D-221") if lines[-1].strip().strip("\"'"): - self.__error(docstringContext.end(), 0, "D-222") + self.addError(docstringContext.end(), 0, "D-222") def __checkEricEndsWithPeriod(self, docstringContext, _context): """ @@ -1156,7 +1125,7 @@ and not summary.endswith(".") and summary.split(None, 1)[0].lower() != "constructor" ): - self.__error( + self.addError( docstringContext.start() + lineNumber + len(summaryLines) - 1, 0, "D-231", @@ -1184,13 +1153,13 @@ set(return_) - {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} != set() ): - self.__error(docstringContext.end(), 0, "D-234r") + self.addError(docstringContext.end(), 0, "D-234r") else: if ( set(return_) - {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} == set() ): - self.__error(docstringContext.end(), 0, "D-235r") + self.addError(docstringContext.end(), 0, "D-235r") def __checkEricYield(self, docstringContext, context): """ @@ -1211,10 +1180,10 @@ ] if "@yield" not in docstringContext.ssource(): if set(yield_) - {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} != set(): - self.__error(docstringContext.end(), 0, "D-234y") + self.addError(docstringContext.end(), 0, "D-234y") else: if set(yield_) - {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} == set(): - self.__error(docstringContext.end(), 0, "D-235y") + self.addError(docstringContext.end(), 0, "D-235y") def __checkEricFunctionArguments(self, docstringContext, context): """ @@ -1253,11 +1222,11 @@ if tagstring.count("@param") + tagstring.count("@keyparam") < len( argNames + kwNames ): - self.__error(docstringContext.end(), 0, "D-236") + self.addError(docstringContext.end(), 0, "D-236") elif tagstring.count("@param") + tagstring.count("@keyparam") > len( argNames + kwNames ): - self.__error(docstringContext.end(), 0, "D-237") + self.addError(docstringContext.end(), 0, "D-237") else: # extract @param and @keyparam from docstring args = [] @@ -1274,10 +1243,10 @@ # do the checks for name in kwNames: if name not in kwargs: - self.__error(docstringContext.end(), 0, "D-238") + self.addError(docstringContext.end(), 0, "D-238") return if argNames + kwNames != args: - self.__error(docstringContext.end(), 0, "D-239") + self.addError(docstringContext.end(), 0, "D-239") def __checkEricException(self, docstringContext, context): """ @@ -1317,10 +1286,10 @@ and "@raise" not in docstringContext.ssource() ): if exceptions - {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} != set(): - self.__error(docstringContext.end(), 0, "D-250") + self.addError(docstringContext.end(), 0, "D-250") else: if exceptions - {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} == set(): - self.__error(docstringContext.end(), 0, "D-251") + self.addError(docstringContext.end(), 0, "D-251") else: # step 1: extract documented exceptions documentedExceptions = set() @@ -1334,12 +1303,12 @@ # step 2: report undocumented exceptions for exception in raisedExceptions: if exception not in documentedExceptions: - self.__error(docstringContext.end(), 0, "D-252", exception) + self.addError(docstringContext.end(), 0, "D-252", exception) # step 3: report undefined signals for exception in documentedExceptions: if exception not in raisedExceptions: - self.__error(docstringContext.end(), 0, "D-253", exception) + self.addError(docstringContext.end(), 0, "D-253", exception) def __checkEricSignal(self, docstringContext, context): """ @@ -1368,10 +1337,10 @@ definedSignals.add(tokens[i - 2][1]) if "@signal" not in docstringContext.ssource() and definedSignals: - self.__error(docstringContext.end(), 0, "D-260") + self.addError(docstringContext.end(), 0, "D-260") elif "@signal" in docstringContext.ssource(): if not definedSignals: - self.__error(docstringContext.end(), 0, "D-261") + self.addError(docstringContext.end(), 0, "D-261") else: # step 1: extract documented signals documentedSignals = set() @@ -1388,12 +1357,12 @@ # step 2: report undocumented signals for signal in definedSignals: if signal not in documentedSignals: - self.__error(docstringContext.end(), 0, "D-262", signal) + self.addError(docstringContext.end(), 0, "D-262", signal) # step 3: report undefined signals for signal in documentedSignals: if signal not in definedSignals: - self.__error(docstringContext.end(), 0, "D-263", signal) + self.addError(docstringContext.end(), 0, "D-263", signal) def __checkEricBlankAfterSummary(self, docstringContext, _context): """ @@ -1418,7 +1387,7 @@ len(docstrings) - 2 > lineNumber + len(summaryLines) - 1 and docstrings[lineNumber + len(summaryLines)].strip() ): - self.__error(docstringContext.start() + lineNumber, 0, "D-246") + self.addError(docstringContext.start() + lineNumber, 0, "D-246") def __checkEricNoBlankBeforeAndAfterClassOrFunction( self, docstringContext, context @@ -1460,14 +1429,14 @@ if isClassContext: if not contextLines[start - 1].strip(): - self.__error(docstringContext.start(), 0, "D-242") + self.addError(docstringContext.start(), 0, "D-242") if not contextLines[end + 1].strip() and self.__docType == "eric": - self.__error(docstringContext.end(), 0, "D-243") + self.addError(docstringContext.end(), 0, "D-243") elif contextLines[end + 1].strip() and self.__docType == "eric_black": - self.__error(docstringContext.end(), 0, "D-143") + self.addError(docstringContext.end(), 0, "D-143") else: if not contextLines[start - 1].strip(): - self.__error(docstringContext.start(), 0, "D-244") + self.addError(docstringContext.start(), 0, "D-244") if not contextLines[end + 1].strip(): if ( self.__docType == "eric_black" @@ -1476,7 +1445,7 @@ ): return - self.__error(docstringContext.end(), 0, "D-245") + self.addError(docstringContext.end(), 0, "D-245") def __checkEricNBlankAfterLastParagraph(self, docstringContext, _context): """ @@ -1497,7 +1466,7 @@ return if not docstrings[-2].strip(): - self.__error(docstringContext.end(), 0, "D-247") + self.addError(docstringContext.end(), 0, "D-247") def __checkEricSummary(self, docstringContext, context): """ @@ -1522,24 +1491,24 @@ firstWord = summary.strip().split(None, 1)[0].lower() if functionName == "__init__": if firstWord != "constructor": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", "constructor" ) elif functionName.startswith("__") and functionName.endswith("__"): if firstWord != "special": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", "special" ) elif context.special() == "staticmethod": secondWord = summary.strip().split(None, 2)[1].lower() if firstWord != "static" and secondWord != "static": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", "static" ) elif secondWord == "static": if functionName.startswith(("__", "on_")): if firstWord != "private": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", @@ -1547,7 +1516,7 @@ ) elif functionName.startswith("_") or functionName.endswith("Event"): if firstWord != "protected": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", @@ -1555,7 +1524,7 @@ ) else: if firstWord != "public": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", @@ -1567,13 +1536,13 @@ ): secondWord = summary.strip().split(None, 2)[1].lower() if firstWord != "class" and secondWord != "class": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", "class" ) elif secondWord == "class": if functionName.startswith(("__", "on_")): if firstWord != "private": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", @@ -1581,7 +1550,7 @@ ) elif functionName.startswith("_") or functionName.endswith("Event"): if firstWord != "protected": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", @@ -1589,7 +1558,7 @@ ) else: if firstWord != "public": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", @@ -1597,17 +1566,17 @@ ) elif functionName.startswith(("__", "on_")): if firstWord != "private": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", "private" ) elif functionName.startswith("_") or functionName.endswith("Event"): if firstWord != "protected": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", "protected" ) else: if firstWord != "public": - self.__error( + self.addError( docstringContext.start() + lineNumber, 0, "D-232", "public" ) @@ -1642,7 +1611,7 @@ and lineno > 0 and lines[lineno - 1].strip() == "" ): - self.__error( + self.addError( docstringContext.start() + lineno, 0, "D-271", docToken ) @@ -1656,15 +1625,15 @@ docToken2 = "" if docToken in ("@param", "@keyparam") and docToken2 != "@type": - self.__error( + self.addError( docstringContext.start() + lineno, 0, "D-270", docToken, "@type" ) elif docToken == "@return" and docToken2 != "@rtype": - self.__error( + self.addError( docstringContext.start() + lineno, 0, "D-270", docToken, "@rtype" ) elif docToken == "@yield" and docToken2 != "@ytype": - self.__error( + self.addError( docstringContext.start() + lineno, 0, "D-270", docToken, "@ytype" ) @@ -1698,7 +1667,7 @@ # it is a tag line tag = strippedLine.split(None, 1)[0] with contextlib.suppress(KeyError): - self.__error( + self.addError( docstringContext.start() + lineno, 0, "D-272", @@ -1737,4 +1706,4 @@ tag = strippedLine.split(None, 1)[0] currentIndentation = len(line) - len(strippedLine) if currentIndentation != indentationLength: - self.__error(docstringContext.start() + lineno, 0, "D-273", tag) + self.addError(docstringContext.start() + lineno, 0, "D-273", tag)