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

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

eric ide

mercurial