|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a checker for miscellaneous checks. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import ast |
|
12 import re |
|
13 import itertools |
|
14 from string import Formatter |
|
15 from collections import defaultdict |
|
16 |
|
17 |
|
18 def composeCallPath(node): |
|
19 """ |
|
20 Generator function to assemble the call path of a given node. |
|
21 |
|
22 @param node node to assemble call path for |
|
23 @type ast.Node |
|
24 @return call path components |
|
25 @rtype str |
|
26 """ |
|
27 if isinstance(node, ast.Attribute): |
|
28 for v in composeCallPath(node.value): |
|
29 yield v |
|
30 yield node.attr |
|
31 elif isinstance(node, ast.Name): |
|
32 yield node.id |
|
33 |
|
34 |
|
35 class MiscellaneousChecker(object): |
|
36 """ |
|
37 Class implementing a checker for miscellaneous checks. |
|
38 """ |
|
39 Codes = [ |
|
40 "M101", "M102", |
|
41 "M111", "M112", |
|
42 "M131", "M132", |
|
43 |
|
44 "M191", "M192", "M193", "M194", |
|
45 "M195", "M196", "M197", "M198", |
|
46 |
|
47 "M201", |
|
48 |
|
49 "M501", "M502", "M503", "M504", "M505", "M506", "M507", |
|
50 "M511", "M512", "M513", "M514", |
|
51 |
|
52 "M601", |
|
53 "M611", "M612", "M613", |
|
54 "M621", "M622", "M623", "M624", "M625", |
|
55 "M631", "M632", |
|
56 "M651", "M652", "M653", "M654", "M655", |
|
57 |
|
58 "M701", "M702", |
|
59 "M711", |
|
60 |
|
61 "M801", |
|
62 "M811", |
|
63 "M821", "M822", |
|
64 "M831", "M832", "M833", "M834", |
|
65 |
|
66 "M901", |
|
67 ] |
|
68 |
|
69 Formatter = Formatter() |
|
70 FormatFieldRegex = re.compile(r'^((?:\s|.)*?)(\..*|\[.*\])?$') |
|
71 |
|
72 BuiltinsWhiteList = [ |
|
73 "__name__", |
|
74 "__doc__", |
|
75 "credits", |
|
76 ] |
|
77 |
|
78 def __init__(self, source, filename, select, ignore, expected, repeat, |
|
79 args): |
|
80 """ |
|
81 Constructor |
|
82 |
|
83 @param source source code to be checked |
|
84 @type list of str |
|
85 @param filename name of the source file |
|
86 @type str |
|
87 @param select list of selected codes |
|
88 @type list of str |
|
89 @param ignore list of codes to be ignored |
|
90 @type list of str |
|
91 @param expected list of expected codes |
|
92 @type list of str |
|
93 @param repeat flag indicating to report each occurrence of a code |
|
94 @type bool |
|
95 @param args dictionary of arguments for the miscellaneous checks |
|
96 @type dict |
|
97 """ |
|
98 self.__select = tuple(select) |
|
99 self.__ignore = ('',) if select else tuple(ignore) |
|
100 self.__expected = expected[:] |
|
101 self.__repeat = repeat |
|
102 self.__filename = filename |
|
103 self.__source = source[:] |
|
104 self.__args = args |
|
105 |
|
106 self.__pep3101FormatRegex = re.compile( |
|
107 r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%') |
|
108 |
|
109 if sys.version_info >= (3, 0): |
|
110 import builtins |
|
111 self.__builtins = [b for b in dir(builtins) |
|
112 if b not in self.BuiltinsWhiteList] |
|
113 else: |
|
114 import __builtin__ |
|
115 self.__builtins = [b for b in dir(__builtin__) |
|
116 if b not in self.BuiltinsWhiteList] |
|
117 |
|
118 # statistics counters |
|
119 self.counters = {} |
|
120 |
|
121 # collection of detected errors |
|
122 self.errors = [] |
|
123 |
|
124 checkersWithCodes = [ |
|
125 (self.__checkCoding, ("M101", "M102")), |
|
126 (self.__checkCopyright, ("M111", "M112")), |
|
127 (self.__checkBuiltins, ("M131", "M132")), |
|
128 (self.__checkComprehensions, ("M191", "M192", "M193", "M194", |
|
129 "M195", "M196", "M197", "M198")), |
|
130 (self.__checkDictWithSortedKeys, ("M201",)), |
|
131 (self.__checkPep3101, ("M601",)), |
|
132 (self.__checkFormatString, ("M611", "M612", "M613", |
|
133 "M621", "M622", "M623", "M624", "M625", |
|
134 "M631", "M632")), |
|
135 (self.__checkBugBear, ("M501", "M502", "M503", "M504", "M505", |
|
136 "M506", "M507", |
|
137 "M511", "M512", "M513", "M514")), |
|
138 (self.__checkLogging, ("M651", "M652", "M653", "M654", "M655")), |
|
139 (self.__checkFuture, ("M701", "M702")), |
|
140 (self.__checkGettext, ("M711",)), |
|
141 (self.__checkPrintStatements, ("M801",)), |
|
142 (self.__checkTuple, ("M811", )), |
|
143 (self.__checkMutableDefault, ("M821", "M822")), |
|
144 (self.__checkReturn, ("M831", "M832", "M833", "M834")), |
|
145 ] |
|
146 |
|
147 self.__defaultArgs = { |
|
148 "BuiltinsChecker": { |
|
149 "chr": ["unichr", ], |
|
150 "str": ["unicode", ], |
|
151 }, |
|
152 "CodingChecker": 'latin-1, utf-8', |
|
153 "CopyrightChecker": { |
|
154 "Author": "", |
|
155 "MinFilesize": 0, |
|
156 }, |
|
157 } |
|
158 |
|
159 self.__checkers = [] |
|
160 for checker, codes in checkersWithCodes: |
|
161 if any(not (code and self.__ignoreCode(code)) |
|
162 for code in codes): |
|
163 self.__checkers.append(checker) |
|
164 |
|
165 def __ignoreCode(self, code): |
|
166 """ |
|
167 Private method to check if the message code should be ignored. |
|
168 |
|
169 @param code message code to check for |
|
170 @type str |
|
171 @return flag indicating to ignore the given code |
|
172 @rtype bool |
|
173 """ |
|
174 return (code.startswith(self.__ignore) and |
|
175 not code.startswith(self.__select)) |
|
176 |
|
177 def __error(self, lineNumber, offset, code, *args): |
|
178 """ |
|
179 Private method to record an issue. |
|
180 |
|
181 @param lineNumber line number of the issue |
|
182 @type int |
|
183 @param offset position within line of the issue |
|
184 @type int |
|
185 @param code message code |
|
186 @type str |
|
187 @param args arguments for the message |
|
188 @type list |
|
189 """ |
|
190 if self.__ignoreCode(code): |
|
191 return |
|
192 |
|
193 if code in self.counters: |
|
194 self.counters[code] += 1 |
|
195 else: |
|
196 self.counters[code] = 1 |
|
197 |
|
198 # Don't care about expected codes |
|
199 if code in self.__expected: |
|
200 return |
|
201 |
|
202 if code and (self.counters[code] == 1 or self.__repeat): |
|
203 # record the issue with one based line number |
|
204 self.errors.append( |
|
205 (self.__filename, lineNumber + 1, offset, (code, args))) |
|
206 |
|
207 def __reportInvalidSyntax(self): |
|
208 """ |
|
209 Private method to report a syntax error. |
|
210 """ |
|
211 exc_type, exc = sys.exc_info()[:2] |
|
212 if len(exc.args) > 1: |
|
213 offset = exc.args[1] |
|
214 if len(offset) > 2: |
|
215 offset = offset[1:3] |
|
216 else: |
|
217 offset = (1, 0) |
|
218 self.__error(offset[0] - 1, offset[1] or 0, |
|
219 'M901', exc_type.__name__, exc.args[0]) |
|
220 |
|
221 def run(self): |
|
222 """ |
|
223 Public method to check the given source against miscellaneous |
|
224 conditions. |
|
225 """ |
|
226 if not self.__filename: |
|
227 # don't do anything, if essential data is missing |
|
228 return |
|
229 |
|
230 if not self.__checkers: |
|
231 # don't do anything, if no codes were selected |
|
232 return |
|
233 |
|
234 source = "".join(self.__source) |
|
235 # Check type for py2: if not str it's unicode |
|
236 if sys.version_info[0] == 2: |
|
237 try: |
|
238 source = source.encode('utf-8') |
|
239 except UnicodeError: |
|
240 pass |
|
241 try: |
|
242 self.__tree = compile(source, self.__filename, 'exec', |
|
243 ast.PyCF_ONLY_AST) |
|
244 except (SyntaxError, TypeError): |
|
245 self.__reportInvalidSyntax() |
|
246 return |
|
247 |
|
248 for check in self.__checkers: |
|
249 check() |
|
250 |
|
251 def __getCoding(self): |
|
252 """ |
|
253 Private method to get the defined coding of the source. |
|
254 |
|
255 @return tuple containing the line number and the coding |
|
256 @rtype tuple of int and str |
|
257 """ |
|
258 for lineno, line in enumerate(self.__source[:5]): |
|
259 matched = re.search(r'coding[:=]\s*([-\w_.]+)', |
|
260 line, re.IGNORECASE) |
|
261 if matched: |
|
262 return lineno, matched.group(1) |
|
263 else: |
|
264 return 0, "" |
|
265 |
|
266 def __checkCoding(self): |
|
267 """ |
|
268 Private method to check the presence of a coding line and valid |
|
269 encodings. |
|
270 """ |
|
271 if len(self.__source) == 0: |
|
272 return |
|
273 |
|
274 encodings = [e.lower().strip() |
|
275 for e in self.__args.get( |
|
276 "CodingChecker", self.__defaultArgs["CodingChecker"]) |
|
277 .split(",")] |
|
278 lineno, coding = self.__getCoding() |
|
279 if coding: |
|
280 if coding.lower() not in encodings: |
|
281 self.__error(lineno, 0, "M102", coding) |
|
282 else: |
|
283 self.__error(0, 0, "M101") |
|
284 |
|
285 def __checkCopyright(self): |
|
286 """ |
|
287 Private method to check the presence of a copyright statement. |
|
288 """ |
|
289 source = "".join(self.__source) |
|
290 copyrightArgs = self.__args.get( |
|
291 "CopyrightChecker", self.__defaultArgs["CopyrightChecker"]) |
|
292 copyrightMinFileSize = copyrightArgs.get( |
|
293 "MinFilesize", |
|
294 self.__defaultArgs["CopyrightChecker"]["MinFilesize"]) |
|
295 copyrightAuthor = copyrightArgs.get( |
|
296 "Author", |
|
297 self.__defaultArgs["CopyrightChecker"]["Author"]) |
|
298 copyrightRegexStr = \ |
|
299 r"Copyright\s+(\(C\)\s+)?(\d{{4}}\s+-\s+)?\d{{4}}\s+{author}" |
|
300 |
|
301 tocheck = max(1024, copyrightMinFileSize) |
|
302 topOfSource = source[:tocheck] |
|
303 if len(topOfSource) < copyrightMinFileSize: |
|
304 return |
|
305 |
|
306 copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"), |
|
307 re.IGNORECASE) |
|
308 if not copyrightRe.search(topOfSource): |
|
309 self.__error(0, 0, "M111") |
|
310 return |
|
311 |
|
312 if copyrightAuthor: |
|
313 copyrightAuthorRe = re.compile( |
|
314 copyrightRegexStr.format(author=copyrightAuthor), |
|
315 re.IGNORECASE) |
|
316 if not copyrightAuthorRe.search(topOfSource): |
|
317 self.__error(0, 0, "M112") |
|
318 |
|
319 def __checkPrintStatements(self): |
|
320 """ |
|
321 Private method to check for print statements. |
|
322 """ |
|
323 for node in ast.walk(self.__tree): |
|
324 if (isinstance(node, ast.Call) and |
|
325 getattr(node.func, 'id', None) == 'print') or \ |
|
326 (hasattr(ast, 'Print') and isinstance(node, ast.Print)): |
|
327 self.__error(node.lineno - 1, node.col_offset, "M801") |
|
328 |
|
329 def __checkTuple(self): |
|
330 """ |
|
331 Private method to check for one element tuples. |
|
332 """ |
|
333 for node in ast.walk(self.__tree): |
|
334 if isinstance(node, ast.Tuple) and \ |
|
335 len(node.elts) == 1: |
|
336 self.__error(node.lineno - 1, node.col_offset, "M811") |
|
337 |
|
338 def __checkFuture(self): |
|
339 """ |
|
340 Private method to check the __future__ imports. |
|
341 """ |
|
342 expectedImports = { |
|
343 i.strip() |
|
344 for i in self.__args.get("FutureChecker", "").split(",") |
|
345 if bool(i.strip())} |
|
346 if len(expectedImports) == 0: |
|
347 # nothing to check for; disabling the check |
|
348 return |
|
349 |
|
350 imports = set() |
|
351 node = None |
|
352 hasCode = False |
|
353 |
|
354 for node in ast.walk(self.__tree): |
|
355 if (isinstance(node, ast.ImportFrom) and |
|
356 node.module == '__future__'): |
|
357 imports |= {name.name for name in node.names} |
|
358 elif isinstance(node, ast.Expr): |
|
359 if not isinstance(node.value, ast.Str): |
|
360 hasCode = True |
|
361 break |
|
362 elif not isinstance(node, (ast.Module, ast.Str)): |
|
363 hasCode = True |
|
364 break |
|
365 |
|
366 if isinstance(node, ast.Module) or not hasCode: |
|
367 return |
|
368 |
|
369 if not (imports >= expectedImports): |
|
370 if imports: |
|
371 self.__error(node.lineno - 1, node.col_offset, "M701", |
|
372 ", ".join(expectedImports), ", ".join(imports)) |
|
373 else: |
|
374 self.__error(node.lineno - 1, node.col_offset, "M702", |
|
375 ", ".join(expectedImports)) |
|
376 |
|
377 def __checkPep3101(self): |
|
378 """ |
|
379 Private method to check for old style string formatting. |
|
380 """ |
|
381 for lineno, line in enumerate(self.__source): |
|
382 match = self.__pep3101FormatRegex.search(line) |
|
383 if match: |
|
384 lineLen = len(line) |
|
385 pos = line.find('%') |
|
386 formatPos = pos |
|
387 formatter = '%' |
|
388 if line[pos + 1] == "(": |
|
389 pos = line.find(")", pos) |
|
390 c = line[pos] |
|
391 while c not in "diouxXeEfFgGcrs": |
|
392 pos += 1 |
|
393 if pos >= lineLen: |
|
394 break |
|
395 c = line[pos] |
|
396 if c in "diouxXeEfFgGcrs": |
|
397 formatter += c |
|
398 self.__error(lineno, formatPos, "M601", formatter) |
|
399 |
|
400 def __checkFormatString(self): |
|
401 """ |
|
402 Private method to check string format strings. |
|
403 """ |
|
404 coding = self.__getCoding()[1] |
|
405 if not coding: |
|
406 # default to utf-8 |
|
407 coding = "utf-8" |
|
408 |
|
409 visitor = TextVisitor() |
|
410 visitor.visit(self.__tree) |
|
411 for node in visitor.nodes: |
|
412 text = node.s |
|
413 if sys.version_info[0] > 2 and isinstance(text, bytes): |
|
414 try: |
|
415 text = text.decode(coding) |
|
416 except UnicodeDecodeError: |
|
417 continue |
|
418 fields, implicit, explicit = self.__getFields(text) |
|
419 if implicit: |
|
420 if node in visitor.calls: |
|
421 self.__error(node.lineno - 1, node.col_offset, "M611") |
|
422 else: |
|
423 if node.is_docstring: |
|
424 self.__error(node.lineno - 1, node.col_offset, "M612") |
|
425 else: |
|
426 self.__error(node.lineno - 1, node.col_offset, "M613") |
|
427 |
|
428 if node in visitor.calls: |
|
429 call, strArgs = visitor.calls[node] |
|
430 |
|
431 numbers = set() |
|
432 names = set() |
|
433 # Determine which fields require a keyword and which an arg |
|
434 for name in fields: |
|
435 fieldMatch = self.FormatFieldRegex.match(name) |
|
436 try: |
|
437 number = int(fieldMatch.group(1)) |
|
438 except ValueError: |
|
439 number = -1 |
|
440 # negative numbers are considered keywords |
|
441 if number >= 0: |
|
442 numbers.add(number) |
|
443 else: |
|
444 names.add(fieldMatch.group(1)) |
|
445 |
|
446 keywords = {keyword.arg for keyword in call.keywords} |
|
447 numArgs = len(call.args) |
|
448 if strArgs: |
|
449 numArgs -= 1 |
|
450 if sys.version_info < (3, 5): |
|
451 hasKwArgs = bool(call.kwargs) |
|
452 hasStarArgs = bool(call.starargs) |
|
453 else: |
|
454 hasKwArgs = any(kw.arg is None for kw in call.keywords) |
|
455 hasStarArgs = sum(1 for arg in call.args |
|
456 if isinstance(arg, ast.Starred)) |
|
457 |
|
458 if hasKwArgs: |
|
459 keywords.discard(None) |
|
460 if hasStarArgs: |
|
461 numArgs -= 1 |
|
462 |
|
463 # if starargs or kwargs is not None, it can't count the |
|
464 # parameters but at least check if the args are used |
|
465 if hasKwArgs: |
|
466 if not names: |
|
467 # No names but kwargs |
|
468 self.__error(call.lineno - 1, call.col_offset, "M623") |
|
469 if hasStarArgs: |
|
470 if not numbers: |
|
471 # No numbers but args |
|
472 self.__error(call.lineno - 1, call.col_offset, "M624") |
|
473 |
|
474 if not hasKwArgs and not hasStarArgs: |
|
475 # can actually verify numbers and names |
|
476 for number in sorted(numbers): |
|
477 if number >= numArgs: |
|
478 self.__error(call.lineno - 1, call.col_offset, |
|
479 "M621", number) |
|
480 |
|
481 for name in sorted(names): |
|
482 if name not in keywords: |
|
483 self.__error(call.lineno - 1, call.col_offset, |
|
484 "M622", name) |
|
485 |
|
486 for arg in range(numArgs): |
|
487 if arg not in numbers: |
|
488 self.__error(call.lineno - 1, call.col_offset, "M631", |
|
489 arg) |
|
490 |
|
491 for keyword in keywords: |
|
492 if keyword not in names: |
|
493 self.__error(call.lineno - 1, call.col_offset, "M632", |
|
494 keyword) |
|
495 |
|
496 if implicit and explicit: |
|
497 self.__error(call.lineno - 1, call.col_offset, "M625") |
|
498 |
|
499 def __getFields(self, string): |
|
500 """ |
|
501 Private method to extract the format field information. |
|
502 |
|
503 @param string format string to be parsed |
|
504 @type str |
|
505 @return format field information as a tuple with fields, implicit |
|
506 field definitions present and explicit field definitions present |
|
507 @rtype tuple of set of str, bool, bool |
|
508 """ |
|
509 fields = set() |
|
510 cnt = itertools.count() |
|
511 implicit = False |
|
512 explicit = False |
|
513 try: |
|
514 for _literal, field, spec, conv in self.Formatter.parse(string): |
|
515 if field is not None and (conv is None or conv in 'rsa'): |
|
516 if not field: |
|
517 field = str(next(cnt)) |
|
518 implicit = True |
|
519 else: |
|
520 explicit = True |
|
521 fields.add(field) |
|
522 fields.update(parsedSpec[1] |
|
523 for parsedSpec in self.Formatter.parse(spec) |
|
524 if parsedSpec[1] is not None) |
|
525 except ValueError: |
|
526 return set(), False, False |
|
527 else: |
|
528 return fields, implicit, explicit |
|
529 |
|
530 def __checkBuiltins(self): |
|
531 """ |
|
532 Private method to check, if built-ins are shadowed. |
|
533 """ |
|
534 functionDefs = [ast.FunctionDef] |
|
535 try: |
|
536 functionDefs.append(ast.AsyncFunctionDef) |
|
537 except AttributeError: |
|
538 pass |
|
539 |
|
540 ignoreBuiltinAssignments = self.__args.get( |
|
541 "BuiltinsChecker", self.__defaultArgs["BuiltinsChecker"]) |
|
542 |
|
543 for node in ast.walk(self.__tree): |
|
544 if isinstance(node, ast.Assign): |
|
545 # assign statement |
|
546 for element in node.targets: |
|
547 if isinstance(element, ast.Name) and \ |
|
548 element.id in self.__builtins: |
|
549 value = node.value |
|
550 if isinstance(value, ast.Name) and \ |
|
551 element.id in ignoreBuiltinAssignments and \ |
|
552 value.id in ignoreBuiltinAssignments[element.id]: |
|
553 # ignore compatibility assignments |
|
554 continue |
|
555 self.__error(element.lineno - 1, element.col_offset, |
|
556 "M131", element.id) |
|
557 elif isinstance(element, (ast.Tuple, ast.List)): |
|
558 for tupleElement in element.elts: |
|
559 if isinstance(tupleElement, ast.Name) and \ |
|
560 tupleElement.id in self.__builtins: |
|
561 self.__error(tupleElement.lineno - 1, |
|
562 tupleElement.col_offset, |
|
563 "M131", tupleElement.id) |
|
564 elif isinstance(node, ast.For): |
|
565 # for loop |
|
566 target = node.target |
|
567 if isinstance(target, ast.Name) and \ |
|
568 target.id in self.__builtins: |
|
569 self.__error(target.lineno - 1, target.col_offset, |
|
570 "M131", target.id) |
|
571 elif isinstance(target, (ast.Tuple, ast.List)): |
|
572 for element in target.elts: |
|
573 if isinstance(element, ast.Name) and \ |
|
574 element.id in self.__builtins: |
|
575 self.__error(element.lineno - 1, |
|
576 element.col_offset, |
|
577 "M131", element.id) |
|
578 elif any(isinstance(node, functionDef) |
|
579 for functionDef in functionDefs): |
|
580 # (asynchronous) function definition |
|
581 if sys.version_info >= (3, 0): |
|
582 for arg in node.args.args: |
|
583 if isinstance(arg, ast.arg) and \ |
|
584 arg.arg in self.__builtins: |
|
585 self.__error(arg.lineno - 1, arg.col_offset, |
|
586 "M132", arg.arg) |
|
587 else: |
|
588 for arg in node.args.args: |
|
589 if isinstance(arg, ast.Name) and \ |
|
590 arg.id in self.__builtins: |
|
591 self.__error(arg.lineno - 1, arg.col_offset, |
|
592 "M132", arg.id) |
|
593 |
|
594 def __checkComprehensions(self): |
|
595 """ |
|
596 Private method to check some comprehension related things. |
|
597 """ |
|
598 for node in ast.walk(self.__tree): |
|
599 if (isinstance(node, ast.Call) and |
|
600 len(node.args) == 1 and |
|
601 isinstance(node.func, ast.Name)): |
|
602 if (isinstance(node.args[0], ast.GeneratorExp) and |
|
603 node.func.id in ('list', 'set', 'dict')): |
|
604 errorCode = { |
|
605 "dict": "M193", |
|
606 "list": "M191", |
|
607 "set": "M192", |
|
608 }[node.func.id] |
|
609 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
610 |
|
611 elif (isinstance(node.args[0], ast.ListComp) and |
|
612 node.func.id in ('set', 'dict')): |
|
613 errorCode = { |
|
614 'dict': 'M195', |
|
615 'set': 'M194', |
|
616 }[node.func.id] |
|
617 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
618 |
|
619 elif (isinstance(node.args[0], ast.List) and |
|
620 node.func.id in ('set', 'dict')): |
|
621 errorCode = { |
|
622 'dict': 'M197', |
|
623 'set': 'M196', |
|
624 }[node.func.id] |
|
625 self.__error(node.lineno - 1, node.col_offset, errorCode) |
|
626 |
|
627 elif (isinstance(node.args[0], ast.ListComp) and |
|
628 node.func.id in ('all', 'any', 'frozenset', 'max', 'min', |
|
629 'sorted', 'sum', 'tuple',)): |
|
630 self.__error(node.lineno - 1, node.col_offset, "M198", |
|
631 node.func.id) |
|
632 |
|
633 def __checkMutableDefault(self): |
|
634 """ |
|
635 Private method to check for use of mutable types as default arguments. |
|
636 """ |
|
637 mutableTypes = ( |
|
638 ast.Call, |
|
639 ast.Dict, |
|
640 ast.List, |
|
641 ast.Set, |
|
642 ) |
|
643 mutableCalls = ( |
|
644 "Counter", |
|
645 "OrderedDict", |
|
646 "collections.Counter", |
|
647 "collections.OrderedDict", |
|
648 "collections.defaultdict", |
|
649 "collections.deque", |
|
650 "defaultdict", |
|
651 "deque", |
|
652 "dict", |
|
653 "list", |
|
654 "set", |
|
655 ) |
|
656 functionDefs = [ast.FunctionDef] |
|
657 try: |
|
658 functionDefs.append(ast.AsyncFunctionDef) |
|
659 except AttributeError: |
|
660 pass |
|
661 |
|
662 for node in ast.walk(self.__tree): |
|
663 if any(isinstance(node, functionDef) |
|
664 for functionDef in functionDefs): |
|
665 for default in node.args.defaults: |
|
666 if any(isinstance(default, mutableType) |
|
667 for mutableType in mutableTypes): |
|
668 typeName = type(default).__name__ |
|
669 if isinstance(default, ast.Call): |
|
670 callPath = '.'.join(composeCallPath(default.func)) |
|
671 if callPath in mutableCalls: |
|
672 self.__error(default.lineno - 1, |
|
673 default.col_offset, |
|
674 "M823", callPath + "()") |
|
675 else: |
|
676 self.__error(default.lineno - 1, |
|
677 default.col_offset, |
|
678 "M822", typeName) |
|
679 else: |
|
680 self.__error(default.lineno - 1, |
|
681 default.col_offset, |
|
682 "M821", typeName) |
|
683 |
|
684 def __dictShouldBeChecked(self, node): |
|
685 """ |
|
686 Private function to test, if the node should be checked. |
|
687 |
|
688 @param node reference to the AST node |
|
689 @return flag indicating to check the node |
|
690 @rtype bool |
|
691 """ |
|
692 if not all(isinstance(key, ast.Str) for key in node.keys): |
|
693 return False |
|
694 |
|
695 if "__IGNORE_WARNING__" in self.__source[node.lineno - 1] or \ |
|
696 "__IGNORE_WARNING_M201__" in self.__source[node.lineno - 1]: |
|
697 return False |
|
698 |
|
699 lineNumbers = [key.lineno for key in node.keys] |
|
700 return len(lineNumbers) == len(set(lineNumbers)) |
|
701 |
|
702 def __checkDictWithSortedKeys(self): |
|
703 """ |
|
704 Private method to check, if dictionary keys appear in sorted order. |
|
705 """ |
|
706 for node in ast.walk(self.__tree): |
|
707 if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node): |
|
708 for key1, key2 in zip(node.keys, node.keys[1:]): |
|
709 if key2.s < key1.s: |
|
710 self.__error(key2.lineno - 1, key2.col_offset, |
|
711 "M201", key2.s, key1.s) |
|
712 |
|
713 def __checkLogging(self): |
|
714 """ |
|
715 Private method to check logging statements. |
|
716 """ |
|
717 visitor = LoggingVisitor() |
|
718 visitor.visit(self.__tree) |
|
719 for node, reason in visitor.violations: |
|
720 self.__error(node.lineno - 1, node.col_offset, reason) |
|
721 |
|
722 def __checkGettext(self): |
|
723 """ |
|
724 Private method to check the 'gettext' import statement. |
|
725 """ |
|
726 for node in ast.walk(self.__tree): |
|
727 if isinstance(node, ast.ImportFrom) and \ |
|
728 any(name.asname == '_' for name in node.names): |
|
729 self.__error(node.lineno - 1, node.col_offset, "M711", |
|
730 node.names[0].name) |
|
731 |
|
732 def __checkBugBear(self): |
|
733 """ |
|
734 Private method to bugbear checks. |
|
735 """ |
|
736 visitor = BugBearVisitor() |
|
737 visitor.visit(self.__tree) |
|
738 for violation in visitor.violations: |
|
739 node = violation[0] |
|
740 reason = violation[1] |
|
741 params = violation[2:] |
|
742 self.__error(node.lineno - 1, node.col_offset, reason, *params) |
|
743 |
|
744 def __checkReturn(self): |
|
745 """ |
|
746 Private method to check return statements. |
|
747 """ |
|
748 visitor = ReturnVisitor() |
|
749 visitor.visit(self.__tree) |
|
750 for violation in visitor.violations: |
|
751 node = violation[0] |
|
752 reason = violation[1] |
|
753 self.__error(node.lineno - 1, node.col_offset, reason) |
|
754 |
|
755 |
|
756 class TextVisitor(ast.NodeVisitor): |
|
757 """ |
|
758 Class implementing a node visitor for bytes and str instances. |
|
759 |
|
760 It tries to detect docstrings as string of the first expression of each |
|
761 module, class or function. |
|
762 """ |
|
763 # modelled after the string format flake8 extension |
|
764 |
|
765 def __init__(self): |
|
766 """ |
|
767 Constructor |
|
768 """ |
|
769 super(TextVisitor, self).__init__() |
|
770 self.nodes = [] |
|
771 self.calls = {} |
|
772 |
|
773 def __addNode(self, node): |
|
774 """ |
|
775 Private method to add a node to our list of nodes. |
|
776 |
|
777 @param node reference to the node to add |
|
778 @type ast.AST |
|
779 """ |
|
780 if not hasattr(node, 'is_docstring'): |
|
781 node.is_docstring = False |
|
782 self.nodes.append(node) |
|
783 |
|
784 def __isBaseString(self, node): |
|
785 """ |
|
786 Private method to determine, if a node is a base string node. |
|
787 |
|
788 @param node reference to the node to check |
|
789 @type ast.AST |
|
790 @return flag indicating a base string |
|
791 @rtype bool |
|
792 """ |
|
793 typ = (ast.Str,) |
|
794 if sys.version_info[0] > 2: |
|
795 typ += (ast.Bytes,) |
|
796 return isinstance(node, typ) |
|
797 |
|
798 def visit_Str(self, node): |
|
799 """ |
|
800 Public method to record a string node. |
|
801 |
|
802 @param node reference to the string node |
|
803 @type ast.Str |
|
804 """ |
|
805 self.__addNode(node) |
|
806 |
|
807 def visit_Bytes(self, node): |
|
808 """ |
|
809 Public method to record a bytes node. |
|
810 |
|
811 @param node reference to the bytes node |
|
812 @type ast.Bytes |
|
813 """ |
|
814 self.__addNode(node) |
|
815 |
|
816 def __visitDefinition(self, node): |
|
817 """ |
|
818 Private method handling class and function definitions. |
|
819 |
|
820 @param node reference to the node to handle |
|
821 @type ast.FunctionDef, ast.AsyncFunctionDef or ast.ClassDef |
|
822 """ |
|
823 # Manually traverse class or function definition |
|
824 # * Handle decorators normally |
|
825 # * Use special check for body content |
|
826 # * Don't handle the rest (e.g. bases) |
|
827 for decorator in node.decorator_list: |
|
828 self.visit(decorator) |
|
829 self.__visitBody(node) |
|
830 |
|
831 def __visitBody(self, node): |
|
832 """ |
|
833 Private method to traverse the body of the node manually. |
|
834 |
|
835 If the first node is an expression which contains a string or bytes it |
|
836 marks that as a docstring. |
|
837 |
|
838 @param node reference to the node to traverse |
|
839 @type ast.AST |
|
840 """ |
|
841 if (node.body and isinstance(node.body[0], ast.Expr) and |
|
842 self.__isBaseString(node.body[0].value)): |
|
843 node.body[0].value.is_docstring = True |
|
844 |
|
845 for subnode in node.body: |
|
846 self.visit(subnode) |
|
847 |
|
848 def visit_Module(self, node): |
|
849 """ |
|
850 Public method to handle a module. |
|
851 |
|
852 @param node reference to the node to handle |
|
853 @type ast.Module |
|
854 """ |
|
855 self.__visitBody(node) |
|
856 |
|
857 def visit_ClassDef(self, node): |
|
858 """ |
|
859 Public method to handle a class definition. |
|
860 |
|
861 @param node reference to the node to handle |
|
862 @type ast.ClassDef |
|
863 """ |
|
864 # Skipped nodes: ('name', 'bases', 'keywords', 'starargs', 'kwargs') |
|
865 self.__visitDefinition(node) |
|
866 |
|
867 def visit_FunctionDef(self, node): |
|
868 """ |
|
869 Public method to handle a function definition. |
|
870 |
|
871 @param node reference to the node to handle |
|
872 @type ast.FunctionDef |
|
873 """ |
|
874 # Skipped nodes: ('name', 'args', 'returns') |
|
875 self.__visitDefinition(node) |
|
876 |
|
877 def visit_AsyncFunctionDef(self, node): |
|
878 """ |
|
879 Public method to handle an asynchronous function definition. |
|
880 |
|
881 @param node reference to the node to handle |
|
882 @type ast.AsyncFunctionDef |
|
883 """ |
|
884 # Skipped nodes: ('name', 'args', 'returns') |
|
885 self.__visitDefinition(node) |
|
886 |
|
887 def visit_Call(self, node): |
|
888 """ |
|
889 Public method to handle a function call. |
|
890 |
|
891 @param node reference to the node to handle |
|
892 @type ast.Call |
|
893 """ |
|
894 if (isinstance(node.func, ast.Attribute) and |
|
895 node.func.attr == 'format'): |
|
896 if self.__isBaseString(node.func.value): |
|
897 self.calls[node.func.value] = (node, False) |
|
898 elif (isinstance(node.func.value, ast.Name) and |
|
899 node.func.value.id == 'str' and node.args and |
|
900 self.__isBaseString(node.args[0])): |
|
901 self.calls[node.args[0]] = (node, True) |
|
902 super(TextVisitor, self).generic_visit(node) |
|
903 |
|
904 |
|
905 class LoggingVisitor(ast.NodeVisitor): |
|
906 """ |
|
907 Class implementing a node visitor to check logging statements. |
|
908 """ |
|
909 LoggingLevels = { |
|
910 "debug", |
|
911 "critical", |
|
912 "error", |
|
913 "info", |
|
914 "warn", |
|
915 "warning", |
|
916 } |
|
917 |
|
918 def __init__(self): |
|
919 """ |
|
920 Constructor |
|
921 """ |
|
922 super(LoggingVisitor, self).__init__() |
|
923 |
|
924 self.__currentLoggingCall = None |
|
925 self.__currentLoggingArgument = None |
|
926 self.__currentLoggingLevel = None |
|
927 self.__currentExtraKeyword = None |
|
928 self.violations = [] |
|
929 |
|
930 def __withinLoggingStatement(self): |
|
931 """ |
|
932 Private method to check, if we are inside a logging statement. |
|
933 |
|
934 @return flag indicating we are inside a logging statement |
|
935 @rtype bool |
|
936 """ |
|
937 return self.__currentLoggingCall is not None |
|
938 |
|
939 def __withinLoggingArgument(self): |
|
940 """ |
|
941 Private method to check, if we are inside a logging argument. |
|
942 |
|
943 @return flag indicating we are inside a logging argument |
|
944 @rtype bool |
|
945 """ |
|
946 return self.__currentLoggingArgument is not None |
|
947 |
|
948 def __withinExtraKeyword(self, node): |
|
949 """ |
|
950 Private method to check, if we are inside the extra keyword. |
|
951 |
|
952 @param node reference to the node to be checked |
|
953 @type ast.keyword |
|
954 @return flag indicating we are inside the extra keyword |
|
955 @rtype bool |
|
956 """ |
|
957 return self.__currentExtraKeyword is not None and \ |
|
958 self.__currentExtraKeyword != node |
|
959 |
|
960 def __detectLoggingLevel(self, node): |
|
961 """ |
|
962 Private method to decide whether an AST Call is a logging call. |
|
963 |
|
964 @param node reference to the node to be processed |
|
965 @type ast.Call |
|
966 @return logging level |
|
967 @rtype str or None |
|
968 """ |
|
969 try: |
|
970 if node.func.value.id == "warnings": |
|
971 return None |
|
972 |
|
973 if node.func.attr in LoggingVisitor.LoggingLevels: |
|
974 return node.func.attr |
|
975 except AttributeError: |
|
976 pass |
|
977 |
|
978 return None |
|
979 |
|
980 def __isFormatCall(self, node): |
|
981 """ |
|
982 Private method to check if a function call uses format. |
|
983 |
|
984 @param node reference to the node to be processed |
|
985 @type ast.Call |
|
986 @return flag indicating the function call uses format |
|
987 @rtype bool |
|
988 """ |
|
989 try: |
|
990 return node.func.attr == "format" |
|
991 except AttributeError: |
|
992 return False |
|
993 |
|
994 def visit_Call(self, node): |
|
995 """ |
|
996 Public method to handle a function call. |
|
997 |
|
998 Every logging statement and string format is expected to be a function |
|
999 call. |
|
1000 |
|
1001 @param node reference to the node to be processed |
|
1002 @type ast.Call |
|
1003 """ |
|
1004 # we are in a logging statement |
|
1005 if self.__withinLoggingStatement(): |
|
1006 if self.__withinLoggingArgument() and self.__isFormatCall(node): |
|
1007 self.violations.append((node, "M651")) |
|
1008 super(LoggingVisitor, self).generic_visit(node) |
|
1009 return |
|
1010 |
|
1011 loggingLevel = self.__detectLoggingLevel(node) |
|
1012 |
|
1013 if loggingLevel and self.__currentLoggingLevel is None: |
|
1014 self.__currentLoggingLevel = loggingLevel |
|
1015 |
|
1016 # we are in some other statement |
|
1017 if loggingLevel is None: |
|
1018 super(LoggingVisitor, self).generic_visit(node) |
|
1019 return |
|
1020 |
|
1021 # we are entering a new logging statement |
|
1022 self.__currentLoggingCall = node |
|
1023 |
|
1024 if loggingLevel == "warn": |
|
1025 self.violations.append((node, "M655")) |
|
1026 |
|
1027 for index, child in enumerate(ast.iter_child_nodes(node)): |
|
1028 if index == 1: |
|
1029 self.__currentLoggingArgument = child |
|
1030 if index > 1 and isinstance(child, ast.keyword) and \ |
|
1031 child.arg == "extra": |
|
1032 self.__currentExtraKeyword = child |
|
1033 |
|
1034 super(LoggingVisitor, self).visit(child) |
|
1035 |
|
1036 self.__currentLoggingArgument = None |
|
1037 self.__currentExtraKeyword = None |
|
1038 |
|
1039 self.__currentLoggingCall = None |
|
1040 self.__currentLoggingLevel = None |
|
1041 |
|
1042 def visit_BinOp(self, node): |
|
1043 """ |
|
1044 Public method to handle binary operations while processing the first |
|
1045 logging argument. |
|
1046 |
|
1047 @param node reference to the node to be processed |
|
1048 @type ast.BinOp |
|
1049 """ |
|
1050 if self.__withinLoggingStatement() and self.__withinLoggingArgument(): |
|
1051 # handle percent format |
|
1052 if isinstance(node.op, ast.Mod): |
|
1053 self.violations.append((node, "M652")) |
|
1054 |
|
1055 # handle string concat |
|
1056 if isinstance(node.op, ast.Add): |
|
1057 self.violations.append((node, "M653")) |
|
1058 |
|
1059 super(LoggingVisitor, self).generic_visit(node) |
|
1060 |
|
1061 def visit_JoinedStr(self, node): |
|
1062 """ |
|
1063 Public method to handle f-string arguments. |
|
1064 |
|
1065 @param node reference to the node to be processed |
|
1066 @type ast.JoinedStr |
|
1067 """ |
|
1068 if sys.version_info >= (3, 6): |
|
1069 if self.__withinLoggingStatement(): |
|
1070 if any(isinstance(i, ast.FormattedValue) for i in node.values): |
|
1071 if self.__withinLoggingArgument(): |
|
1072 self.violations.append((node, "M654")) |
|
1073 |
|
1074 super(LoggingVisitor, self).generic_visit(node) |
|
1075 |
|
1076 |
|
1077 class BugBearVisitor(ast.NodeVisitor): |
|
1078 """ |
|
1079 Class implementing a node visitor to check for various topics. |
|
1080 """ |
|
1081 # |
|
1082 # This class was implemented along the BugBear flake8 extension (v 18.2.0). |
|
1083 # Original: Copyright (c) 2016 Łukasz Langa |
|
1084 # |
|
1085 |
|
1086 NodeWindowSize = 4 |
|
1087 |
|
1088 def __init__(self): |
|
1089 """ |
|
1090 Constructor |
|
1091 """ |
|
1092 super(BugBearVisitor, self).__init__() |
|
1093 |
|
1094 self.__nodeStack = [] |
|
1095 self.__nodeWindow = [] |
|
1096 self.violations = [] |
|
1097 |
|
1098 def visit(self, node): |
|
1099 """ |
|
1100 Public method to traverse a given AST node. |
|
1101 |
|
1102 @param node AST node to be traversed |
|
1103 @type ast.Node |
|
1104 """ |
|
1105 self.__nodeStack.append(node) |
|
1106 self.__nodeWindow.append(node) |
|
1107 self.__nodeWindow = \ |
|
1108 self.__nodeWindow[-BugBearVisitor.NodeWindowSize:] |
|
1109 |
|
1110 super(BugBearVisitor, self).visit(node) |
|
1111 |
|
1112 self.__nodeStack.pop() |
|
1113 |
|
1114 def visit_UAdd(self, node): |
|
1115 """ |
|
1116 Public method to handle unary additions. |
|
1117 |
|
1118 @param node reference to the node to be processed |
|
1119 @type ast.UAdd |
|
1120 """ |
|
1121 trailingNodes = list(map(type, self.__nodeWindow[-4:])) |
|
1122 if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]: |
|
1123 originator = self.__nodeWindow[-4] |
|
1124 self.violations.append((originator, "M501")) |
|
1125 |
|
1126 self.generic_visit(node) |
|
1127 |
|
1128 def visit_Call(self, node): |
|
1129 """ |
|
1130 Public method to handle a function call. |
|
1131 |
|
1132 @param node reference to the node to be processed |
|
1133 @type ast.Call |
|
1134 """ |
|
1135 if sys.version_info >= (3, 0): |
|
1136 validPaths = ("six", "future.utils", "builtins") |
|
1137 methodsDict = { |
|
1138 "M511": ("iterkeys", "itervalues", "iteritems", "iterlists"), |
|
1139 "M512": ("viewkeys", "viewvalues", "viewitems", "viewlists"), |
|
1140 "M513": ("next",), |
|
1141 } |
|
1142 else: |
|
1143 validPaths = () |
|
1144 methodsDict = {} |
|
1145 |
|
1146 if isinstance(node.func, ast.Attribute): |
|
1147 for code, methods in methodsDict.items(): |
|
1148 if node.func.attr in methods: |
|
1149 callPath = ".".join(composeCallPath(node.func.value)) |
|
1150 if callPath not in validPaths: |
|
1151 self.violations.append((node, code)) |
|
1152 break |
|
1153 else: |
|
1154 self.__checkForM502(node) |
|
1155 else: |
|
1156 try: |
|
1157 if ( |
|
1158 node.func.id in ("getattr", "hasattr") and |
|
1159 node.args[1].s == "__call__" |
|
1160 ): |
|
1161 self.violations.append((node, "M503")) |
|
1162 except (AttributeError, IndexError): |
|
1163 pass |
|
1164 |
|
1165 self.generic_visit(node) |
|
1166 |
|
1167 def visit_Attribute(self, node): |
|
1168 """ |
|
1169 Public method to handle attributes. |
|
1170 |
|
1171 @param node reference to the node to be processed |
|
1172 @type ast.Attribute |
|
1173 """ |
|
1174 callPath = list(composeCallPath(node)) |
|
1175 |
|
1176 if '.'.join(callPath) == 'sys.maxint' and sys.version_info >= (3, 0): |
|
1177 self.violations.append((node, "M504")) |
|
1178 |
|
1179 elif len(callPath) == 2 and callPath[1] == 'message' and \ |
|
1180 sys.version_info >= (2, 6): |
|
1181 name = callPath[0] |
|
1182 for elem in reversed(self.__nodeStack[:-1]): |
|
1183 if isinstance(elem, ast.ExceptHandler) and elem.name == name: |
|
1184 self.violations.append((node, "M505")) |
|
1185 break |
|
1186 |
|
1187 def visit_Assign(self, node): |
|
1188 """ |
|
1189 Public method to handle assignments. |
|
1190 |
|
1191 @param node reference to the node to be processed |
|
1192 @type ast.Assign |
|
1193 """ |
|
1194 if isinstance(self.__nodeStack[-2], ast.ClassDef): |
|
1195 # By using 'hasattr' below we're ignoring starred arguments, slices |
|
1196 # and tuples for simplicity. |
|
1197 assignTargets = {t.id for t in node.targets if hasattr(t, 'id')} |
|
1198 if '__metaclass__' in assignTargets and sys.version_info >= (3, 0): |
|
1199 self.violations.append((node, "M514")) |
|
1200 |
|
1201 elif len(node.targets) == 1: |
|
1202 target = node.targets[0] |
|
1203 if isinstance(target, ast.Attribute) and \ |
|
1204 isinstance(target.value, ast.Name): |
|
1205 if (target.value.id, target.attr) == ('os', 'environ'): |
|
1206 self.violations.append((node, "M506")) |
|
1207 |
|
1208 self.generic_visit(node) |
|
1209 |
|
1210 def visit_For(self, node): |
|
1211 """ |
|
1212 Public method to handle 'for' statements. |
|
1213 |
|
1214 @param node reference to the node to be processed |
|
1215 @type ast.For |
|
1216 """ |
|
1217 self.__checkForM507(node) |
|
1218 |
|
1219 self.generic_visit(node) |
|
1220 |
|
1221 def __checkForM502(self, node): |
|
1222 """ |
|
1223 Private method to check the use of *strip(). |
|
1224 |
|
1225 @param node reference to the node to be processed |
|
1226 @type ast.Call |
|
1227 """ |
|
1228 if node.func.attr not in ("lstrip", "rstrip", "strip"): |
|
1229 return # method name doesn't match |
|
1230 |
|
1231 if len(node.args) != 1 or not isinstance(node.args[0], ast.Str): |
|
1232 return # used arguments don't match the builtin strip |
|
1233 |
|
1234 s = node.args[0].s |
|
1235 if len(s) == 1: |
|
1236 return # stripping just one character |
|
1237 |
|
1238 if len(s) == len(set(s)): |
|
1239 return # no characters appear more than once |
|
1240 |
|
1241 self.violations.append((node, "M502")) |
|
1242 |
|
1243 def __checkForM507(self, node): |
|
1244 """ |
|
1245 Private method to check for unused loop variables. |
|
1246 |
|
1247 @param node reference to the node to be processed |
|
1248 @type ast.For |
|
1249 """ |
|
1250 targets = NameFinder() |
|
1251 targets.visit(node.target) |
|
1252 ctrlNames = set(filter(lambda s: not s.startswith('_'), |
|
1253 targets.getNames())) |
|
1254 body = NameFinder() |
|
1255 for expr in node.body: |
|
1256 body.visit(expr) |
|
1257 usedNames = set(body.getNames()) |
|
1258 for name in sorted(ctrlNames - usedNames): |
|
1259 n = targets.getNames()[name][0] |
|
1260 self.violations.append((n, "M507", name)) |
|
1261 |
|
1262 |
|
1263 class NameFinder(ast.NodeVisitor): |
|
1264 """ |
|
1265 Class to extract a name out of a tree of nodes. |
|
1266 """ |
|
1267 def __init__(self): |
|
1268 """ |
|
1269 Constructor |
|
1270 """ |
|
1271 super(NameFinder, self).__init__() |
|
1272 |
|
1273 self.__names = {} |
|
1274 |
|
1275 def visit_Name(self, node): |
|
1276 """ |
|
1277 Public method to handle 'Name' nodes. |
|
1278 |
|
1279 @param node reference to the node to be processed |
|
1280 @type ast.Name |
|
1281 """ |
|
1282 self.__names.setdefault(node.id, []).append(node) |
|
1283 |
|
1284 def visit(self, node): |
|
1285 """ |
|
1286 Public method to traverse a given AST node. |
|
1287 |
|
1288 @param node AST node to be traversed |
|
1289 @type ast.Node |
|
1290 """ |
|
1291 if isinstance(node, list): |
|
1292 for elem in node: |
|
1293 super(NameFinder, self).visit(elem) |
|
1294 else: |
|
1295 super(NameFinder, self).visit(node) |
|
1296 |
|
1297 def getNames(self): |
|
1298 """ |
|
1299 Public method to return the extracted names and Name nodes. |
|
1300 |
|
1301 @return dictionary containing the names as keys and the list of nodes |
|
1302 @rtype dict |
|
1303 """ |
|
1304 return self.__names |
|
1305 |
|
1306 |
|
1307 class ReturnVisitor(ast.NodeVisitor): |
|
1308 """ |
|
1309 Class implementing a node visitor to check return statements. |
|
1310 """ |
|
1311 Assigns = 'assigns' |
|
1312 Refs = 'refs' |
|
1313 Returns = 'returns' |
|
1314 |
|
1315 def __init__(self): |
|
1316 """ |
|
1317 Constructor |
|
1318 """ |
|
1319 super(ReturnVisitor, self).__init__() |
|
1320 |
|
1321 self.__stack = [] |
|
1322 self.violations = [] |
|
1323 |
|
1324 @property |
|
1325 def assigns(self): |
|
1326 """ |
|
1327 Public method to get the Assign nodes. |
|
1328 |
|
1329 @return dictionary containing the node name as key and line number |
|
1330 as value |
|
1331 @rtype dict |
|
1332 """ |
|
1333 return self.__stack[-1][ReturnVisitor.Assigns] |
|
1334 |
|
1335 @property |
|
1336 def refs(self): |
|
1337 """ |
|
1338 Public method to get the References nodes. |
|
1339 |
|
1340 @return dictionary containing the node name as key and line number |
|
1341 as value |
|
1342 @rtype dict |
|
1343 """ |
|
1344 return self.__stack[-1][ReturnVisitor.Refs] |
|
1345 |
|
1346 @property |
|
1347 def returns(self): |
|
1348 """ |
|
1349 Public method to get the Return nodes. |
|
1350 |
|
1351 @return dictionary containing the node name as key and line number |
|
1352 as value |
|
1353 @rtype dict |
|
1354 """ |
|
1355 return self.__stack[-1][ReturnVisitor.Returns] |
|
1356 |
|
1357 def __visitWithStack(self, node): |
|
1358 """ |
|
1359 Private method to traverse a given function node using a stack. |
|
1360 |
|
1361 @param node AST node to be traversed |
|
1362 @type ast.FunctionDef or ast.AsyncFunctionDef |
|
1363 """ |
|
1364 self.__stack.append({ |
|
1365 ReturnVisitor.Assigns: defaultdict(list), |
|
1366 ReturnVisitor.Refs: defaultdict(list), |
|
1367 ReturnVisitor.Returns: [] |
|
1368 }) |
|
1369 |
|
1370 self.generic_visit(node) |
|
1371 self.__checkFunction(node) |
|
1372 self.__stack.pop() |
|
1373 |
|
1374 def visit_FunctionDef(self, node): |
|
1375 """ |
|
1376 Public method to handle a function definition. |
|
1377 |
|
1378 @param node reference to the node to handle |
|
1379 @type ast.FunctionDef |
|
1380 """ |
|
1381 self.__visitWithStack(node) |
|
1382 |
|
1383 def visit_AsyncFunctionDef(self, node): |
|
1384 """ |
|
1385 Public method to handle a function definition. |
|
1386 |
|
1387 @param node reference to the node to handle |
|
1388 @type ast.AsyncFunctionDef |
|
1389 """ |
|
1390 self.__visitWithStack(node) |
|
1391 |
|
1392 def visit_Return(self, node): |
|
1393 """ |
|
1394 Public method to handle a return node. |
|
1395 |
|
1396 @param node reference to the node to handle |
|
1397 @type ast.Return |
|
1398 """ |
|
1399 self.returns.append(node) |
|
1400 self.generic_visit(node) |
|
1401 |
|
1402 def visit_Assign(self, node): |
|
1403 """ |
|
1404 Public method to handle an assign node. |
|
1405 |
|
1406 @param node reference to the node to handle |
|
1407 @type ast.Assign |
|
1408 """ |
|
1409 if not self.__stack: |
|
1410 return |
|
1411 |
|
1412 for target in node.targets: |
|
1413 self.__visitAssignTarget(target) |
|
1414 self.generic_visit(node.value) |
|
1415 |
|
1416 def visit_Name(self, node): |
|
1417 """ |
|
1418 Public method to handle a name node. |
|
1419 |
|
1420 @param node reference to the node to handle |
|
1421 @type ast.Name |
|
1422 """ |
|
1423 if self.__stack: |
|
1424 self.refs[node.id].append(node.lineno) |
|
1425 |
|
1426 def __visitAssignTarget(self, node): |
|
1427 """ |
|
1428 Private method to handle an assign target node. |
|
1429 |
|
1430 @param node reference to the node to handle |
|
1431 @type ast.AST |
|
1432 """ |
|
1433 if isinstance(node, ast.Tuple): |
|
1434 for elt in node.elts: |
|
1435 self.__visitAssignTarget(elt) |
|
1436 return |
|
1437 |
|
1438 if isinstance(node, ast.Name): |
|
1439 self.assigns[node.id].append(node.lineno) |
|
1440 return |
|
1441 |
|
1442 self.generic_visit(node) |
|
1443 |
|
1444 def __checkFunction(self, node): |
|
1445 """ |
|
1446 Private method to check a function definition node. |
|
1447 |
|
1448 @param node reference to the node to check |
|
1449 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
1450 """ |
|
1451 if not self.returns or not node.body: |
|
1452 return |
|
1453 |
|
1454 if len(node.body) == 1 and isinstance(node.body[-1], ast.Return): |
|
1455 # skip functions that consist of `return None` only |
|
1456 return |
|
1457 |
|
1458 if not self.__resultExists(): |
|
1459 self.__checkUnnecessaryReturnNone() |
|
1460 return |
|
1461 |
|
1462 self.__checkImplicitReturnValue() |
|
1463 self.__checkImplicitReturn(node.body[-1]) |
|
1464 |
|
1465 for n in self.returns: |
|
1466 if n.value: |
|
1467 self.__checkUnnecessaryAssign(n.value) |
|
1468 |
|
1469 def __isNone(self, node): |
|
1470 """ |
|
1471 Private method to check, if a node value is None. |
|
1472 |
|
1473 @param node reference to the node to check |
|
1474 @type ast.AST |
|
1475 @return flag indicating the node contains a None value |
|
1476 """ |
|
1477 try: |
|
1478 return isinstance(node, ast.NameConstant) and node.value is None |
|
1479 except AttributeError: |
|
1480 # try Py2 |
|
1481 return isinstance(node, ast.Name) and node.id == "None" |
|
1482 |
|
1483 def __resultExists(self): |
|
1484 """ |
|
1485 Private method to check the existance of a return result. |
|
1486 |
|
1487 @return flag indicating the existence of a return result |
|
1488 @rtype bool |
|
1489 """ |
|
1490 for node in self.returns: |
|
1491 value = node.value |
|
1492 if value and not self.__isNone(value): |
|
1493 return True |
|
1494 |
|
1495 return False |
|
1496 |
|
1497 def __checkImplicitReturnValue(self): |
|
1498 """ |
|
1499 Private method to check for implicit return values. |
|
1500 """ |
|
1501 for node in self.returns: |
|
1502 if not node.value: |
|
1503 self.violations.append((node, "M832")) |
|
1504 |
|
1505 def __checkUnnecessaryReturnNone(self): |
|
1506 """ |
|
1507 Private method to check for an unnecessary 'return None' statement. |
|
1508 """ |
|
1509 for node in self.returns: |
|
1510 if self.__isNone(node.value): |
|
1511 self.violations.append((node, "M831")) |
|
1512 |
|
1513 def __checkImplicitReturn(self, node): |
|
1514 """ |
|
1515 Private method to check for an implicit return statement. |
|
1516 |
|
1517 @param node reference to the node to check |
|
1518 @type ast.AST |
|
1519 """ |
|
1520 if isinstance(node, ast.If): |
|
1521 if not node.body or not node.orelse: |
|
1522 self.violations.append((node, "M833")) |
|
1523 return |
|
1524 |
|
1525 self.__checkImplicitReturn(node.body[-1]) |
|
1526 self.__checkImplicitReturn(node.orelse[-1]) |
|
1527 return |
|
1528 |
|
1529 if isinstance(node, ast.For) and node.orelse: |
|
1530 self.__checkImplicitReturn(node.orelse[-1]) |
|
1531 return |
|
1532 |
|
1533 if isinstance(node, ast.With): |
|
1534 self.__checkImplicitReturn(node.body[-1]) |
|
1535 return |
|
1536 |
|
1537 try: |
|
1538 okNodes = (ast.Return, ast.Raise, ast.While, ast.Try) |
|
1539 except AttributeError: |
|
1540 # Py2 |
|
1541 okNodes = (ast.Return, ast.Raise, ast.While) |
|
1542 if not isinstance(node, okNodes): |
|
1543 self.violations.append((node, "M833")) |
|
1544 |
|
1545 def __checkUnnecessaryAssign(self, node): |
|
1546 """ |
|
1547 Private method to check for an unnecessary assign statement. |
|
1548 |
|
1549 @param node reference to the node to check |
|
1550 @type ast.AST |
|
1551 """ |
|
1552 if not isinstance(node, ast.Name): |
|
1553 return |
|
1554 |
|
1555 varname = node.id |
|
1556 returnLineno = node.lineno |
|
1557 |
|
1558 if varname not in self.assigns: |
|
1559 return |
|
1560 |
|
1561 if varname not in self.refs: |
|
1562 self.violations.append((node, "M834")) |
|
1563 return |
|
1564 |
|
1565 if self.__hasRefsBeforeNextAssign(varname, returnLineno): |
|
1566 return |
|
1567 |
|
1568 self.violations.append((node, "M834")) |
|
1569 |
|
1570 def __hasRefsBeforeNextAssign(self, varname, returnLineno): |
|
1571 """ |
|
1572 Private method to check for references before a following assign |
|
1573 statement. |
|
1574 |
|
1575 @param varname variable name to check for |
|
1576 @type str |
|
1577 @param returnLineno line number of the return statement |
|
1578 @type int |
|
1579 @return flag indicating the existence of references |
|
1580 @rtype bool |
|
1581 """ |
|
1582 beforeAssign = 0 |
|
1583 afterAssign = None |
|
1584 |
|
1585 for lineno in sorted(self.assigns[varname]): |
|
1586 if lineno > returnLineno: |
|
1587 afterAssign = lineno |
|
1588 break |
|
1589 |
|
1590 if lineno <= returnLineno: |
|
1591 beforeAssign = lineno |
|
1592 |
|
1593 for lineno in self.refs[varname]: |
|
1594 if lineno == returnLineno: |
|
1595 continue |
|
1596 |
|
1597 if afterAssign: |
|
1598 if beforeAssign < lineno <= afterAssign: |
|
1599 return True |
|
1600 |
|
1601 elif beforeAssign < lineno: |
|
1602 return True |
|
1603 |
|
1604 return False |
|
1605 # |
|
1606 # eflag: noqa = M702 |