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

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

eric ide

mercurial