eric6/Plugins/CheckerPlugins/CodeStyleChecker/MiscellaneousChecker.py

changeset 6942
2602857055c5
parent 6889
334257ef9435
child 7021
2894aa889a4e
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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

eric ide

mercurial