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

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 - 2022 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", "M185", "M186", "M187", "M188",
59 "M189",
60 "M191", "M192", "M193", "M195", "M196", "M197", "M198",
61
62 ## Dictionaries with sorted keys
63 "M201",
64
65 ## Naive datetime usage
66 "M301", "M302", "M303", "M304", "M305", "M306", "M307", "M308",
67 "M311", "M312", "M313", "M314", "M315",
68 "M321",
69
70 ## sys.version and sys.version_info usage
71 "M401", "M402", "M403",
72 "M411", "M412", "M413", "M414",
73 "M421", "M422", "M423",
74
75 ## Bugbear
76 "M501", "M502", "M503", "M504", "M505", "M506", "M507", "M508",
77 "M509",
78 "M511", "M512", "M513",
79 "M521", "M522", "M523", "M524",
80
81 ## Format Strings
82 "M601",
83 "M611", "M612", "M613",
84 "M621", "M622", "M623", "M624", "M625",
85 "M631", "M632",
86
87 ## Logging
88 "M651", "M652", "M653", "M654", "M655",
89
90 ## Future statements
91 "M701", "M702",
92
93 ## Gettext
94 "M711",
95
96 ## print
97 "M801",
98
99 ## one element tuple
100 "M811",
101
102 ## Mutable Defaults
103 "M821", "M822",
104
105 ## return statements
106 "M831", "M832", "M833", "M834",
107
108 ## line continuation
109 "M841",
110
111 ## commented code
112 "M891",
113 ]
114
115 Formatter = Formatter()
116 FormatFieldRegex = re.compile(r'^((?:\s|.)*?)(\..*|\[.*\])?$')
117
118 BuiltinsWhiteList = [
119 "__name__",
120 "__doc__",
121 "credits",
122 ]
123
124 def __init__(self, source, filename, tree, select, ignore, expected,
125 repeat, args):
126 """
127 Constructor
128
129 @param source source code to be checked
130 @type list of str
131 @param filename name of the source file
132 @type str
133 @param tree AST tree of the source code
134 @type ast.Module
135 @param select list of selected codes
136 @type list of str
137 @param ignore list of codes to be ignored
138 @type list of str
139 @param expected list of expected codes
140 @type list of str
141 @param repeat flag indicating to report each occurrence of a code
142 @type bool
143 @param args dictionary of arguments for the miscellaneous checks
144 @type dict
145 """
146 self.__select = tuple(select)
147 self.__ignore = ('',) if select else tuple(ignore)
148 self.__expected = expected[:]
149 self.__repeat = repeat
150 self.__filename = filename
151 self.__source = source[:]
152 self.__tree = copy.deepcopy(tree)
153 self.__args = args
154
155 self.__pep3101FormatRegex = re.compile(
156 r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%')
157
158 import builtins
159 self.__builtins = [b for b in dir(builtins)
160 if b not in self.BuiltinsWhiteList]
161
162 self.__eradicator = Eradicator()
163
164 # statistics counters
165 self.counters = {}
166
167 # collection of detected errors
168 self.errors = []
169
170 checkersWithCodes = [
171 (self.__checkCoding, ("M101", "M102")),
172 (self.__checkCopyright, ("M111", "M112")),
173 (self.__checkBuiltins, ("M131", "M132")),
174 (self.__checkComprehensions, ("M181", "M182", "M183", "M184",
175 "M185", "M186", "M187", "M188",
176 "M189",
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 nKwArgs = len(node.keywords)
686
687 if (
688 nArgs == 1 and
689 isinstance(node.args[0], ast.GeneratorExp) and
690 node.func.id in ('list', 'set')
691 ):
692 errorCode = {
693 "list": "M181",
694 "set": "M182",
695 }[node.func.id]
696 self.__error(node.lineno - 1, node.col_offset, errorCode)
697
698 elif (
699 nArgs == 1 and
700 isinstance(node.args[0],
701 (ast.GeneratorExp, ast.ListComp)) and
702 isinstance(node.args[0].elt, ast.Tuple) and
703 len(node.args[0].elt.elts) == 2 and
704 node.func.id == "dict"
705 ):
706 if isinstance(node.args[0], ast.GeneratorExp):
707 errorCode = "M183"
708 else:
709 errorCode = "M185"
710 self.__error(node.lineno - 1, node.col_offset, errorCode)
711
712 elif (
713 nArgs == 1 and
714 isinstance(node.args[0], ast.ListComp) and
715 node.func.id in ('list', 'set')
716 ):
717 errorCode = {
718 'list': 'M195',
719 'set': 'M184',
720 }[node.func.id]
721 self.__error(node.lineno - 1, node.col_offset, errorCode)
722
723 elif nArgs == 1 and (
724 isinstance(node.args[0], ast.Tuple) and
725 node.func.id == "tuple" or
726 isinstance(node.args[0], ast.List) and
727 node.func.id == "list"
728 ):
729 errorCode = {
730 'tuple': 'M197',
731 'list': 'M198',
732 }[node.func.id]
733 self.__error(node.lineno - 1, node.col_offset, errorCode,
734 type(node.args[0]).__name__.lower(),
735 node.func.id)
736
737 elif (
738 nArgs == 1 and
739 isinstance(node.args[0], (ast.Tuple, ast.List)) and
740 node.func.id in ("tuple", "list", "set", "dict")
741 ):
742 errorCode = {
743 "tuple": "M192",
744 "list": "M193",
745 "set": "M191",
746 "dict": "M191",
747 }[node.func.id]
748 self.__error(node.lineno - 1, node.col_offset, errorCode,
749 type(node.args[0]).__name__.lower(),
750 node.func.id)
751
752 elif (
753 nArgs == 0 and
754 not any(isinstance(a, ast.Starred) for a in node.args) and
755 not any(k.arg is None for k in node.keywords) and
756 node.func.id == "dict"
757 ) or (
758 nArgs == 0 and
759 nKwArgs == 0 and
760 node.func.id in ("tuple", "list")
761 ):
762 self.__error(node.lineno - 1, node.col_offset, "M186",
763 node.func.id)
764
765 elif (
766 node.func.id in {"list", "reversed"} and
767 nArgs > 0 and
768 isinstance(node.args[0], ast.Call) and
769 isinstance(node.args[0].func, ast.Name) and
770 node.args[0].func.id == "sorted"
771 ):
772 if node.func.id == "reversed":
773 reverseFlagValue = False
774 for kw in node.args[0].keywords:
775 if kw.arg != "reverse":
776 continue
777 if isinstance(kw.value, ast.NameConstant):
778 reverseFlagValue = kw.value.value
779 elif isinstance(kw.value, ast.Num):
780 reverseFlagValue = bool(kw.value.n)
781 else:
782 # Complex value
783 reverseFlagValue = None
784
785 if reverseFlagValue is None:
786 self.__error(node.lineno - 1, node.col_offset,
787 "M187a", node.func.id,
788 node.args[0].func.id)
789 else:
790 self.__error(node.lineno - 1, node.col_offset,
791 "M187b", node.func.id,
792 node.args[0].func.id,
793 not reverseFlagValue)
794 else:
795 self.__error(node.lineno - 1, node.col_offset,
796 "M187c", node.func.id,
797 node.args[0].func.id)
798
799 elif (
800 nArgs > 0 and
801 isinstance(node.args[0], ast.Call) and
802 isinstance(node.args[0].func, ast.Name) and
803 (
804 (
805 node.func.id in {"set", "sorted"} and
806 node.args[0].func.id in {
807 "list", "reversed", "sorted", "tuple"}
808 ) or (
809 node.func.id in {"list", "tuple"} and
810 node.args[0].func.id in {"list", "tuple"}
811 ) or (
812 node.func.id == "set" and
813 node.args[0].func.id == "set"
814 )
815 )
816 ):
817 self.__error(node.lineno - 1, node.col_offset, "M188",
818 node.args[0].func.id, node.func.id)
819
820 elif (
821 node.func.id in {"reversed", "set", "sorted"} and
822 nArgs > 0 and
823 isinstance(node.args[0], ast.Subscript) and
824 isinstance(node.args[0].slice, ast.Slice) and
825 node.args[0].slice.lower is None and
826 node.args[0].slice.upper is None and
827 isinstance(node.args[0].slice.step, ast.UnaryOp) and
828 isinstance(node.args[0].slice.step.op, ast.USub) and
829 isinstance(node.args[0].slice.step.operand, ast.Num) and
830 node.args[0].slice.step.operand.n == 1
831 ):
832 self.__error(node.lineno - 1, node.col_offset,
833 "M189", node.func.id)
834
835 elif (
836 isinstance(node, (ast.ListComp, ast.SetComp)) and (
837 len(node.generators) == 1 and
838 not node.generators[0].ifs and
839 not node.generators[0].is_async and (
840 isinstance(node.elt, ast.Name) and
841 isinstance(node.generators[0].target, ast.Name) and
842 node.elt.id == node.generators[0].target.id
843 )
844 )
845 ):
846 compType = {
847 ast.DictComp: "dict",
848 ast.ListComp: "list",
849 ast.SetComp: "set",
850 }[node.__class__]
851
852 self.__error(node.lineno - 1, node.col_offset,
853 "M196", compType)
854
855 def __checkMutableDefault(self):
856 """
857 Private method to check for use of mutable types as default arguments.
858 """
859 mutableTypes = (
860 ast.Call,
861 ast.Dict,
862 ast.List,
863 ast.Set,
864 )
865 mutableCalls = (
866 "Counter",
867 "OrderedDict",
868 "collections.Counter",
869 "collections.OrderedDict",
870 "collections.defaultdict",
871 "collections.deque",
872 "defaultdict",
873 "deque",
874 "dict",
875 "list",
876 "set",
877 )
878 immutableCalls = (
879 "tuple",
880 "frozenset",
881 )
882 functionDefs = [ast.FunctionDef]
883 with contextlib.suppress(AttributeError):
884 functionDefs.append(ast.AsyncFunctionDef)
885
886 for node in ast.walk(self.__tree):
887 if any(isinstance(node, functionDef)
888 for functionDef in functionDefs):
889 defaults = node.args.defaults[:]
890 with contextlib.suppress(AttributeError):
891 defaults += node.args.kw_defaults[:]
892 for default in defaults:
893 if any(isinstance(default, mutableType)
894 for mutableType in mutableTypes):
895 typeName = type(default).__name__
896 if isinstance(default, ast.Call):
897 callPath = '.'.join(composeCallPath(default.func))
898 if callPath in mutableCalls:
899 self.__error(default.lineno - 1,
900 default.col_offset,
901 "M823", callPath + "()")
902 elif callPath not in immutableCalls:
903 self.__error(default.lineno - 1,
904 default.col_offset,
905 "M822", typeName)
906 else:
907 self.__error(default.lineno - 1,
908 default.col_offset,
909 "M821", typeName)
910
911 def __dictShouldBeChecked(self, node):
912 """
913 Private function to test, if the node should be checked.
914
915 @param node reference to the AST node
916 @return flag indicating to check the node
917 @rtype bool
918 """
919 if not all(AstUtilities.isString(key) for key in node.keys):
920 return False
921
922 if (
923 "__IGNORE_WARNING__" in self.__source[node.lineno - 1] or
924 "__IGNORE_WARNING_M201__" in self.__source[node.lineno - 1]
925 ):
926 return False
927
928 lineNumbers = [key.lineno for key in node.keys]
929 return len(lineNumbers) == len(set(lineNumbers))
930
931 def __checkDictWithSortedKeys(self):
932 """
933 Private method to check, if dictionary keys appear in sorted order.
934 """
935 for node in ast.walk(self.__tree):
936 if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node):
937 for key1, key2 in zip(node.keys, node.keys[1:]):
938 if key2.s < key1.s:
939 self.__error(key2.lineno - 1, key2.col_offset,
940 "M201", key2.s, key1.s)
941
942 def __checkLogging(self):
943 """
944 Private method to check logging statements.
945 """
946 visitor = LoggingVisitor()
947 visitor.visit(self.__tree)
948 for node, reason in visitor.violations:
949 self.__error(node.lineno - 1, node.col_offset, reason)
950
951 def __checkGettext(self):
952 """
953 Private method to check the 'gettext' import statement.
954 """
955 for node in ast.walk(self.__tree):
956 if (
957 isinstance(node, ast.ImportFrom) and
958 any(name.asname == '_' for name in node.names)
959 ):
960 self.__error(node.lineno - 1, node.col_offset, "M711",
961 node.names[0].name)
962
963 def __checkBugBear(self):
964 """
965 Private method for bugbear checks.
966 """
967 visitor = BugBearVisitor()
968 visitor.visit(self.__tree)
969 for violation in visitor.violations:
970 node = violation[0]
971 reason = violation[1]
972 params = violation[2:]
973 self.__error(node.lineno - 1, node.col_offset, reason, *params)
974
975 def __checkReturn(self):
976 """
977 Private method to check return statements.
978 """
979 visitor = ReturnVisitor()
980 visitor.visit(self.__tree)
981 for violation in visitor.violations:
982 node = violation[0]
983 reason = violation[1]
984 self.__error(node.lineno - 1, node.col_offset, reason)
985
986 def __checkDateTime(self):
987 """
988 Private method to check use of naive datetime functions.
989 """
990 # step 1: generate an augmented node tree containing parent info
991 # for each child node
992 tree = copy.deepcopy(self.__tree)
993 for node in ast.walk(tree):
994 for childNode in ast.iter_child_nodes(node):
995 childNode._dtCheckerParent = node
996
997 # step 2: perform checks and report issues
998 visitor = DateTimeVisitor()
999 visitor.visit(tree)
1000 for violation in visitor.violations:
1001 node = violation[0]
1002 reason = violation[1]
1003 self.__error(node.lineno - 1, node.col_offset, reason)
1004
1005 def __checkSysVersion(self):
1006 """
1007 Private method to check the use of sys.version and sys.version_info.
1008 """
1009 visitor = SysVersionVisitor()
1010 visitor.visit(self.__tree)
1011 for violation in visitor.violations:
1012 node = violation[0]
1013 reason = violation[1]
1014 self.__error(node.lineno - 1, node.col_offset, reason)
1015
1016
1017 class TextVisitor(ast.NodeVisitor):
1018 """
1019 Class implementing a node visitor for bytes and str instances.
1020
1021 It tries to detect docstrings as string of the first expression of each
1022 module, class or function.
1023 """
1024 # modelled after the string format flake8 extension
1025
1026 def __init__(self):
1027 """
1028 Constructor
1029 """
1030 super().__init__()
1031 self.nodes = []
1032 self.calls = {}
1033
1034 def __addNode(self, node):
1035 """
1036 Private method to add a node to our list of nodes.
1037
1038 @param node reference to the node to add
1039 @type ast.AST
1040 """
1041 if not hasattr(node, 'is_docstring'):
1042 node.is_docstring = False
1043 self.nodes.append(node)
1044
1045 def visit_Str(self, node):
1046 """
1047 Public method to record a string node.
1048
1049 @param node reference to the string node
1050 @type ast.Str
1051 """
1052 self.__addNode(node)
1053
1054 def visit_Bytes(self, node):
1055 """
1056 Public method to record a bytes node.
1057
1058 @param node reference to the bytes node
1059 @type ast.Bytes
1060 """
1061 self.__addNode(node)
1062
1063 def visit_Constant(self, node):
1064 """
1065 Public method to handle constant nodes.
1066
1067 @param node reference to the bytes node
1068 @type ast.Constant
1069 """
1070 if sys.version_info >= (3, 8, 0):
1071 if AstUtilities.isBaseString(node):
1072 self.__addNode(node)
1073 else:
1074 super().generic_visit(node)
1075 else:
1076 super().generic_visit(node)
1077
1078 def __visitDefinition(self, node):
1079 """
1080 Private method handling class and function definitions.
1081
1082 @param node reference to the node to handle
1083 @type ast.FunctionDef, ast.AsyncFunctionDef or ast.ClassDef
1084 """
1085 # Manually traverse class or function definition
1086 # * Handle decorators normally
1087 # * Use special check for body content
1088 # * Don't handle the rest (e.g. bases)
1089 for decorator in node.decorator_list:
1090 self.visit(decorator)
1091 self.__visitBody(node)
1092
1093 def __visitBody(self, node):
1094 """
1095 Private method to traverse the body of the node manually.
1096
1097 If the first node is an expression which contains a string or bytes it
1098 marks that as a docstring.
1099
1100 @param node reference to the node to traverse
1101 @type ast.AST
1102 """
1103 if (
1104 node.body and
1105 isinstance(node.body[0], ast.Expr) and
1106 AstUtilities.isBaseString(node.body[0].value)
1107 ):
1108 node.body[0].value.is_docstring = True
1109
1110 for subnode in node.body:
1111 self.visit(subnode)
1112
1113 def visit_Module(self, node):
1114 """
1115 Public method to handle a module.
1116
1117 @param node reference to the node to handle
1118 @type ast.Module
1119 """
1120 self.__visitBody(node)
1121
1122 def visit_ClassDef(self, node):
1123 """
1124 Public method to handle a class definition.
1125
1126 @param node reference to the node to handle
1127 @type ast.ClassDef
1128 """
1129 # Skipped nodes: ('name', 'bases', 'keywords', 'starargs', 'kwargs')
1130 self.__visitDefinition(node)
1131
1132 def visit_FunctionDef(self, node):
1133 """
1134 Public method to handle a function definition.
1135
1136 @param node reference to the node to handle
1137 @type ast.FunctionDef
1138 """
1139 # Skipped nodes: ('name', 'args', 'returns')
1140 self.__visitDefinition(node)
1141
1142 def visit_AsyncFunctionDef(self, node):
1143 """
1144 Public method to handle an asynchronous function definition.
1145
1146 @param node reference to the node to handle
1147 @type ast.AsyncFunctionDef
1148 """
1149 # Skipped nodes: ('name', 'args', 'returns')
1150 self.__visitDefinition(node)
1151
1152 def visit_Call(self, node):
1153 """
1154 Public method to handle a function call.
1155
1156 @param node reference to the node to handle
1157 @type ast.Call
1158 """
1159 if (
1160 isinstance(node.func, ast.Attribute) and
1161 node.func.attr == 'format'
1162 ):
1163 if AstUtilities.isBaseString(node.func.value):
1164 self.calls[node.func.value] = (node, False)
1165 elif (
1166 isinstance(node.func.value, ast.Name) and
1167 node.func.value.id == 'str' and
1168 node.args and
1169 AstUtilities.isBaseString(node.args[0])
1170 ):
1171 self.calls[node.args[0]] = (node, True)
1172 super().generic_visit(node)
1173
1174
1175 class LoggingVisitor(ast.NodeVisitor):
1176 """
1177 Class implementing a node visitor to check logging statements.
1178 """
1179 LoggingLevels = {
1180 "debug",
1181 "critical",
1182 "error",
1183 "info",
1184 "warn",
1185 "warning",
1186 }
1187
1188 def __init__(self):
1189 """
1190 Constructor
1191 """
1192 super().__init__()
1193
1194 self.__currentLoggingCall = None
1195 self.__currentLoggingArgument = None
1196 self.__currentLoggingLevel = None
1197 self.__currentExtraKeyword = None
1198 self.violations = []
1199
1200 def __withinLoggingStatement(self):
1201 """
1202 Private method to check, if we are inside a logging statement.
1203
1204 @return flag indicating we are inside a logging statement
1205 @rtype bool
1206 """
1207 return self.__currentLoggingCall is not None
1208
1209 def __withinLoggingArgument(self):
1210 """
1211 Private method to check, if we are inside a logging argument.
1212
1213 @return flag indicating we are inside a logging argument
1214 @rtype bool
1215 """
1216 return self.__currentLoggingArgument is not None
1217
1218 def __withinExtraKeyword(self, node):
1219 """
1220 Private method to check, if we are inside the extra keyword.
1221
1222 @param node reference to the node to be checked
1223 @type ast.keyword
1224 @return flag indicating we are inside the extra keyword
1225 @rtype bool
1226 """
1227 return (
1228 self.__currentExtraKeyword is not None and
1229 self.__currentExtraKeyword != node
1230 )
1231
1232 def __detectLoggingLevel(self, node):
1233 """
1234 Private method to decide whether an AST Call is a logging call.
1235
1236 @param node reference to the node to be processed
1237 @type ast.Call
1238 @return logging level
1239 @rtype str or None
1240 """
1241 with contextlib.suppress(AttributeError):
1242 if node.func.value.id == "warnings":
1243 return None
1244
1245 if node.func.attr in LoggingVisitor.LoggingLevels:
1246 return node.func.attr
1247
1248 return None
1249
1250 def __isFormatCall(self, node):
1251 """
1252 Private method to check if a function call uses format.
1253
1254 @param node reference to the node to be processed
1255 @type ast.Call
1256 @return flag indicating the function call uses format
1257 @rtype bool
1258 """
1259 try:
1260 return node.func.attr == "format"
1261 except AttributeError:
1262 return False
1263
1264 def visit_Call(self, node):
1265 """
1266 Public method to handle a function call.
1267
1268 Every logging statement and string format is expected to be a function
1269 call.
1270
1271 @param node reference to the node to be processed
1272 @type ast.Call
1273 """
1274 # we are in a logging statement
1275 if (
1276 self.__withinLoggingStatement() and
1277 self.__withinLoggingArgument() and
1278 self.__isFormatCall(node)
1279 ):
1280 self.violations.append((node, "M651"))
1281 super().generic_visit(node)
1282 return
1283
1284 loggingLevel = self.__detectLoggingLevel(node)
1285
1286 if loggingLevel and self.__currentLoggingLevel is None:
1287 self.__currentLoggingLevel = loggingLevel
1288
1289 # we are in some other statement
1290 if loggingLevel is None:
1291 super().generic_visit(node)
1292 return
1293
1294 # we are entering a new logging statement
1295 self.__currentLoggingCall = node
1296
1297 if loggingLevel == "warn":
1298 self.violations.append((node, "M655"))
1299
1300 for index, child in enumerate(ast.iter_child_nodes(node)):
1301 if index == 1:
1302 self.__currentLoggingArgument = child
1303 if (
1304 index > 1 and
1305 isinstance(child, ast.keyword) and
1306 child.arg == "extra"
1307 ):
1308 self.__currentExtraKeyword = child
1309
1310 super().visit(child)
1311
1312 self.__currentLoggingArgument = None
1313 self.__currentExtraKeyword = None
1314
1315 self.__currentLoggingCall = None
1316 self.__currentLoggingLevel = None
1317
1318 def visit_BinOp(self, node):
1319 """
1320 Public method to handle binary operations while processing the first
1321 logging argument.
1322
1323 @param node reference to the node to be processed
1324 @type ast.BinOp
1325 """
1326 if self.__withinLoggingStatement() and self.__withinLoggingArgument():
1327 # handle percent format
1328 if isinstance(node.op, ast.Mod):
1329 self.violations.append((node, "M652"))
1330
1331 # handle string concat
1332 if isinstance(node.op, ast.Add):
1333 self.violations.append((node, "M653"))
1334
1335 super().generic_visit(node)
1336
1337 def visit_JoinedStr(self, node):
1338 """
1339 Public method to handle f-string arguments.
1340
1341 @param node reference to the node to be processed
1342 @type ast.JoinedStr
1343 """
1344 if (
1345 self.__withinLoggingStatement() and
1346 any(isinstance(i, ast.FormattedValue) for i in node.values) and
1347 self.__withinLoggingArgument()
1348 ):
1349 self.violations.append((node, "M654"))
1350
1351 super().generic_visit(node)
1352
1353
1354 class BugBearVisitor(ast.NodeVisitor):
1355 """
1356 Class implementing a node visitor to check for various topics.
1357 """
1358 #
1359 # This class was implemented along the BugBear flake8 extension (v 19.3.0).
1360 # Original: Copyright (c) 2016 Łukasz Langa
1361 #
1362
1363 NodeWindowSize = 4
1364
1365 def __init__(self):
1366 """
1367 Constructor
1368 """
1369 super().__init__()
1370
1371 self.__nodeStack = []
1372 self.__nodeWindow = []
1373 self.violations = []
1374
1375 def visit(self, node):
1376 """
1377 Public method to traverse a given AST node.
1378
1379 @param node AST node to be traversed
1380 @type ast.Node
1381 """
1382 self.__nodeStack.append(node)
1383 self.__nodeWindow.append(node)
1384 self.__nodeWindow = self.__nodeWindow[-BugBearVisitor.NodeWindowSize:]
1385
1386 super().visit(node)
1387
1388 self.__nodeStack.pop()
1389
1390 def visit_UAdd(self, node):
1391 """
1392 Public method to handle unary additions.
1393
1394 @param node reference to the node to be processed
1395 @type ast.UAdd
1396 """
1397 trailingNodes = list(map(type, self.__nodeWindow[-4:]))
1398 if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]:
1399 originator = self.__nodeWindow[-4]
1400 self.violations.append((originator, "M501"))
1401
1402 self.generic_visit(node)
1403
1404 def visit_Call(self, node):
1405 """
1406 Public method to handle a function call.
1407
1408 @param node reference to the node to be processed
1409 @type ast.Call
1410 """
1411 validPaths = ("six", "future.utils", "builtins")
1412 methodsDict = {
1413 "M521": ("iterkeys", "itervalues", "iteritems", "iterlists"),
1414 "M522": ("viewkeys", "viewvalues", "viewitems", "viewlists"),
1415 "M523": ("next",),
1416 }
1417
1418 if isinstance(node.func, ast.Attribute):
1419 for code, methods in methodsDict.items():
1420 if node.func.attr in methods:
1421 callPath = ".".join(composeCallPath(node.func.value))
1422 if callPath not in validPaths:
1423 self.violations.append((node, code))
1424 break
1425 else:
1426 self.__checkForM502(node)
1427 else:
1428 with contextlib.suppress(AttributeError, IndexError):
1429 # bad super() call
1430 if isinstance(node.func, ast.Name) and node.func.id == "super":
1431 args = node.args
1432 if (
1433 len(args) == 2 and
1434 isinstance(args[0], ast.Attribute) and
1435 isinstance(args[0].value, ast.Name) and
1436 args[0].value.id == 'self' and
1437 args[0].attr == '__class__'
1438 ):
1439 self.violations.append((node, "M509"))
1440
1441 # bad getattr and setattr
1442 if (
1443 node.func.id in ("getattr", "hasattr") and
1444 node.args[1].s == "__call__"
1445 ):
1446 self.violations.append((node, "M511"))
1447 if (
1448 node.func.id == "getattr" and
1449 len(node.args) == 2 and
1450 AstUtilities.isString(node.args[1])
1451 ):
1452 self.violations.append((node, "M512"))
1453 elif (
1454 node.func.id == "setattr" and
1455 len(node.args) == 3 and
1456 AstUtilities.isString(node.args[1])
1457 ):
1458 self.violations.append((node, "M513"))
1459
1460 self.generic_visit(node)
1461
1462 def visit_Attribute(self, node):
1463 """
1464 Public method to handle attributes.
1465
1466 @param node reference to the node to be processed
1467 @type ast.Attribute
1468 """
1469 callPath = list(composeCallPath(node))
1470
1471 if '.'.join(callPath) == 'sys.maxint':
1472 self.violations.append((node, "M504"))
1473
1474 elif (
1475 len(callPath) == 2 and
1476 callPath[1] == 'message'
1477 ):
1478 name = callPath[0]
1479 for elem in reversed(self.__nodeStack[:-1]):
1480 if isinstance(elem, ast.ExceptHandler) and elem.name == name:
1481 self.violations.append((node, "M505"))
1482 break
1483
1484 def visit_Assign(self, node):
1485 """
1486 Public method to handle assignments.
1487
1488 @param node reference to the node to be processed
1489 @type ast.Assign
1490 """
1491 if isinstance(self.__nodeStack[-2], ast.ClassDef):
1492 # By using 'hasattr' below we're ignoring starred arguments, slices
1493 # and tuples for simplicity.
1494 assignTargets = {t.id for t in node.targets if hasattr(t, 'id')}
1495 if '__metaclass__' in assignTargets:
1496 self.violations.append((node, "M524"))
1497
1498 elif len(node.targets) == 1:
1499 target = node.targets[0]
1500 if (
1501 isinstance(target, ast.Attribute) and
1502 isinstance(target.value, ast.Name) and
1503 (target.value.id, target.attr) == ('os', 'environ')
1504 ):
1505 self.violations.append((node, "M506"))
1506
1507 self.generic_visit(node)
1508
1509 def visit_For(self, node):
1510 """
1511 Public method to handle 'for' statements.
1512
1513 @param node reference to the node to be processed
1514 @type ast.For
1515 """
1516 self.__checkForM507(node)
1517
1518 self.generic_visit(node)
1519
1520 def visit_AsyncFor(self, node):
1521 """
1522 Public method to handle 'for' statements.
1523
1524 @param node reference to the node to be processed
1525 @type ast.AsyncFor
1526 """
1527 self.__checkForM507(node)
1528
1529 self.generic_visit(node)
1530
1531 def visit_Assert(self, node):
1532 """
1533 Public method to handle 'assert' statements.
1534
1535 @param node reference to the node to be processed
1536 @type ast.Assert
1537 """
1538 if (
1539 AstUtilities.isNameConstant(node.test) and
1540 AstUtilities.getValue(node.test) is False
1541 ):
1542 self.violations.append((node, "M503"))
1543
1544 self.generic_visit(node)
1545
1546 def visit_JoinedStr(self, node):
1547 """
1548 Public method to handle f-string arguments.
1549
1550 @param node reference to the node to be processed
1551 @type ast.JoinedStr
1552 """
1553 for value in node.values:
1554 if isinstance(value, ast.FormattedValue):
1555 return
1556
1557 self.violations.append((node, "M508"))
1558
1559 def __checkForM502(self, node):
1560 """
1561 Private method to check the use of *strip().
1562
1563 @param node reference to the node to be processed
1564 @type ast.Call
1565 """
1566 if node.func.attr not in ("lstrip", "rstrip", "strip"):
1567 return # method name doesn't match
1568
1569 if len(node.args) != 1 or not AstUtilities.isString(node.args[0]):
1570 return # used arguments don't match the builtin strip
1571
1572 s = AstUtilities.getValue(node.args[0])
1573 if len(s) == 1:
1574 return # stripping just one character
1575
1576 if len(s) == len(set(s)):
1577 return # no characters appear more than once
1578
1579 self.violations.append((node, "M502"))
1580
1581 def __checkForM507(self, node):
1582 """
1583 Private method to check for unused loop variables.
1584
1585 @param node reference to the node to be processed
1586 @type ast.For
1587 """
1588 targets = NameFinder()
1589 targets.visit(node.target)
1590 ctrlNames = set(filter(lambda s: not s.startswith('_'),
1591 targets.getNames()))
1592 body = NameFinder()
1593 for expr in node.body:
1594 body.visit(expr)
1595 usedNames = set(body.getNames())
1596 for name in sorted(ctrlNames - usedNames):
1597 n = targets.getNames()[name][0]
1598 self.violations.append((n, "M507", name))
1599
1600
1601 class NameFinder(ast.NodeVisitor):
1602 """
1603 Class to extract a name out of a tree of nodes.
1604 """
1605 def __init__(self):
1606 """
1607 Constructor
1608 """
1609 super().__init__()
1610
1611 self.__names = {}
1612
1613 def visit_Name(self, node):
1614 """
1615 Public method to handle 'Name' nodes.
1616
1617 @param node reference to the node to be processed
1618 @type ast.Name
1619 """
1620 self.__names.setdefault(node.id, []).append(node)
1621
1622 def visit(self, node):
1623 """
1624 Public method to traverse a given AST node.
1625
1626 @param node AST node to be traversed
1627 @type ast.Node
1628 """
1629 if isinstance(node, list):
1630 for elem in node:
1631 super().visit(elem)
1632 else:
1633 super().visit(node)
1634
1635 def getNames(self):
1636 """
1637 Public method to return the extracted names and Name nodes.
1638
1639 @return dictionary containing the names as keys and the list of nodes
1640 @rtype dict
1641 """
1642 return self.__names
1643
1644
1645 class ReturnVisitor(ast.NodeVisitor):
1646 """
1647 Class implementing a node visitor to check return statements.
1648 """
1649 Assigns = 'assigns'
1650 Refs = 'refs'
1651 Returns = 'returns'
1652
1653 def __init__(self):
1654 """
1655 Constructor
1656 """
1657 super().__init__()
1658
1659 self.__stack = []
1660 self.violations = []
1661 self.__loopCount = 0
1662
1663 @property
1664 def assigns(self):
1665 """
1666 Public method to get the Assign nodes.
1667
1668 @return dictionary containing the node name as key and line number
1669 as value
1670 @rtype dict
1671 """
1672 return self.__stack[-1][ReturnVisitor.Assigns]
1673
1674 @property
1675 def refs(self):
1676 """
1677 Public method to get the References nodes.
1678
1679 @return dictionary containing the node name as key and line number
1680 as value
1681 @rtype dict
1682 """
1683 return self.__stack[-1][ReturnVisitor.Refs]
1684
1685 @property
1686 def returns(self):
1687 """
1688 Public method to get the Return nodes.
1689
1690 @return dictionary containing the node name as key and line number
1691 as value
1692 @rtype dict
1693 """
1694 return self.__stack[-1][ReturnVisitor.Returns]
1695
1696 def visit_For(self, node):
1697 """
1698 Public method to handle a for loop.
1699
1700 @param node reference to the for node to handle
1701 @type ast.For
1702 """
1703 self.__visitLoop(node)
1704
1705 def visit_AsyncFor(self, node):
1706 """
1707 Public method to handle an async for loop.
1708
1709 @param node reference to the async for node to handle
1710 @type ast.AsyncFor
1711 """
1712 self.__visitLoop(node)
1713
1714 def visit_While(self, node):
1715 """
1716 Public method to handle a while loop.
1717
1718 @param node reference to the while node to handle
1719 @type ast.While
1720 """
1721 self.__visitLoop(node)
1722
1723 def __visitLoop(self, node):
1724 """
1725 Private method to handle loop nodes.
1726
1727 @param node reference to the loop node to handle
1728 @type ast.For, ast.AsyncFor or ast.While
1729 """
1730 self.__loopCount += 1
1731 self.generic_visit(node)
1732 self.__loopCount -= 1
1733
1734 def __visitWithStack(self, node):
1735 """
1736 Private method to traverse a given function node using a stack.
1737
1738 @param node AST node to be traversed
1739 @type ast.FunctionDef or ast.AsyncFunctionDef
1740 """
1741 self.__stack.append({
1742 ReturnVisitor.Assigns: defaultdict(list),
1743 ReturnVisitor.Refs: defaultdict(list),
1744 ReturnVisitor.Returns: []
1745 })
1746
1747 self.generic_visit(node)
1748 self.__checkFunction(node)
1749 self.__stack.pop()
1750
1751 def visit_FunctionDef(self, node):
1752 """
1753 Public method to handle a function definition.
1754
1755 @param node reference to the node to handle
1756 @type ast.FunctionDef
1757 """
1758 self.__visitWithStack(node)
1759
1760 def visit_AsyncFunctionDef(self, node):
1761 """
1762 Public method to handle a function definition.
1763
1764 @param node reference to the node to handle
1765 @type ast.AsyncFunctionDef
1766 """
1767 self.__visitWithStack(node)
1768
1769 def visit_Return(self, node):
1770 """
1771 Public method to handle a return node.
1772
1773 @param node reference to the node to handle
1774 @type ast.Return
1775 """
1776 self.returns.append(node)
1777 self.generic_visit(node)
1778
1779 def visit_Assign(self, node):
1780 """
1781 Public method to handle an assign node.
1782
1783 @param node reference to the node to handle
1784 @type ast.Assign
1785 """
1786 if not self.__stack:
1787 return
1788
1789 self.generic_visit(node.value)
1790
1791 target = node.targets[0]
1792 if (
1793 isinstance(target, ast.Tuple) and
1794 not isinstance(node.value, ast.Tuple)
1795 ):
1796 # skip unpacking assign
1797 return
1798
1799 self.__visitAssignTarget(target)
1800
1801 def visit_Name(self, node):
1802 """
1803 Public method to handle a name node.
1804
1805 @param node reference to the node to handle
1806 @type ast.Name
1807 """
1808 if self.__stack:
1809 self.refs[node.id].append(node.lineno)
1810
1811 def __visitAssignTarget(self, node):
1812 """
1813 Private method to handle an assign target node.
1814
1815 @param node reference to the node to handle
1816 @type ast.AST
1817 """
1818 if isinstance(node, ast.Tuple):
1819 for elt in node.elts:
1820 self.__visitAssignTarget(elt)
1821 return
1822
1823 if not self.__loopCount and isinstance(node, ast.Name):
1824 self.assigns[node.id].append(node.lineno)
1825 return
1826
1827 self.generic_visit(node)
1828
1829 def __checkFunction(self, node):
1830 """
1831 Private method to check a function definition node.
1832
1833 @param node reference to the node to check
1834 @type ast.AsyncFunctionDef or ast.FunctionDef
1835 """
1836 if not self.returns or not node.body:
1837 return
1838
1839 if len(node.body) == 1 and isinstance(node.body[-1], ast.Return):
1840 # skip functions that consist of `return None` only
1841 return
1842
1843 if not self.__resultExists():
1844 self.__checkUnnecessaryReturnNone()
1845 return
1846
1847 self.__checkImplicitReturnValue()
1848 self.__checkImplicitReturn(node.body[-1])
1849
1850 for n in self.returns:
1851 if n.value:
1852 self.__checkUnnecessaryAssign(n.value)
1853
1854 def __isNone(self, node):
1855 """
1856 Private method to check, if a node value is None.
1857
1858 @param node reference to the node to check
1859 @type ast.AST
1860 @return flag indicating the node contains a None value
1861 @rtype bool
1862 """
1863 return (
1864 AstUtilities.isNameConstant(node) and
1865 AstUtilities.getValue(node) is None
1866 )
1867
1868 def __isFalse(self, node):
1869 """
1870 Private method to check, if a node value is False.
1871
1872 @param node reference to the node to check
1873 @type ast.AST
1874 @return flag indicating the node contains a False value
1875 @rtype bool
1876 """
1877 return (
1878 AstUtilities.isNameConstant(node) and
1879 AstUtilities.getValue(node) is False
1880 )
1881
1882 def __resultExists(self):
1883 """
1884 Private method to check the existance of a return result.
1885
1886 @return flag indicating the existence of a return result
1887 @rtype bool
1888 """
1889 for node in self.returns:
1890 value = node.value
1891 if value and not self.__isNone(value):
1892 return True
1893
1894 return False
1895
1896 def __checkImplicitReturnValue(self):
1897 """
1898 Private method to check for implicit return values.
1899 """
1900 for node in self.returns:
1901 if not node.value:
1902 self.violations.append((node, "M832"))
1903
1904 def __checkUnnecessaryReturnNone(self):
1905 """
1906 Private method to check for an unnecessary 'return None' statement.
1907 """
1908 for node in self.returns:
1909 if self.__isNone(node.value):
1910 self.violations.append((node, "M831"))
1911
1912 def __checkImplicitReturn(self, node):
1913 """
1914 Private method to check for an implicit return statement.
1915
1916 @param node reference to the node to check
1917 @type ast.AST
1918 """
1919 if isinstance(node, ast.If):
1920 if not node.body or not node.orelse:
1921 self.violations.append((node, "M833"))
1922 return
1923
1924 self.__checkImplicitReturn(node.body[-1])
1925 self.__checkImplicitReturn(node.orelse[-1])
1926 return
1927
1928 if isinstance(node, (ast.For, ast.AsyncFor)) and node.orelse:
1929 self.__checkImplicitReturn(node.orelse[-1])
1930 return
1931
1932 if isinstance(node, (ast.With, ast.AsyncWith)):
1933 self.__checkImplicitReturn(node.body[-1])
1934 return
1935
1936 if isinstance(node, ast.Assert) and self.__isFalse(node.test):
1937 return
1938
1939 try:
1940 okNodes = (ast.Return, ast.Raise, ast.While, ast.Try)
1941 except AttributeError:
1942 okNodes = (ast.Return, ast.Raise, ast.While)
1943 if not isinstance(node, okNodes):
1944 self.violations.append((node, "M833"))
1945
1946 def __checkUnnecessaryAssign(self, node):
1947 """
1948 Private method to check for an unnecessary assign statement.
1949
1950 @param node reference to the node to check
1951 @type ast.AST
1952 """
1953 if not isinstance(node, ast.Name):
1954 return
1955
1956 varname = node.id
1957 returnLineno = node.lineno
1958
1959 if varname not in self.assigns:
1960 return
1961
1962 if varname not in self.refs:
1963 self.violations.append((node, "M834"))
1964 return
1965
1966 if self.__hasRefsBeforeNextAssign(varname, returnLineno):
1967 return
1968
1969 self.violations.append((node, "M834"))
1970
1971 def __hasRefsBeforeNextAssign(self, varname, returnLineno):
1972 """
1973 Private method to check for references before a following assign
1974 statement.
1975
1976 @param varname variable name to check for
1977 @type str
1978 @param returnLineno line number of the return statement
1979 @type int
1980 @return flag indicating the existence of references
1981 @rtype bool
1982 """
1983 beforeAssign = 0
1984 afterAssign = None
1985
1986 for lineno in sorted(self.assigns[varname]):
1987 if lineno > returnLineno:
1988 afterAssign = lineno
1989 break
1990
1991 if lineno <= returnLineno:
1992 beforeAssign = lineno
1993
1994 for lineno in self.refs[varname]:
1995 if lineno == returnLineno:
1996 continue
1997
1998 if afterAssign:
1999 if beforeAssign < lineno <= afterAssign:
2000 return True
2001
2002 elif beforeAssign < lineno:
2003 return True
2004
2005 return False
2006
2007
2008 class DateTimeVisitor(ast.NodeVisitor):
2009 """
2010 Class implementing a node visitor to check datetime function calls.
2011
2012 Note: This class is modelled after flake8_datetimez checker.
2013 """
2014 def __init__(self):
2015 """
2016 Constructor
2017 """
2018 super().__init__()
2019
2020 self.violations = []
2021
2022 def __getFromKeywords(self, keywords, name):
2023 """
2024 Private method to get a keyword node given its name.
2025
2026 @param keywords list of keyword argument nodes
2027 @type list of ast.AST
2028 @param name name of the keyword node
2029 @type str
2030 @return keyword node
2031 @rtype ast.AST
2032 """
2033 for keyword in keywords:
2034 if keyword.arg == name:
2035 return keyword
2036
2037 return None
2038
2039 def visit_Call(self, node):
2040 """
2041 Public method to handle a function call.
2042
2043 Every datetime related function call is check for use of the naive
2044 variant (i.e. use without TZ info).
2045
2046 @param node reference to the node to be processed
2047 @type ast.Call
2048 """
2049 # datetime.something()
2050 isDateTimeClass = (
2051 isinstance(node.func, ast.Attribute) and
2052 isinstance(node.func.value, ast.Name) and
2053 node.func.value.id == 'datetime')
2054
2055 # datetime.datetime.something()
2056 isDateTimeModuleAndClass = (
2057 isinstance(node.func, ast.Attribute) and
2058 isinstance(node.func.value, ast.Attribute) and
2059 node.func.value.attr == 'datetime' and
2060 isinstance(node.func.value.value, ast.Name) and
2061 node.func.value.value.id == 'datetime')
2062
2063 if isDateTimeClass:
2064 if node.func.attr == 'datetime':
2065 # datetime.datetime(2000, 1, 1, 0, 0, 0, 0,
2066 # datetime.timezone.utc)
2067 isCase1 = (
2068 len(node.args) >= 8 and
2069 not (
2070 AstUtilities.isNameConstant(node.args[7]) and
2071 AstUtilities.getValue(node.args[7]) is None
2072 )
2073 )
2074
2075 # datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
2076 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo')
2077 isCase2 = (
2078 tzinfoKeyword is not None and
2079 not (
2080 AstUtilities.isNameConstant(tzinfoKeyword.value) and
2081 AstUtilities.getValue(tzinfoKeyword.value) is None
2082 )
2083 )
2084
2085 if not (isCase1 or isCase2):
2086 self.violations.append((node, "M301"))
2087
2088 elif node.func.attr == 'time':
2089 # time(12, 10, 45, 0, datetime.timezone.utc)
2090 isCase1 = (
2091 len(node.args) >= 5 and
2092 not (
2093 AstUtilities.isNameConstant(node.args[4]) and
2094 AstUtilities.getValue(node.args[4]) is None
2095 )
2096 )
2097
2098 # datetime.time(12, 10, 45, tzinfo=datetime.timezone.utc)
2099 tzinfoKeyword = self.__getFromKeywords(node.keywords, 'tzinfo')
2100 isCase2 = (
2101 tzinfoKeyword is not None and
2102 not (
2103 AstUtilities.isNameConstant(tzinfoKeyword.value) and
2104 AstUtilities.getValue(tzinfoKeyword.value) is None
2105 )
2106 )
2107
2108 if not (isCase1 or isCase2):
2109 self.violations.append((node, "M321"))
2110
2111 elif node.func.attr == 'date':
2112 self.violations.append((node, "M311"))
2113
2114 if isDateTimeClass or isDateTimeModuleAndClass:
2115 if node.func.attr == 'today':
2116 self.violations.append((node, "M302"))
2117
2118 elif node.func.attr == 'utcnow':
2119 self.violations.append((node, "M303"))
2120
2121 elif node.func.attr == 'utcfromtimestamp':
2122 self.violations.append((node, "M304"))
2123
2124 elif node.func.attr in 'now':
2125 # datetime.now(UTC)
2126 isCase1 = (
2127 len(node.args) == 1 and
2128 len(node.keywords) == 0 and
2129 not (
2130 AstUtilities.isNameConstant(node.args[0]) and
2131 AstUtilities.getValue(node.args[0]) is None
2132 )
2133 )
2134
2135 # datetime.now(tz=UTC)
2136 tzKeyword = self.__getFromKeywords(node.keywords, 'tz')
2137 isCase2 = (
2138 tzKeyword is not None and
2139 not (
2140 AstUtilities.isNameConstant(tzKeyword.value) and
2141 AstUtilities.getValue(tzKeyword.value) is None
2142 )
2143 )
2144
2145 if not (isCase1 or isCase2):
2146 self.violations.append((node, "M305"))
2147
2148 elif node.func.attr == 'fromtimestamp':
2149 # datetime.fromtimestamp(1234, UTC)
2150 isCase1 = (
2151 len(node.args) == 2 and
2152 len(node.keywords) == 0 and
2153 not (
2154 AstUtilities.isNameConstant(node.args[1]) and
2155 AstUtilities.getValue(node.args[1]) is None
2156 )
2157 )
2158
2159 # datetime.fromtimestamp(1234, tz=UTC)
2160 tzKeyword = self.__getFromKeywords(node.keywords, 'tz')
2161 isCase2 = (
2162 tzKeyword is not None and
2163 not (
2164 AstUtilities.isNameConstant(tzKeyword.value) and
2165 AstUtilities.getValue(tzKeyword.value) is None
2166 )
2167 )
2168
2169 if not (isCase1 or isCase2):
2170 self.violations.append((node, "M306"))
2171
2172 elif node.func.attr == 'strptime':
2173 # datetime.strptime(...).replace(tzinfo=UTC)
2174 parent = getattr(node, '_dtCheckerParent', None)
2175 pparent = getattr(parent, '_dtCheckerParent', None)
2176 if (
2177 not (isinstance(parent, ast.Attribute) and
2178 parent.attr == 'replace') or
2179 not isinstance(pparent, ast.Call)
2180 ):
2181 isCase1 = False
2182 else:
2183 tzinfoKeyword = self.__getFromKeywords(pparent.keywords,
2184 'tzinfo')
2185 isCase1 = (
2186 tzinfoKeyword is not None and
2187 not (
2188 AstUtilities.isNameConstant(
2189 tzinfoKeyword.value) and
2190 AstUtilities.getValue(tzinfoKeyword.value) is None
2191 )
2192 )
2193
2194 if not isCase1:
2195 self.violations.append((node, "M307"))
2196
2197 elif node.func.attr == 'fromordinal':
2198 self.violations.append((node, "M308"))
2199
2200 # date.something()
2201 isDateClass = (isinstance(node.func, ast.Attribute) and
2202 isinstance(node.func.value, ast.Name) and
2203 node.func.value.id == 'date')
2204
2205 # datetime.date.something()
2206 isDateModuleAndClass = (isinstance(node.func, ast.Attribute) and
2207 isinstance(node.func.value, ast.Attribute) and
2208 node.func.value.attr == 'date' and
2209 isinstance(node.func.value.value, ast.Name) and
2210 node.func.value.value.id == 'datetime')
2211
2212 if isDateClass or isDateModuleAndClass:
2213 if node.func.attr == 'today':
2214 self.violations.append((node, "M312"))
2215
2216 elif node.func.attr == 'fromtimestamp':
2217 self.violations.append((node, "M313"))
2218
2219 elif node.func.attr == 'fromordinal':
2220 self.violations.append((node, "M314"))
2221
2222 elif node.func.attr == 'fromisoformat':
2223 self.violations.append((node, "M315"))
2224
2225 self.generic_visit(node)
2226
2227
2228 class SysVersionVisitor(ast.NodeVisitor):
2229 """
2230 Class implementing a node visitor to check the use of sys.version and
2231 sys.version_info.
2232
2233 Note: This class is modelled after flake8-2020 checker.
2234 """
2235 def __init__(self):
2236 """
2237 Constructor
2238 """
2239 super().__init__()
2240
2241 self.violations = []
2242 self.__fromImports = {}
2243
2244 def visit_ImportFrom(self, node):
2245 """
2246 Public method to handle a from ... import ... statement.
2247
2248 @param node reference to the node to be processed
2249 @type ast.ImportFrom
2250 """
2251 for alias in node.names:
2252 if node.module is not None and not alias.asname:
2253 self.__fromImports[alias.name] = node.module
2254
2255 self.generic_visit(node)
2256
2257 def __isSys(self, attr, node):
2258 """
2259 Private method to check for a reference to sys attribute.
2260
2261 @param attr attribute name
2262 @type str
2263 @param node reference to the node to be checked
2264 @type ast.Node
2265 @return flag indicating a match
2266 @rtype bool
2267 """
2268 match = False
2269 if (
2270 (isinstance(node, ast.Attribute) and
2271 isinstance(node.value, ast.Name) and
2272 node.value.id == "sys" and
2273 node.attr == attr) or
2274 (isinstance(node, ast.Name) and
2275 node.id == attr and
2276 self.__fromImports.get(node.id) == "sys")
2277 ):
2278 match = True
2279
2280 return match
2281
2282 def __isSysVersionUpperSlice(self, node, n):
2283 """
2284 Private method to check the upper slice of sys.version.
2285
2286 @param node reference to the node to be checked
2287 @type ast.Node
2288 @param n slice value to check against
2289 @type int
2290 @return flag indicating a match
2291 @rtype bool
2292 """
2293 return (
2294 self.__isSys("version", node.value) and
2295 isinstance(node.slice, ast.Slice) and
2296 node.slice.lower is None and
2297 AstUtilities.isNumber(node.slice.upper) and
2298 AstUtilities.getValue(node.slice.upper) == n and
2299 node.slice.step is None
2300 )
2301
2302 def visit_Subscript(self, node):
2303 """
2304 Public method to handle a subscript.
2305
2306 @param node reference to the node to be processed
2307 @type ast.Subscript
2308 """
2309 if self.__isSysVersionUpperSlice(node, 1):
2310 self.violations.append((node.value, "M423"))
2311 elif self.__isSysVersionUpperSlice(node, 3):
2312 self.violations.append((node.value, "M401"))
2313 elif (
2314 self.__isSys('version', node.value) and
2315 isinstance(node.slice, ast.Index) and
2316 AstUtilities.isNumber(node.slice.value) and
2317 AstUtilities.getValue(node.slice.value) == 2
2318 ):
2319 self.violations.append((node.value, "M402"))
2320 elif (
2321 self.__isSys('version', node.value) and
2322 isinstance(node.slice, ast.Index) and
2323 AstUtilities.isNumber(node.slice.value) and
2324 AstUtilities.getValue(node.slice.value) == 0
2325 ):
2326 self.violations.append((node.value, "M421"))
2327
2328 self.generic_visit(node)
2329
2330 def visit_Compare(self, node):
2331 """
2332 Public method to handle a comparison.
2333
2334 @param node reference to the node to be processed
2335 @type ast.Compare
2336 """
2337 if (
2338 isinstance(node.left, ast.Subscript) and
2339 self.__isSys('version_info', node.left.value) and
2340 isinstance(node.left.slice, ast.Index) and
2341 AstUtilities.isNumber(node.left.slice.value) and
2342 AstUtilities.getValue(node.left.slice.value) == 0 and
2343 len(node.ops) == 1 and
2344 isinstance(node.ops[0], ast.Eq) and
2345 AstUtilities.isNumber(node.comparators[0]) and
2346 AstUtilities.getValue(node.comparators[0]) == 3
2347 ):
2348 self.violations.append((node.left, "M411"))
2349 elif (
2350 self.__isSys('version', node.left) and
2351 len(node.ops) == 1 and
2352 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and
2353 AstUtilities.isString(node.comparators[0])
2354 ):
2355 if len(AstUtilities.getValue(node.comparators[0])) == 1:
2356 errorCode = "M422"
2357 else:
2358 errorCode = "M403"
2359 self.violations.append((node.left, errorCode))
2360 elif (
2361 isinstance(node.left, ast.Subscript) and
2362 self.__isSys('version_info', node.left.value) and
2363 isinstance(node.left.slice, ast.Index) and
2364 AstUtilities.isNumber(node.left.slice.value) and
2365 AstUtilities.getValue(node.left.slice.value) == 1 and
2366 len(node.ops) == 1 and
2367 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and
2368 AstUtilities.isNumber(node.comparators[0])
2369 ):
2370 self.violations.append((node, "M413"))
2371 elif (
2372 isinstance(node.left, ast.Attribute) and
2373 self.__isSys('version_info', node.left.value) and
2374 node.left.attr == 'minor' and
2375 len(node.ops) == 1 and
2376 isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and
2377 AstUtilities.isNumber(node.comparators[0])
2378 ):
2379 self.violations.append((node, "M414"))
2380
2381 self.generic_visit(node)
2382
2383 def visit_Attribute(self, node):
2384 """
2385 Public method to handle an attribute.
2386
2387 @param node reference to the node to be processed
2388 @type ast.Attribute
2389 """
2390 if (
2391 isinstance(node.value, ast.Name) and
2392 node.value.id == 'six' and
2393 node.attr == 'PY3'
2394 ):
2395 self.violations.append((node, "M412"))
2396
2397 self.generic_visit(node)
2398
2399 def visit_Name(self, node):
2400 """
2401 Public method to handle an name.
2402
2403 @param node reference to the node to be processed
2404 @type ast.Name
2405 """
2406 if node.id == 'PY3' and self.__fromImports.get(node.id) == 'six':
2407 self.violations.append((node, "M412"))
2408
2409 self.generic_visit(node)
2410
2411 #
2412 # eflag: noqa = M891

eric ide

mercurial