|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2013 - 2021 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=79, 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 == "eric": |
|
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 @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 @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 @param and/or |
|
1101 @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 @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 @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(): |
|
1339 self.__error(docstringContext.end(), 0, "D243") |
|
1340 else: |
|
1341 if not contextLines[start - 1].strip(): |
|
1342 self.__error(docstringContext.start(), 0, "D244") |
|
1343 if not contextLines[end + 1].strip(): |
|
1344 self.__error(docstringContext.end(), 0, "D245") |
|
1345 |
|
1346 def __checkEricNBlankAfterLastParagraph(self, docstringContext, context): |
|
1347 """ |
|
1348 Private method to check, that the last paragraph of docstrings is |
|
1349 not followed by a blank line. |
|
1350 |
|
1351 @param docstringContext docstring context (DocStyleContext) |
|
1352 @param context context of the docstring (DocStyleContext) |
|
1353 """ |
|
1354 if docstringContext is None: |
|
1355 return |
|
1356 |
|
1357 docstrings = docstringContext.source() |
|
1358 if len(docstrings) <= 3: |
|
1359 # correct/invalid one-liner |
|
1360 return |
|
1361 |
|
1362 if not docstrings[-2].strip(): |
|
1363 self.__error(docstringContext.end(), 0, "D247") |
|
1364 |
|
1365 def __checkEricSummary(self, docstringContext, context): |
|
1366 """ |
|
1367 Private method to check, that method docstring summaries start with |
|
1368 specific words. |
|
1369 |
|
1370 @param docstringContext docstring context (DocStyleContext) |
|
1371 @param context context of the docstring (DocStyleContext) |
|
1372 """ |
|
1373 if docstringContext is None: |
|
1374 return |
|
1375 |
|
1376 summary, lineNumber = self.__getSummaryLine(docstringContext) |
|
1377 if summary: |
|
1378 # check, if the first word is 'Constructor', 'Public', |
|
1379 # 'Protected' or 'Private' |
|
1380 functionName, arguments = ( |
|
1381 context.source()[0].lstrip().split()[1].split("(", 1) |
|
1382 ) |
|
1383 firstWord = summary.strip().split(None, 1)[0].lower() |
|
1384 if functionName == '__init__': |
|
1385 if firstWord != 'constructor': |
|
1386 self.__error(docstringContext.start() + lineNumber, 0, |
|
1387 "D232", 'constructor') |
|
1388 elif ( |
|
1389 functionName.startswith('__') and |
|
1390 functionName.endswith('__') |
|
1391 ): |
|
1392 if firstWord != 'special': |
|
1393 self.__error(docstringContext.start() + lineNumber, 0, |
|
1394 "D232", 'special') |
|
1395 elif context.special() == "staticmethod": |
|
1396 secondWord = summary.strip().split(None, 2)[1].lower() |
|
1397 if firstWord != 'static' and secondWord != 'static': |
|
1398 self.__error(docstringContext.start() + lineNumber, 0, |
|
1399 "D232", 'static') |
|
1400 elif secondWord == 'static': |
|
1401 if functionName.startswith(('__', 'on_')): |
|
1402 if firstWord != 'private': |
|
1403 self.__error(docstringContext.start() + lineNumber, |
|
1404 0, "D232", 'private static') |
|
1405 elif ( |
|
1406 functionName.startswith('_') or |
|
1407 functionName.endswith('Event') |
|
1408 ): |
|
1409 if firstWord != 'protected': |
|
1410 self.__error(docstringContext.start() + lineNumber, |
|
1411 0, "D232", 'protected static') |
|
1412 else: |
|
1413 if firstWord != 'public': |
|
1414 self.__error(docstringContext.start() + lineNumber, |
|
1415 0, "D232", 'public static') |
|
1416 elif ( |
|
1417 arguments.startswith(('cls,', 'cls)')) or |
|
1418 context.special() == "classmethod" |
|
1419 ): |
|
1420 secondWord = summary.strip().split(None, 2)[1].lower() |
|
1421 if firstWord != 'class' and secondWord != 'class': |
|
1422 self.__error(docstringContext.start() + lineNumber, 0, |
|
1423 "D232", 'class') |
|
1424 elif secondWord == 'class': |
|
1425 if functionName.startswith(('__', 'on_')): |
|
1426 if firstWord != 'private': |
|
1427 self.__error(docstringContext.start() + lineNumber, |
|
1428 0, "D232", 'private class') |
|
1429 elif ( |
|
1430 functionName.startswith('_') or |
|
1431 functionName.endswith('Event') |
|
1432 ): |
|
1433 if firstWord != 'protected': |
|
1434 self.__error(docstringContext.start() + lineNumber, |
|
1435 0, "D232", 'protected class') |
|
1436 else: |
|
1437 if firstWord != 'public': |
|
1438 self.__error(docstringContext.start() + lineNumber, |
|
1439 0, "D232", 'public class') |
|
1440 elif functionName.startswith(('__', 'on_')): |
|
1441 if firstWord != 'private': |
|
1442 self.__error(docstringContext.start() + lineNumber, 0, |
|
1443 "D232", 'private') |
|
1444 elif ( |
|
1445 functionName.startswith('_') or |
|
1446 functionName.endswith('Event') |
|
1447 ): |
|
1448 if firstWord != 'protected': |
|
1449 self.__error(docstringContext.start() + lineNumber, 0, |
|
1450 "D232", 'protected') |
|
1451 else: |
|
1452 if firstWord != 'public': |
|
1453 self.__error(docstringContext.start() + lineNumber, 0, |
|
1454 "D232", 'public') |