src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/DocStyle/DocStyleChecker.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9182
bc72e2fc0d8e
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2013 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a checker for documentation string conventions.
8 """
9
10 #
11 # The routines of the checker class are modeled after the ones found in
12 # pep257.py (version 0.2.4).
13 #
14
15 import tokenize
16 import ast
17 from io import StringIO
18 import contextlib
19
20 try:
21 ast.AsyncFunctionDef # __IGNORE_EXCEPTION__
22 except AttributeError:
23 ast.AsyncFunctionDef = ast.FunctionDef
24
25
26 class DocStyleContext:
27 """
28 Class implementing the source context.
29 """
30 def __init__(self, source, startLine, contextType):
31 """
32 Constructor
33
34 @param source source code of the context (list of string or string)
35 @param startLine line number the context starts in the source (integer)
36 @param contextType type of the context object (string)
37 """
38 if isinstance(source, str):
39 self.__source = source.splitlines(True)
40 else:
41 self.__source = source[:]
42 self.__start = startLine
43 self.__indent = ""
44 self.__type = contextType
45 self.__special = ""
46
47 # ensure first line is left justified
48 if self.__source:
49 self.__indent = self.__source[0].replace(
50 self.__source[0].lstrip(), "")
51 self.__source[0] = self.__source[0].lstrip()
52
53 def source(self):
54 """
55 Public method to get the source.
56
57 @return source (list of string)
58 """
59 return self.__source
60
61 def ssource(self):
62 """
63 Public method to get the joined source lines.
64
65 @return source (string)
66 """
67 return "".join(self.__source)
68
69 def start(self):
70 """
71 Public method to get the start line number.
72
73 @return start line number (integer)
74 """
75 return self.__start
76
77 def end(self):
78 """
79 Public method to get the end line number.
80
81 @return end line number (integer)
82 """
83 return self.__start + len(self.__source) - 1
84
85 def indent(self):
86 """
87 Public method to get the indentation of the first line.
88
89 @return indentation string (string)
90 """
91 return self.__indent
92
93 def contextType(self):
94 """
95 Public method to get the context type.
96
97 @return context type (string)
98 """
99 return self.__type
100
101 def setSpecial(self, special):
102 """
103 Public method to set a special attribute for the context.
104
105 @param special attribute string
106 @type str
107 """
108 self.__special = special
109
110 def special(self):
111 """
112 Public method to get the special context attribute string.
113
114 @return attribute string
115 @rtype str
116 """
117 return self.__special
118
119
120 class DocStyleChecker:
121 """
122 Class implementing a checker for documentation string conventions.
123 """
124 Codes = [
125 "D101", "D102", "D103", "D104", "D105",
126 "D111", "D112",
127 "D121", "D122",
128 "D130", "D131", "D132", "D133", "D134",
129 "D141", "D142", "D143", "D144", "D145",
130
131 "D201", "D202.1", "D202.2", "D203", "D205", "D206",
132 "D221", "D222",
133 "D231", "D232", "D234r", "D234y", "D235r", "D235y", "D236", "D237",
134 "D238", "D239",
135 "D242", "D243", "D244", "D245", "D246", "D247",
136 "D250", "D251", "D252", "D253",
137 "D260", "D261", "D262", "D263",
138 ]
139
140 def __init__(self, source, filename, select, ignore, expected, repeat,
141 maxLineLength=88, docType="pep257"):
142 """
143 Constructor
144
145 @param source source code to be checked (list of string)
146 @param filename name of the source file (string)
147 @param select list of selected codes (list of string)
148 @param ignore list of codes to be ignored (list of string)
149 @param expected list of expected codes (list of string)
150 @param repeat flag indicating to report each occurrence of a code
151 (boolean)
152 @param maxLineLength allowed line length (integer)
153 @param docType type of the documentation strings
154 (string, one of 'eric' or 'pep257')
155 """
156 self.__select = tuple(select)
157 self.__ignore = ('',) if select else tuple(ignore)
158 self.__expected = expected[:]
159 self.__repeat = repeat
160 self.__maxLineLength = maxLineLength
161 self.__docType = docType
162 self.__filename = filename
163 self.__source = source[:]
164
165 # statistics counters
166 self.counters = {}
167
168 # collection of detected errors
169 self.errors = []
170
171 self.__lineNumber = 0
172
173 # caches
174 self.__functionsCache = None
175 self.__classesCache = None
176 self.__methodsCache = None
177
178 self.__keywords = [
179 'moduleDocstring', 'functionDocstring',
180 'classDocstring', 'methodDocstring',
181 'defDocstring', 'docstring'
182 ]
183 if self.__docType == "pep257":
184 checkersWithCodes = {
185 "moduleDocstring": [
186 (self.__checkModulesDocstrings, ("D101",)),
187 ],
188 "functionDocstring": [
189 ],
190 "classDocstring": [
191 (self.__checkClassDocstring, ("D104", "D105")),
192 (self.__checkBlankBeforeAndAfterClass, ("D142", "D143")),
193 ],
194 "methodDocstring": [
195 ],
196 "defDocstring": [
197 (self.__checkFunctionDocstring, ("D102", "D103")),
198 (self.__checkImperativeMood, ("D132",)),
199 (self.__checkNoSignature, ("D133",)),
200 (self.__checkReturnType, ("D134",)),
201 (self.__checkNoBlankLineBefore, ("D141",)),
202 ],
203 "docstring": [
204 (self.__checkTripleDoubleQuotes, ("D111",)),
205 (self.__checkBackslashes, ("D112",)),
206 (self.__checkOneLiner, ("D121",)),
207 (self.__checkIndent, ("D122",)),
208 (self.__checkSummary, ("D130",)),
209 (self.__checkEndsWithPeriod, ("D131",)),
210 (self.__checkBlankAfterSummary, ("D144",)),
211 (self.__checkBlankAfterLastParagraph, ("D145",)),
212 ],
213 }
214 elif self.__docType in ("eric", "eric_black"):
215 checkersWithCodes = {
216 "moduleDocstring": [
217 (self.__checkModulesDocstrings, ("D101", "D201")),
218 ],
219 "functionDocstring": [
220 ],
221 "classDocstring": [
222 (self.__checkClassDocstring, ("D104", "D205", "D206")),
223 (self.__checkEricNoBlankBeforeAndAfterClassOrFunction,
224 ("D242", "D243")),
225 (self.__checkEricSignal, ("D260", "D261", "D262", "D263")),
226 ],
227 "methodDocstring": [
228 (self.__checkEricSummary, ("D232")),
229 ],
230 "defDocstring": [
231 (self.__checkFunctionDocstring,
232 ("D102", "D202.1", "D202.2", "D203")),
233 (self.__checkImperativeMood, ("D132",)),
234 (self.__checkNoSignature, ("D133",)),
235 (self.__checkEricReturn, ("D234r", "D235r")),
236 (self.__checkEricYield, ("D234y", "D235y")),
237 (self.__checkEricFunctionArguments,
238 ("D236", "D237", "D238", "D239")),
239 (self.__checkEricNoBlankBeforeAndAfterClassOrFunction,
240 ("D244", "D245")),
241 (self.__checkEricException,
242 ("D250", "D251", "D252", "D253")),
243 ],
244 "docstring": [
245 (self.__checkTripleDoubleQuotes, ("D111",)),
246 (self.__checkBackslashes, ("D112",)),
247 (self.__checkIndent, ("D122",)),
248 (self.__checkSummary, ("D130",)),
249 (self.__checkEricEndsWithPeriod, ("D231",)),
250 (self.__checkEricBlankAfterSummary, ("D246",)),
251 (self.__checkEricNBlankAfterLastParagraph, ("D247",)),
252 (self.__checkEricQuotesOnSeparateLines, ("D222", "D223"))
253 ],
254 }
255
256 self.__checkers = {}
257 for key, checkers in checkersWithCodes.items():
258 for checker, codes in checkers:
259 if any(not (code and self.__ignoreCode(code))
260 for code in codes):
261 if key not in self.__checkers:
262 self.__checkers[key] = []
263 self.__checkers[key].append(checker)
264
265 def __ignoreCode(self, code):
266 """
267 Private method to check if the error code should be ignored.
268
269 @param code message code to check for (string)
270 @return flag indicating to ignore the given code (boolean)
271 """
272 return (code.startswith(self.__ignore) and
273 not code.startswith(self.__select))
274
275 def __error(self, lineNumber, offset, code, *args):
276 """
277 Private method to record an issue.
278
279 @param lineNumber line number of the issue (integer)
280 @param offset position within line of the issue (integer)
281 @param code message code (string)
282 @param args arguments for the message (list)
283 """
284 if self.__ignoreCode(code):
285 return
286
287 if code in self.counters:
288 self.counters[code] += 1
289 else:
290 self.counters[code] = 1
291
292 # Don't care about expected codes
293 if code in self.__expected:
294 return
295
296 if code and (self.counters[code] == 1 or self.__repeat):
297 # record the issue with one based line number
298 self.errors.append(
299 {
300 "file": self.__filename,
301 "line": lineNumber + 1,
302 "offset": offset,
303 "code": code,
304 "args": args,
305 }
306 )
307
308 def __resetReadline(self):
309 """
310 Private method to reset the internal readline function.
311 """
312 self.__lineNumber = 0
313
314 def __readline(self):
315 """
316 Private method to get the next line from the source.
317
318 @return next line of source (string)
319 """
320 self.__lineNumber += 1
321 if self.__lineNumber > len(self.__source):
322 return ''
323 return self.__source[self.__lineNumber - 1]
324
325 def run(self):
326 """
327 Public method to check the given source for violations of doc string
328 conventions.
329 """
330 if not self.__filename:
331 # don't do anything, if essential data is missing
332 return
333
334 if not self.__checkers:
335 # don't do anything, if no codes were selected
336 return
337
338 for keyword in self.__keywords:
339 if keyword in self.__checkers:
340 for check in self.__checkers[keyword]:
341 for context in self.__parseContexts(keyword):
342 docstring = self.__parseDocstring(context, keyword)
343 check(docstring, context)
344
345 def __getSummaryLine(self, docstringContext):
346 """
347 Private method to extract the summary line.
348
349 @param docstringContext docstring context (DocStyleContext)
350 @return summary line (string) and the line it was found on (integer)
351 """
352 lines = docstringContext.source()
353
354 line = (lines[0]
355 .replace('r"""', "", 1)
356 .replace('u"""', "", 1)
357 .replace('"""', "")
358 .replace("r'''", "", 1)
359 .replace("u'''", "", 1)
360 .replace("'''", "")
361 .strip())
362
363 if len(lines) == 1 or len(line) > 0:
364 return line, 0
365 return lines[1].strip().replace('"""', "").replace("'''", ""), 1
366
367 def __getSummaryLines(self, docstringContext):
368 """
369 Private method to extract the summary lines.
370
371 @param docstringContext docstring context (DocStyleContext)
372 @return summary lines (list of string) and the line it was found on
373 (integer)
374 """
375 summaries = []
376 lines = docstringContext.source()
377
378 line0 = (lines[0]
379 .replace('r"""', "", 1)
380 .replace('u"""', "", 1)
381 .replace('"""', "")
382 .replace("r'''", "", 1)
383 .replace("u'''", "", 1)
384 .replace("'''", "")
385 .strip())
386 line1 = (
387 lines[1].strip().replace('"""', "").replace("'''", "")
388 if len(lines) > 1 else
389 ""
390 )
391 line2 = (
392 lines[2].strip().replace('"""', "").replace("'''", "")
393 if len(lines) > 2 else
394 ""
395 )
396 if line0:
397 lineno = 0
398 summaries.append(line0)
399 if not line0.endswith(".") and line1:
400 # two line summary
401 summaries.append(line1)
402 elif line1:
403 lineno = 1
404 summaries.append(line1)
405 if not line1.endswith(".") and line2:
406 # two line summary
407 summaries.append(line2)
408 else:
409 lineno = 2
410 summaries.append(line2)
411 return summaries, lineno
412
413 def __getArgNames(self, node):
414 """
415 Private method to get the argument names of a function node.
416
417 @param node AST node to extract arguments names from
418 @return tuple of two list of argument names, one for arguments
419 and one for keyword arguments (tuple of list of string)
420 """
421 arguments = []
422 arguments.extend([arg.arg for arg in node.args.args])
423 if node.args.vararg is not None:
424 arguments.append(node.args.vararg.arg)
425
426 kwarguments = []
427 kwarguments.extend([arg.arg for arg in node.args.kwonlyargs])
428 if node.args.kwarg is not None:
429 kwarguments.append(node.args.kwarg.arg)
430 return arguments, kwarguments
431
432 ##################################################################
433 ## Parsing functionality below
434 ##################################################################
435
436 def __parseModuleDocstring(self, source):
437 """
438 Private method to extract a docstring given a module source.
439
440 @param source source to parse (list of string)
441 @return context of extracted docstring (DocStyleContext)
442 """
443 for kind, value, (line, _char), _, _ in tokenize.generate_tokens(
444 StringIO("".join(source)).readline):
445 if kind in [tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL]:
446 continue
447 elif kind == tokenize.STRING: # first STRING should be docstring
448 return DocStyleContext(value, line - 1, "docstring")
449 else:
450 return None
451
452 return None
453
454 def __parseDocstring(self, context, what=''):
455 """
456 Private method to extract a docstring given `def` or `class` source.
457
458 @param context context data to get the docstring from (DocStyleContext)
459 @param what string denoting what is being parsed (string)
460 @return context of extracted docstring (DocStyleContext)
461 """
462 moduleDocstring = self.__parseModuleDocstring(context.source())
463 if what.startswith('module') or context.contextType() == "module":
464 return moduleDocstring
465 if moduleDocstring:
466 return moduleDocstring
467
468 tokenGenerator = tokenize.generate_tokens(
469 StringIO(context.ssource()).readline)
470 with contextlib.suppress(StopIteration):
471 kind = None
472 while kind != tokenize.INDENT:
473 kind, _, _, _, _ = next(tokenGenerator)
474 kind, value, (line, char), _, _ = next(tokenGenerator)
475 if kind == tokenize.STRING: # STRING after INDENT is a docstring
476 return DocStyleContext(
477 value, context.start() + line - 1, "docstring")
478
479 return None
480
481 def __parseTopLevel(self, keyword):
482 """
483 Private method to extract top-level functions or classes.
484
485 @param keyword keyword signaling what to extract (string)
486 @return extracted function or class contexts (list of DocStyleContext)
487 """
488 self.__resetReadline()
489 tokenGenerator = tokenize.generate_tokens(self.__readline)
490 kind, value, char = None, None, None
491 contexts = []
492 try:
493 while True:
494 start, end = None, None
495 while not (kind == tokenize.NAME and
496 value == keyword and
497 char == 0):
498 kind, value, (line, char), _, _ = next(tokenGenerator)
499 start = line - 1, char
500 while not (kind == tokenize.DEDENT and
501 value == '' and
502 char == 0):
503 kind, value, (line, char), _, _ = next(tokenGenerator)
504 end = line - 1, char
505 contexts.append(DocStyleContext(
506 self.__source[start[0]:end[0]], start[0], keyword))
507 except StopIteration:
508 return contexts
509
510 def __parseFunctions(self):
511 """
512 Private method to extract top-level functions.
513
514 @return extracted function contexts (list of DocStyleContext)
515 """
516 if not self.__functionsCache:
517 self.__functionsCache = self.__parseTopLevel('def')
518 return self.__functionsCache
519
520 def __parseClasses(self):
521 """
522 Private method to extract top-level classes.
523
524 @return extracted class contexts (list of DocStyleContext)
525 """
526 if not self.__classesCache:
527 self.__classesCache = self.__parseTopLevel('class')
528 return self.__classesCache
529
530 def __skipIndentedBlock(self, tokenGenerator):
531 """
532 Private method to skip over an indented block of source code.
533
534 @param tokenGenerator token generator
535 @return last token of the indented block
536 """
537 kind, value, start, end, raw = next(tokenGenerator)
538 while kind != tokenize.INDENT:
539 kind, value, start, end, raw = next(tokenGenerator)
540 indent = 1
541 for kind, value, start, end, raw in tokenGenerator:
542 if kind == tokenize.INDENT:
543 indent += 1
544 elif kind == tokenize.DEDENT:
545 indent -= 1
546 if indent == 0:
547 return kind, value, start, end, raw
548
549 return None
550
551 def __parseMethods(self):
552 """
553 Private method to extract methods of all classes.
554
555 @return extracted method contexts (list of DocStyleContext)
556 """
557 if not self.__methodsCache:
558 contexts = []
559 for classContext in self.__parseClasses():
560 tokenGenerator = tokenize.generate_tokens(
561 StringIO(classContext.ssource()).readline)
562 kind, value, char = None, None, None
563 with contextlib.suppress(StopIteration):
564 while True:
565 start, end = None, None
566 while not (kind == tokenize.NAME and value == 'def'):
567 kind, value, (line, char), _, _ = (
568 next(tokenGenerator)
569 )
570 start = line - 1, char
571 kind, value, (line, char), _, _ = (
572 self.__skipIndentedBlock(tokenGenerator)
573 )
574 end = line - 1, char
575 startLine = classContext.start() + start[0]
576 endLine = classContext.start() + end[0]
577 context = DocStyleContext(
578 self.__source[startLine:endLine],
579 startLine, "def")
580 if startLine > 0:
581 if (
582 self.__source[startLine - 1].strip() ==
583 "@staticmethod"
584 ):
585 context.setSpecial("staticmethod")
586 elif (
587 self.__source[startLine - 1].strip() ==
588 "@classmethod"
589 ):
590 context.setSpecial("classmethod")
591 contexts.append(context)
592 self.__methodsCache = contexts
593
594 return self.__methodsCache
595
596 def __parseContexts(self, kind):
597 """
598 Private method to extract a context from the source.
599
600 @param kind kind of context to extract (string)
601 @return requested contexts (list of DocStyleContext)
602 """
603 if kind == 'moduleDocstring':
604 return [DocStyleContext(self.__source, 0, "module")]
605 if kind == 'functionDocstring':
606 return self.__parseFunctions()
607 if kind == 'classDocstring':
608 return self.__parseClasses()
609 if kind == 'methodDocstring':
610 return self.__parseMethods()
611 if kind == 'defDocstring':
612 return self.__parseFunctions() + self.__parseMethods()
613 if kind == 'docstring':
614 return ([DocStyleContext(self.__source, 0, "module")] +
615 self.__parseFunctions() +
616 self.__parseClasses() +
617 self.__parseMethods())
618 return [] # fall back
619
620 ##################################################################
621 ## Checking functionality below (PEP-257)
622 ##################################################################
623
624 def __checkModulesDocstrings(self, docstringContext, context):
625 """
626 Private method to check, if the module has a docstring.
627
628 @param docstringContext docstring context (DocStyleContext)
629 @param context context of the docstring (DocStyleContext)
630 """
631 if docstringContext is None:
632 self.__error(context.start(), 0, "D101")
633 return
634
635 docstring = docstringContext.ssource()
636 if (not docstring or not docstring.strip() or
637 not docstring.strip('\'"')):
638 self.__error(context.start(), 0, "D101")
639
640 if (
641 self.__docType == "eric" and
642 docstring.strip('\'"').strip() ==
643 "Module documentation goes here."
644 ):
645 self.__error(docstringContext.end(), 0, "D201")
646 return
647
648 def __checkFunctionDocstring(self, docstringContext, context):
649 """
650 Private method to check, that all public functions and methods
651 have a docstring.
652
653 @param docstringContext docstring context (DocStyleContext)
654 @param context context of the docstring (DocStyleContext)
655 """
656 functionName = context.source()[0].lstrip().split()[1].split("(")[0]
657 if functionName.startswith('_') and not functionName.endswith('__'):
658 if self.__docType == "eric":
659 code = "D203"
660 else:
661 code = "D103"
662 else:
663 code = "D102"
664
665 if docstringContext is None:
666 self.__error(context.start(), 0, code)
667 return
668
669 docstring = docstringContext.ssource()
670 if (not docstring or not docstring.strip() or
671 not docstring.strip('\'"')):
672 self.__error(context.start(), 0, code)
673
674 if self.__docType == "eric":
675 if (
676 docstring.strip('\'"').strip() ==
677 "Function documentation goes here."
678 ):
679 self.__error(docstringContext.end(), 0, "D202.1")
680 return
681
682 if (
683 "DESCRIPTION" in docstring or
684 "TYPE" in docstring
685 ):
686 self.__error(docstringContext.end(), 0, "D202.2")
687 return
688
689 def __checkClassDocstring(self, docstringContext, context):
690 """
691 Private method to check, that all public functions and methods
692 have a docstring.
693
694 @param docstringContext docstring context (DocStyleContext)
695 @param context context of the docstring (DocStyleContext)
696 """
697 className = context.source()[0].lstrip().split()[1].split("(")[0]
698 if className.startswith('_'):
699 if self.__docType == "eric":
700 code = "D205"
701 else:
702 code = "D105"
703 else:
704 code = "D104"
705
706 if docstringContext is None:
707 self.__error(context.start(), 0, code)
708 return
709
710 docstring = docstringContext.ssource()
711 if (not docstring or not docstring.strip() or
712 not docstring.strip('\'"')):
713 self.__error(context.start(), 0, code)
714 return
715
716 if (
717 self.__docType == "eric" and
718 docstring.strip('\'"').strip() == "Class documentation goes here."
719 ):
720 self.__error(docstringContext.end(), 0, "D206")
721 return
722
723 def __checkTripleDoubleQuotes(self, docstringContext, context):
724 """
725 Private method to check, that all docstrings are surrounded
726 by triple double quotes.
727
728 @param docstringContext docstring context (DocStyleContext)
729 @param context context of the docstring (DocStyleContext)
730 """
731 if docstringContext is None:
732 return
733
734 docstring = docstringContext.ssource().strip()
735 if not docstring.startswith(('"""', 'r"""', 'u"""')):
736 self.__error(docstringContext.start(), 0, "D111")
737
738 def __checkBackslashes(self, docstringContext, context):
739 """
740 Private method to check, that all docstrings containing
741 backslashes are surrounded by raw triple double quotes.
742
743 @param docstringContext docstring context (DocStyleContext)
744 @param context context of the docstring (DocStyleContext)
745 """
746 if docstringContext is None:
747 return
748
749 docstring = docstringContext.ssource().strip()
750 if "\\" in docstring and not docstring.startswith('r"""'):
751 self.__error(docstringContext.start(), 0, "D112")
752
753 def __checkOneLiner(self, docstringContext, context):
754 """
755 Private method to check, that one-liner docstrings fit on
756 one line with quotes.
757
758 @param docstringContext docstring context (DocStyleContext)
759 @param context context of the docstring (DocStyleContext)
760 """
761 if docstringContext is None:
762 return
763
764 lines = docstringContext.source()
765 if len(lines) > 1:
766 nonEmptyLines = [line for line in lines
767 if line.strip().strip('\'"')]
768 if len(nonEmptyLines) == 1:
769 modLen = len(context.indent() + '"""' +
770 nonEmptyLines[0].strip() + '"""')
771 if context.contextType() != "module":
772 modLen += 4
773 if not nonEmptyLines[0].strip().endswith("."):
774 # account for a trailing dot
775 modLen += 1
776 if modLen <= self.__maxLineLength:
777 self.__error(docstringContext.start(), 0, "D121")
778
779 def __checkIndent(self, docstringContext, context):
780 """
781 Private method to check, that docstrings are properly indented.
782
783 @param docstringContext docstring context (DocStyleContext)
784 @param context context of the docstring (DocStyleContext)
785 """
786 if docstringContext is None:
787 return
788
789 lines = docstringContext.source()
790 if len(lines) == 1:
791 return
792
793 nonEmptyLines = [line.rstrip() for line in lines[1:] if line.strip()]
794 if not nonEmptyLines:
795 return
796
797 indent = min(len(line) - len(line.strip()) for line in nonEmptyLines)
798 expectedIndent = (
799 0
800 if context.contextType() == "module" else
801 len(context.indent()) + 4
802 )
803 if indent != expectedIndent:
804 self.__error(docstringContext.start(), 0, "D122")
805
806 def __checkSummary(self, docstringContext, context):
807 """
808 Private method to check, that docstring summaries contain some text.
809
810 @param docstringContext docstring context (DocStyleContext)
811 @param context context of the docstring (DocStyleContext)
812 """
813 if docstringContext is None:
814 return
815
816 summary, lineNumber = self.__getSummaryLine(docstringContext)
817 if summary == "":
818 self.__error(docstringContext.start() + lineNumber, 0, "D130")
819
820 def __checkEndsWithPeriod(self, docstringContext, context):
821 """
822 Private method to check, that docstring summaries end with a period.
823
824 @param docstringContext docstring context (DocStyleContext)
825 @param context context of the docstring (DocStyleContext)
826 """
827 if docstringContext is None:
828 return
829
830 summary, lineNumber = self.__getSummaryLine(docstringContext)
831 if not summary.endswith("."):
832 self.__error(docstringContext.start() + lineNumber, 0, "D131")
833
834 def __checkImperativeMood(self, docstringContext, context):
835 """
836 Private method to check, that docstring summaries are in
837 imperative mood.
838
839 @param docstringContext docstring context (DocStyleContext)
840 @param context context of the docstring (DocStyleContext)
841 """
842 if docstringContext is None:
843 return
844
845 summary, lineNumber = self.__getSummaryLine(docstringContext)
846 if summary:
847 firstWord = summary.strip().split()[0]
848 if firstWord.endswith("s") and not firstWord.endswith("ss"):
849 self.__error(docstringContext.start() + lineNumber, 0, "D132")
850
851 def __checkNoSignature(self, docstringContext, context):
852 """
853 Private method to check, that docstring summaries don't repeat
854 the function's signature.
855
856 @param docstringContext docstring context (DocStyleContext)
857 @param context context of the docstring (DocStyleContext)
858 """
859 if docstringContext is None:
860 return
861
862 functionName = context.source()[0].lstrip().split()[1].split("(")[0]
863 summary, lineNumber = self.__getSummaryLine(docstringContext)
864 if (
865 functionName + "(" in summary.replace(" ", "") and
866 functionName + "()" not in summary.replace(" ", "")
867 ):
868 # report only, if it is not an abbreviated form (i.e. function() )
869 self.__error(docstringContext.start() + lineNumber, 0, "D133")
870
871 def __checkReturnType(self, docstringContext, context):
872 """
873 Private method to check, that docstrings mention the return value type.
874
875 @param docstringContext docstring context (DocStyleContext)
876 @param context context of the docstring (DocStyleContext)
877 """
878 if docstringContext is None:
879 return
880
881 if "return" not in docstringContext.ssource().lower():
882 tokens = list(
883 tokenize.generate_tokens(StringIO(context.ssource()).readline))
884 return_ = [tokens[i + 1][0] for i, token in enumerate(tokens)
885 if token[1] == "return"]
886 if (set(return_) -
887 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
888 set()):
889 self.__error(docstringContext.end(), 0, "D134")
890
891 def __checkNoBlankLineBefore(self, docstringContext, context):
892 """
893 Private method to check, that function/method docstrings are not
894 preceded by a blank line.
895
896 @param docstringContext docstring context (DocStyleContext)
897 @param context context of the docstring (DocStyleContext)
898 """
899 if docstringContext is None:
900 return
901
902 contextLines = context.source()
903 cti = 0
904 while (
905 cti < len(contextLines) and
906 not contextLines[cti].strip().startswith(
907 ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''"))
908 ):
909 cti += 1
910 if cti == len(contextLines):
911 return
912
913 if not contextLines[cti - 1].strip():
914 self.__error(docstringContext.start(), 0, "D141")
915
916 def __checkBlankBeforeAndAfterClass(self, docstringContext, context):
917 """
918 Private method to check, that class docstrings have one
919 blank line around them.
920
921 @param docstringContext docstring context (DocStyleContext)
922 @param context context of the docstring (DocStyleContext)
923 """
924 if docstringContext is None:
925 return
926
927 contextLines = context.source()
928 cti = 0
929 while (
930 cti < len(contextLines) and
931 not contextLines[cti].strip().startswith(
932 ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''"))
933 ):
934 cti += 1
935 if cti == len(contextLines):
936 return
937
938 start = cti
939 if contextLines[cti].strip() in (
940 '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"):
941 # it is a multi line docstring
942 cti += 1
943
944 while (
945 cti < len(contextLines) and
946 not contextLines[cti].strip().endswith(('"""', "'''"))
947 ):
948 cti += 1
949 end = cti
950 if cti >= len(contextLines) - 1:
951 return
952
953 if contextLines[start - 1].strip():
954 self.__error(docstringContext.start(), 0, "D142")
955 if contextLines[end + 1].strip():
956 self.__error(docstringContext.end(), 0, "D143")
957
958 def __checkBlankAfterSummary(self, docstringContext, context):
959 """
960 Private method to check, that docstring summaries are followed
961 by a blank line.
962
963 @param docstringContext docstring context (DocStyleContext)
964 @param context context of the docstring (DocStyleContext)
965 """
966 if docstringContext is None:
967 return
968
969 docstrings = docstringContext.source()
970 if len(docstrings) <= 3:
971 # correct/invalid one-liner
972 return
973
974 summary, lineNumber = self.__getSummaryLine(docstringContext)
975 if (
976 len(docstrings) > 2 and
977 docstrings[lineNumber + 1].strip()
978 ):
979 self.__error(docstringContext.start() + lineNumber, 0, "D144")
980
981 def __checkBlankAfterLastParagraph(self, docstringContext, context):
982 """
983 Private method to check, that the last paragraph of docstrings is
984 followed by a blank line.
985
986 @param docstringContext docstring context (DocStyleContext)
987 @param context context of the docstring (DocStyleContext)
988 """
989 if docstringContext is None:
990 return
991
992 docstrings = docstringContext.source()
993 if len(docstrings) <= 3:
994 # correct/invalid one-liner
995 return
996
997 if docstrings[-2].strip():
998 self.__error(docstringContext.end(), 0, "D145")
999
1000 ##################################################################
1001 ## Checking functionality below (eric specific ones)
1002 ##################################################################
1003
1004 def __checkEricQuotesOnSeparateLines(self, docstringContext, context):
1005 """
1006 Private method to check, that leading and trailing quotes are on
1007 a line by themselves.
1008
1009 @param docstringContext docstring context (DocStyleContext)
1010 @param context context of the docstring (DocStyleContext)
1011 """
1012 if docstringContext is None:
1013 return
1014
1015 lines = docstringContext.source()
1016 if lines[0].strip().strip('ru"\''):
1017 self.__error(docstringContext.start(), 0, "D221")
1018 if lines[-1].strip().strip('"\''):
1019 self.__error(docstringContext.end(), 0, "D222")
1020
1021 def __checkEricEndsWithPeriod(self, docstringContext, context):
1022 """
1023 Private method to check, that docstring summaries end with a period.
1024
1025 @param docstringContext docstring context (DocStyleContext)
1026 @param context context of the docstring (DocStyleContext)
1027 """
1028 if docstringContext is None:
1029 return
1030
1031 summaryLines, lineNumber = self.__getSummaryLines(docstringContext)
1032 if summaryLines:
1033 if summaryLines[-1].lstrip().startswith("@"):
1034 summaryLines.pop(-1)
1035 summary = " ".join([s.strip() for s in summaryLines if s])
1036 if (
1037 summary and
1038 not summary.endswith(".") and
1039 summary.split(None, 1)[0].lower() != "constructor"
1040 ):
1041 self.__error(
1042 docstringContext.start() + lineNumber +
1043 len(summaryLines) - 1,
1044 0, "D231")
1045
1046 def __checkEricReturn(self, docstringContext, context):
1047 """
1048 Private method to check, that docstrings contain an &#64;return line
1049 if they return anything and don't otherwise.
1050
1051 @param docstringContext docstring context (DocStyleContext)
1052 @param context context of the docstring (DocStyleContext)
1053 """
1054 if docstringContext is None:
1055 return
1056
1057 tokens = list(
1058 tokenize.generate_tokens(StringIO(context.ssource()).readline))
1059 return_ = [tokens[i + 1][0] for i, token in enumerate(tokens)
1060 if token[1] == "return"]
1061 if "@return" not in docstringContext.ssource():
1062 if (set(return_) -
1063 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
1064 set()):
1065 self.__error(docstringContext.end(), 0, "D234r")
1066 else:
1067 if (set(return_) -
1068 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} ==
1069 set()):
1070 self.__error(docstringContext.end(), 0, "D235r")
1071
1072 def __checkEricYield(self, docstringContext, context):
1073 """
1074 Private method to check, that docstrings contain an &#64;yield line
1075 if they return anything and don't otherwise.
1076
1077 @param docstringContext docstring context (DocStyleContext)
1078 @param context context of the docstring (DocStyleContext)
1079 """
1080 if docstringContext is None:
1081 return
1082
1083 tokens = list(
1084 tokenize.generate_tokens(StringIO(context.ssource()).readline))
1085 yield_ = [tokens[i + 1][0] for i, token in enumerate(tokens)
1086 if token[1] == "yield"]
1087 if "@yield" not in docstringContext.ssource():
1088 if (set(yield_) -
1089 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
1090 set()):
1091 self.__error(docstringContext.end(), 0, "D234y")
1092 else:
1093 if (set(yield_) -
1094 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} ==
1095 set()):
1096 self.__error(docstringContext.end(), 0, "D235y")
1097
1098 def __checkEricFunctionArguments(self, docstringContext, context):
1099 """
1100 Private method to check, that docstrings contain an &#64;param and/or
1101 &#64;keyparam line for each argument.
1102
1103 @param docstringContext docstring context (DocStyleContext)
1104 @param context context of the docstring (DocStyleContext)
1105 """
1106 if docstringContext is None:
1107 return
1108
1109 try:
1110 tree = ast.parse(context.ssource())
1111 except (SyntaxError, TypeError):
1112 return
1113 if (isinstance(tree, ast.Module) and len(tree.body) == 1 and
1114 isinstance(tree.body[0],
1115 (ast.FunctionDef, ast.AsyncFunctionDef))):
1116 functionDef = tree.body[0]
1117 argNames, kwNames = self.__getArgNames(functionDef)
1118 if "self" in argNames:
1119 argNames.remove("self")
1120 if "cls" in argNames:
1121 argNames.remove("cls")
1122
1123 docstring = docstringContext.ssource()
1124 if (docstring.count("@param") + docstring.count("@keyparam") <
1125 len(argNames + kwNames)):
1126 self.__error(docstringContext.end(), 0, "D236")
1127 elif (docstring.count("@param") + docstring.count("@keyparam") >
1128 len(argNames + kwNames)):
1129 self.__error(docstringContext.end(), 0, "D237")
1130 else:
1131 # extract @param and @keyparam from docstring
1132 args = []
1133 kwargs = []
1134 for line in docstringContext.source():
1135 if line.strip().startswith(("@param", "@keyparam")):
1136 paramParts = line.strip().split(None, 2)
1137 if len(paramParts) >= 2:
1138 at, name = paramParts[:2]
1139 if at == "@keyparam":
1140 kwargs.append(name.lstrip("*"))
1141 args.append(name.lstrip("*"))
1142
1143 # do the checks
1144 for name in kwNames:
1145 if name not in kwargs:
1146 self.__error(docstringContext.end(), 0, "D238")
1147 return
1148 if argNames + kwNames != args:
1149 self.__error(docstringContext.end(), 0, "D239")
1150
1151 def __checkEricException(self, docstringContext, context):
1152 """
1153 Private method to check, that docstrings contain an &#64;exception line
1154 if they raise an exception and don't otherwise.
1155
1156 Note: This method also checks the raised and documented exceptions for
1157 completeness (i.e. raised exceptions that are not documented or
1158 documented exceptions that are not raised)
1159
1160 @param docstringContext docstring context (DocStyleContext)
1161 @param context context of the docstring (DocStyleContext)
1162 """
1163 if docstringContext is None:
1164 return
1165
1166 tokens = list(
1167 tokenize.generate_tokens(StringIO(context.ssource()).readline))
1168 exceptions = set()
1169 raisedExceptions = set()
1170 tokensLen = len(tokens)
1171 for i, token in enumerate(tokens):
1172 if token[1] == "raise":
1173 exceptions.add(tokens[i + 1][0])
1174 if tokens[i + 1][0] == tokenize.NAME:
1175 if (
1176 tokensLen > (i + 2) and
1177 tokens[i + 2][1] == "."
1178 ):
1179 raisedExceptions.add("{0}.{1}".format(
1180 tokens[i + 1][1], tokens[i + 3][1]))
1181 else:
1182 raisedExceptions.add(tokens[i + 1][1])
1183
1184 if (
1185 "@exception" not in docstringContext.ssource() and
1186 "@throws" not in docstringContext.ssource() and
1187 "@raise" not in docstringContext.ssource()
1188 ):
1189 if (exceptions -
1190 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
1191 set()):
1192 self.__error(docstringContext.end(), 0, "D250")
1193 else:
1194 if (exceptions -
1195 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} ==
1196 set()):
1197 self.__error(docstringContext.end(), 0, "D251")
1198 else:
1199 # step 1: extract documented exceptions
1200 documentedExceptions = set()
1201 for line in docstringContext.source():
1202 line = line.strip()
1203 if line.startswith(("@exception", "@throws", "@raise")):
1204 exceptionTokens = line.split(None, 2)
1205 if len(exceptionTokens) >= 2:
1206 documentedExceptions.add(exceptionTokens[1])
1207
1208 # step 2: report undocumented exceptions
1209 for exception in raisedExceptions:
1210 if exception not in documentedExceptions:
1211 self.__error(docstringContext.end(), 0, "D252",
1212 exception)
1213
1214 # step 3: report undefined signals
1215 for exception in documentedExceptions:
1216 if exception not in raisedExceptions:
1217 self.__error(docstringContext.end(), 0, "D253",
1218 exception)
1219
1220 def __checkEricSignal(self, docstringContext, context):
1221 """
1222 Private method to check, that docstrings contain an &#64;signal line
1223 if they define signals and don't otherwise.
1224
1225 Note: This method also checks the defined and documented signals for
1226 completeness (i.e. defined signals that are not documented or
1227 documented signals that are not defined)
1228
1229 @param docstringContext docstring context (DocStyleContext)
1230 @param context context of the docstring (DocStyleContext)
1231 """
1232 if docstringContext is None:
1233 return
1234
1235 tokens = list(
1236 tokenize.generate_tokens(StringIO(context.ssource()).readline))
1237 definedSignals = set()
1238 for i, token in enumerate(tokens):
1239 if token[1] in ("pyqtSignal", "Signal"):
1240 if tokens[i - 1][1] == "." and tokens[i - 2][1] == "QtCore":
1241 definedSignals.add(tokens[i - 4][1])
1242 elif tokens[i - 1][1] == "=":
1243 definedSignals.add(tokens[i - 2][1])
1244
1245 if "@signal" not in docstringContext.ssource() and definedSignals:
1246 self.__error(docstringContext.end(), 0, "D260")
1247 elif "@signal" in docstringContext.ssource():
1248 if not definedSignals:
1249 self.__error(docstringContext.end(), 0, "D261")
1250 else:
1251 # step 1: extract documented signals
1252 documentedSignals = set()
1253 for line in docstringContext.source():
1254 line = line.strip()
1255 if line.startswith("@signal"):
1256 signalTokens = line.split(None, 2)
1257 if len(signalTokens) >= 2:
1258 signal = signalTokens[1]
1259 if "(" in signal:
1260 signal = signal.split("(", 1)[0]
1261 documentedSignals.add(signal)
1262
1263 # step 2: report undocumented signals
1264 for signal in definedSignals:
1265 if signal not in documentedSignals:
1266 self.__error(docstringContext.end(), 0, "D262", signal)
1267
1268 # step 3: report undefined signals
1269 for signal in documentedSignals:
1270 if signal not in definedSignals:
1271 self.__error(docstringContext.end(), 0, "D263", signal)
1272
1273 def __checkEricBlankAfterSummary(self, docstringContext, context):
1274 """
1275 Private method to check, that docstring summaries are followed
1276 by a blank line.
1277
1278 @param docstringContext docstring context (DocStyleContext)
1279 @param context context of the docstring (DocStyleContext)
1280 """
1281 if docstringContext is None:
1282 return
1283
1284 docstrings = docstringContext.source()
1285 if len(docstrings) <= 3:
1286 # correct/invalid one-liner
1287 return
1288
1289 summaryLines, lineNumber = self.__getSummaryLines(docstringContext)
1290 if (
1291 len(docstrings) - 2 > lineNumber + len(summaryLines) - 1 and
1292 docstrings[lineNumber + len(summaryLines)].strip()
1293 ):
1294 self.__error(docstringContext.start() + lineNumber, 0, "D246")
1295
1296 def __checkEricNoBlankBeforeAndAfterClassOrFunction(
1297 self, docstringContext, context):
1298 """
1299 Private method to check, that class and function/method docstrings
1300 have no blank line around them.
1301
1302 @param docstringContext docstring context (DocStyleContext)
1303 @param context context of the docstring (DocStyleContext)
1304 """
1305 if docstringContext is None:
1306 return
1307
1308 contextLines = context.source()
1309 isClassContext = contextLines[0].lstrip().startswith("class ")
1310 cti = 0
1311 while (
1312 cti < len(contextLines) and
1313 not contextLines[cti].strip().startswith(
1314 ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''"))
1315 ):
1316 cti += 1
1317 if cti == len(contextLines):
1318 return
1319
1320 start = cti
1321 if contextLines[cti].strip() in (
1322 '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"):
1323 # it is a multi line docstring
1324 cti += 1
1325
1326 while (
1327 cti < len(contextLines) and
1328 not contextLines[cti].strip().endswith(('"""', "'''"))
1329 ):
1330 cti += 1
1331 end = cti
1332 if cti >= len(contextLines) - 1:
1333 return
1334
1335 if isClassContext:
1336 if not contextLines[start - 1].strip():
1337 self.__error(docstringContext.start(), 0, "D242")
1338 if not contextLines[end + 1].strip() and self.__docType == "eric":
1339 self.__error(docstringContext.end(), 0, "D243")
1340 elif contextLines[end + 1].strip() and self.__docType == "eric_black":
1341 self.__error(docstringContext.end(), 0, "D143")
1342 else:
1343 if not contextLines[start - 1].strip():
1344 self.__error(docstringContext.start(), 0, "D244")
1345 if not contextLines[end + 1].strip():
1346 if (
1347 self.__docType == "eric_black" and
1348 len(contextLines) > end + 2 and
1349 contextLines[end + 2].strip().startswith("def ")
1350 ):
1351 return
1352
1353 self.__error(docstringContext.end(), 0, "D245")
1354
1355 def __checkEricNBlankAfterLastParagraph(self, docstringContext, context):
1356 """
1357 Private method to check, that the last paragraph of docstrings is
1358 not followed by a blank line.
1359
1360 @param docstringContext docstring context (DocStyleContext)
1361 @param context context of the docstring (DocStyleContext)
1362 """
1363 if docstringContext is None:
1364 return
1365
1366 docstrings = docstringContext.source()
1367 if len(docstrings) <= 3:
1368 # correct/invalid one-liner
1369 return
1370
1371 if not docstrings[-2].strip():
1372 self.__error(docstringContext.end(), 0, "D247")
1373
1374 def __checkEricSummary(self, docstringContext, context):
1375 """
1376 Private method to check, that method docstring summaries start with
1377 specific words.
1378
1379 @param docstringContext docstring context (DocStyleContext)
1380 @param context context of the docstring (DocStyleContext)
1381 """
1382 if docstringContext is None:
1383 return
1384
1385 summary, lineNumber = self.__getSummaryLine(docstringContext)
1386 if summary:
1387 # check, if the first word is 'Constructor', 'Public',
1388 # 'Protected' or 'Private'
1389 functionName, arguments = (
1390 context.source()[0].lstrip().split()[1].split("(", 1)
1391 )
1392 firstWord = summary.strip().split(None, 1)[0].lower()
1393 if functionName == '__init__':
1394 if firstWord != 'constructor':
1395 self.__error(docstringContext.start() + lineNumber, 0,
1396 "D232", 'constructor')
1397 elif (
1398 functionName.startswith('__') and
1399 functionName.endswith('__')
1400 ):
1401 if firstWord != 'special':
1402 self.__error(docstringContext.start() + lineNumber, 0,
1403 "D232", 'special')
1404 elif context.special() == "staticmethod":
1405 secondWord = summary.strip().split(None, 2)[1].lower()
1406 if firstWord != 'static' and secondWord != 'static':
1407 self.__error(docstringContext.start() + lineNumber, 0,
1408 "D232", 'static')
1409 elif secondWord == 'static':
1410 if functionName.startswith(('__', 'on_')):
1411 if firstWord != 'private':
1412 self.__error(docstringContext.start() + lineNumber,
1413 0, "D232", 'private static')
1414 elif (
1415 functionName.startswith('_') or
1416 functionName.endswith('Event')
1417 ):
1418 if firstWord != 'protected':
1419 self.__error(docstringContext.start() + lineNumber,
1420 0, "D232", 'protected static')
1421 else:
1422 if firstWord != 'public':
1423 self.__error(docstringContext.start() + lineNumber,
1424 0, "D232", 'public static')
1425 elif (
1426 arguments.startswith(('cls,', 'cls)')) or
1427 context.special() == "classmethod"
1428 ):
1429 secondWord = summary.strip().split(None, 2)[1].lower()
1430 if firstWord != 'class' and secondWord != 'class':
1431 self.__error(docstringContext.start() + lineNumber, 0,
1432 "D232", 'class')
1433 elif secondWord == 'class':
1434 if functionName.startswith(('__', 'on_')):
1435 if firstWord != 'private':
1436 self.__error(docstringContext.start() + lineNumber,
1437 0, "D232", 'private class')
1438 elif (
1439 functionName.startswith('_') or
1440 functionName.endswith('Event')
1441 ):
1442 if firstWord != 'protected':
1443 self.__error(docstringContext.start() + lineNumber,
1444 0, "D232", 'protected class')
1445 else:
1446 if firstWord != 'public':
1447 self.__error(docstringContext.start() + lineNumber,
1448 0, "D232", 'public class')
1449 elif functionName.startswith(('__', 'on_')):
1450 if firstWord != 'private':
1451 self.__error(docstringContext.start() + lineNumber, 0,
1452 "D232", 'private')
1453 elif (
1454 functionName.startswith('_') or
1455 functionName.endswith('Event')
1456 ):
1457 if firstWord != 'protected':
1458 self.__error(docstringContext.start() + lineNumber, 0,
1459 "D232", 'protected')
1460 else:
1461 if firstWord != 'public':
1462 self.__error(docstringContext.start() + lineNumber, 0,
1463 "D232", 'public')

eric ide

mercurial