eric6/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py

changeset 7784
3257703e10c5
parent 7651
ca87b7490449
child 7894
4370a8b30648
child 7924
8a96736d465e
equal deleted inserted replaced
7783:36f66ce496bd 7784:3257703e10c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 - 2020 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 import tokenize
17
18 import AstUtilities
19
20
21 def composeCallPath(node):
22 """
23 Generator function to assemble the call path of a given node.
24
25 @param node node to assemble call path for
26 @type ast.Node
27 @return call path components
28 @rtype str
29 """
30 if isinstance(node, ast.Attribute):
31 for v in composeCallPath(node.value):
32 yield v
33 yield node.attr
34 elif isinstance(node, ast.Name):
35 yield node.id
36
37
38 class MiscellaneousChecker(object):
39 """
40 Class implementing a checker for miscellaneous checks.
41 """
42 Codes = [
43 ## Coding line
44 "M101", "M102",
45
46 ## Copyright
47 "M111", "M112",
48
49 ## Shadowed Builtins
50 "M131", "M132",
51
52 ## Comprehensions
53 "M181", "M182", "M183", "M184",
54 "M185", "M186", "M187",
55 "M191", "M192", "M193",
56 "M195", "M196", "M197", "M198",
57
58 ## Dictionaries with sorted keys
59 "M201",
60
61 ## Naive datetime usage
62 "M301", "M302", "M303", "M304", "M305", "M306", "M307", "M308",
63 "M311", "M312", "M313", "M314", "M315",
64 "M321",
65
66 ## sys.version and sys.version_info usage
67 "M401", "M402", "M403",
68 "M411", "M412", "M413", "M414",
69 "M421", "M422", "M423",
70
71 ## Bugbear
72 "M501", "M502", "M503", "M504", "M505", "M506", "M507", "M508",
73 "M509",
74 "M511", "M512", "M513",
75 "M521", "M522", "M523", "M524",
76
77 ## Format Strings
78 "M601",
79 "M611", "M612", "M613",
80 "M621", "M622", "M623", "M624", "M625",
81 "M631", "M632",
82
83 ## Logging
84 "M651", "M652", "M653", "M654", "M655",
85
86 ## Future statements
87 "M701", "M702",
88
89 ## Gettext
90 "M711",
91
92 ## print
93 "M801",
94
95 ## one element tuple
96 "M811",
97
98 ## Mutable Defaults
99 "M821", "M822",
100
101 ## return statements
102 "M831", "M832", "M833", "M834",
103
104 ## line continuation
105 "M841",
106
107 ## commented code
108 "M891",
109
110 ## syntax error
111 "M901",
112 ]
113
114 Formatter = Formatter()
115 FormatFieldRegex = re.compile(r'^((?:\s|.)*?)(\..*|\[.*\])?$')
116
117 BuiltinsWhiteList = [
118 "__name__",
119 "__doc__",
120 "credits",
121 ]
122
123 def __init__(self, source, filename, select, ignore, expected, repeat,
124 args):
125 """
126 Constructor
127
128 @param source source code to be checked
129 @type list of str
130 @param filename name of the source file
131 @type str
132 @param select list of selected codes
133 @type list of str
134 @param ignore list of codes to be ignored
135 @type list of str
136 @param expected list of expected codes
137 @type list of str
138 @param repeat flag indicating to report each occurrence of a code
139 @type bool
140 @param args dictionary of arguments for the miscellaneous checks
141 @type dict
142 """
143 self.__select = tuple(select)
144 self.__ignore = ('',) if select else tuple(ignore)
145 self.__expected = expected[:]
146 self.__repeat = repeat
147 self.__filename = filename
148 self.__source = source[:]
149 self.__args = args
150
151 self.__pep3101FormatRegex = re.compile(
152 r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%')
153
154 import builtins
155 self.__builtins = [b for b in dir(builtins)
156 if b not in self.BuiltinsWhiteList]
157
158 # statistics counters
159 self.counters = {}
160
161 # collection of detected errors
162 self.errors = []
163
164 checkersWithCodes = [
165 (self.__checkCoding, ("M101", "M102")),
166 (self.__checkCopyright, ("M111", "M112")),
167 (self.__checkBuiltins, ("M131", "M132")),
168 (self.__checkComprehensions, ("M181", "M182", "M183", "M184",
169 "M185", "M186", "M187",
170 "M191", "M192", "M193",
171 "M195", "M196", "M197", "M198")),
172 (self.__checkDictWithSortedKeys, ("M201",)),
173 (self.__checkDateTime, ("M301", "M302", "M303", "M304", "M305",
174 "M306", "M307", "M308", "M311", "M312",
175 "M313", "M314", "M315", "M321")),
176 (self.__checkSysVersion, ("M401", "M402", "M403",
177 "M411", "M412", "M413", "M414",
178 "M421", "M422", "M423")),
179 (self.__checkBugBear, ("M501", "M502", "M503", "M504", "M505",
180 "M506", "M507", "M508", "M509",
181 "M511", "M512", "M513",
182 "M521", "M522", "M523", "M524")),
183 (self.__checkPep3101, ("M601",)),
184 (self.__checkFormatString, ("M611", "M612", "M613",
185 "M621", "M622", "M623", "M624", "M625",
186 "M631", "M632")),
187 (self.__checkLogging, ("M651", "M652", "M653", "M654", "M655")),
188 (self.__checkFuture, ("M701", "M702")),
189 (self.__checkGettext, ("M711",)),
190 (self.__checkPrintStatements, ("M801",)),
191 (self.__checkTuple, ("M811",)),
192 (self.__checkMutableDefault, ("M821", "M822")),
193 (self.__checkReturn, ("M831", "M832", "M833", "M834")),
194 (self.__checkLineContinuation, ("M841",)),
195 (self.__checkCommentedCode, ("M891",)),
196 ]
197
198 self.__defaultArgs = {
199 "BuiltinsChecker": {
200 "chr": ["unichr", ],
201 "str": ["unicode", ],
202 },
203 "CodingChecker": 'latin-1, utf-8',
204 "CopyrightChecker": {
205 "Author": "",
206 "MinFilesize": 0,
207 },
208 "CommentedCodeChecker": {
209 "Aggressive": False,
210 }
211 }
212
213 self.__checkers = []
214 for checker, codes in checkersWithCodes:
215 if any(not (code and self.__ignoreCode(code))
216 for code in codes):
217 self.__checkers.append(checker)
218
219 def __ignoreCode(self, code):
220 """
221 Private method to check if the message code should be ignored.
222
223 @param code message code to check for
224 @type str
225 @return flag indicating to ignore the given code
226 @rtype bool
227 """
228 return (code.startswith(self.__ignore) and
229 not code.startswith(self.__select))
230
231 def __error(self, lineNumber, offset, code, *args):
232 """
233 Private method to record an issue.
234
235 @param lineNumber line number of the issue
236 @type int
237 @param offset position within line of the issue
238 @type int
239 @param code message code
240 @type str
241 @param args arguments for the message
242 @type list
243 """
244 if self.__ignoreCode(code):
245 return
246
247 if code in self.counters:
248 self.counters[code] += 1
249 else:
250 self.counters[code] = 1
251
252 # Don't care about expected codes
253 if code in self.__expected:
254 return
255
256 if code and (self.counters[code] == 1 or self.__repeat):
257 # record the issue with one based line number
258 self.errors.append(
259 {
260 "file": self.__filename,
261 "line": lineNumber + 1,
262 "offset": offset,
263 "code": code,
264 "args": args,
265 }
266 )
267
268 def __reportInvalidSyntax(self):
269 """
270 Private method to report a syntax error.
271 """
272 exc_type, exc = sys.exc_info()[:2]
273 if len(exc.args) > 1:
274 offset = exc.args[1]
275 if len(offset) > 2:
276 offset = offset[1:3]
277 else:
278 offset = (1, 0)
279 self.__error(offset[0] - 1, offset[1] or 0,
280 'M901', exc_type.__name__, exc.args[0])
281
282 def __generateTree(self):
283 """
284 Private method to generate an AST for our source.
285
286 @return generated AST
287 @rtype ast.AST
288 """
289 source = "".join(self.__source)
290 return compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST)
291
292 def run(self):
293 """
294 Public method to check the given source against miscellaneous
295 conditions.
296 """
297 if not self.__filename:
298 # don't do anything, if essential data is missing
299 return
300
301 if not self.__checkers:
302 # don't do anything, if no codes were selected
303 return
304
305 try:
306 self.__tree = self.__generateTree()
307 except (SyntaxError, TypeError):
308 self.__reportInvalidSyntax()
309 return
310
311 for check in self.__checkers:
312 check()
313
314 def __getCoding(self):
315 """
316 Private method to get the defined coding of the source.
317
318 @return tuple containing the line number and the coding
319 @rtype tuple of int and str
320 """
321 for lineno, line in enumerate(self.__source[:5]):
322 matched = re.search(r'coding[:=]\s*([-\w_.]+)',
323 line, re.IGNORECASE)
324 if matched:
325 return lineno, matched.group(1)
326 else:
327 return 0, ""
328
329 def __checkCoding(self):
330 """
331 Private method to check the presence of a coding line and valid
332 encodings.
333 """
334 if len(self.__source) == 0:
335 return
336
337 encodings = [e.lower().strip()
338 for e in self.__args.get(
339 "CodingChecker", self.__defaultArgs["CodingChecker"])
340 .split(",")]
341 lineno, coding = self.__getCoding()
342 if coding:
343 if coding.lower() not in encodings:
344 self.__error(lineno, 0, "M102", coding)
345 else:
346 self.__error(0, 0, "M101")
347
348 def __checkCopyright(self):
349 """
350 Private method to check the presence of a copyright statement.
351 """
352 source = "".join(self.__source)
353 copyrightArgs = self.__args.get(
354 "CopyrightChecker", self.__defaultArgs["CopyrightChecker"])
355 copyrightMinFileSize = copyrightArgs.get(
356 "MinFilesize",
357 self.__defaultArgs["CopyrightChecker"]["MinFilesize"])
358 copyrightAuthor = copyrightArgs.get(
359 "Author",
360 self.__defaultArgs["CopyrightChecker"]["Author"])
361 copyrightRegexStr = (
362 r"Copyright\s+(\(C\)\s+)?(\d{{4}}\s+-\s+)?\d{{4}}\s+{author}"
363 )
364
365 tocheck = max(1024, copyrightMinFileSize)
366 topOfSource = source[:tocheck]
367 if len(topOfSource) < copyrightMinFileSize:
368 return
369
370 copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"),
371 re.IGNORECASE)
372 if not copyrightRe.search(topOfSource):
373 self.__error(0, 0, "M111")
374 return
375
376 if copyrightAuthor:
377 copyrightAuthorRe = re.compile(
378 copyrightRegexStr.format(author=copyrightAuthor),
379 re.IGNORECASE)
380 if not copyrightAuthorRe.search(topOfSource):
381 self.__error(0, 0, "M112")
382
383 def __checkCommentedCode(self):
384 """
385 Private method to check for commented code.
386 """
387 from eradicate import commented_out_code_line_numbers
388
389 source = "".join(self.__source)
390 commentedCodeCheckerArgs = self.__args.get(
391 "CommentedCodeChecker", self.__defaultArgs["CommentedCodeChecker"])
392 aggressive = commentedCodeCheckerArgs.get(
393 "Aggressive",
394 self.__defaultArgs["CommentedCodeChecker"]["Aggressive"])
395 for markedLine in commented_out_code_line_numbers(
396 source, aggressive=aggressive):
397 self.__error(markedLine - 1, 0, "M891")
398
399 def __checkLineContinuation(self):
400 """
401 Private method to check line continuation using backslash.
402 """
403 # generate source lines without comments
404 linesIterator = iter(self.__source)
405 tokens = tokenize.generate_tokens(lambda: next(linesIterator))
406 comments = [token for token in tokens if token[0] == tokenize.COMMENT]
407 stripped = self.__source[:]
408 for comment in comments:
409 lineno = comment[3][0]
410 start = comment[2][1]
411 stop = comment[3][1]
412 content = stripped[lineno - 1]
413 withoutComment = content[:start] + content[stop:]
414 stripped[lineno - 1] = withoutComment.rstrip()
415
416 # perform check with 'cleaned' source
417 for lineIndex, line in enumerate(stripped):
418 strippedLine = line.strip()
419 if (strippedLine.endswith('\\') and
420 not strippedLine.startswith(('assert', 'with'))):
421 self.__error(lineIndex, len(line), "M841")
422
423 def __checkPrintStatements(self):
424 """
425 Private method to check for print statements.
426 """
427 for node in ast.walk(self.__tree):
428 if (
429 (isinstance(node, ast.Call) and
430 getattr(node.func, 'id', None) == 'print') or
431 (hasattr(ast, 'Print') and isinstance(node, ast.Print))
432 ):
433 self.__error(node.lineno - 1, node.col_offset, "M801")
434
435 def __checkTuple(self):
436 """
437 Private method to check for one element tuples.
438 """
439 for node in ast.walk(self.__tree):
440 if (
441 isinstance(node, ast.Tuple) and
442 len(node.elts) == 1
443 ):
444 self.__error(node.lineno - 1, node.col_offset, "M811")
445
446 def __checkFuture(self):
447 """
448 Private method to check the __future__ imports.
449 """
450 expectedImports = {
451 i.strip()
452 for i in self.__args.get("FutureChecker", "").split(",")
453 if bool(i.strip())}
454 if len(expectedImports) == 0:
455 # nothing to check for; disabling the check
456 return
457
458 imports = set()
459 node = None
460 hasCode = False
461
462 for node in ast.walk(self.__tree):
463 if (isinstance(node, ast.ImportFrom) and
464 node.module == '__future__'):
465 imports |= {name.name for name in node.names}
466 elif isinstance(node, ast.Expr):
467 if not AstUtilities.isString(node.value):
468 hasCode = True
469 break
470 elif not (
471 AstUtilities.isString(node) or
472 isinstance(node, ast.Module)
473 ):
474 hasCode = True
475 break
476
477 if isinstance(node, ast.Module) or not hasCode:
478 return
479
480 if not (imports >= expectedImports):
481 if imports:
482 self.__error(node.lineno - 1, node.col_offset, "M701",
483 ", ".join(expectedImports), ", ".join(imports))
484 else:
485 self.__error(node.lineno - 1, node.col_offset, "M702",
486 ", ".join(expectedImports))
487
488 def __checkPep3101(self):
489 """
490 Private method to check for old style string formatting.
491 """
492 for lineno, line in enumerate(self.__source):
493 match = self.__pep3101FormatRegex.search(line)
494 if match:
495 lineLen = len(line)
496 pos = line.find('%')
497 formatPos = pos
498 formatter = '%'
499 if line[pos + 1] == "(":
500 pos = line.find(")", pos)
501 c = line[pos]
502 while c not in "diouxXeEfFgGcrs":
503 pos += 1
504 if pos >= lineLen:
505 break
506 c = line[pos]
507 if c in "diouxXeEfFgGcrs":
508 formatter += c
509 self.__error(lineno, formatPos, "M601", formatter)
510
511 def __checkFormatString(self):
512 """
513 Private method to check string format strings.
514 """
515 coding = self.__getCoding()[1]
516 if not coding:
517 # default to utf-8
518 coding = "utf-8"
519
520 visitor = TextVisitor()
521 visitor.visit(self.__tree)
522 for node in visitor.nodes:
523 text = node.s
524 if isinstance(text, bytes):
525 try:
526 text = text.decode(coding)
527 except UnicodeDecodeError:
528 continue
529 fields, implicit, explicit = self.__getFields(text)
530 if implicit:
531 if node in visitor.calls:
532 self.__error(node.lineno - 1, node.col_offset, "M611")
533 else:
534 if node.is_docstring:
535 self.__error(node.lineno - 1, node.col_offset, "M612")
536 else:
537 self.__error(node.lineno - 1, node.col_offset, "M613")
538
539 if node in visitor.calls:
540 call, strArgs = visitor.calls[node]
541
542 numbers = set()
543 names = set()
544 # Determine which fields require a keyword and which an arg
545 for name in fields:
546 fieldMatch = self.FormatFieldRegex.match(name)
547 try:
548 number = int(fieldMatch.group(1))
549 except ValueError:
550 number = -1
551 # negative numbers are considered keywords
552 if number >= 0:
553 numbers.add(number)
554 else:
555 names.add(fieldMatch.group(1))
556
557 keywords = {keyword.arg for keyword in call.keywords}
558 numArgs = len(call.args)
559 if strArgs:
560 numArgs -= 1
561 if sys.version_info < (3, 5):
562 hasKwArgs = bool(call.kwargs)
563 hasStarArgs = bool(call.starargs)
564 else:
565 hasKwArgs = any(kw.arg is None for kw in call.keywords)
566 hasStarArgs = sum(1 for arg in call.args
567 if isinstance(arg, ast.Starred))
568
569 if hasKwArgs:
570 keywords.discard(None)
571 if hasStarArgs:
572 numArgs -= 1
573
574 # if starargs or kwargs is not None, it can't count the
575 # parameters but at least check if the args are used
576 if hasKwArgs:
577 if not names:
578 # No names but kwargs
579 self.__error(call.lineno - 1, call.col_offset, "M623")
580 if hasStarArgs:
581 if not numbers:
582 # No numbers but args
583 self.__error(call.lineno - 1, call.col_offset, "M624")
584
585 if not hasKwArgs and not hasStarArgs:
586 # can actually verify numbers and names
587 for number in sorted(numbers):
588 if number >= numArgs:
589 self.__error(call.lineno - 1, call.col_offset,
590 "M621", number)
591
592 for name in sorted(names):
593 if name not in keywords:
594 self.__error(call.lineno - 1, call.col_offset,
595 "M622", name)
596
597 for arg in range(numArgs):
598 if arg not in numbers:
599 self.__error(call.lineno - 1, call.col_offset, "M631",
600 arg)
601
602 for keyword in keywords:
603 if keyword not in names:
604 self.__error(call.lineno - 1, call.col_offset, "M632",
605 keyword)
606
607 if implicit and explicit:
608 self.__error(call.lineno - 1, call.col_offset, "M625")
609
610 def __getFields(self, string):
611 """
612 Private method to extract the format field information.
613
614 @param string format string to be parsed
615 @type str
616 @return format field information as a tuple with fields, implicit
617 field definitions present and explicit field definitions present
618 @rtype tuple of set of str, bool, bool
619 """
620 fields = set()
621 cnt = itertools.count()
622 implicit = False
623 explicit = False
624 try:
625 for _literal, field, spec, conv in self.Formatter.parse(string):
626 if field is not None and (conv is None or conv in 'rsa'):
627 if not field:
628 field = str(next(cnt))
629 implicit = True
630 else:
631 explicit = True
632 fields.add(field)
633 fields.update(parsedSpec[1]
634 for parsedSpec in self.Formatter.parse(spec)
635 if parsedSpec[1] is not None)
636 except ValueError:
637 return set(), False, False
638 else:
639 return fields, implicit, explicit
640
641 def __checkBuiltins(self):
642 """
643 Private method to check, if built-ins are shadowed.
644 """
645 functionDefs = [ast.FunctionDef]
646 try:
647 functionDefs.append(ast.AsyncFunctionDef)
648 except AttributeError:
649 pass
650
651 ignoreBuiltinAssignments = self.__args.get(
652 "BuiltinsChecker", self.__defaultArgs["BuiltinsChecker"])
653
654 for node in ast.walk(self.__tree):
655 if isinstance(node, ast.Assign):
656 # assign statement
657 for element in node.targets:
658 if (
659 isinstance(element, ast.Name) and
660 element.id in self.__builtins
661 ):
662 value = node.value
663 if (
664 isinstance(value, ast.Name) and
665 element.id in ignoreBuiltinAssignments and
666 value.id in ignoreBuiltinAssignments[element.id]
667 ):
668 # ignore compatibility assignments
669 continue
670 self.__error(element.lineno - 1, element.col_offset,
671 "M131", element.id)
672 elif isinstance(element, (ast.Tuple, ast.List)):
673 for tupleElement in element.elts:
674 if (
675 isinstance(tupleElement, ast.Name) and
676 tupleElement.id in self.__builtins
677 ):
678 self.__error(tupleElement.lineno - 1,
679 tupleElement.col_offset,
680 "M131", tupleElement.id)
681 elif isinstance(node, ast.For):
682 # for loop
683 target = node.target
684 if (
685 isinstance(target, ast.Name) and
686 target.id in self.__builtins
687 ):
688 self.__error(target.lineno - 1, target.col_offset,
689 "M131", target.id)
690 elif isinstance(target, (ast.Tuple, ast.List)):
691 for element in target.elts:
692 if (
693 isinstance(element, ast.Name) and
694 element.id in self.__builtins
695 ):
696 self.__error(element.lineno - 1,
697 element.col_offset,
698 "M131", element.id)
699 elif any(isinstance(node, functionDef)
700 for functionDef in functionDefs):
701 # (asynchronous) function definition
702 for arg in node.args.args:
703 if (
704 isinstance(arg, ast.arg) and
705 arg.arg in self.__builtins
706 ):
707 self.__error(arg.lineno - 1, arg.col_offset,
708 "M132", arg.arg)
709
710 def __checkComprehensions(self):
711 """
712 Private method to check some comprehension related things.
713 """
714 for node in ast.walk(self.__tree):
715 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
716 nArgs = len(node.args)
717
718 if (
719 nArgs == 1 and
720 isinstance(node.args[0], ast.GeneratorExp) and
721 node.func.id in ('list', 'set')
722 ):
723 errorCode = {
724 "list": "M181",
725 "set": "M182",
726 }[node.func.id]
727 self.__error(node.lineno - 1, node.col_offset, errorCode)
728
729 elif (
730 nArgs == 1 and
731 isinstance(node.args[0], ast.GeneratorExp) and
732 isinstance(node.args[0].elt, ast.Tuple) and
733 len(node.args[0].elt.elts) == 2 and
734 node.func.id == "dict"
735 ):
736 self.__error(node.lineno - 1, node.col_offset, "M183")
737
738 elif (
739 nArgs == 1 and
740 isinstance(node.args[0], ast.ListComp) and
741 node.func.id in ('list', 'set', 'dict')
742 ):
743 errorCode = {
744 'list': 'M195',
745 'dict': 'M185',
746 'set': 'M184',
747 }[node.func.id]
748 self.__error(node.lineno - 1, node.col_offset, errorCode)
749
750 elif nArgs == 1 and (
751 isinstance(node.args[0], ast.Tuple) and
752 node.func.id == "tuple" or
753 isinstance(node.args[0], ast.List) and
754 node.func.id == "list"
755 ):
756 errorCode = {
757 'tuple': 'M197',
758 'list': 'M198',
759 }[node.func.id]
760 self.__error(node.lineno - 1, node.col_offset, errorCode,
761 type(node.args[0]).__name__.lower(),
762 node.func.id)
763
764 elif (
765 nArgs == 1 and
766 isinstance(node.args[0], (ast.Tuple, ast.List)) and
767 node.func.id in ("tuple", "list", "set", "dict")
768 ):
769 errorCode = {
770 "tuple": "M192",
771 "list": "M193",
772 "set": "M191",
773 "dict": "M191",
774 }[node.func.id]
775 self.__error(node.lineno - 1, node.col_offset, errorCode,
776 type(node.args[0]).__name__.lower(),
777 node.func.id)
778
779 elif (
780 nArgs == 1 and
781 isinstance(node.args[0], ast.ListComp) and
782 node.func.id in ('all', 'any', 'enumerate', 'frozenset',
783 'max', 'min', 'sorted', 'sum', 'tuple',)
784 ):
785 self.__error(node.lineno - 1, node.col_offset, "M187",
786 node.func.id)
787
788 elif (
789 nArgs == 0 and
790 not any(isinstance(a, ast.Starred) for a in node.args) and
791 not any(k.arg is None for k in node.keywords) and
792 node.func.id in ("tuple", "list", "dict")
793 ):
794 self.__error(node.lineno - 1, node.col_offset, "M186",
795 node.func.id)
796
797 elif isinstance(node, ast.Compare) and (
798 len(node.ops) == 1 and
799 isinstance(node.ops[0], ast.In) and
800 len(node.comparators) == 1 and
801 isinstance(node.comparators[0], ast.ListComp)
802 ):
803 self.__error(node.lineno - 1, node.col_offset, "M196")
804
805 def __checkMutableDefault(self):
806 """
807 Private method to check for use of mutable types as default arguments.
808 """
809 mutableTypes = (
810 ast.Call,
811 ast.Dict,
812 ast.List,
813 ast.Set,
814 )
815 mutableCalls = (
816 "Counter",
817 "OrderedDict",
818 "collections.Counter",
819 "collections.OrderedDict",
820 "collections.defaultdict",
821 "collections.deque",
822 "defaultdict",
823 "deque",
824 "dict",
825 "list",
826 "set",
827 )
828 immutableCalls = (
829 "tuple",
830 "frozenset",
831 )
832 functionDefs = [ast.FunctionDef]
833 try:
834 functionDefs.append(ast.AsyncFunctionDef)
835 except AttributeError:
836 pass
837
838 for node in ast.walk(self.__tree):
839 if any(isinstance(node, functionDef)
840 for functionDef in functionDefs):
841 defaults = node.args.defaults[:]
842 try:
843 defaults += node.args.kw_defaults[:]
844 except AttributeError:
845 pass
846 for default in defaults:
847 if any(isinstance(default, mutableType)
848 for mutableType in mutableTypes):
849 typeName = type(default).__name__
850 if isinstance(default, ast.Call):
851 callPath = '.'.join(composeCallPath(default.func))
852 if callPath in mutableCalls:
853 self.__error(default.lineno - 1,
854 default.col_offset,
855 "M823", callPath + "()")
856 elif callPath not in immutableCalls:
857 self.__error(default.lineno - 1,
858 default.col_offset,
859 "M822", typeName)
860 else:
861 self.__error(default.lineno - 1,
862 default.col_offset,
863 "M821", typeName)
864
865 def __dictShouldBeChecked(self, node):
866 """
867 Private function to test, if the node should be checked.
868
869 @param node reference to the AST node
870 @return flag indicating to check the node
871 @rtype bool
872 """
873 if not all(AstUtilities.isString(key) for key in node.keys):
874 return False
875
876 if (
877 "__IGNORE_WARNING__" in self.__source[node.lineno - 1] or
878 "__IGNORE_WARNING_M201__" in self.__source[node.lineno - 1]
879 ):
880 return False
881
882 lineNumbers = [key.lineno for key in node.keys]
883 return len(lineNumbers) == len(set(lineNumbers))
884
885 def __checkDictWithSortedKeys(self):
886 """
887 Private method to check, if dictionary keys appear in sorted order.
888 """
889 for node in ast.walk(self.__tree):
890 if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node):
891 for key1, key2 in zip(node.keys, node.keys[1:]):
892 if key2.s < key1.s:
893 self.__error(key2.lineno - 1, key2.col_offset,
894 "M201", key2.s, key1.s)
895
896 def __checkLogging(self):
897 """
898 Private method to check logging statements.
899 """
900 visitor = LoggingVisitor()
901 visitor.visit(self.__tree)
902 for node, reason in visitor.violations:
903 self.__error(node.lineno - 1, node.col_offset, reason)
904
905 def __checkGettext(self):
906 """
907 Private method to check the 'gettext' import statement.
908 """
909 for node in ast.walk(self.__tree):
910 if (
911 isinstance(node, ast.ImportFrom) and
912 any(name.asname == '_' for name in node.names)
913 ):
914 self.__error(node.lineno - 1, node.col_offset, "M711",
915 node.names[0].name)
916
917 def __checkBugBear(self):
918 """
919 Private method for bugbear checks.
920 """
921 visitor = BugBearVisitor()
922 visitor.visit(self.__tree)
923 for violation in visitor.violations:
924 node = violation[0]
925 reason = violation[1]
926 params = violation[2:]
927 self.__error(node.lineno - 1, node.col_offset, reason, *params)
928
929 def __checkReturn(self):
930 """
931 Private method to check return statements.
932 """
933 visitor = ReturnVisitor()
934 visitor.visit(self.__tree)
935 for violation in visitor.violations:
936 node = violation[0]
937 reason = violation[1]
938 self.__error(node.lineno - 1, node.col_offset, reason)
939
940 def __checkDateTime(self):
941 """
942 Private method to check use of naive datetime functions.
943 """
944 # step 1: generate an augmented node tree containing parent info
945 # for each child node
946 tree = self.__generateTree()
947 for node in ast.walk(tree):
948 for childNode in ast.iter_child_nodes(node):
949 childNode._dtCheckerParent = node
950
951 # step 2: perform checks and report issues
952 visitor = DateTimeVisitor()
953 visitor.visit(tree)
954 for violation in visitor.violations:
955 node = violation[0]
956 reason = violation[1]
957 self.__error(node.lineno - 1, node.col_offset, reason)
958
959 def __checkSysVersion(self):
960 """
961 Private method to check the use of sys.version and sys.version_info.
962 """
963 visitor = SysVersionVisitor()
964 visitor.visit(self.__tree)
965 for violation in visitor.violations:
966 node = violation[0]
967 reason = violation[1]
968 self.__error(node.lineno - 1, node.col_offset, reason)
969
970
971 class TextVisitor(ast.NodeVisitor):
972 """
973 Class implementing a node visitor for bytes and str instances.
974
975 It tries to detect docstrings as string of the first expression of each
976 module, class or function.
977 """
978 # modelled after the string format flake8 extension
979
980 def __init__(self):
981 """
982 Constructor
983 """
984 super(TextVisitor, self).__init__()
985 self.nodes = []
986 self.calls = {}
987
988 def __addNode(self, node):
989 """
990 Private method to add a node to our list of nodes.
991
992 @param node reference to the node to add
993 @type ast.AST
994 """
995 if not hasattr(node, 'is_docstring'):
996 node.is_docstring = False
997 self.nodes.append(node)
998
999 def visit_Str(self, node):
1000 """
1001 Public method to record a string node.
1002
1003 @param node reference to the string node
1004 @type ast.Str
1005 """
1006 self.__addNode(node)
1007
1008 def visit_Bytes(self, node):
1009 """
1010 Public method to record a bytes node.
1011
1012 @param node reference to the bytes node
1013 @type ast.Bytes
1014 """
1015 self.__addNode(node)
1016
1017 def visit_Constant(self, node):
1018 """
1019 Public method to handle constant nodes.
1020
1021 @param node reference to the bytes node
1022 @type ast.Constant
1023 """
1024 if sys.version_info >= (3, 8, 0):
1025 if AstUtilities.isBaseString(node):
1026 self.__addNode(node)
1027 else:
1028 super(TextVisitor, self).generic_visit(node)
1029 else:
1030 super(TextVisitor, self).generic_visit(node)
1031
1032 def __visitDefinition(self, node):
1033 """
1034 Private method handling class and function definitions.
1035
1036 @param node reference to the node to handle
1037 @type ast.FunctionDef, ast.AsyncFunctionDef or ast.ClassDef
1038 """
1039 # Manually traverse class or function definition
1040 # * Handle decorators normally
1041 # * Use special check for body content
1042 # * Don't handle the rest (e.g. bases)
1043 for decorator in node.decorator_list:
1044 self.visit(decorator)
1045 self.__visitBody(node)
1046
1047 def __visitBody(self, node):
1048 """
1049 Private method to traverse the body of the node manually.
1050
1051 If the first node is an expression which contains a string or bytes it
1052 marks that as a docstring.
1053
1054 @param node reference to the node to traverse
1055 @type ast.AST
1056 """
1057 if (
1058 node.body and
1059 isinstance(node.body[0], ast.Expr) and
1060 AstUtilities.isBaseString(node.body[0].value)
1061 ):
1062 node.body[0].value.is_docstring = True
1063
1064 for subnode in node.body:
1065 self.visit(subnode)
1066
1067 def visit_Module(self, node):
1068 """
1069 Public method to handle a module.
1070
1071 @param node reference to the node to handle
1072 @type ast.Module
1073 """
1074 self.__visitBody(node)
1075
1076 def visit_ClassDef(self, node):
1077 """
1078 Public method to handle a class definition.
1079
1080 @param node reference to the node to handle
1081 @type ast.ClassDef
1082 """
1083 # Skipped nodes: ('name', 'bases', 'keywords', 'starargs', 'kwargs')
1084 self.__visitDefinition(node)
1085
1086 def visit_FunctionDef(self, node):
1087 """
1088 Public method to handle a function definition.
1089
1090 @param node reference to the node to handle
1091 @type ast.FunctionDef
1092 """
1093 # Skipped nodes: ('name', 'args', 'returns')
1094 self.__visitDefinition(node)
1095
1096 def visit_AsyncFunctionDef(self, node):
1097 """
1098 Public method to handle an asynchronous function definition.
1099
1100 @param node reference to the node to handle
1101 @type ast.AsyncFunctionDef
1102 """
1103 # Skipped nodes: ('name', 'args', 'returns')
1104 self.__visitDefinition(node)
1105
1106 def visit_Call(self, node):
1107 """
1108 Public method to handle a function call.
1109
1110 @param node reference to the node to handle
1111 @type ast.Call
1112 """
1113 if (
1114 isinstance(node.func, ast.Attribute) and
1115 node.func.attr == 'format'
1116 ):
1117 if AstUtilities.isBaseString(node.func.value):
1118 self.calls[node.func.value] = (node, False)
1119 elif (
1120 isinstance(node.func.value, ast.Name) and
1121 node.func.value.id == 'str' and
1122 node.args and
1123 AstUtilities.isBaseString(node.args[0])
1124 ):
1125 self.calls[node.args[0]] = (node, True)
1126 super(TextVisitor, self).generic_visit(node)
1127
1128
1129 class LoggingVisitor(ast.NodeVisitor):
1130 """
1131 Class implementing a node visitor to check logging statements.
1132 """
1133 LoggingLevels = {
1134 "debug",
1135 "critical",
1136 "error",
1137 "info",
1138 "warn",
1139 "warning",
1140 }
1141
1142 def __init__(self):
1143 """
1144 Constructor
1145 """
1146 super(LoggingVisitor, self).__init__()
1147
1148 self.__currentLoggingCall = None
1149 self.__currentLoggingArgument = None
1150 self.__currentLoggingLevel = None
1151 self.__currentExtraKeyword = None
1152 self.violations = []
1153
1154 def __withinLoggingStatement(self):
1155 """
1156 Private method to check, if we are inside a logging statement.
1157
1158 @return flag indicating we are inside a logging statement
1159 @rtype bool
1160 """
1161 return self.__currentLoggingCall is not None
1162
1163 def __withinLoggingArgument(self):
1164 """
1165 Private method to check, if we are inside a logging argument.
1166
1167 @return flag indicating we are inside a logging argument
1168 @rtype bool
1169 """
1170 return self.__currentLoggingArgument is not None
1171
1172 def __withinExtraKeyword(self, node):
1173 """
1174 Private method to check, if we are inside the extra keyword.
1175
1176 @param node reference to the node to be checked
1177 @type ast.keyword
1178 @return flag indicating we are inside the extra keyword
1179 @rtype bool
1180 """
1181 return (
1182 self.__currentExtraKeyword is not None and
1183 self.__currentExtraKeyword != node
1184 )
1185
1186 def __detectLoggingLevel(self, node):
1187 """
1188 Private method to decide whether an AST Call is a logging call.
1189
1190 @param node reference to the node to be processed
1191 @type ast.Call
1192 @return logging level
1193 @rtype str or None
1194 """
1195 try:
1196 if node.func.value.id == "warnings":
1197 return None
1198
1199 if node.func.attr in LoggingVisitor.LoggingLevels:
1200 return node.func.attr
1201 except AttributeError:
1202 pass
1203
1204 return None
1205
1206 def __isFormatCall(self, node):
1207 """
1208 Private method to check if a function call uses format.
1209
1210 @param node reference to the node to be processed
1211 @type ast.Call
1212 @return flag indicating the function call uses format
1213 @rtype bool
1214 """
1215 try:
1216 return node.func.attr == "format"
1217 except AttributeError:
1218 return False
1219
1220 def visit_Call(self, node):
1221 """
1222 Public method to handle a function call.
1223
1224 Every logging statement and string format is expected to be a function
1225 call.
1226
1227 @param node reference to the node to be processed
1228 @type ast.Call
1229 """
1230 # we are in a logging statement
1231 if self.__withinLoggingStatement():
1232 if self.__withinLoggingArgument() and self.__isFormatCall(node):
1233 self.violations.append((node, "M651"))
1234 super(LoggingVisitor, self).generic_visit(node)
1235 return
1236
1237 loggingLevel = self.__detectLoggingLevel(node)
1238
1239 if loggingLevel and self.__currentLoggingLevel is None:
1240 self.__currentLoggingLevel = loggingLevel
1241
1242 # we are in some other statement
1243 if loggingLevel is None:
1244 super(LoggingVisitor, self).generic_visit(node)
1245 return
1246
1247 # we are entering a new logging statement
1248 self.__currentLoggingCall = node
1249
1250 if loggingLevel == "warn":
1251 self.violations.append((node, "M655"))
1252
1253 for index, child in enumerate(ast.iter_child_nodes(node)):
1254 if index == 1:
1255 self.__currentLoggingArgument = child
1256 if (
1257 index > 1 and
1258 isinstance(child, ast.keyword) and
1259 child.arg == "extra"
1260 ):
1261 self.__currentExtraKeyword = child
1262
1263 super(LoggingVisitor, self).visit(child)
1264
1265 self.__currentLoggingArgument = None
1266 self.__currentExtraKeyword = None
1267
1268 self.__currentLoggingCall = None
1269 self.__currentLoggingLevel = None
1270
1271 def visit_BinOp(self, node):
1272 """
1273 Public method to handle binary operations while processing the first
1274 logging argument.
1275
1276 @param node reference to the node to be processed
1277 @type ast.BinOp
1278 """
1279 if self.__withinLoggingStatement() and self.__withinLoggingArgument():
1280 # handle percent format
1281 if isinstance(node.op, ast.Mod):
1282 self.violations.append((node, "M652"))
1283
1284 # handle string concat
1285 if isinstance(node.op, ast.Add):
1286 self.violations.append((node, "M653"))
1287
1288 super(LoggingVisitor, self).generic_visit(node)
1289
1290 def visit_JoinedStr(self, node):
1291 """
1292 Public method to handle f-string arguments.
1293
1294 @param node reference to the node to be processed
1295 @type ast.JoinedStr
1296 """
1297 if sys.version_info >= (3, 6):
1298 if self.__withinLoggingStatement():
1299 if any(isinstance(i, ast.FormattedValue) for i in node.values):
1300 if self.__withinLoggingArgument():
1301 self.violations.append((node, "M654"))
1302
1303 super(LoggingVisitor, self).generic_visit(node)
1304
1305
1306 class BugBearVisitor(ast.NodeVisitor):
1307 """
1308 Class implementing a node visitor to check for various topics.
1309 """
1310 #
1311 # This class was implemented along the BugBear flake8 extension (v 19.3.0).
1312 # Original: Copyright (c) 2016 Łukasz Langa
1313 #
1314
1315 NodeWindowSize = 4
1316
1317 def __init__(self):
1318 """
1319 Constructor
1320 """
1321 super(BugBearVisitor, self).__init__()
1322
1323 self.__nodeStack = []
1324 self.__nodeWindow = []
1325 self.violations = []
1326
1327 def visit(self, node):
1328 """
1329 Public method to traverse a given AST node.
1330
1331 @param node AST node to be traversed
1332 @type ast.Node
1333 """
1334 self.__nodeStack.append(node)
1335 self.__nodeWindow.append(node)
1336 self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize:]
1337
1338 super(BugBearVisitor, self).visit(node)
1339
1340 self.__nodeStack.pop()
1341
1342 def visit_UAdd(self, node):
1343 """
1344 Public method to handle unary additions.
1345
1346 @param node reference to the node to be processed
1347 @type ast.UAdd
1348 """
1349 trailingNodes = list(map(type, self.__nodeWindow[-4:]))
1350 if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]:
1351 originator = self.__nodeWindow[-4]
1352 self.violations.append((originator, "M501"))
1353
1354 self.generic_visit(node)
1355
1356 def visit_Call(self, node):
1357 """
1358 Public method to handle a function call.
1359
1360 @param node reference to the node to be processed
1361 @type ast.Call
1362 """
1363 validPaths = ("six", "future.utils", "builtins")
1364 methodsDict = {
1365 "M521": ("iterkeys", "itervalues", "iteritems", "iterlists"),
1366 "M522": ("viewkeys", "viewvalues", "viewitems", "viewlists"),
1367 "M523": ("next",),
1368 }
1369
1370 if isinstance(node.func, ast.Attribute):
1371 for code, methods in methodsDict.items():
1372 if node.func.attr in methods:
1373 callPath = ".".join(composeCallPath(node.func.value))
1374 if callPath not in validPaths:
1375 self.violations.append((node, code))
1376 break
1377 else:
1378 self.__checkForM502(node)
1379 else:
1380 try:
1381 # bad super() call
1382 if isinstance(node.func, ast.Name) and node.func.id == "super":
1383 args = node.args
1384 if (
1385 len(args) == 2 and
1386 isinstance(args[0], ast.Attribute) and
1387 isinstance(args[0].value, ast.Name) and
1388 args[0].value.id == 'self' and
1389 args[0].attr == '__class__'
1390 ):
1391 self.violations.append((node, "M509"))
1392
1393 # bad getattr and setattr
1394 if (
1395 node.func.id in ("getattr", "hasattr") and
1396 node.args[1].s == "__call__"
1397 ):
1398 self.violations.append((node, "M511"))
1399 if (
1400 node.func.id == "getattr" and
1401 len(node.args) == 2 and
1402 AstUtilities.isString(node.args[1])
1403 ):
1404 self.violations.append((node, "M512"))
1405 elif (
1406 node.func.id == "setattr" and
1407 len(node.args) == 3 and
1408 AstUtilities.isString(node.args[1])
1409 ):
1410 self.violations.append((node, "M513"))
1411 except (AttributeError, IndexError):
1412 pass
1413
1414 self.generic_visit(node)
1415
1416 def visit_Attribute(self, node):
1417 """
1418 Public method to handle attributes.
1419
1420 @param node reference to the node to be processed
1421 @type ast.Attribute
1422 """
1423 callPath = list(composeCallPath(node))
1424
1425 if '.'.join(callPath) == 'sys.maxint':
1426 self.violations.append((node, "M504"))
1427
1428 elif (
1429 len(callPath) == 2 and
1430 callPath[1] == 'message'
1431 ):
1432 name = callPath[0]
1433 for elem in reversed(self.__nodeStack[:-1]):
1434 if isinstance(elem, ast.ExceptHandler) and elem.name == name:
1435 self.violations.append((node, "M505"))
1436 break
1437
1438 def visit_Assign(self, node):
1439 """
1440 Public method to handle assignments.
1441
1442 @param node reference to the node to be processed
1443 @type ast.Assign
1444 """
1445 if isinstance(self.__nodeStack[-2], ast.ClassDef):
1446 # By using 'hasattr' below we're ignoring starred arguments, slices
1447 # and tuples for simplicity.
1448 assignTargets = {t.id for t in node.targets if hasattr(t, 'id')}
1449 if '__metaclass__' in assignTargets:
1450 self.violations.append((node, "M524"))
1451
1452 elif len(node.targets) == 1:
1453 target = node.targets[0]
1454 if (
1455 isinstance(target, ast.Attribute) and
1456 isinstance(target.value, ast.Name)
1457 ):
1458 if (target.value.id, target.attr) == ('os', 'environ'):
1459 self.violations.append((node, "M506"))
1460
1461 self.generic_visit(node)
1462
1463 def visit_For(self, node):
1464 """
1465 Public method to handle 'for' statements.
1466
1467 @param node reference to the node to be processed
1468 @type ast.For
1469 """
1470 self.__checkForM507(node)
1471
1472 self.generic_visit(node)
1473
1474 def visit_AsyncFor(self, node):
1475 """
1476 Public method to handle 'for' statements.
1477
1478 @param node reference to the node to be processed
1479 @type ast.AsyncFor
1480 """
1481 self.__checkForM507(node)
1482
1483 self.generic_visit(node)
1484
1485 def visit_Assert(self, node):
1486 """
1487 Public method to handle 'assert' statements.
1488
1489 @param node reference to the node to be processed
1490 @type ast.Assert
1491 """
1492 if (
1493 AstUtilities.isNameConstant(node.test) and
1494 AstUtilities.getValue(node.test) is False
1495 ):
1496 self.violations.append((node, "M503"))
1497
1498 self.generic_visit(node)
1499
1500 def visit_JoinedStr(self, node):
1501 """
1502 Public method to handle f-string arguments.
1503
1504 @param node reference to the node to be processed
1505 @type ast.JoinedStr
1506 """
1507 if sys.version_info >= (3, 6):
1508 for value in node.values:
1509 if isinstance(value, ast.FormattedValue):
1510 return
1511
1512 self.violations.append((node, "M508"))
1513
1514 def __checkForM502(self, node):
1515 """
1516 Private method to check the use of *strip().
1517
1518 @param node reference to the node to be processed
1519 @type ast.Call
1520 """
1521 if node.func.attr not in ("lstrip", "rstrip", "strip"):
1522 return # method name doesn't match
1523
1524 if len(node.args) != 1 or not AstUtilities.isString(node.args[0]):
1525 return # used arguments don't match the builtin strip
1526
1527 s = AstUtilities.getValue(node.args[0])
1528 if len(s) == 1:
1529 return # stripping just one character
1530
1531 if len(s) == len(set(s)):
1532 return # no characters appear more than once
1533
1534 self.violations.append((node, "M502"))
1535
1536 def __checkForM507(self, node):
1537 """
1538 Private method to check for unused loop variables.
1539
1540 @param node reference to the node to be processed
1541 @type ast.For
1542 """
1543 targets = NameFinder()
1544 targets.visit(node.target)
1545 ctrlNames = set(filter(lambda s: not s.startswith('_'),
1546 targets.getNames()))
1547 body = NameFinder()
1548 for expr in node.body:
1549 body.visit(expr)
1550 usedNames = set(body.getNames())
1551 for name in sorted(ctrlNames - usedNames):
1552 n = targets.getNames()[name][0]
1553 self.violations.append((n, "M507", name))
1554
1555
1556 class NameFinder(ast.NodeVisitor):
1557 """
1558 Class to extract a name out of a tree of nodes.
1559 """
1560 def __init__(self):
1561 """
1562 Constructor
1563 """
1564 super(NameFinder, self).__init__()
1565
1566 self.__names = {}
1567
1568 def visit_Name(self, node):
1569 """
1570 Public method to handle 'Name' nodes.
1571
1572 @param node reference to the node to be processed
1573 @type ast.Name
1574 """
1575 self.__names.setdefault(node.id, []).append(node)
1576
1577 def visit(self, node):
1578 """
1579 Public method to traverse a given AST node.
1580
1581 @param node AST node to be traversed
1582 @type ast.Node
1583 """
1584 if isinstance(node, list):
1585 for elem in node:
1586 super(NameFinder, self).visit(elem)
1587 else:
1588 super(NameFinder, self).visit(node)
1589
1590 def getNames(self):
1591 """
1592 Public method to return the extracted names and Name nodes.
1593
1594 @return dictionary containing the names as keys and the list of nodes
1595 @rtype dict
1596 """
1597 return self.__names
1598
1599
1600 class ReturnVisitor(ast.NodeVisitor):
1601 """
1602 Class implementing a node visitor to check return statements.
1603 """
1604 Assigns = 'assigns'
1605 Refs = 'refs'
1606 Returns = 'returns'
1607
1608 def __init__(self):
1609 """
1610 Constructor
1611 """
1612 super(ReturnVisitor, self).__init__()
1613
1614 self.__stack = []
1615 self.violations = []
1616 self.__loopCount = 0
1617
1618 @property
1619 def assigns(self):
1620 """
1621 Public method to get the Assign nodes.
1622
1623 @return dictionary containing the node name as key and line number
1624 as value
1625 @rtype dict
1626 """
1627 return self.__stack[-1][ReturnVisitor.Assigns]
1628
1629 @property
1630 def refs(self):
1631 """
1632 Public method to get the References nodes.
1633
1634 @return dictionary containing the node name as key and line number
1635 as value
1636 @rtype dict
1637 """
1638 return self.__stack[-1][ReturnVisitor.Refs]
1639
1640 @property
1641 def returns(self):
1642 """
1643 Public method to get the Return nodes.
1644
1645 @return dictionary containing the node name as key and line number
1646 as value
1647 @rtype dict
1648 """
1649 return self.__stack[-1][ReturnVisitor.Returns]
1650
1651 def visit_For(self, node):
1652 """
1653 Public method to handle a for loop.
1654
1655 @param node reference to the for node to handle
1656 @type ast.For
1657 """
1658 self.__visitLoop(node)
1659
1660 def visit_AsyncFor(self, node):
1661 """
1662 Public method to handle an async for loop.
1663
1664 @param node reference to the async for node to handle
1665 @type ast.AsyncFor
1666 """
1667 self.__visitLoop(node)
1668
1669 def visit_While(self, node):
1670 """
1671 Public method to handle a while loop.
1672
1673 @param node reference to the while node to handle
1674 @type ast.While
1675 """
1676 self.__visitLoop(node)
1677
1678 def __visitLoop(self, node):
1679 """
1680 Private method to handle loop nodes.
1681
1682 @param node reference to the loop node to handle
1683 @type ast.For, ast.AsyncFor or ast.While
1684 """
1685 self.__loopCount += 1
1686 self.generic_visit(node)
1687 self.__loopCount -= 1
1688
1689 def __visitWithStack(self, node):
1690 """
1691 Private method to traverse a given function node using a stack.
1692
1693 @param node AST node to be traversed
1694 @type ast.FunctionDef or ast.AsyncFunctionDef
1695 """
1696 self.__stack.append({
1697 ReturnVisitor.Assigns: defaultdict(list),
1698 ReturnVisitor.Refs: defaultdict(list),
1699 ReturnVisitor.Returns: []
1700 })
1701
1702 self.generic_visit(node)
1703 self.__checkFunction(node)
1704 self.__stack.pop()
1705
1706 def visit_FunctionDef(self, node):
1707 """
1708 Public method to handle a function definition.
1709
1710 @param node reference to the node to handle
1711 @type ast.FunctionDef
1712 """
1713 self.__visitWithStack(node)
1714
1715 def visit_AsyncFunctionDef(self, node):
1716 """
1717 Public method to handle a function definition.
1718
1719 @param node reference to the node to handle
1720 @type ast.AsyncFunctionDef
1721 """
1722 self.__visitWithStack(node)
1723
1724 def visit_Return(self, node):
1725 """
1726 Public method to handle a return node.
1727
1728 @param node reference to the node to handle
1729 @type ast.Return
1730 """
1731 self.returns.append(node)
1732 self.generic_visit(node)
1733
1734 def visit_Assign(self, node):
1735 """
1736 Public method to handle an assign node.
1737
1738 @param node reference to the node to handle
1739 @type ast.Assign
1740 """
1741 if not self.__stack:
1742 return
1743
1744 self.generic_visit(node.value)
1745
1746 target = node.targets[0]
1747 if (
1748 isinstance(target, ast.Tuple) and
1749 not isinstance(node.value, ast.Tuple)
1750 ):
1751 # skip unpacking assign
1752 return
1753
1754 self.__visitAssignTarget(target)
1755
1756 def visit_Name(self, node):
1757 """
1758 Public method to handle a name node.
1759
1760 @param node reference to the node to handle
1761 @type ast.Name
1762 """
1763 if self.__stack:
1764 self.refs[node.id].append(node.lineno)
1765
1766 def __visitAssignTarget(self, node):
1767 """
1768 Private method to handle an assign target node.
1769
1770 @param node reference to the node to handle
1771 @type ast.AST
1772 """
1773 if isinstance(node, ast.Tuple):
1774 for elt in node.elts:
1775 self.__visitAssignTarget(elt)
1776 return
1777
1778 if not self.__loopCount and isinstance(node, ast.Name):
1779 self.assigns[node.id].append(node.lineno)
1780 return
1781
1782 self.generic_visit(node)
1783
1784 def __checkFunction(self, node):
1785 """
1786 Private method to check a function definition node.
1787
1788 @param node reference to the node to check
1789 @type ast.AsyncFunctionDef or ast.FunctionDef
1790 """
1791 if not self.returns or not node.body:
1792 return
1793
1794 if len(node.body) == 1 and isinstance(node.body[-1], ast.Return):
1795 # skip functions that consist of `return None` only
1796 return
1797
1798 if not self.__resultExists():
1799 self.__checkUnnecessaryReturnNone()
1800 return
1801
1802 self.__checkImplicitReturnValue()
1803 self.__checkImplicitReturn(node.body[-1])
1804
1805 for n in self.returns:
1806 if n.value:
1807 self.__checkUnnecessaryAssign(n.value)
1808
1809 def __isNone(self, node):
1810 """
1811 Private method to check, if a node value is None.
1812
1813 @param node reference to the node to check
1814 @type ast.AST
1815 @return flag indicating the node contains a None value
1816 @rtype bool
1817 """
1818 return (
1819 AstUtilities.isNameConstant(node) and
1820 AstUtilities.getValue(node) is None
1821 )
1822
1823 def __isFalse(self, node):
1824 """
1825 Private method to check, if a node value is False.
1826
1827 @param node reference to the node to check
1828 @type ast.AST
1829 @return flag indicating the node contains a False value
1830 @rtype bool
1831 """
1832 return (
1833 AstUtilities.isNameConstant(node) and
1834 AstUtilities.getValue(node) is False
1835 )
1836
1837 def __resultExists(self):
1838 """
1839 Private method to check the existance of a return result.
1840
1841 @return flag indicating the existence of a return result
1842 @rtype bool
1843 """
1844 for node in self.returns:
1845 value = node.value
1846 if value and not self.__isNone(value):
1847 return True
1848
1849 return False
1850
1851 def __checkImplicitReturnValue(self):
1852 """
1853 Private method to check for implicit return values.
1854 """
1855 for node in self.returns:
1856 if not node.value:
1857 self.violations.append((node, "M832"))
1858
1859 def __checkUnnecessaryReturnNone(self):
1860 """
1861 Private method to check for an unnecessary 'return None' statement.
1862 """
1863 for node in self.returns:
1864 if self.__isNone(node.value):
1865 self.violations.append((node, "M831"))
1866
1867 def __checkImplicitReturn(self, node):
1868 """
1869 Private method to check for an implicit return statement.
1870
1871 @param node reference to the node to check
1872 @type ast.AST
1873 """
1874 if isinstance(node, ast.If):
1875 if not node.body or not node.orelse:
1876 self.violations.append((node, "M833"))
1877 return
1878
1879 self.__checkImplicitReturn(node.body[-1])
1880 self.__checkImplicitReturn(node.orelse[-1])
1881 return
1882
1883 if isinstance(node, (ast.For, ast.AsyncFor)) and node.orelse:
1884 self.__checkImplicitReturn(node.orelse[-1])
1885 return
1886
1887 if isinstance(node, (ast.With, ast.AsyncWith)):
1888 self.__checkImplicitReturn(node.body[-1])
1889 return
1890
1891 if isinstance(node, ast.Assert) and self.__isFalse(node.test):
1892 return
1893
1894 try:
1895 okNodes = (ast.Return, ast.Raise, ast.While, ast.Try)
1896 except AttributeError:
1897 okNodes = (ast.Return, ast.Raise, ast.While)
1898 if not isinstance(node, okNodes):
1899 self.violations.append((node, "M833"))
1900
1901 def __checkUnnecessaryAssign(self, node):
1902 """
1903 Private method to check for an unnecessary assign statement.
1904
1905 @param node reference to the node to check
1906 @type ast.AST
1907 """
1908 if not isinstance(node, ast.Name):
1909 return
1910
1911 varname = node.id
1912 returnLineno = node.lineno
1913
1914 if varname not in self.assigns:
1915 return
1916
1917 if varname not in self.refs:
1918 self.violations.append((node, "M834"))
1919 return
1920
1921 if self.__hasRefsBeforeNextAssign(varname, returnLineno):
1922 return
1923
1924 self.violations.append((node, "M834"))
1925
1926 def __hasRefsBeforeNextAssign(self, varname, returnLineno):
1927 """
1928 Private method to check for references before a following assign
1929 statement.
1930
1931 @param varname variable name to check for
1932 @type str
1933 @param returnLineno line number of the return statement
1934 @type int
1935 @return flag indicating the existence of references
1936 @rtype bool
1937 """
1938 beforeAssign = 0
1939 afterAssign = None
1940
1941 for lineno in sorted(self.assigns[varname]):
1942 if lineno > returnLineno:
1943 afterAssign = lineno
1944 break
1945
1946 if lineno <= returnLineno:
1947 beforeAssign = lineno
1948
1949 for lineno in self.refs[varname]:
1950 if lineno == returnLineno:
1951 continue
1952
1953 if afterAssign:
1954 if beforeAssign < lineno <= afterAssign:
1955 return True
1956
1957 elif beforeAssign < lineno:
1958 return True
1959
1960 return False
1961
1962
1963 class DateTimeVisitor(ast.NodeVisitor):
1964 """
1965 Class implementing a node visitor to check datetime function calls.
1966
1967 Note: This class is modelled after flake8_datetimez checker.
1968 """
1969 def __init__(self):
1970 """
1971 Constructor
1972 """
1973 super(DateTimeVisitor, self).__init__()
1974
1975 self.violations = []
1976
1977 def __getFromKeywords(self, keywords, name):
1978 """
1979 Private method to get a keyword node given its name.
1980
1981 @param keywords list of keyword argument nodes
1982 @type list of ast.AST
1983 @param name name of the keyword node
1984 @type str
1985 @return keyword node
1986 @rtype ast.AST
1987 """
1988 for keyword in keywords:
1989 if keyword.arg == name:
1990 return keyword
1991
1992 return None
1993
1994 def visit_Call(self, node):
1995 """
1996 Public method to handle a function call.
1997
1998 Every datetime related function call is check for use of the naive
1999 variant (i.e. use without TZ info).
2000
2001 @param node reference to the node to be processed
2002 @type ast.Call
2003 """
2004 # datetime.something()
2005 isDateTimeClass = (
2006 isinstance(node.func, ast.Attribute) and
2007 isinstance(node.func.value, ast.Name) and
2008 node.func.value.id == 'datetime')
2009
2010 # datetime.datetime.something()
2011 isDateTimeModuleAndClass = (
2012 isinstance(node.func, ast.Attribute) and
2013 isinstance(node.func.value, ast.Attribute) and
2014 node.func.value.attr == 'datetime' and
2015 isinstance(node.func.value.value, ast.Name) and
2016 node.func.value.value.id == 'datetime')
2017
2018 if isDateTimeClass:
2019 if node.func.attr == 'datetime':
2020 # datetime.datetime(2000, 1, 1, 0, 0, 0, 0,
2021 # datetime.timezone.utc)
2022 isCase1 = (
2023 len(node.args) >= 8 and
2024 not (
2025 AstUtilities.isNameConstant(node.args[7]) and
2026 AstUtilities.getValue(node.args[7]) is None
2027 )
2028 )
2029
2030 # datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
2031 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo')
2032 isCase2 = (
2033 tzinfoKeyword is not None and
2034 not (
2035 AstUtilities.isNameConstant(tzinfoKeyword.value) and
2036 AstUtilities.getValue(tzinfoKeyword.value) is None
2037 )
2038 )
2039
2040 if not (isCase1 or isCase2):
2041 self.violations.append((node, "M301"))
2042
2043 elif node.func.attr == 'time':
2044 # time(12, 10, 45, 0, datetime.timezone.utc)
2045 isCase1 = (
2046 len(node.args) >= 5 and
2047 not (
2048 AstUtilities.isNameConstant(node.args[4]) and
2049 AstUtilities.getValue(node.args[4]) is None
2050 )
2051 )
2052
2053 # datetime.time(12, 10, 45, tzinfo=datetime.timezone.utc)
2054 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo')
2055 isCase2 = (
2056 tzinfoKeyword is not None and
2057 not (
2058 AstUtilities.isNameConstant(tzinfoKeyword.value) and
2059 AstUtilities.getValue(tzinfoKeyword.value) is None
2060 )
2061 )
2062
2063 if not (isCase1 or isCase2):
2064 self.violations.append((node, "M321"))
2065
2066 elif node.func.attr == 'date':
2067 self.violations.append((node, "M311"))
2068
2069 if isDateTimeClass or isDateTimeModuleAndClass:
2070 if node.func.attr == 'today':
2071 self.violations.append((node, "M302"))
2072
2073 elif node.func.attr == 'utcnow':
2074 self.violations.append((node, "M303"))
2075
2076 elif node.func.attr == 'utcfromtimestamp':
2077 self.violations.append((node, "M304"))
2078
2079 elif node.func.attr in 'now':
2080 # datetime.now(UTC)
2081 isCase1 = (
2082 len(node.args) == 1 and
2083 len(node.keywords) == 0 and
2084 not (
2085 AstUtilities.isNameConstant(node.args[0]) and
2086 AstUtilities.getValue(node.args[0]) is None
2087 )
2088 )
2089
2090 # datetime.now(tz=UTC)
2091 tzKeyword = self.__getFromKeywords(node.keywords, 'tz')
2092 isCase2 = (
2093 tzKeyword is not None and
2094 not (
2095 AstUtilities.isNameConstant(tzKeyword.value) and
2096 AstUtilities.getValue(tzKeyword.value) is None
2097 )
2098 )
2099
2100 if not (isCase1 or isCase2):
2101 self.violations.append((node, "M305"))
2102
2103 elif node.func.attr == 'fromtimestamp':
2104 # datetime.fromtimestamp(1234, UTC)
2105 isCase1 = (
2106 len(node.args) == 2 and
2107 len(node.keywords) == 0 and
2108 not (
2109 AstUtilities.isNameConstant(node.args[1]) and
2110 AstUtilities.getValue(node.args[1]) is None
2111 )
2112 )
2113
2114 # datetime.fromtimestamp(1234, tz=UTC)
2115 tzKeyword = self.__getFromKeywords(node.keywords, 'tz')
2116 isCase2 = (
2117 tzKeyword is not None and
2118 not (
2119 AstUtilities.isNameConstant(tzKeyword.value) and
2120 AstUtilities.getValue(tzKeyword.value) is None
2121 )
2122 )
2123
2124 if not (isCase1 or isCase2):
2125 self.violations.append((node, "M306"))
2126
2127 elif node.func.attr == 'strptime':
2128 # datetime.strptime(...).replace(tzinfo=UTC)
2129 parent = getattr(node, '_dtCheckerParent', None)
2130 pparent = getattr(parent, '_dtCheckerParent', None)
2131 if not (isinstance(parent, ast.Attribute) and
2132 parent.attr == 'replace'):
2133 isCase1 = False
2134 elif not isinstance(pparent, ast.Call):
2135 isCase1 = False
2136 else:
2137 tzinfoKeyword = self.__getFromKeywords(pparent.keywords,
2138 'tzinfo')
2139 isCase1 = (
2140 tzinfoKeyword is not None and
2141 not (
2142 AstUtilities.isNameConstant(
2143 tzinfoKeyword.value) and
2144 AstUtilities.getValue(tzinfoKeyword.value) is None
2145 )
2146 )
2147
2148 if not isCase1:
2149 self.violations.append((node, "M307"))
2150
2151 elif node.func.attr == 'fromordinal':
2152 self.violations.append((node, "M308"))
2153
2154 # date.something()
2155 isDateClass = (isinstance(node.func, ast.Attribute) and
2156 isinstance(node.func.value, ast.Name) and
2157 node.func.value.id == 'date')
2158
2159 # datetime.date.something()
2160 isDateModuleAndClass = (isinstance(node.func, ast.Attribute) and
2161 isinstance(node.func.value, ast.Attribute) and
2162 node.func.value.attr == 'date' and
2163 isinstance(node.func.value.value, ast.Name) and
2164 node.func.value.value.id == 'datetime')
2165
2166 if isDateClass or isDateModuleAndClass:
2167 if node.func.attr == 'today':
2168 self.violations.append((node, "M312"))
2169
2170 elif node.func.attr == 'fromtimestamp':
2171 self.violations.append((node, "M313"))
2172
2173 elif node.func.attr == 'fromordinal':
2174 self.violations.append((node, "M314"))
2175
2176 elif node.func.attr == 'fromisoformat':
2177 self.violations.append((node, "M315"))
2178
2179 self.generic_visit(node)
2180
2181
2182 class SysVersionVisitor(ast.NodeVisitor):
2183 """
2184 Class implementing a node visitor to check the use of sys.version and
2185 sys.version_info.
2186
2187 Note: This class is modelled after flake8-2020 checker.
2188 """
2189 def __init__(self):
2190 """
2191 Constructor
2192 """
2193 super(SysVersionVisitor, self).__init__()
2194
2195 self.violations = []
2196 self.__fromImports = {}
2197
2198 def visit_ImportFrom(self, node):
2199 """
2200 Public method to handle a from ... import ... statement.
2201
2202 @param node reference to the node to be processed
2203 @type ast.ImportFrom
2204 """
2205 for alias in node.names:
2206 if node.module is not None and not alias.asname:
2207 self.__fromImports[alias.name] = node.module
2208
2209 self.generic_visit(node)
2210
2211 def __isSys(self, attr, node):
2212 """
2213 Private method to check for a reference to sys attribute.
2214
2215 @param attr attribute name
2216 @type str
2217 @param node reference to the node to be checked
2218 @type ast.Node
2219 @return flag indicating a match
2220 @rtype bool
2221 """
2222 match = False
2223 if (
2224 isinstance(node, ast.Attribute) and
2225 isinstance(node.value, ast.Name) and
2226 node.value.id == "sys" and
2227 node.attr == attr
2228 ):
2229 match = True
2230 elif (
2231 isinstance(node, ast.Name) and
2232 node.id == attr and
2233 self.__fromImports.get(node.id) == "sys"
2234 ):
2235 match = True
2236
2237 return match
2238
2239 def __isSysVersionUpperSlice(self, node, n):
2240 """
2241 Private method to check the upper slice of sys.version.
2242
2243 @param node reference to the node to be checked
2244 @type ast.Node
2245 @param n slice value to check against
2246 @type int
2247 @return flag indicating a match
2248 @rtype bool
2249 """
2250 return (
2251 self.__isSys("version", node.value) and
2252 isinstance(node.slice, ast.Slice) and
2253 node.slice.lower is None and
2254 AstUtilities.isNumber(node.slice.upper) and
2255 AstUtilities.getValue(node.slice.upper) == n and
2256 node.slice.step is None
2257 )
2258
2259 def visit_Subscript(self, node):
2260 """
2261 Public method to handle a subscript.
2262
2263 @param node reference to the node to be processed
2264 @type ast.Subscript
2265 """
2266 if self.__isSysVersionUpperSlice(node, 1):
2267 self.violations.append((node.value, "M423"))
2268 elif self.__isSysVersionUpperSlice(node, 3):
2269 self.violations.append((node.value, "M401"))
2270 elif (
2271 self.__isSys('version', node.value) and
2272 isinstance(node.slice, ast.Index) and
2273 AstUtilities.isNumber(node.slice.value) and
2274 AstUtilities.getValue(node.slice.value) == 2
2275 ):
2276 self.violations.append((node.value, "M402"))
2277 elif (
2278 self.__isSys('version', node.value) and
2279 isinstance(node.slice, ast.Index) and
2280 AstUtilities.isNumber(node.slice.value) and
2281 AstUtilities.getValue(node.slice.value) == 0
2282 ):
2283 self.violations.append((node.value, "M421"))
2284
2285 self.generic_visit(node)
2286
2287 def visit_Compare(self, node):
2288 """
2289 Public method to handle a comparison.
2290
2291 @param node reference to the node to be processed
2292 @type ast.Compare
2293 """
2294 if (
2295 isinstance(node.left, ast.Subscript) and
2296 self.__isSys('version_info', node.left.value) and
2297 isinstance(node.left.slice, ast.Index) and
2298 AstUtilities.isNumber(node.left.slice.value) and
2299 AstUtilities.getValue(node.left.slice.value) == 0 and
2300 len(node.ops) == 1 and
2301 isinstance(node.ops[0], ast.Eq) and
2302 AstUtilities.isNumber(node.comparators[0]) and
2303 AstUtilities.getValue(node.comparators[0]) == 3
2304 ):
2305 self.violations.append((node.left, "M411"))
2306 elif (
2307 self.__isSys('version', node.left) and
2308 len(node.ops) == 1 and
2309 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and
2310 AstUtilities.isString(node.comparators[0])
2311 ):
2312 if len(AstUtilities.getValue(node.comparators[0])) == 1:
2313 errorCode = "M422"
2314 else:
2315 errorCode = "M403"
2316 self.violations.append((node.left, errorCode))
2317 elif (
2318 isinstance(node.left, ast.Subscript) and
2319 self.__isSys('version_info', node.left.value) and
2320 isinstance(node.left.slice, ast.Index) and
2321 AstUtilities.isNumber(node.left.slice.value) and
2322 AstUtilities.getValue(node.left.slice.value) == 1 and
2323 len(node.ops) == 1 and
2324 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and
2325 AstUtilities.isNumber(node.comparators[0])
2326 ):
2327 self.violations.append((node, "M413"))
2328 elif (
2329 isinstance(node.left, ast.Attribute) and
2330 self.__isSys('version_info', node.left.value) and
2331 node.left.attr == 'minor' and
2332 len(node.ops) == 1 and
2333 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and
2334 AstUtilities.isNumber(node.comparators[0])
2335 ):
2336 self.violations.append((node, "M414"))
2337
2338 self.generic_visit(node)
2339
2340 def visit_Attribute(self, node):
2341 """
2342 Public method to handle an attribute.
2343
2344 @param node reference to the node to be processed
2345 @type ast.Attribute
2346 """
2347 if (
2348 isinstance(node.value, ast.Name) and
2349 node.value.id == 'six' and
2350 node.attr == 'PY3'
2351 ):
2352 self.violations.append((node, "M412"))
2353
2354 self.generic_visit(node)
2355
2356 def visit_Name(self, node):
2357 """
2358 Public method to handle an name.
2359
2360 @param node reference to the node to be processed
2361 @type ast.Name
2362 """
2363 if node.id == 'PY3' and self.__fromImports.get(node.id) == 'six':
2364 self.violations.append((node, "M412"))
2365
2366 self.generic_visit(node)
2367
2368 #
2369 # eflag: noqa = M891

eric ide

mercurial