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

branch
eric7
changeset 11150
73d80859079c
parent 11148
15e30f0c76a8
equal deleted inserted replaced
11149:fc45672fae42 11150:73d80859079c
10 import ast 10 import ast
11 import builtins 11 import builtins
12 import contextlib 12 import contextlib
13 import copy 13 import copy
14 import itertools 14 import itertools
15 import math
16 import re 15 import re
17 import sys 16 import sys
18 import tokenize 17 import tokenize
19 18
20 from collections import Counter, defaultdict, namedtuple
21 from dataclasses import dataclass
22 from keyword import iskeyword
23 from string import Formatter 19 from string import Formatter
24 20
25 try: 21 try:
26 # Python 3.10+ 22 # Python 3.10+
27 from itertools import pairwise 23 from itertools import pairwise
35 return zip(a, b) 31 return zip(a, b)
36 32
37 33
38 import AstUtilities 34 import AstUtilities
39 35
36 from CodeStyleTopicChecker import CodeStyleTopicChecker
37
38 from .BugBearVisitor import BugBearVisitor
39 from .DateTimeVisitor import DateTimeVisitor
40 from .DefaultMatchCaseVisitor import DefaultMatchCaseVisitor
40 from .eradicate import Eradicator 41 from .eradicate import Eradicator
41 from .MiscellaneousDefaults import MiscellaneousCheckerDefaultArgs 42 from .MiscellaneousDefaults import MiscellaneousCheckerDefaultArgs
42 43 from .ReturnVisitor import ReturnVisitor
43 BugbearMutableLiterals = ("Dict", "List", "Set") 44 from .SysVersionVisitor import SysVersionVisitor
44 BugbearMutableComprehensions = ("ListComp", "DictComp", "SetComp") 45 from .TextVisitor import TextVisitor
45 BugbearMutableCalls = ( 46
46 "Counter", 47
47 "OrderedDict", 48 class MiscellaneousChecker(CodeStyleTopicChecker):
48 "collections.Counter",
49 "collections.OrderedDict",
50 "collections.defaultdict",
51 "collections.deque",
52 "defaultdict",
53 "deque",
54 "dict",
55 "list",
56 "set",
57 )
58 BugbearImmutableCalls = (
59 "tuple",
60 "frozenset",
61 "types.MappingProxyType",
62 "MappingProxyType",
63 "re.compile",
64 "operator.attrgetter",
65 "operator.itemgetter",
66 "operator.methodcaller",
67 "attrgetter",
68 "itemgetter",
69 "methodcaller",
70 )
71
72
73 def composeCallPath(node):
74 """
75 Generator function to assemble the call path of a given node.
76
77 @param node node to assemble call path for
78 @type ast.Node
79 @yield call path components
80 @ytype str
81 """
82 if isinstance(node, ast.Attribute):
83 yield from composeCallPath(node.value)
84 yield node.attr
85 elif isinstance(node, ast.Call):
86 yield from composeCallPath(node.func)
87 elif isinstance(node, ast.Name):
88 yield node.id
89
90
91 class MiscellaneousChecker:
92 """ 49 """
93 Class implementing a checker for miscellaneous checks. 50 Class implementing a checker for miscellaneous checks.
94 """ 51 """
95 52
96 Codes = [ 53 Codes = [
232 "M-711", 189 "M-711",
233 ## print() statements 190 ## print() statements
234 "M-801", 191 "M-801",
235 ## one element tuple 192 ## one element tuple
236 "M-811", 193 "M-811",
237 ## return statements 194 ## return statements # noqa: M-891
238 "M-831", 195 "M-831",
239 "M-832", 196 "M-832",
240 "M-833", 197 "M-833",
241 "M-834", 198 "M-834",
242 ## line continuation 199 ## line continuation
249 "M-891", 206 "M-891",
250 ## structural pattern matching 207 ## structural pattern matching
251 "M-901", 208 "M-901",
252 "M-902", 209 "M-902",
253 ] 210 ]
211 Category = "M"
254 212
255 Formatter = Formatter() 213 Formatter = Formatter()
256 FormatFieldRegex = re.compile(r"^((?:\s|.)*?)(\..*|\[.*\])?$") 214 FormatFieldRegex = re.compile(r"^((?:\s|.)*?)(\..*|\[.*\])?$")
257 215
258 BuiltinsWhiteList = [ 216 BuiltinsWhiteList = [
280 @param repeat flag indicating to report each occurrence of a code 238 @param repeat flag indicating to report each occurrence of a code
281 @type bool 239 @type bool
282 @param args dictionary of arguments for the miscellaneous checks 240 @param args dictionary of arguments for the miscellaneous checks
283 @type dict 241 @type dict
284 """ 242 """
285 self.__select = tuple(select) 243 super().__init__(
286 self.__ignore = tuple(ignore) 244 MiscellaneousChecker.Category,
287 self.__expected = expected[:] 245 source,
288 self.__repeat = repeat 246 filename,
289 self.__filename = filename 247 tree,
290 self.__source = source[:] 248 select,
291 self.__tree = copy.deepcopy(tree) 249 ignore,
292 self.__args = args 250 expected,
293 251 repeat,
294 linesIterator = iter(self.__source) 252 args,
253 )
254
255 linesIterator = iter(self.source)
295 self.__tokens = list(tokenize.generate_tokens(lambda: next(linesIterator))) 256 self.__tokens = list(tokenize.generate_tokens(lambda: next(linesIterator)))
296 257
297 self.__pep3101FormatRegex = re.compile( 258 self.__pep3101FormatRegex = re.compile(
298 r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%' 259 r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%'
299 ) 260 )
300 261
301 self.__builtins = [b for b in dir(builtins) if b not in self.BuiltinsWhiteList] 262 self.__builtins = [b for b in dir(builtins) if b not in self.BuiltinsWhiteList]
302 263
303 self.__eradicator = Eradicator() 264 self.__eradicator = Eradicator()
304
305 # statistics counters
306 self.counters = {}
307
308 # collection of detected errors
309 self.errors = []
310 265
311 checkersWithCodes = [ 266 checkersWithCodes = [
312 (self.__checkCoding, ("M-101", "M-102")), 267 (self.__checkCoding, ("M-101", "M-102")),
313 (self.__checkCopyright, ("M-111", "M-112")), 268 (self.__checkCopyright, ("M-111", "M-112")),
314 (self.__checkBuiltins, ("M-131", "M-132")), 269 (self.__checkBuiltins, ("M-131", "M-132")),
464 (self.__checkImplicitStringConcat, ("M-851", "M-852")), 419 (self.__checkImplicitStringConcat, ("M-851", "M-852")),
465 (self.__checkExplicitStringConcat, ("M-853",)), 420 (self.__checkExplicitStringConcat, ("M-853",)),
466 (self.__checkCommentedCode, ("M-891",)), 421 (self.__checkCommentedCode, ("M-891",)),
467 (self.__checkDefaultMatchCase, ("M-901", "M-902")), 422 (self.__checkDefaultMatchCase, ("M-901", "M-902")),
468 ] 423 ]
424 self._initializeCheckers(checkersWithCodes)
469 425
470 # the eradicate whitelist 426 # the eradicate whitelist
471 commentedCodeCheckerArgs = self.__args.get( 427 commentedCodeCheckerArgs = self.args.get(
472 "CommentedCodeChecker", 428 "CommentedCodeChecker",
473 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"], 429 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"],
474 ) 430 )
475 commentedCodeCheckerWhitelist = commentedCodeCheckerArgs.get( 431 commentedCodeCheckerWhitelist = commentedCodeCheckerArgs.get(
476 "WhiteList", 432 "WhiteList",
478 ) 434 )
479 self.__eradicator.update_whitelist( 435 self.__eradicator.update_whitelist(
480 commentedCodeCheckerWhitelist, extend_default=False 436 commentedCodeCheckerWhitelist, extend_default=False
481 ) 437 )
482 438
483 self.__checkers = []
484 for checker, codes in checkersWithCodes:
485 if any(not (code and self.__ignoreCode(code)) for code in codes):
486 self.__checkers.append(checker)
487
488 def __ignoreCode(self, code):
489 """
490 Private method to check if the message code should be ignored.
491
492 @param code message code to check for
493 @type str
494 @return flag indicating to ignore the given code
495 @rtype bool
496 """
497 return code in self.__ignore or (
498 code.startswith(self.__ignore) and not code.startswith(self.__select)
499 )
500
501 def __error(self, lineNumber, offset, code, *args):
502 """
503 Private method to record an issue.
504
505 @param lineNumber line number of the issue
506 @type int
507 @param offset position within line of the issue
508 @type int
509 @param code message code
510 @type str
511 @param args arguments for the message
512 @type list
513 """
514 if self.__ignoreCode(code):
515 return
516
517 if code in self.counters:
518 self.counters[code] += 1
519 else:
520 self.counters[code] = 1
521
522 # Don't care about expected codes
523 if code in self.__expected:
524 return
525
526 if code and (self.counters[code] == 1 or self.__repeat):
527 # record the issue with one based line number
528 self.errors.append(
529 {
530 "file": self.__filename,
531 "line": lineNumber + 1,
532 "offset": offset,
533 "code": code,
534 "args": args,
535 }
536 )
537
538 def run(self):
539 """
540 Public method to check the given source against miscellaneous
541 conditions.
542 """
543 if not self.__filename:
544 # don't do anything, if essential data is missing
545 return
546
547 if not self.__checkers:
548 # don't do anything, if no codes were selected
549 return
550
551 for check in self.__checkers:
552 check()
553
554 def __getCoding(self): 439 def __getCoding(self):
555 """ 440 """
556 Private method to get the defined coding of the source. 441 Private method to get the defined coding of the source.
557 442
558 @return tuple containing the line number and the coding 443 @return tuple containing the line number and the coding
559 @rtype tuple of int and str 444 @rtype tuple of int and str
560 """ 445 """
561 for lineno, line in enumerate(self.__source[:5]): 446 for lineno, line in enumerate(self.source[:5], start=1):
562 matched = re.search(r"coding[:=]\s*([-\w_.]+)", line, re.IGNORECASE) 447 matched = re.search(r"coding[:=]\s*([-\w_.]+)", line, re.IGNORECASE)
563 if matched: 448 if matched:
564 return lineno, matched.group(1) 449 return lineno, matched.group(1)
565 else: 450 else:
566 return 0, "" 451 return 0, ""
568 def __checkCoding(self): 453 def __checkCoding(self):
569 """ 454 """
570 Private method to check the presence of a coding line and valid 455 Private method to check the presence of a coding line and valid
571 encodings. 456 encodings.
572 """ 457 """
573 if len(self.__source) == 0: 458 if len(self.source) == 0:
574 return 459 return
575 460
576 encodings = [ 461 encodings = [
577 e.lower().strip() 462 e.lower().strip()
578 for e in self.__args.get( 463 for e in self.args.get(
579 "CodingChecker", MiscellaneousCheckerDefaultArgs["CodingChecker"] 464 "CodingChecker", MiscellaneousCheckerDefaultArgs["CodingChecker"]
580 ).split(",") 465 ).split(",")
581 ] 466 ]
582 lineno, coding = self.__getCoding() 467 lineno, coding = self.__getCoding()
583 if coding: 468 if coding:
584 if coding.lower() not in encodings: 469 if coding.lower() not in encodings:
585 self.__error(lineno, 0, "M-102", coding) 470 self.addError(lineno, 0, "M-102", coding)
586 else: 471 else:
587 self.__error(0, 0, "M-101") 472 self.addError(1, 0, "M-101")
588 473
589 def __checkCopyright(self): 474 def __checkCopyright(self):
590 """ 475 """
591 Private method to check the presence of a copyright statement. 476 Private method to check the presence of a copyright statement.
592 """ 477 """
593 source = "".join(self.__source) 478 source = "".join(self.source)
594 copyrightArgs = self.__args.get( 479 copyrightArgs = self.args.get(
595 "CopyrightChecker", MiscellaneousCheckerDefaultArgs["CopyrightChecker"] 480 "CopyrightChecker", MiscellaneousCheckerDefaultArgs["CopyrightChecker"]
596 ) 481 )
597 copyrightMinFileSize = copyrightArgs.get( 482 copyrightMinFileSize = copyrightArgs.get(
598 "MinFilesize", 483 "MinFilesize",
599 MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["MinFilesize"], 484 MiscellaneousCheckerDefaultArgs["CopyrightChecker"]["MinFilesize"],
610 if len(topOfSource) < copyrightMinFileSize: 495 if len(topOfSource) < copyrightMinFileSize:
611 return 496 return
612 497
613 copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"), re.IGNORECASE) 498 copyrightRe = re.compile(copyrightRegexStr.format(author=r".*"), re.IGNORECASE)
614 if not copyrightRe.search(topOfSource): 499 if not copyrightRe.search(topOfSource):
615 self.__error(0, 0, "M-111") 500 self.addError(1, 0, "M-111")
616 return 501 return
617 502
618 if copyrightAuthor: 503 if copyrightAuthor:
619 copyrightAuthorRe = re.compile( 504 copyrightAuthorRe = re.compile(
620 copyrightRegexStr.format(author=copyrightAuthor), re.IGNORECASE 505 copyrightRegexStr.format(author=copyrightAuthor), re.IGNORECASE
621 ) 506 )
622 if not copyrightAuthorRe.search(topOfSource): 507 if not copyrightAuthorRe.search(topOfSource):
623 self.__error(0, 0, "M-112") 508 self.addError(1, 0, "M-112")
624 509
625 def __checkCommentedCode(self): 510 def __checkCommentedCode(self):
626 """ 511 """
627 Private method to check for commented code. 512 Private method to check for commented code.
628 """ 513 """
629 source = "".join(self.__source) 514 source = "".join(self.source)
630 commentedCodeCheckerArgs = self.__args.get( 515 commentedCodeCheckerArgs = self.args.get(
631 "CommentedCodeChecker", 516 "CommentedCodeChecker",
632 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"], 517 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"],
633 ) 518 )
634 aggressive = commentedCodeCheckerArgs.get( 519 aggressive = commentedCodeCheckerArgs.get(
635 "Aggressive", 520 "Aggressive",
636 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]["Aggressive"], 521 MiscellaneousCheckerDefaultArgs["CommentedCodeChecker"]["Aggressive"],
637 ) 522 )
638 for markedLine in self.__eradicator.commented_out_code_line_numbers( 523 for markedLine in self.__eradicator.commented_out_code_line_numbers(
639 source, aggressive=aggressive 524 source, aggressive=aggressive
640 ): 525 ):
641 self.__error(markedLine - 1, 0, "M-891") 526 self.addError(markedLine, 0, "M-891")
642 527
643 def __checkLineContinuation(self): 528 def __checkLineContinuation(self):
644 """ 529 """
645 Private method to check line continuation using backslash. 530 Private method to check line continuation using backslash.
646 """ 531 """
647 # generate source lines without comments 532 # generate source lines without comments
648 comments = [tok for tok in self.__tokens if tok[0] == tokenize.COMMENT] 533 comments = [tok for tok in self.__tokens if tok[0] == tokenize.COMMENT]
649 stripped = self.__source[:] 534 stripped = self.source[:]
650 for comment in comments: 535 for comment in comments:
651 lineno = comment[3][0] 536 lineno = comment[3][0]
652 start = comment[2][1] 537 start = comment[2][1]
653 stop = comment[3][1] 538 stop = comment[3][1]
654 content = stripped[lineno - 1] 539 content = stripped[lineno - 1]
659 for lineIndex, line in enumerate(stripped): 544 for lineIndex, line in enumerate(stripped):
660 strippedLine = line.strip() 545 strippedLine = line.strip()
661 if strippedLine.endswith("\\") and not strippedLine.startswith( 546 if strippedLine.endswith("\\") and not strippedLine.startswith(
662 ("assert", "with") 547 ("assert", "with")
663 ): 548 ):
664 self.__error(lineIndex, len(line), "M-841") 549 self.addError(lineIndex + 1, len(line), "M-841")
665 550
666 def __checkPrintStatements(self): 551 def __checkPrintStatements(self):
667 """ 552 """
668 Private method to check for print statements. 553 Private method to check for print statements.
669 """ 554 """
670 for node in ast.walk(self.__tree): 555 for node in ast.walk(self.tree):
671 if ( 556 if (
672 isinstance(node, ast.Call) and getattr(node.func, "id", None) == "print" 557 isinstance(node, ast.Call) and getattr(node.func, "id", None) == "print"
673 ) or (hasattr(ast, "Print") and isinstance(node, ast.Print)): 558 ) or (hasattr(ast, "Print") and isinstance(node, ast.Print)):
674 self.__error(node.lineno - 1, node.col_offset, "M-801") 559 self.addErrorFromNode(node, "M-801")
675 560
676 def __checkTuple(self): 561 def __checkTuple(self):
677 """ 562 """
678 Private method to check for one element tuples. 563 Private method to check for one element tuples.
679 """ 564 """
680 for node in ast.walk(self.__tree): 565 for node in ast.walk(self.tree):
681 if isinstance(node, ast.Tuple) and len(node.elts) == 1: 566 if isinstance(node, ast.Tuple) and len(node.elts) == 1:
682 self.__error(node.lineno - 1, node.col_offset, "M-811") 567 self.addErrorFromNode(node, "M-811")
683 568
684 def __checkFuture(self): 569 def __checkFuture(self):
685 """ 570 """
686 Private method to check the __future__ imports. 571 Private method to check the __future__ imports.
687 """ 572 """
688 expectedImports = { 573 expectedImports = {
689 i.strip() 574 i.strip()
690 for i in self.__args.get("FutureChecker", "").split(",") 575 for i in self.args.get("FutureChecker", "").split(",")
691 if bool(i.strip()) 576 if bool(i.strip())
692 } 577 }
693 if len(expectedImports) == 0: 578 if len(expectedImports) == 0:
694 # nothing to check for; disabling the check 579 # nothing to check for; disabling the check
695 return 580 return
696 581
697 imports = set() 582 imports = set()
698 node = None 583 node = None
699 hasCode = False 584 hasCode = False
700 585
701 for node in ast.walk(self.__tree): 586 for node in ast.walk(self.tree):
702 if isinstance(node, ast.ImportFrom) and node.module == "__future__": 587 if isinstance(node, ast.ImportFrom) and node.module == "__future__":
703 imports |= {name.name for name in node.names} 588 imports |= {name.name for name in node.names}
704 elif isinstance(node, ast.Expr): 589 elif isinstance(node, ast.Expr):
705 if not AstUtilities.isString(node.value): 590 if not AstUtilities.isString(node.value):
706 hasCode = True 591 hasCode = True
712 if isinstance(node, ast.Module) or not hasCode: 597 if isinstance(node, ast.Module) or not hasCode:
713 return 598 return
714 599
715 if imports < expectedImports: 600 if imports < expectedImports:
716 if imports: 601 if imports:
717 self.__error( 602 self.addErrorFromNode(
718 node.lineno - 1, 603 node, "M-701", ", ".join(expectedImports), ", ".join(imports)
719 node.col_offset,
720 "M-701",
721 ", ".join(expectedImports),
722 ", ".join(imports),
723 ) 604 )
724 else: 605 else:
725 self.__error( 606 self.addErrorFromNode(node, "M-702", ", ".join(expectedImports))
726 node.lineno - 1,
727 node.col_offset,
728 "M-702",
729 ", ".join(expectedImports),
730 )
731 607
732 def __checkPep3101(self): 608 def __checkPep3101(self):
733 """ 609 """
734 Private method to check for old style string formatting. 610 Private method to check for old style string formatting.
735 """ 611 """
736 for lineno, line in enumerate(self.__source): 612 for lineno, line in enumerate(self.source, start=1):
737 match = self.__pep3101FormatRegex.search(line) 613 match = self.__pep3101FormatRegex.search(line)
738 if match: 614 if match:
739 lineLen = len(line) 615 lineLen = len(line)
740 pos = line.find("%") 616 pos = line.find("%")
741 formatPos = pos 617 formatPos = pos
748 if pos >= lineLen: 624 if pos >= lineLen:
749 break 625 break
750 c = line[pos] 626 c = line[pos]
751 if c in "diouxXeEfFgGcrs": 627 if c in "diouxXeEfFgGcrs":
752 formatter += c 628 formatter += c
753 self.__error(lineno, formatPos, "M-601", formatter) 629 self.addError(lineno, formatPos, "M-601", formatter)
754 630
755 def __checkFormatString(self): 631 def __checkFormatString(self):
756 """ 632 """
757 Private method to check string format strings. 633 Private method to check string format strings.
758 """ 634 """
760 if not coding: 636 if not coding:
761 # default to utf-8 637 # default to utf-8
762 coding = "utf-8" 638 coding = "utf-8"
763 639
764 visitor = TextVisitor() 640 visitor = TextVisitor()
765 visitor.visit(self.__tree) 641 visitor.visit(self.tree)
766 for node in visitor.nodes: 642 for node in visitor.nodes:
767 text = node.value 643 text = node.value
768 if isinstance(text, bytes): 644 if isinstance(text, bytes):
769 try: 645 try:
770 text = text.decode(coding) 646 text = text.decode(coding)
771 except UnicodeDecodeError: 647 except UnicodeDecodeError:
772 continue 648 continue
773 fields, implicit, explicit = self.__getFields(text) 649 fields, implicit, explicit = self.__getFields(text)
774 if implicit: 650 if implicit:
775 if node in visitor.calls: 651 if node in visitor.calls:
776 self.__error(node.lineno - 1, node.col_offset, "M-611") 652 self.addErrorFromNode(node, "M-611")
777 else: 653 else:
778 if node.is_docstring: 654 if node.is_docstring:
779 self.__error(node.lineno - 1, node.col_offset, "M-612") 655 self.addErrorFromNode(node, "M-612")
780 else: 656 else:
781 self.__error(node.lineno - 1, node.col_offset, "M-613") 657 self.addErrorFromNode(node, "M-613")
782 658
783 if node in visitor.calls: 659 if node in visitor.calls:
784 call, strArgs = visitor.calls[node] 660 call, strArgs = visitor.calls[node]
785 661
786 numbers = set() 662 numbers = set()
796 if number >= 0: 672 if number >= 0:
797 numbers.add(number) 673 numbers.add(number)
798 else: 674 else:
799 names.add(fieldMatch.group(1)) 675 names.add(fieldMatch.group(1))
800 676
801 keywords = {keyword.arg for keyword in call.keywords} 677 keywords = {kw.arg for kw in call.keywords}
802 numArgs = len(call.args) 678 numArgs = len(call.args)
803 if strArgs: 679 if strArgs:
804 numArgs -= 1 680 numArgs -= 1
805 hasKwArgs = any(kw.arg is None for kw in call.keywords) 681 hasKwArgs = any(kw.arg is None for kw in call.keywords)
806 hasStarArgs = sum( 682 hasStarArgs = sum(
814 690
815 # if starargs or kwargs is not None, it can't count the 691 # if starargs or kwargs is not None, it can't count the
816 # parameters but at least check if the args are used 692 # parameters but at least check if the args are used
817 if hasKwArgs and not names: 693 if hasKwArgs and not names:
818 # No names but kwargs 694 # No names but kwargs
819 self.__error(call.lineno - 1, call.col_offset, "M-623") 695 self.addErrorFromNode(call, "M-623")
820 if hasStarArgs and not numbers: 696 if hasStarArgs and not numbers:
821 # No numbers but args 697 # No numbers but args
822 self.__error(call.lineno - 1, call.col_offset, "M-624") 698 self.addErrorFromNode(call, "M-624")
823 699
824 if not hasKwArgs and not hasStarArgs: 700 if not hasKwArgs and not hasStarArgs:
825 # can actually verify numbers and names 701 # can actually verify numbers and names
826 for number in sorted(numbers): 702 for number in sorted(numbers):
827 if number >= numArgs: 703 if number >= numArgs:
828 self.__error( 704 self.addErrorFromNode(call, "M-621", number)
829 call.lineno - 1, call.col_offset, "M-621", number
830 )
831 705
832 for name in sorted(names): 706 for name in sorted(names):
833 if name not in keywords: 707 if name not in keywords:
834 self.__error( 708 self.addErrorFromNode(call, "M-622", name)
835 call.lineno - 1, call.col_offset, "M-622", name
836 )
837 709
838 for arg in range(numArgs): 710 for arg in range(numArgs):
839 if arg not in numbers: 711 if arg not in numbers:
840 self.__error(call.lineno - 1, call.col_offset, "M-631", arg) 712 self.addErrorFromNode(call, "M-631", arg)
841 713
842 for keyword in keywords: 714 for kw in keywords:
843 if keyword not in names: 715 if kw not in names:
844 self.__error(call.lineno - 1, call.col_offset, "M-632", keyword) 716 self.addErrorFromNode(call, "M-632", kw)
845 717
846 if implicit and explicit: 718 if implicit and explicit:
847 self.__error(call.lineno - 1, call.col_offset, "M-625") 719 self.addErrorFromNode(call, "M-625")
848 720
849 def __getFields(self, string): 721 def __getFields(self, string):
850 """ 722 """
851 Private method to extract the format field information. 723 Private method to extract the format field information.
852 724
885 """ 757 """
886 functionDefs = [ast.FunctionDef] 758 functionDefs = [ast.FunctionDef]
887 with contextlib.suppress(AttributeError): 759 with contextlib.suppress(AttributeError):
888 functionDefs.append(ast.AsyncFunctionDef) 760 functionDefs.append(ast.AsyncFunctionDef)
889 761
890 ignoreBuiltinAssignments = self.__args.get( 762 ignoreBuiltinAssignments = self.args.get(
891 "BuiltinsChecker", MiscellaneousCheckerDefaultArgs["BuiltinsChecker"] 763 "BuiltinsChecker", MiscellaneousCheckerDefaultArgs["BuiltinsChecker"]
892 ) 764 )
893 765
894 for node in ast.walk(self.__tree): 766 for node in ast.walk(self.tree):
895 if isinstance(node, ast.Assign): 767 if isinstance(node, ast.Assign):
896 # assign statement 768 # assign statement
897 for element in node.targets: 769 for element in node.targets:
898 if isinstance(element, ast.Name) and element.id in self.__builtins: 770 if isinstance(element, ast.Name) and element.id in self.__builtins:
899 value = node.value 771 value = node.value
902 and element.id in ignoreBuiltinAssignments 774 and element.id in ignoreBuiltinAssignments
903 and value.id in ignoreBuiltinAssignments[element.id] 775 and value.id in ignoreBuiltinAssignments[element.id]
904 ): 776 ):
905 # ignore compatibility assignments 777 # ignore compatibility assignments
906 continue 778 continue
907 self.__error( 779 self.addErrorFromNode(element, "M-131", element.id)
908 element.lineno - 1, element.col_offset, "M-131", element.id
909 )
910 elif isinstance(element, (ast.Tuple, ast.List)): 780 elif isinstance(element, (ast.Tuple, ast.List)):
911 for tupleElement in element.elts: 781 for tupleElement in element.elts:
912 if ( 782 if (
913 isinstance(tupleElement, ast.Name) 783 isinstance(tupleElement, ast.Name)
914 and tupleElement.id in self.__builtins 784 and tupleElement.id in self.__builtins
915 ): 785 ):
916 self.__error( 786 self.addErrorFromNode(
917 tupleElement.lineno - 1, 787 tupleElement, "M-131", tupleElement.id
918 tupleElement.col_offset,
919 "M-131",
920 tupleElement.id,
921 ) 788 )
922 elif isinstance(node, ast.For): 789 elif isinstance(node, ast.For):
923 # for loop 790 # for loop
924 target = node.target 791 target = node.target
925 if isinstance(target, ast.Name) and target.id in self.__builtins: 792 if isinstance(target, ast.Name) and target.id in self.__builtins:
926 self.__error( 793 self.addErrorFromNode(target, "M-131", target.id)
927 target.lineno - 1, target.col_offset, "M-131", target.id
928 )
929 elif isinstance(target, (ast.Tuple, ast.List)): 794 elif isinstance(target, (ast.Tuple, ast.List)):
930 for element in target.elts: 795 for element in target.elts:
931 if ( 796 if (
932 isinstance(element, ast.Name) 797 isinstance(element, ast.Name)
933 and element.id in self.__builtins 798 and element.id in self.__builtins
934 ): 799 ):
935 self.__error( 800 self.addErrorFromNode(element, "M-131", element.id)
936 element.lineno - 1,
937 element.col_offset,
938 "M-131",
939 element.id,
940 )
941 elif any(isinstance(node, functionDef) for functionDef in functionDefs): 801 elif any(isinstance(node, functionDef) for functionDef in functionDefs):
942 # (asynchronous) function definition 802 # (asynchronous) function definition
943 for arg in node.args.args: 803 for arg in node.args.args:
944 if isinstance(arg, ast.arg) and arg.arg in self.__builtins: 804 if isinstance(arg, ast.arg) and arg.arg in self.__builtins:
945 self.__error(arg.lineno - 1, arg.col_offset, "M-132", arg.arg) 805 self.addErrorFromNode(arg, "M-132", arg.arg)
946 806
947 def __checkComprehensions(self): 807 def __checkComprehensions(self):
948 """ 808 """
949 Private method to check some comprehension related things. 809 Private method to check some comprehension related things.
950 810
957 ast.SetComp: "set", 817 ast.SetComp: "set",
958 } 818 }
959 819
960 visitedMapCalls = set() 820 visitedMapCalls = set()
961 821
962 for node in ast.walk(self.__tree): 822 for node in ast.walk(self.tree):
963 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): 823 if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
964 numPositionalArgs = len(node.args) 824 numPositionalArgs = len(node.args)
965 numKeywordArgs = len(node.keywords) 825 numKeywordArgs = len(node.keywords)
966 826
967 if ( 827 if (
971 ): 831 ):
972 errorCode = { 832 errorCode = {
973 "list": "M-180", 833 "list": "M-180",
974 "set": "M-181", 834 "set": "M-181",
975 }[node.func.id] 835 }[node.func.id]
976 self.__error(node.lineno - 1, node.col_offset, errorCode) 836 self.addErrorFromNode(node, errorCode)
977 837
978 elif ( 838 elif (
979 numPositionalArgs == 1 839 numPositionalArgs == 1
980 and node.func.id == "dict" 840 and node.func.id == "dict"
981 and len(node.keywords) == 0 841 and len(node.keywords) == 0
985 ): 845 ):
986 if isinstance(node.args[0], ast.GeneratorExp): 846 if isinstance(node.args[0], ast.GeneratorExp):
987 errorCode = "M-182" 847 errorCode = "M-182"
988 else: 848 else:
989 errorCode = "M-184" 849 errorCode = "M-184"
990 self.__error(node.lineno - 1, node.col_offset, errorCode) 850 self.addErrorFromNode(node, errorCode)
991 851
992 elif ( 852 elif (
993 numPositionalArgs == 1 853 numPositionalArgs == 1
994 and isinstance(node.args[0], ast.ListComp) 854 and isinstance(node.args[0], ast.ListComp)
995 and node.func.id in ("list", "set", "any", "all") 855 and node.func.id in ("list", "set", "any", "all")
998 "list": "M-191", 858 "list": "M-191",
999 "set": "M-183", 859 "set": "M-183",
1000 "any": "M-199", 860 "any": "M-199",
1001 "all": "M-199", 861 "all": "M-199",
1002 }[node.func.id] 862 }[node.func.id]
1003 self.__error( 863 self.addErrorFromNode(node, errorCode, node.func.id)
1004 node.lineno - 1, node.col_offset, errorCode, node.func.id
1005 )
1006 864
1007 elif numPositionalArgs == 1 and ( 865 elif numPositionalArgs == 1 and (
1008 isinstance(node.args[0], ast.Tuple) 866 isinstance(node.args[0], ast.Tuple)
1009 and node.func.id == "tuple" 867 and node.func.id == "tuple"
1010 or isinstance(node.args[0], ast.List) 868 or isinstance(node.args[0], ast.List)
1012 ): 870 ):
1013 errorCode = { 871 errorCode = {
1014 "tuple": "M-189a", 872 "tuple": "M-189a",
1015 "list": "M-190a", 873 "list": "M-190a",
1016 }[node.func.id] 874 }[node.func.id]
1017 self.__error( 875 self.addErrorFromNode(
1018 node.lineno - 1, 876 node,
1019 node.col_offset,
1020 errorCode, 877 errorCode,
1021 type(node.args[0]).__name__.lower(), 878 type(node.args[0]).__name__.lower(),
1022 node.func.id, 879 node.func.id,
1023 ) 880 )
1024 881
1030 ): 887 ):
1031 if isinstance(node.args[0], ast.Dict): 888 if isinstance(node.args[0], ast.Dict):
1032 type_ = "dict" 889 type_ = "dict"
1033 else: 890 else:
1034 type_ = "dict comprehension" 891 type_ = "dict comprehension"
1035 self.__error( 892 self.addErrorFromNode(node, "M-198", type_)
1036 node.lineno - 1,
1037 node.col_offset,
1038 "M-198",
1039 type_,
1040 )
1041 893
1042 elif ( 894 elif (
1043 numPositionalArgs == 1 895 numPositionalArgs == 1
1044 and isinstance(node.args[0], (ast.Tuple, ast.List)) 896 and isinstance(node.args[0], (ast.Tuple, ast.List))
1045 and ( 897 and (
1057 "tuple": "M-189b", 909 "tuple": "M-189b",
1058 "list": "M-190b", 910 "list": "M-190b",
1059 "set": "M-185", 911 "set": "M-185",
1060 "dict": "M-186", 912 "dict": "M-186",
1061 }[node.func.id] 913 }[node.func.id]
1062 self.__error( 914 self.addErrorFromNode(
1063 node.lineno - 1, 915 node,
1064 node.col_offset,
1065 errorCode, 916 errorCode,
1066 type(node.args[0]).__name__.lower(), 917 type(node.args[0]).__name__.lower(),
1067 node.func.id, 918 node.func.id,
1068 ) 919 )
1069 920
1075 ) or ( 926 ) or (
1076 numPositionalArgs == 0 927 numPositionalArgs == 0
1077 and numKeywordArgs == 0 928 and numKeywordArgs == 0
1078 and node.func.id in ("tuple", "list") 929 and node.func.id in ("tuple", "list")
1079 ): 930 ):
1080 self.__error( 931 self.addErrorFromNode(node, "M-188", node.func.id)
1081 node.lineno - 1, node.col_offset, "M-188", node.func.id
1082 )
1083 932
1084 elif ( 933 elif (
1085 node.func.id in {"list", "reversed"} 934 node.func.id in {"list", "reversed"}
1086 and numPositionalArgs > 0 935 and numPositionalArgs > 0
1087 and isinstance(node.args[0], ast.Call) 936 and isinstance(node.args[0], ast.Call)
1098 if isinstance(kw.value, ast.Constant) 947 if isinstance(kw.value, ast.Constant)
1099 else None 948 else None
1100 ) 949 )
1101 950
1102 if reverseFlagValue is None: 951 if reverseFlagValue is None:
1103 self.__error( 952 self.addErrorFromNode(
1104 node.lineno - 1, 953 node, "M-193a", node.func.id, node.args[0].func.id
1105 node.col_offset,
1106 "M-193a",
1107 node.func.id,
1108 node.args[0].func.id,
1109 ) 954 )
1110 else: 955 else:
1111 self.__error( 956 self.addErrorFromNode(
1112 node.lineno - 1, 957 node,
1113 node.col_offset,
1114 "M-193b", 958 "M-193b",
1115 node.func.id, 959 node.func.id,
1116 node.args[0].func.id, 960 node.args[0].func.id,
1117 not reverseFlagValue, 961 not reverseFlagValue,
1118 ) 962 )
1119 963
1120 else: 964 else:
1121 self.__error( 965 self.addErrorFromNode(
1122 node.lineno - 1, 966 node, "M-193c", node.func.id, node.args[0].func.id
1123 node.col_offset,
1124 "M-193c",
1125 node.func.id,
1126 node.args[0].func.id,
1127 ) 967 )
1128 968
1129 elif ( 969 elif (
1130 numPositionalArgs > 0 970 numPositionalArgs > 0
1131 and isinstance(node.args[0], ast.Call) 971 and isinstance(node.args[0], ast.Call)
1141 and node.args[0].func.id in {"list", "tuple"} 981 and node.args[0].func.id in {"list", "tuple"}
1142 ) 982 )
1143 or (node.func.id == "set" and node.args[0].func.id == "set") 983 or (node.func.id == "set" and node.args[0].func.id == "set")
1144 ) 984 )
1145 ): 985 ):
1146 self.__error( 986 self.addErrorFromNode(
1147 node.lineno - 1, 987 node, "M-194", node.args[0].func.id, node.func.id
1148 node.col_offset,
1149 "M-194",
1150 node.args[0].func.id,
1151 node.func.id,
1152 ) 988 )
1153 989
1154 elif ( 990 elif (
1155 node.func.id in {"reversed", "set", "sorted"} 991 node.func.id in {"reversed", "set", "sorted"}
1156 and numPositionalArgs > 0 992 and numPositionalArgs > 0
1161 and isinstance(node.args[0].slice.step, ast.UnaryOp) 997 and isinstance(node.args[0].slice.step, ast.UnaryOp)
1162 and isinstance(node.args[0].slice.step.op, ast.USub) 998 and isinstance(node.args[0].slice.step.op, ast.USub)
1163 and isinstance(node.args[0].slice.step.operand, ast.Constant) 999 and isinstance(node.args[0].slice.step.operand, ast.Constant)
1164 and node.args[0].slice.step.operand.n == 1 1000 and node.args[0].slice.step.operand.n == 1
1165 ): 1001 ):
1166 self.__error( 1002 self.addErrorFromNode(node, "M-195", node.func.id)
1167 node.lineno - 1, node.col_offset, "M-195", node.func.id
1168 )
1169 1003
1170 elif ( 1004 elif (
1171 node.func.id == "map" 1005 node.func.id == "map"
1172 and node not in visitedMapCalls 1006 and node not in visitedMapCalls
1173 and len(node.args) == 2 1007 and len(node.args) == 2
1174 and isinstance(node.args[0], ast.Lambda) 1008 and isinstance(node.args[0], ast.Lambda)
1175 ): 1009 ):
1176 self.__error( 1010 self.addErrorFromNode(node, "M-197", "generator expression")
1177 node.lineno - 1,
1178 node.col_offset,
1179 "M-197",
1180 "generator expression",
1181 )
1182 1011
1183 elif ( 1012 elif (
1184 node.func.id in ("list", "set", "dict") 1013 node.func.id in ("list", "set", "dict")
1185 and len(node.args) == 1 1014 and len(node.args) == 1
1186 and isinstance(node.args[0], ast.Call) 1015 and isinstance(node.args[0], ast.Call)
1204 ): 1033 ):
1205 rewriteable = False 1034 rewriteable = False
1206 1035
1207 if rewriteable: 1036 if rewriteable:
1208 comprehensionType = f"{node.func.id} comprehension" 1037 comprehensionType = f"{node.func.id} comprehension"
1209 self.__error( 1038 self.addErrorFromNode(node, "M-197", comprehensionType)
1210 node.lineno - 1, node.col_offset, "M-197", comprehensionType
1211 )
1212 1039
1213 elif isinstance(node, (ast.DictComp, ast.ListComp, ast.SetComp)) and ( 1040 elif isinstance(node, (ast.DictComp, ast.ListComp, ast.SetComp)) and (
1214 len(node.generators) == 1 1041 len(node.generators) == 1
1215 and not node.generators[0].ifs 1042 and not node.generators[0].ifs
1216 and not node.generators[0].is_async 1043 and not node.generators[0].is_async
1229 and isinstance(node.generators[0].target.elts[0], ast.Name) 1056 and isinstance(node.generators[0].target.elts[0], ast.Name)
1230 and node.generators[0].target.elts[0].id == node.key.id 1057 and node.generators[0].target.elts[0].id == node.key.id
1231 and isinstance(node.generators[0].target.elts[1], ast.Name) 1058 and isinstance(node.generators[0].target.elts[1], ast.Name)
1232 and node.generators[0].target.elts[1].id == node.value.id 1059 and node.generators[0].target.elts[1].id == node.value.id
1233 ): 1060 ):
1234 self.__error( 1061 self.addErrorFromNode(node, "M-196", compType[node.__class__])
1235 node.lineno - 1,
1236 node.col_offset,
1237 "M-196",
1238 compType[node.__class__],
1239 )
1240 1062
1241 elif ( 1063 elif (
1242 isinstance(node, ast.DictComp) 1064 isinstance(node, ast.DictComp)
1243 and isinstance(node.key, ast.Name) 1065 and isinstance(node.key, ast.Name)
1244 and isinstance(node.value, ast.Constant) 1066 and isinstance(node.value, ast.Constant)
1245 and isinstance(node.generators[0].target, ast.Name) 1067 and isinstance(node.generators[0].target, ast.Name)
1246 and node.key.id == node.generators[0].target.id 1068 and node.key.id == node.generators[0].target.id
1247 ): 1069 ):
1248 self.__error( 1070 self.addErrorFromNode(node, "M-200", compType[node.__class__])
1249 node.lineno - 1,
1250 node.col_offset,
1251 "M-200",
1252 compType[node.__class__],
1253 )
1254 1071
1255 def __dictShouldBeChecked(self, node): 1072 def __dictShouldBeChecked(self, node):
1256 """ 1073 """
1257 Private function to test, if the node should be checked. 1074 Private function to test, if the node should be checked.
1258 1075
1263 """ 1080 """
1264 if not all(AstUtilities.isString(key) for key in node.keys): 1081 if not all(AstUtilities.isString(key) for key in node.keys):
1265 return False 1082 return False
1266 1083
1267 if ( 1084 if (
1268 "__IGNORE_WARNING__" in self.__source[node.lineno - 1] 1085 "__IGNORE_WARNING__" in self.source[node.lineno - 1]
1269 or "__IGNORE_WARNING_M251__" in self.__source[node.lineno - 1] 1086 or "__IGNORE_WARNING_M-251__" in self.source[node.lineno - 1]
1087 or "noqa: M-251" in self.source[node.lineno - 1]
1270 ): 1088 ):
1271 return False 1089 return False
1272 1090
1273 lineNumbers = [key.lineno for key in node.keys] 1091 lineNumbers = [key.lineno for key in node.keys]
1274 return len(lineNumbers) == len(set(lineNumbers)) 1092 return len(lineNumbers) == len(set(lineNumbers))
1275 1093
1276 def __checkDictWithSortedKeys(self): 1094 def __checkDictWithSortedKeys(self):
1277 """ 1095 """
1278 Private method to check, if dictionary keys appear in sorted order. 1096 Private method to check, if dictionary keys appear in sorted order.
1279 """ 1097 """
1280 for node in ast.walk(self.__tree): 1098 for node in ast.walk(self.tree):
1281 if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node): 1099 if isinstance(node, ast.Dict) and self.__dictShouldBeChecked(node):
1282 for key1, key2 in zip(node.keys, node.keys[1:]): 1100 for key1, key2 in zip(node.keys, node.keys[1:]):
1283 if key2.value < key1.value: 1101 if key2.value < key1.value:
1284 self.__error( 1102 self.addErrorFromNode(key2, "M-251", key2.value, key1.value)
1285 key2.lineno - 1,
1286 key2.col_offset,
1287 "M-251",
1288 key2.value,
1289 key1.value,
1290 )
1291 1103
1292 def __checkGettext(self): 1104 def __checkGettext(self):
1293 """ 1105 """
1294 Private method to check the 'gettext' import statement. 1106 Private method to check the 'gettext' import statement.
1295 """ 1107 """
1296 for node in ast.walk(self.__tree): 1108 for node in ast.walk(self.tree):
1297 if isinstance(node, ast.ImportFrom) and any( 1109 if isinstance(node, ast.ImportFrom) and any(
1298 name.asname == "_" for name in node.names 1110 name.asname == "_" for name in node.names
1299 ): 1111 ):
1300 self.__error( 1112 self.addErrorFromNode(node, "M-711", node.names[0].name)
1301 node.lineno - 1, node.col_offset, "M-711", node.names[0].name
1302 )
1303 1113
1304 def __checkBugBear(self): 1114 def __checkBugBear(self):
1305 """ 1115 """
1306 Private method for bugbear checks. 1116 Private method for bugbear checks.
1307 """ 1117 """
1308 visitor = BugBearVisitor() 1118 visitor = BugBearVisitor()
1309 visitor.visit(self.__tree) 1119 visitor.visit(self.tree)
1310 for violation in visitor.violations: 1120 for violation in visitor.violations:
1311 node = violation[0] 1121 self.addErrorFromNode(*violation)
1312 reason = violation[1]
1313 params = violation[2:]
1314 self.__error(node.lineno - 1, node.col_offset, reason, *params)
1315 1122
1316 def __checkReturn(self): 1123 def __checkReturn(self):
1317 """ 1124 """
1318 Private method to check return statements. 1125 Private method to check return statements.
1319 """ 1126 """
1320 visitor = ReturnVisitor() 1127 visitor = ReturnVisitor()
1321 visitor.visit(self.__tree) 1128 visitor.visit(self.tree)
1322 for violation in visitor.violations: 1129 for violation in visitor.violations:
1323 node = violation[0] 1130 self.addErrorFromNode(*violation)
1324 reason = violation[1]
1325 self.__error(node.lineno - 1, node.col_offset, reason)
1326 1131
1327 def __checkDateTime(self): 1132 def __checkDateTime(self):
1328 """ 1133 """
1329 Private method to check use of naive datetime functions. 1134 Private method to check use of naive datetime functions.
1330 """ 1135 """
1331 # step 1: generate an augmented node tree containing parent info 1136 # step 1: generate an augmented node tree containing parent info
1332 # for each child node 1137 # for each child node
1333 tree = copy.deepcopy(self.__tree) 1138 tree = copy.deepcopy(self.tree)
1334 for node in ast.walk(tree): 1139 for node in ast.walk(tree):
1335 for childNode in ast.iter_child_nodes(node): 1140 for childNode in ast.iter_child_nodes(node):
1336 childNode._dtCheckerParent = node 1141 childNode._dtCheckerParent = node
1337 1142
1338 # step 2: perform checks and report issues 1143 # step 2: perform checks and report issues
1339 visitor = DateTimeVisitor() 1144 visitor = DateTimeVisitor()
1340 visitor.visit(tree) 1145 visitor.visit(tree)
1341 for violation in visitor.violations: 1146 for violation in visitor.violations:
1342 node = violation[0] 1147 self.addErrorFromNode(*violation)
1343 reason = violation[1]
1344 self.__error(node.lineno - 1, node.col_offset, reason)
1345 1148
1346 def __checkSysVersion(self): 1149 def __checkSysVersion(self):
1347 """ 1150 """
1348 Private method to check the use of sys.version and sys.version_info. 1151 Private method to check the use of sys.version and sys.version_info.
1349 """ 1152 """
1350 visitor = SysVersionVisitor() 1153 visitor = SysVersionVisitor()
1351 visitor.visit(self.__tree) 1154 visitor.visit(self.tree)
1352 for violation in visitor.violations: 1155 for violation in visitor.violations:
1353 node = violation[0] 1156 self.addErrorFromNode(*violation)
1354 reason = violation[1]
1355 self.__error(node.lineno - 1, node.col_offset, reason)
1356 1157
1357 def __checkProperties(self): 1158 def __checkProperties(self):
1358 """ 1159 """
1359 Private method to check for issue with property related methods. 1160 Private method to check for issue with property related methods.
1360 """ 1161 """
1361 properties = [] 1162 properties = []
1362 for node in ast.walk(self.__tree): 1163 for node in ast.walk(self.tree):
1363 if isinstance(node, ast.ClassDef): 1164 if isinstance(node, ast.ClassDef):
1364 properties.clear() 1165 properties.clear()
1365 1166
1366 elif isinstance(node, ast.FunctionDef): 1167 elif isinstance(node, ast.FunctionDef):
1367 propertyCount = 0 1168 propertyCount = 0
1369 # property getter method 1170 # property getter method
1370 if isinstance(decorator, ast.Name) and decorator.id == "property": 1171 if isinstance(decorator, ast.Name) and decorator.id == "property":
1371 propertyCount += 1 1172 propertyCount += 1
1372 properties.append(node.name) 1173 properties.append(node.name)
1373 if len(node.args.args) != 1: 1174 if len(node.args.args) != 1:
1374 self.__error( 1175 self.addErrorFromNode(node, "M-260", len(node.args.args))
1375 node.lineno - 1,
1376 node.col_offset,
1377 "M-260",
1378 len(node.args.args),
1379 )
1380 1176
1381 if isinstance(decorator, ast.Attribute): 1177 if isinstance(decorator, ast.Attribute):
1382 # property setter method 1178 # property setter method
1383 if decorator.attr == "setter": 1179 if decorator.attr == "setter":
1384 propertyCount += 1 1180 propertyCount += 1
1385 if node.name != decorator.value.id: 1181 if node.name != decorator.value.id:
1386 if node.name in properties: 1182 if node.name in properties:
1387 self.__error( 1183 self.addErrorFromNode(
1388 node.lineno - 1, 1184 node, "M-265", node.name, decorator.value.id
1389 node.col_offset,
1390 "M-265",
1391 node.name,
1392 decorator.value.id,
1393 ) 1185 )
1394 else: 1186 else:
1395 self.__error( 1187 self.addErrorFromNode(
1396 node.lineno - 1, 1188 node, "M-263", decorator.value.id, node.name
1397 node.col_offset,
1398 "M-263",
1399 decorator.value.id,
1400 node.name,
1401 ) 1189 )
1402 if len(node.args.args) != 2: 1190 if len(node.args.args) != 2:
1403 self.__error( 1191 self.addErrorFromNode(
1404 node.lineno - 1, 1192 node, "M-261", len(node.args.args)
1405 node.col_offset,
1406 "M-261",
1407 len(node.args.args),
1408 ) 1193 )
1409 1194
1410 # property deleter method 1195 # property deleter method
1411 if decorator.attr == "deleter": 1196 if decorator.attr == "deleter":
1412 propertyCount += 1 1197 propertyCount += 1
1413 if node.name != decorator.value.id: 1198 if node.name != decorator.value.id:
1414 if node.name in properties: 1199 if node.name in properties:
1415 self.__error( 1200 self.addErrorFromNode(
1416 node.lineno - 1, 1201 node, "M-266", node.name, decorator.value.id
1417 node.col_offset,
1418 "M-266",
1419 node.name,
1420 decorator.value.id,
1421 ) 1202 )
1422 else: 1203 else:
1423 self.__error( 1204 self.addErrorFromNode(
1424 node.lineno - 1, 1205 node, "M-264", decorator.value.id, node.name
1425 node.col_offset,
1426 "M-264",
1427 decorator.value.id,
1428 node.name,
1429 ) 1206 )
1430 if len(node.args.args) != 1: 1207 if len(node.args.args) != 1:
1431 self.__error( 1208 self.addErrorFromNode(
1432 node.lineno - 1, 1209 node, "M-262", len(node.args.args)
1433 node.col_offset,
1434 "M-262",
1435 len(node.args.args),
1436 ) 1210 )
1437 1211
1438 if propertyCount > 1: 1212 if propertyCount > 1:
1439 self.__error(node.lineno - 1, node.col_offset, "M-267", node.name) 1213 self.addErrorFromNode(node, "M-267", node.name)
1440 1214
1441 ####################################################################### 1215 #######################################################################
1442 ## The following methods check for implicitly concatenated strings. 1216 ## The following methods check for implicitly concatenated strings.
1443 ## 1217 ##
1444 ## These methods are adapted from: flake8-implicit-str-concat v0.5.0 1218 ## These methods are adapted from: flake8-implicit-str-concat v0.5.0
1507 tokenize.COMMENT, 1281 tokenize.COMMENT,
1508 ) 1282 )
1509 ) 1283 )
1510 for a, b in pairwise(tokensWithoutWhitespace): 1284 for a, b in pairwise(tokensWithoutWhitespace):
1511 if self.__isImplicitStringConcat(a, b): 1285 if self.__isImplicitStringConcat(a, b):
1512 self.__error( 1286 self.addError(
1513 a.end[0] - 1, 1287 a.end[0],
1514 a.end[1], 1288 a.end[1],
1515 "M-851" if a.end[0] == b.start[0] else "M-852", 1289 "M-851" if a.end[0] == b.start[0] else "M-852",
1516 ) 1290 )
1517 1291
1518 def __checkExplicitStringConcat(self): 1292 def __checkExplicitStringConcat(self):
1519 """ 1293 """
1520 Private method to check for explicitly concatenated strings. 1294 Private method to check for explicitly concatenated strings.
1521 """ 1295 """
1522 for node in ast.walk(self.__tree): 1296 for node in ast.walk(self.tree):
1523 if ( 1297 if (
1524 isinstance(node, ast.BinOp) 1298 isinstance(node, ast.BinOp)
1525 and isinstance(node.op, ast.Add) 1299 and isinstance(node.op, ast.Add)
1526 and all( 1300 and all(
1527 AstUtilities.isBaseString(operand) 1301 AstUtilities.isBaseString(operand)
1528 or isinstance(operand, ast.JoinedStr) 1302 or isinstance(operand, ast.JoinedStr)
1529 for operand in (node.left, node.right) 1303 for operand in (node.left, node.right)
1530 ) 1304 )
1531 ): 1305 ):
1532 self.__error(node.lineno - 1, node.col_offset, "M-853") 1306 self.addErrorFromNode(node, "M-853")
1533 1307
1534 ################################################################################# 1308 #################################################################################
1535 ## The following method checks default match cases. 1309 ## The following method checks default match cases.
1536 ################################################################################# 1310 #################################################################################
1537 1311
1538 def __checkDefaultMatchCase(self): 1312 def __checkDefaultMatchCase(self):
1539 """ 1313 """
1540 Private method to check the default match case. 1314 Private method to check the default match case.
1541 """ 1315 """
1542 visitor = DefaultMatchCaseVisitor() 1316 visitor = DefaultMatchCaseVisitor()
1543 visitor.visit(self.__tree) 1317 visitor.visit(self.tree)
1544 for violation in visitor.violations: 1318 for violation in visitor.violations:
1545 node = violation[0] 1319 self.addErrorFromNode(*violation)
1546 reason = violation[1]
1547 self.__error(node.lineno - 1, node.col_offset, reason)
1548
1549
1550 class TextVisitor(ast.NodeVisitor):
1551 """
1552 Class implementing a node visitor for bytes and str instances.
1553
1554 It tries to detect docstrings as string of the first expression of each
1555 module, class or function.
1556 """
1557
1558 # modeled after the string format flake8 extension
1559
1560 def __init__(self):
1561 """
1562 Constructor
1563 """
1564 super().__init__()
1565 self.nodes = []
1566 self.calls = {}
1567
1568 def __addNode(self, node):
1569 """
1570 Private method to add a node to our list of nodes.
1571
1572 @param node reference to the node to add
1573 @type ast.AST
1574 """
1575 if not hasattr(node, "is_docstring"):
1576 node.is_docstring = False
1577 self.nodes.append(node)
1578
1579 def visit_Constant(self, node):
1580 """
1581 Public method to handle constant nodes.
1582
1583 @param node reference to the bytes node
1584 @type ast.Constant
1585 """
1586 if AstUtilities.isBaseString(node):
1587 self.__addNode(node)
1588 else:
1589 super().generic_visit(node)
1590
1591 def __visitDefinition(self, node):
1592 """
1593 Private method handling class and function definitions.
1594
1595 @param node reference to the node to handle
1596 @type ast.FunctionDef, ast.AsyncFunctionDef or ast.ClassDef
1597 """
1598 # Manually traverse class or function definition
1599 # * Handle decorators normally
1600 # * Use special check for body content
1601 # * Don't handle the rest (e.g. bases)
1602 for decorator in node.decorator_list:
1603 self.visit(decorator)
1604 self.__visitBody(node)
1605
1606 def __visitBody(self, node):
1607 """
1608 Private method to traverse the body of the node manually.
1609
1610 If the first node is an expression which contains a string or bytes it
1611 marks that as a docstring.
1612
1613 @param node reference to the node to traverse
1614 @type ast.AST
1615 """
1616 if (
1617 node.body
1618 and isinstance(node.body[0], ast.Expr)
1619 and AstUtilities.isBaseString(node.body[0].value)
1620 ):
1621 node.body[0].value.is_docstring = True
1622
1623 for subnode in node.body:
1624 self.visit(subnode)
1625
1626 def visit_Module(self, node):
1627 """
1628 Public method to handle a module.
1629
1630 @param node reference to the node to handle
1631 @type ast.Module
1632 """
1633 self.__visitBody(node)
1634
1635 def visit_ClassDef(self, node):
1636 """
1637 Public method to handle a class definition.
1638
1639 @param node reference to the node to handle
1640 @type ast.ClassDef
1641 """
1642 # Skipped nodes: ('name', 'bases', 'keywords', 'starargs', 'kwargs')
1643 self.__visitDefinition(node)
1644
1645 def visit_FunctionDef(self, node):
1646 """
1647 Public method to handle a function definition.
1648
1649 @param node reference to the node to handle
1650 @type ast.FunctionDef
1651 """
1652 # Skipped nodes: ('name', 'args', 'returns')
1653 self.__visitDefinition(node)
1654
1655 def visit_AsyncFunctionDef(self, node):
1656 """
1657 Public method to handle an asynchronous function definition.
1658
1659 @param node reference to the node to handle
1660 @type ast.AsyncFunctionDef
1661 """
1662 # Skipped nodes: ('name', 'args', 'returns')
1663 self.__visitDefinition(node)
1664
1665 def visit_Call(self, node):
1666 """
1667 Public method to handle a function call.
1668
1669 @param node reference to the node to handle
1670 @type ast.Call
1671 """
1672 if isinstance(node.func, ast.Attribute) and node.func.attr == "format":
1673 if AstUtilities.isBaseString(node.func.value):
1674 self.calls[node.func.value] = (node, False)
1675 elif (
1676 isinstance(node.func.value, ast.Name)
1677 and node.func.value.id == "str"
1678 and node.args
1679 and AstUtilities.isBaseString(node.args[0])
1680 ):
1681 self.calls[node.args[0]] = (node, True)
1682 super().generic_visit(node)
1683
1684
1685 #######################################################################
1686 ## BugBearVisitor
1687 ##
1688 ## adapted from: flake8-bugbear v24.12.12
1689 ##
1690 ## Original: Copyright (c) 2016 Łukasz Langa
1691 #######################################################################
1692
1693 BugBearContext = namedtuple("BugBearContext", ["node", "stack"])
1694
1695
1696 @dataclass
1697 class M540CaughtException:
1698 """
1699 Class to hold the data for a caught exception.
1700 """
1701
1702 name: str
1703 hasNote: bool
1704
1705
1706 class M541UnhandledKeyType:
1707 """
1708 Class to hold a dictionary key of a type that we do not check for duplicates.
1709 """
1710
1711
1712 class M541VariableKeyType:
1713 """
1714 Class to hold the name of a variable key type.
1715 """
1716
1717 def __init__(self, name):
1718 """
1719 Constructor
1720
1721 @param name name of the variable key type
1722 @type str
1723 """
1724 self.name = name
1725
1726
1727 class BugBearVisitor(ast.NodeVisitor):
1728 """
1729 Class implementing a node visitor to check for various topics.
1730 """
1731
1732 CONTEXTFUL_NODES = (
1733 ast.Module,
1734 ast.ClassDef,
1735 ast.AsyncFunctionDef,
1736 ast.FunctionDef,
1737 ast.Lambda,
1738 ast.ListComp,
1739 ast.SetComp,
1740 ast.DictComp,
1741 ast.GeneratorExp,
1742 )
1743
1744 FUNCTION_NODES = (
1745 ast.AsyncFunctionDef,
1746 ast.FunctionDef,
1747 ast.Lambda,
1748 )
1749
1750 NodeWindowSize = 4
1751
1752 def __init__(self):
1753 """
1754 Constructor
1755 """
1756 super().__init__()
1757
1758 self.nodeWindow = []
1759 self.violations = []
1760 self.contexts = []
1761
1762 self.__M523Seen = set()
1763 self.__M505Imports = set()
1764 self.__M540CaughtException = None
1765
1766 self.__inTryStar = ""
1767
1768 @property
1769 def nodeStack(self):
1770 """
1771 Public method to get a reference to the most recent node stack.
1772
1773 @return reference to the most recent node stack
1774 @rtype list
1775 """
1776 if len(self.contexts) == 0:
1777 return []
1778
1779 context, stack = self.contexts[-1]
1780 return stack
1781
1782 def __isIdentifier(self, arg):
1783 """
1784 Private method to check if arg is a valid identifier.
1785
1786 See https://docs.python.org/2/reference/lexical_analysis.html#identifiers
1787
1788 @param arg reference to an argument node
1789 @type ast.Node
1790 @return flag indicating a valid identifier
1791 @rtype TYPE
1792 """
1793 if not AstUtilities.isString(arg):
1794 return False
1795
1796 return (
1797 re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", AstUtilities.getValue(arg))
1798 is not None
1799 )
1800
1801 def toNameStr(self, node):
1802 """
1803 Public method to turn Name and Attribute nodes to strings, handling any
1804 depth of attribute accesses.
1805
1806
1807 @param node reference to the node
1808 @type ast.Name or ast.Attribute
1809 @return string representation
1810 @rtype str
1811 """
1812 if isinstance(node, ast.Name):
1813 return node.id
1814 elif isinstance(node, ast.Call):
1815 return self.toNameStr(node.func)
1816 elif isinstance(node, ast.Attribute):
1817 inner = self.toNameStr(node.value)
1818 if inner is None:
1819 return None
1820 return f"{inner}.{node.attr}"
1821 else:
1822 return None
1823
1824 def __typesafeIssubclass(self, obj, classOrTuple):
1825 """
1826 Private method implementing a type safe issubclass() function.
1827
1828 @param obj reference to the object to be tested
1829 @type Any
1830 @param classOrTuple type to check against
1831 @type type
1832 @return flag indicating a subclass
1833 @rtype bool
1834 """
1835 try:
1836 return issubclass(obj, classOrTuple)
1837 except TypeError:
1838 # User code specifies a type that is not a type in our current run.
1839 # Might be their error, might be a difference in our environments.
1840 # We don't know so we ignore this.
1841 return False
1842
1843 def __getAssignedNames(self, loopNode):
1844 """
1845 Private method to get the names of a for loop.
1846
1847 @param loopNode reference to the node to be processed
1848 @type ast.For
1849 @yield DESCRIPTION
1850 @ytype TYPE
1851 """
1852 loopTargets = (ast.For, ast.AsyncFor, ast.comprehension)
1853 for node in self.__childrenInScope(loopNode):
1854 if isinstance(node, (ast.Assign)):
1855 for child in node.targets:
1856 yield from self.__namesFromAssignments(child)
1857 if isinstance(node, loopTargets + (ast.AnnAssign, ast.AugAssign)):
1858 yield from self.__namesFromAssignments(node.target)
1859
1860 def __namesFromAssignments(self, assignTarget):
1861 """
1862 Private method to get names of an assignment.
1863
1864 @param assignTarget reference to the node to be processed
1865 @type ast.Node
1866 @yield name of the assignment
1867 @ytype str
1868 """
1869 if isinstance(assignTarget, ast.Name):
1870 yield assignTarget.id
1871 elif isinstance(assignTarget, ast.Starred):
1872 yield from self.__namesFromAssignments(assignTarget.value)
1873 elif isinstance(assignTarget, (ast.List, ast.Tuple)):
1874 for child in assignTarget.elts:
1875 yield from self.__namesFromAssignments(child)
1876
1877 def __childrenInScope(self, node):
1878 """
1879 Private method to get all child nodes in the given scope.
1880
1881 @param node reference to the node to be processed
1882 @type ast.Node
1883 @yield reference to a child node
1884 @ytype ast.Node
1885 """
1886 yield node
1887 if not isinstance(node, BugBearVisitor.FUNCTION_NODES):
1888 for child in ast.iter_child_nodes(node):
1889 yield from self.__childrenInScope(child)
1890
1891 def __flattenExcepthandler(self, node):
1892 """
1893 Private method to flatten the list of exceptions handled by an except handler.
1894
1895 @param node reference to the node to be processed
1896 @type ast.Node
1897 @yield reference to the exception type node
1898 @ytype ast.Node
1899 """
1900 if not isinstance(node, ast.Tuple):
1901 yield node
1902 return
1903
1904 exprList = node.elts.copy()
1905 while len(exprList):
1906 expr = exprList.pop(0)
1907 if isinstance(expr, ast.Starred) and isinstance(
1908 expr.value, (ast.List, ast.Tuple)
1909 ):
1910 exprList.extend(expr.value.elts)
1911 continue
1912 yield expr
1913
1914 def __checkRedundantExcepthandlers(self, names, node, inTryStar):
1915 """
1916 Private method to check for redundant exception types in an exception handler.
1917
1918 @param names list of exception types to be checked
1919 @type list of ast.Name
1920 @param node reference to the exception handler node
1921 @type ast.ExceptionHandler
1922 @param inTryStar character indicating an 'except*' handler
1923 @type str
1924 @return tuple containing the error data
1925 @rtype tuple of (ast.Node, str, str, str, str)
1926 """
1927 redundantExceptions = {
1928 "OSError": {
1929 # All of these are actually aliases of OSError since Python 3.3
1930 "IOError",
1931 "EnvironmentError",
1932 "WindowsError",
1933 "mmap.error",
1934 "socket.error",
1935 "select.error",
1936 },
1937 "ValueError": {
1938 "binascii.Error",
1939 },
1940 }
1941
1942 # See if any of the given exception names could be removed, e.g. from:
1943 # (MyError, MyError) # duplicate names
1944 # (MyError, BaseException) # everything derives from the Base
1945 # (Exception, TypeError) # builtins where one subclasses another
1946 # (IOError, OSError) # IOError is an alias of OSError since Python3.3
1947 # but note that other cases are impractical to handle from the AST.
1948 # We expect this is mostly useful for users who do not have the
1949 # builtin exception hierarchy memorised, and include a 'shadowed'
1950 # subtype without realising that it's redundant.
1951 good = sorted(set(names), key=names.index)
1952 if "BaseException" in good:
1953 good = ["BaseException"]
1954 # Remove redundant exceptions that the automatic system either handles
1955 # poorly (usually aliases) or can't be checked (e.g. it's not an
1956 # built-in exception).
1957 for primary, equivalents in redundantExceptions.items():
1958 if primary in good:
1959 good = [g for g in good if g not in equivalents]
1960
1961 for name, other in itertools.permutations(tuple(good), 2):
1962 if (
1963 self.__typesafeIssubclass(
1964 getattr(builtins, name, type), getattr(builtins, other, ())
1965 )
1966 and name in good
1967 ):
1968 good.remove(name)
1969 if good != names:
1970 desc = good[0] if len(good) == 1 else "({0})".format(", ".join(good))
1971 as_ = " as " + node.name if node.name is not None else ""
1972 return (node, "M-514", ", ".join(names), as_, desc, inTryStar)
1973
1974 return None
1975
1976 def __walkList(self, nodes):
1977 """
1978 Private method to walk a given list of nodes.
1979
1980 @param nodes list of nodes to walk
1981 @type list of ast.Node
1982 @yield node references as determined by the ast.walk() function
1983 @ytype ast.Node
1984 """
1985 for node in nodes:
1986 yield from ast.walk(node)
1987
1988 def __getNamesFromTuple(self, node):
1989 """
1990 Private method to get the names from an ast.Tuple node.
1991
1992 @param node ast node to be processed
1993 @type ast.Tuple
1994 @yield names
1995 @ytype str
1996 """
1997 for dim in node.elts:
1998 if isinstance(dim, ast.Name):
1999 yield dim.id
2000 elif isinstance(dim, ast.Tuple):
2001 yield from self.__getNamesFromTuple(dim)
2002
2003 def __getDictCompLoopAndNamedExprVarNames(self, node):
2004 """
2005 Private method to get the names of comprehension loop variables.
2006
2007 @param node ast node to be processed
2008 @type ast.DictComp
2009 @yield loop variable names
2010 @ytype str
2011 """
2012 finder = NamedExprFinder()
2013 for gen in node.generators:
2014 if isinstance(gen.target, ast.Name):
2015 yield gen.target.id
2016 elif isinstance(gen.target, ast.Tuple):
2017 yield from self.__getNamesFromTuple(gen.target)
2018
2019 finder.visit(gen.ifs)
2020
2021 yield from finder.getNames().keys()
2022
2023 def __inClassInit(self):
2024 """
2025 Private method to check, if we are inside an '__init__' method.
2026
2027 @return flag indicating being within the '__init__' method
2028 @rtype bool
2029 """
2030 return (
2031 len(self.contexts) >= 2
2032 and isinstance(self.contexts[-2].node, ast.ClassDef)
2033 and isinstance(self.contexts[-1].node, ast.FunctionDef)
2034 and self.contexts[-1].node.name == "__init__"
2035 )
2036
2037 def visit_Return(self, node):
2038 """
2039 Public method to handle 'Return' nodes.
2040
2041 @param node reference to the node to be processed
2042 @type ast.Return
2043 """
2044 if self.__inClassInit() and node.value is not None:
2045 self.violations.append((node, "M-537"))
2046
2047 self.generic_visit(node)
2048
2049 def visit_Yield(self, node):
2050 """
2051 Public method to handle 'Yield' nodes.
2052
2053 @param node reference to the node to be processed
2054 @type ast.Yield
2055 """
2056 if self.__inClassInit():
2057 self.violations.append((node, "M-537"))
2058
2059 self.generic_visit(node)
2060
2061 def visit_YieldFrom(self, node) -> None:
2062 """
2063 Public method to handle 'YieldFrom' nodes.
2064
2065 @param node reference to the node to be processed
2066 @type ast.YieldFrom
2067 """
2068 if self.__inClassInit():
2069 self.violations.append((node, "M-537"))
2070
2071 self.generic_visit(node)
2072
2073 def visit(self, node):
2074 """
2075 Public method to traverse a given AST node.
2076
2077 @param node AST node to be traversed
2078 @type ast.Node
2079 """
2080 isContextful = isinstance(node, BugBearVisitor.CONTEXTFUL_NODES)
2081
2082 if isContextful:
2083 context = BugBearContext(node, [])
2084 self.contexts.append(context)
2085
2086 self.nodeStack.append(node)
2087 self.nodeWindow.append(node)
2088 self.nodeWindow = self.nodeWindow[-BugBearVisitor.NodeWindowSize :]
2089
2090 super().visit(node)
2091
2092 self.nodeStack.pop()
2093
2094 if isContextful:
2095 self.contexts.pop()
2096
2097 self.__checkForM518(node)
2098
2099 def visit_ExceptHandler(self, node):
2100 """
2101 Public method to handle exception handlers.
2102
2103 @param node reference to the node to be processed
2104 @type ast.ExceptHandler
2105 """
2106 if node.type is None:
2107 # bare except is handled by pycodestyle already
2108 self.generic_visit(node)
2109 return
2110
2111 oldM540CaughtException = self.__M540CaughtException
2112 if node.name is None:
2113 self.__M540CaughtException = None
2114 else:
2115 self.__M540CaughtException = M540CaughtException(node.name, False)
2116
2117 names = self.__checkForM513_M514_M529_M530(node)
2118
2119 if "BaseException" in names and not ExceptBaseExceptionVisitor(node).reRaised():
2120 self.violations.append((node, "M-536"))
2121
2122 self.generic_visit(node)
2123
2124 if (
2125 self.__M540CaughtException is not None
2126 and self.__M540CaughtException.hasNote
2127 ):
2128 self.violations.append((node, "M-540"))
2129 self.__M540CaughtException = oldM540CaughtException
2130
2131 def visit_UAdd(self, node):
2132 """
2133 Public method to handle unary additions.
2134
2135 @param node reference to the node to be processed
2136 @type ast.UAdd
2137 """
2138 trailingNodes = list(map(type, self.nodeWindow[-4:]))
2139 if trailingNodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]:
2140 originator = self.nodeWindow[-4]
2141 self.violations.append((originator, "M-502"))
2142
2143 self.generic_visit(node)
2144
2145 def visit_Call(self, node):
2146 """
2147 Public method to handle a function call.
2148
2149 @param node reference to the node to be processed
2150 @type ast.Call
2151 """
2152 isM540AddNote = False
2153
2154 if isinstance(node.func, ast.Attribute):
2155 self.__checkForM505(node)
2156 isM540AddNote = self.__checkForM540AddNote(node.func)
2157 else:
2158 with contextlib.suppress(AttributeError, IndexError):
2159 # bad super() call
2160 if isinstance(node.func, ast.Name) and node.func.id == "super":
2161 args = node.args
2162 if (
2163 len(args) == 2
2164 and isinstance(args[0], ast.Attribute)
2165 and isinstance(args[0].value, ast.Name)
2166 and args[0].value.id == "self"
2167 and args[0].attr == "__class__"
2168 ):
2169 self.violations.append((node, "M-582"))
2170
2171 # bad getattr and setattr
2172 if (
2173 node.func.id in ("getattr", "hasattr")
2174 and node.args[1].value == "__call__"
2175 ):
2176 self.violations.append((node, "M-504"))
2177 if (
2178 node.func.id == "getattr"
2179 and len(node.args) == 2
2180 and self.__isIdentifier(node.args[1])
2181 and iskeyword(AstUtilities.getValue(node.args[1]))
2182 ):
2183 self.violations.append((node, "M-509"))
2184 elif (
2185 node.func.id == "setattr"
2186 and len(node.args) == 3
2187 and self.__isIdentifier(node.args[1])
2188 and iskeyword(AstUtilities.getValue(node.args[1]))
2189 ):
2190 self.violations.append((node, "M-510"))
2191
2192 self.__checkForM526(node)
2193
2194 self.__checkForM528(node)
2195 self.__checkForM534(node)
2196 self.__checkForM539(node)
2197
2198 # no need for copying, if used in nested calls it will be set to None
2199 currentM540CaughtException = self.__M540CaughtException
2200 if not isM540AddNote:
2201 self.__checkForM540Usage(node.args)
2202 self.__checkForM540Usage(node.keywords)
2203
2204 self.generic_visit(node)
2205
2206 if isM540AddNote:
2207 # Avoid nested calls within the parameter list using the variable itself.
2208 # e.g. `e.add_note(str(e))`
2209 self.__M540CaughtException = currentM540CaughtException
2210
2211 def visit_Module(self, node):
2212 """
2213 Public method to handle a module node.
2214
2215 @param node reference to the node to be processed
2216 @type ast.Module
2217 """
2218 self.generic_visit(node)
2219
2220 def visit_Assign(self, node):
2221 """
2222 Public method to handle assignments.
2223
2224 @param node reference to the node to be processed
2225 @type ast.Assign
2226 """
2227 self.__checkForM540Usage(node.value)
2228 if len(node.targets) == 1:
2229 target = node.targets[0]
2230 if (
2231 isinstance(target, ast.Attribute)
2232 and isinstance(target.value, ast.Name)
2233 and (target.value.id, target.attr) == ("os", "environ")
2234 ):
2235 self.violations.append((node, "M-503"))
2236
2237 self.generic_visit(node)
2238
2239 def visit_For(self, node):
2240 """
2241 Public method to handle 'for' statements.
2242
2243 @param node reference to the node to be processed
2244 @type ast.For
2245 """
2246 self.__checkForM507(node)
2247 self.__checkForM520(node)
2248 self.__checkForM523(node)
2249 self.__checkForM531(node)
2250 self.__checkForM569(node)
2251
2252 self.generic_visit(node)
2253
2254 def visit_AsyncFor(self, node):
2255 """
2256 Public method to handle 'for' statements.
2257
2258 @param node reference to the node to be processed
2259 @type ast.AsyncFor
2260 """
2261 self.__checkForM507(node)
2262 self.__checkForM520(node)
2263 self.__checkForM523(node)
2264 self.__checkForM531(node)
2265
2266 self.generic_visit(node)
2267
2268 def visit_While(self, node):
2269 """
2270 Public method to handle 'while' statements.
2271
2272 @param node reference to the node to be processed
2273 @type ast.While
2274 """
2275 self.__checkForM523(node)
2276
2277 self.generic_visit(node)
2278
2279 def visit_ListComp(self, node):
2280 """
2281 Public method to handle list comprehensions.
2282
2283 @param node reference to the node to be processed
2284 @type ast.ListComp
2285 """
2286 self.__checkForM523(node)
2287
2288 self.generic_visit(node)
2289
2290 def visit_SetComp(self, node):
2291 """
2292 Public method to handle set comprehensions.
2293
2294 @param node reference to the node to be processed
2295 @type ast.SetComp
2296 """
2297 self.__checkForM523(node)
2298
2299 self.generic_visit(node)
2300
2301 def visit_DictComp(self, node):
2302 """
2303 Public method to handle dictionary comprehensions.
2304
2305 @param node reference to the node to be processed
2306 @type ast.DictComp
2307 """
2308 self.__checkForM523(node)
2309 self.__checkForM535(node)
2310
2311 self.generic_visit(node)
2312
2313 def visit_GeneratorExp(self, node):
2314 """
2315 Public method to handle generator expressions.
2316
2317 @param node reference to the node to be processed
2318 @type ast.GeneratorExp
2319 """
2320 self.__checkForM523(node)
2321
2322 self.generic_visit(node)
2323
2324 def visit_Assert(self, node):
2325 """
2326 Public method to handle 'assert' statements.
2327
2328 @param node reference to the node to be processed
2329 @type ast.Assert
2330 """
2331 if (
2332 AstUtilities.isNameConstant(node.test)
2333 and AstUtilities.getValue(node.test) is False
2334 ):
2335 self.violations.append((node, "M-511"))
2336
2337 self.generic_visit(node)
2338
2339 def visit_AsyncFunctionDef(self, node):
2340 """
2341 Public method to handle async function definitions.
2342
2343 @param node reference to the node to be processed
2344 @type ast.AsyncFunctionDef
2345 """
2346 self.__checkForM506_M508(node)
2347
2348 self.generic_visit(node)
2349
2350 def visit_FunctionDef(self, node):
2351 """
2352 Public method to handle function definitions.
2353
2354 @param node reference to the node to be processed
2355 @type ast.FunctionDef
2356 """
2357 self.__checkForM506_M508(node)
2358 self.__checkForM519(node)
2359 self.__checkForM521(node)
2360
2361 self.generic_visit(node)
2362
2363 def visit_ClassDef(self, node):
2364 """
2365 Public method to handle class definitions.
2366
2367 @param node reference to the node to be processed
2368 @type ast.ClassDef
2369 """
2370 self.__checkForM521(node)
2371 self.__checkForM524_M527(node)
2372
2373 self.generic_visit(node)
2374
2375 def visit_Try(self, node):
2376 """
2377 Public method to handle 'try' statements.
2378
2379 @param node reference to the node to be processed
2380 @type ast.Try
2381 """
2382 self.__checkForM512(node)
2383 self.__checkForM525(node)
2384
2385 self.generic_visit(node)
2386
2387 def visit_TryStar(self, node):
2388 """
2389 Public method to handle 'except*' statements.
2390
2391 @param node reference to the node to be processed
2392 @type ast.TryStar
2393 """
2394 outerTryStar = self.__inTryStar
2395 self.__inTryStar = "*"
2396 self.visit_Try(node)
2397 self.__inTryStar = outerTryStar
2398
2399 def visit_Compare(self, node):
2400 """
2401 Public method to handle comparison statements.
2402
2403 @param node reference to the node to be processed
2404 @type ast.Compare
2405 """
2406 self.__checkForM515(node)
2407
2408 self.generic_visit(node)
2409
2410 def visit_Raise(self, node):
2411 """
2412 Public method to handle 'raise' statements.
2413
2414 @param node reference to the node to be processed
2415 @type ast.Raise
2416 """
2417 if node.exc is None:
2418 self.__M540CaughtException = None
2419 else:
2420 self.__checkForM540Usage(node.exc)
2421 self.__checkForM540Usage(node.cause)
2422 self.__checkForM516(node)
2423
2424 self.generic_visit(node)
2425
2426 def visit_With(self, node):
2427 """
2428 Public method to handle 'with' statements.
2429
2430 @param node reference to the node to be processed
2431 @type ast.With
2432 """
2433 self.__checkForM517(node)
2434 self.__checkForM522(node)
2435
2436 self.generic_visit(node)
2437
2438 def visit_JoinedStr(self, node):
2439 """
2440 Public method to handle f-string arguments.
2441
2442 @param node reference to the node to be processed
2443 @type ast.JoinedStr
2444 """
2445 for value in node.values:
2446 if isinstance(value, ast.FormattedValue):
2447 return
2448
2449 self.violations.append((node, "M-581"))
2450
2451 def visit_AnnAssign(self, node):
2452 """
2453 Public method to check annotated assign statements.
2454
2455 @param node reference to the node to be processed
2456 @type ast.AnnAssign
2457 """
2458 self.__checkForM532(node)
2459 self.__checkForM540Usage(node.value)
2460
2461 self.generic_visit(node)
2462
2463 def visit_Import(self, node):
2464 """
2465 Public method to check imports.
2466
2467 @param node reference to the node to be processed
2468 @type ast.Import
2469 """
2470 self.__checkForM505(node)
2471
2472 self.generic_visit(node)
2473
2474 def visit_ImportFrom(self, node):
2475 """
2476 Public method to check from imports.
2477
2478 @param node reference to the node to be processed
2479 @type ast.Import
2480 """
2481 self.visit_Import(node)
2482
2483 def visit_Set(self, node):
2484 """
2485 Public method to check a set.
2486
2487 @param node reference to the node to be processed
2488 @type ast.Set
2489 """
2490 self.__checkForM533(node)
2491
2492 self.generic_visit(node)
2493
2494 def visit_Dict(self, node):
2495 """
2496 Public method to check a dictionary.
2497
2498 @param node reference to the node to be processed
2499 @type ast.Dict
2500 """
2501 self.__checkForM541(node)
2502
2503 self.generic_visit(node)
2504
2505 def __checkForM505(self, node):
2506 """
2507 Private method to check the use of *strip().
2508
2509 @param node reference to the node to be processed
2510 @type ast.Call
2511 """
2512 if isinstance(node, ast.Import):
2513 for name in node.names:
2514 self.__M505Imports.add(name.asname or name.name)
2515 elif isinstance(node, ast.ImportFrom):
2516 for name in node.names:
2517 self.__M505Imports.add(f"{node.module}.{name.name or name.asname}")
2518 elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
2519 if node.func.attr not in ("lstrip", "rstrip", "strip"):
2520 return # method name doesn't match
2521
2522 if (
2523 isinstance(node.func.value, ast.Name)
2524 and node.func.value.id in self.__M505Imports
2525 ):
2526 return # method is being run on an imported module
2527
2528 if len(node.args) != 1 or not AstUtilities.isString(node.args[0]):
2529 return # used arguments don't match the builtin strip
2530
2531 value = AstUtilities.getValue(node.args[0])
2532 if len(value) == 1:
2533 return # stripping just one character
2534
2535 if len(value) == len(set(value)):
2536 return # no characters appear more than once
2537
2538 self.violations.append((node, "M-505"))
2539
2540 def __checkForM506_M508(self, node):
2541 """
2542 Private method to check the use of mutable literals, comprehensions and calls.
2543
2544 @param node reference to the node to be processed
2545 @type ast.AsyncFunctionDef or ast.FunctionDef
2546 """
2547 visitor = FunctionDefDefaultsVisitor("M-506", "M-508")
2548 visitor.visit(node.args.defaults + node.args.kw_defaults)
2549 self.violations.extend(visitor.errors)
2550
2551 def __checkForM507(self, node):
2552 """
2553 Private method to check for unused loop variables.
2554
2555 @param node reference to the node to be processed
2556 @type ast.For or ast.AsyncFor
2557 """
2558 targets = NameFinder()
2559 targets.visit(node.target)
2560 ctrlNames = set(filter(lambda s: not s.startswith("_"), targets.getNames()))
2561 body = NameFinder()
2562 for expr in node.body:
2563 body.visit(expr)
2564 usedNames = set(body.getNames())
2565 for name in sorted(ctrlNames - usedNames):
2566 n = targets.getNames()[name][0]
2567 self.violations.append((n, "M-507", name))
2568
2569 def __checkForM512(self, node):
2570 """
2571 Private method to check for return/continue/break inside finally blocks.
2572
2573 @param node reference to the node to be processed
2574 @type ast.Try
2575 """
2576
2577 def _loop(node, badNodeTypes):
2578 if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)):
2579 return
2580
2581 if isinstance(node, (ast.While, ast.For)):
2582 badNodeTypes = (ast.Return,)
2583
2584 elif isinstance(node, badNodeTypes):
2585 self.violations.append((node, "M-512", self.__inTryStar))
2586
2587 for child in ast.iter_child_nodes(node):
2588 _loop(child, badNodeTypes)
2589
2590 for child in node.finalbody:
2591 _loop(child, (ast.Return, ast.Continue, ast.Break))
2592
2593 def __checkForM513_M514_M529_M530(self, node):
2594 """
2595 Private method to check various exception handler situations.
2596
2597 @param node reference to the node to be processed
2598 @type ast.ExceptHandler
2599 @return list of exception handler names
2600 @rtype list of str
2601 """
2602 handlers = self.__flattenExcepthandler(node.type)
2603 names = []
2604 badHandlers = []
2605 ignoredHandlers = []
2606
2607 for handler in handlers:
2608 if isinstance(handler, (ast.Name, ast.Attribute)):
2609 name = self.toNameStr(handler)
2610 if name is None:
2611 ignoredHandlers.append(handler)
2612 else:
2613 names.append(name)
2614 elif isinstance(handler, (ast.Call, ast.Starred)):
2615 ignoredHandlers.append(handler)
2616 else:
2617 badHandlers.append(handler)
2618 if badHandlers:
2619 self.violations.append((node, "M-530"))
2620 if len(names) == 0 and not badHandlers and not ignoredHandlers:
2621 self.violations.append((node, "M-529", self.__inTryStar))
2622 elif (
2623 len(names) == 1
2624 and not badHandlers
2625 and not ignoredHandlers
2626 and isinstance(node.type, ast.Tuple)
2627 ):
2628 self.violations.append((node, "M-513", *names, self.__inTryStar))
2629 else:
2630 maybeError = self.__checkRedundantExcepthandlers(
2631 names, node, self.__inTryStar
2632 )
2633 if maybeError is not None:
2634 self.violations.append(maybeError)
2635 return names
2636
2637 def __checkForM515(self, node):
2638 """
2639 Private method to check for pointless comparisons.
2640
2641 @param node reference to the node to be processed
2642 @type ast.Compare
2643 """
2644 if isinstance(self.nodeStack[-2], ast.Expr):
2645 self.violations.append((node, "M-515"))
2646
2647 def __checkForM516(self, node):
2648 """
2649 Private method to check for raising a literal instead of an exception.
2650
2651 @param node reference to the node to be processed
2652 @type ast.Raise
2653 """
2654 if (
2655 AstUtilities.isNameConstant(node.exc)
2656 or AstUtilities.isNumber(node.exc)
2657 or AstUtilities.isString(node.exc)
2658 ):
2659 self.violations.append((node, "M-516"))
2660
2661 def __checkForM517(self, node):
2662 """
2663 Private method to check for use of the evil syntax
2664 'with assertRaises(Exception): or 'with pytest.raises(Exception):'.
2665
2666 @param node reference to the node to be processed
2667 @type ast.With
2668 """
2669 item = node.items[0]
2670 itemContext = item.context_expr
2671 if (
2672 hasattr(itemContext, "func")
2673 and (
2674 (
2675 isinstance(itemContext.func, ast.Attribute)
2676 and (
2677 itemContext.func.attr == "assertRaises"
2678 or (
2679 itemContext.func.attr == "raises"
2680 and isinstance(itemContext.func.value, ast.Name)
2681 and itemContext.func.value.id == "pytest"
2682 and "match" not in (kwd.arg for kwd in itemContext.keywords)
2683 )
2684 )
2685 )
2686 or (
2687 isinstance(itemContext.func, ast.Name)
2688 and itemContext.func.id == "raises"
2689 and isinstance(itemContext.func.ctx, ast.Load)
2690 and "pytest.raises" in self.__M505Imports
2691 and "match" not in (kwd.arg for kwd in itemContext.keywords)
2692 )
2693 )
2694 and len(itemContext.args) == 1
2695 and isinstance(itemContext.args[0], ast.Name)
2696 and itemContext.args[0].id in ("Exception", "BaseException")
2697 and not item.optional_vars
2698 ):
2699 self.violations.append((node, "M-517"))
2700
2701 def __checkForM518(self, node):
2702 """
2703 Private method to check for useless expressions.
2704
2705 @param node reference to the node to be processed
2706 @type ast.FunctionDef
2707 """
2708 if not isinstance(node, ast.Expr):
2709 return
2710
2711 if isinstance(
2712 node.value,
2713 (ast.List, ast.Set, ast.Dict, ast.Tuple),
2714 ) or (
2715 isinstance(node.value, ast.Constant)
2716 and (
2717 isinstance(
2718 node.value.value,
2719 (int, float, complex, bytes, bool),
2720 )
2721 or node.value.value is None
2722 )
2723 ):
2724 self.violations.append((node, "M-518", node.value.__class__.__name__))
2725
2726 def __checkForM519(self, node):
2727 """
2728 Private method to check for use of 'functools.lru_cache' or 'functools.cache'.
2729
2730 @param node reference to the node to be processed
2731 @type ast.FunctionDef
2732 """
2733 caches = {
2734 "functools.cache",
2735 "functools.lru_cache",
2736 "cache",
2737 "lru_cache",
2738 }
2739
2740 if (
2741 len(node.decorator_list) == 0
2742 or len(self.contexts) < 2
2743 or not isinstance(self.contexts[-2].node, ast.ClassDef)
2744 ):
2745 return
2746
2747 # Preserve decorator order so we can get the lineno from the decorator node
2748 # rather than the function node (this location definition changes in Python 3.8)
2749 resolvedDecorators = (
2750 ".".join(composeCallPath(decorator)) for decorator in node.decorator_list
2751 )
2752 for idx, decorator in enumerate(resolvedDecorators):
2753 if decorator in {"classmethod", "staticmethod"}:
2754 return
2755
2756 if decorator in caches:
2757 self.violations.append((node.decorator_list[idx], "M-519"))
2758 return
2759
2760 def __checkForM520(self, node):
2761 """
2762 Private method to check for a loop that modifies its iterable.
2763
2764 @param node reference to the node to be processed
2765 @type ast.For or ast.AsyncFor
2766 """
2767 targets = NameFinder()
2768 targets.visit(node.target)
2769 ctrlNames = set(targets.getNames())
2770
2771 iterset = M520NameFinder()
2772 iterset.visit(node.iter)
2773 itersetNames = set(iterset.getNames())
2774
2775 for name in sorted(ctrlNames):
2776 if name in itersetNames:
2777 n = targets.getNames()[name][0]
2778 self.violations.append((n, "M-520"))
2779
2780 def __checkForM521(self, node):
2781 """
2782 Private method to check for use of an f-string as docstring.
2783
2784 @param node reference to the node to be processed
2785 @type ast.FunctionDef or ast.ClassDef
2786 """
2787 if (
2788 node.body
2789 and isinstance(node.body[0], ast.Expr)
2790 and isinstance(node.body[0].value, ast.JoinedStr)
2791 ):
2792 self.violations.append((node.body[0].value, "M-521"))
2793
2794 def __checkForM522(self, node):
2795 """
2796 Private method to check for use of an f-string as docstring.
2797
2798 @param node reference to the node to be processed
2799 @type ast.With
2800 """
2801 item = node.items[0]
2802 itemContext = item.context_expr
2803 if (
2804 hasattr(itemContext, "func")
2805 and hasattr(itemContext.func, "value")
2806 and hasattr(itemContext.func.value, "id")
2807 and itemContext.func.value.id == "contextlib"
2808 and hasattr(itemContext.func, "attr")
2809 and itemContext.func.attr == "suppress"
2810 and len(itemContext.args) == 0
2811 ):
2812 self.violations.append((node, "M-522"))
2813
2814 def __checkForM523(self, loopNode):
2815 """
2816 Private method to check that functions (including lambdas) do not use loop
2817 variables.
2818
2819 @param loopNode reference to the node to be processed
2820 @type ast.For, ast.AsyncFor, ast.While, ast.ListComp, ast.SetComp,ast.DictComp,
2821 or ast.GeneratorExp
2822 """
2823 safe_functions = []
2824 suspiciousVariables = []
2825 for node in ast.walk(loopNode):
2826 # check if function is immediately consumed to avoid false alarm
2827 if isinstance(node, ast.Call):
2828 # check for filter&reduce
2829 if (
2830 isinstance(node.func, ast.Name)
2831 and node.func.id in ("filter", "reduce", "map")
2832 ) or (
2833 isinstance(node.func, ast.Attribute)
2834 and node.func.attr == "reduce"
2835 and isinstance(node.func.value, ast.Name)
2836 and node.func.value.id == "functools"
2837 ):
2838 for arg in node.args:
2839 if isinstance(arg, BugBearVisitor.FUNCTION_NODES):
2840 safe_functions.append(arg)
2841
2842 # check for key=
2843 for keyword in node.keywords:
2844 if keyword.arg == "key" and isinstance(
2845 keyword.value, BugBearVisitor.FUNCTION_NODES
2846 ):
2847 safe_functions.append(keyword.value)
2848
2849 # mark `return lambda: x` as safe
2850 # does not (currently) check inner lambdas in a returned expression
2851 # e.g. `return (lambda: x, )
2852 if isinstance(node, ast.Return) and isinstance(
2853 node.value, BugBearVisitor.FUNCTION_NODES
2854 ):
2855 safe_functions.append(node.value)
2856
2857 # find unsafe functions
2858 if (
2859 isinstance(node, BugBearVisitor.FUNCTION_NODES)
2860 and node not in safe_functions
2861 ):
2862 argnames = {
2863 arg.arg for arg in ast.walk(node.args) if isinstance(arg, ast.arg)
2864 }
2865 if isinstance(node, ast.Lambda):
2866 bodyNodes = ast.walk(node.body)
2867 else:
2868 bodyNodes = itertools.chain.from_iterable(map(ast.walk, node.body))
2869 errors = []
2870 for name in bodyNodes:
2871 if isinstance(name, ast.Name) and name.id not in argnames:
2872 if isinstance(name.ctx, ast.Load):
2873 errors.append((name.lineno, name.col_offset, name.id, name))
2874 elif isinstance(name.ctx, ast.Store):
2875 argnames.add(name.id)
2876 for err in errors:
2877 if err[2] not in argnames and err not in self.__M523Seen:
2878 self.__M523Seen.add(err) # dedupe across nested loops
2879 suspiciousVariables.append(err)
2880
2881 if suspiciousVariables:
2882 reassignedInLoop = set(self.__getAssignedNames(loopNode))
2883
2884 for err in sorted(suspiciousVariables):
2885 if reassignedInLoop.issuperset(err[2]):
2886 self.violations.append((err[3], "M-523", err[2]))
2887
2888 def __checkForM524_M527(self, node):
2889 """
2890 Private method to check for inheritance from abstract classes in abc and lack of
2891 any methods decorated with abstract*.
2892
2893 @param node reference to the node to be processed
2894 @type ast.ClassDef
2895 """ # __IGNORE_WARNING_D-234r__
2896
2897 def isAbcClass(value, name="ABC"):
2898 if isinstance(value, ast.keyword):
2899 return value.arg == "metaclass" and isAbcClass(value.value, "ABCMeta")
2900
2901 # class foo(ABC)
2902 # class foo(abc.ABC)
2903 return (isinstance(value, ast.Name) and value.id == name) or (
2904 isinstance(value, ast.Attribute)
2905 and value.attr == name
2906 and isinstance(value.value, ast.Name)
2907 and value.value.id == "abc"
2908 )
2909
2910 def isAbstractDecorator(expr):
2911 return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or (
2912 isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract"
2913 )
2914
2915 def isOverload(expr):
2916 return (isinstance(expr, ast.Name) and expr.id == "overload") or (
2917 isinstance(expr, ast.Attribute) and expr.attr == "overload"
2918 )
2919
2920 def emptyBody(body):
2921 def isStrOrEllipsis(node):
2922 return isinstance(node, ast.Constant) and (
2923 node.value is Ellipsis or isinstance(node.value, str)
2924 )
2925
2926 # Function body consist solely of `pass`, `...`, and/or (doc)string literals
2927 return all(
2928 isinstance(stmt, ast.Pass)
2929 or (isinstance(stmt, ast.Expr) and isStrOrEllipsis(stmt.value))
2930 for stmt in body
2931 )
2932
2933 # don't check multiple inheritance
2934 if len(node.bases) + len(node.keywords) > 1:
2935 return
2936
2937 # only check abstract classes
2938 if not any(map(isAbcClass, (*node.bases, *node.keywords))):
2939 return
2940
2941 hasMethod = False
2942 hasAbstractMethod = False
2943
2944 if not any(map(isAbcClass, (*node.bases, *node.keywords))):
2945 return
2946
2947 for stmt in node.body:
2948 # Ignore abc's that declares a class attribute that must be set
2949 if isinstance(stmt, ast.AnnAssign) and stmt.value is None:
2950 hasAbstractMethod = True
2951 continue
2952
2953 # only check function defs
2954 if not isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
2955 continue
2956 hasMethod = True
2957
2958 hasAbstractDecorator = any(map(isAbstractDecorator, stmt.decorator_list))
2959
2960 hasAbstractMethod |= hasAbstractDecorator
2961
2962 if (
2963 not hasAbstractDecorator
2964 and emptyBody(stmt.body)
2965 and not any(map(isOverload, stmt.decorator_list))
2966 ):
2967 self.violations.append((stmt, "M-527", stmt.name))
2968
2969 if hasMethod and not hasAbstractMethod:
2970 self.violations.append((node, "M-524", node.name))
2971
2972 def __checkForM525(self, node):
2973 """
2974 Private method to check for exceptions being handled multiple times.
2975
2976 @param node reference to the node to be processed
2977 @type ast.Try
2978 """
2979 seen = []
2980
2981 for handler in node.handlers:
2982 if isinstance(handler.type, (ast.Name, ast.Attribute)):
2983 name = ".".join(composeCallPath(handler.type))
2984 seen.append(name)
2985 elif isinstance(handler.type, ast.Tuple):
2986 # to avoid checking the same as M514, remove duplicates per except
2987 uniques = set()
2988 for entry in handler.type.elts:
2989 name = ".".join(composeCallPath(entry))
2990 uniques.add(name)
2991 seen.extend(uniques)
2992
2993 # sort to have a deterministic output
2994 duplicates = sorted({x for x in seen if seen.count(x) > 1})
2995 for duplicate in duplicates:
2996 self.violations.append((node, "M-525", duplicate, self.__inTryStar))
2997
2998 def __checkForM526(self, node):
2999 """
3000 Private method to check for Star-arg unpacking after keyword argument.
3001
3002 @param node reference to the node to be processed
3003 @type ast.Call
3004 """
3005 if not node.keywords:
3006 return
3007
3008 starreds = [arg for arg in node.args if isinstance(arg, ast.Starred)]
3009 if not starreds:
3010 return
3011
3012 firstKeyword = node.keywords[0].value
3013 for starred in starreds:
3014 if (starred.lineno, starred.col_offset) > (
3015 firstKeyword.lineno,
3016 firstKeyword.col_offset,
3017 ):
3018 self.violations.append((node, "M-526"))
3019
3020 def __checkForM528(self, node):
3021 """
3022 Private method to check for warn without stacklevel.
3023
3024 @param node reference to the node to be processed
3025 @type ast.Call
3026 """
3027 if (
3028 isinstance(node.func, ast.Attribute)
3029 and node.func.attr == "warn"
3030 and isinstance(node.func.value, ast.Name)
3031 and node.func.value.id == "warnings"
3032 and not any(kw.arg == "stacklevel" for kw in node.keywords)
3033 and len(node.args) < 3
3034 and not any(isinstance(a, ast.Starred) for a in node.args)
3035 and not any(kw.arg is None for kw in node.keywords)
3036 ):
3037 self.violations.append((node, "M-528"))
3038
3039 def __checkForM531(self, loopNode):
3040 """
3041 Private method to check that 'itertools.groupby' isn't iterated over more than
3042 once.
3043
3044 A warning is emitted when the generator returned by 'groupby()' is used
3045 more than once inside a loop body or when it's used in a nested loop.
3046
3047 @param loopNode reference to the node to be processed
3048 @type ast.For or ast.AsyncFor
3049 """
3050 # for <loop_node.target> in <loop_node.iter>: ...
3051 if isinstance(loopNode.iter, ast.Call):
3052 node = loopNode.iter
3053 if (isinstance(node.func, ast.Name) and node.func.id in ("groupby",)) or (
3054 isinstance(node.func, ast.Attribute)
3055 and node.func.attr == "groupby"
3056 and isinstance(node.func.value, ast.Name)
3057 and node.func.value.id == "itertools"
3058 ):
3059 # We have an invocation of groupby which is a simple unpacking
3060 if isinstance(loopNode.target, ast.Tuple) and isinstance(
3061 loopNode.target.elts[1], ast.Name
3062 ):
3063 groupName = loopNode.target.elts[1].id
3064 else:
3065 # Ignore any 'groupby()' invocation that isn't unpacked
3066 return
3067
3068 numUsages = 0
3069 for node in self.__walkList(loopNode.body):
3070 # Handled nested loops
3071 if isinstance(node, ast.For):
3072 for nestedNode in self.__walkList(node.body):
3073 if (
3074 isinstance(nestedNode, ast.Name)
3075 and nestedNode.id == groupName
3076 ):
3077 self.violations.append((nestedNode, "M-531"))
3078
3079 # Handle multiple uses
3080 if isinstance(node, ast.Name) and node.id == groupName:
3081 numUsages += 1
3082 if numUsages > 1:
3083 self.violations.append((nestedNode, "M-531"))
3084
3085 def __checkForM532(self, node):
3086 """
3087 Private method to check for possible unintentional typing annotation.
3088
3089 @param node reference to the node to be processed
3090 @type ast.AnnAssign
3091 """
3092 if (
3093 node.value is None
3094 and hasattr(node.target, "value")
3095 and isinstance(node.target.value, ast.Name)
3096 and (
3097 isinstance(node.target, ast.Subscript)
3098 or (
3099 isinstance(node.target, ast.Attribute)
3100 and node.target.value.id != "self"
3101 )
3102 )
3103 ):
3104 self.violations.append((node, "M-532"))
3105
3106 def __checkForM533(self, node):
3107 """
3108 Private method to check a set for duplicate items.
3109
3110 @param node reference to the node to be processed
3111 @type ast.Set
3112 """
3113 seen = set()
3114 for elt in node.elts:
3115 if not isinstance(elt, ast.Constant):
3116 continue
3117 if elt.value in seen:
3118 self.violations.append((node, "M-533", repr(elt.value)))
3119 else:
3120 seen.add(elt.value)
3121
3122 def __checkForM534(self, node):
3123 """
3124 Private method to check that re.sub/subn/split arguments flags/count/maxsplit
3125 are passed as keyword arguments.
3126
3127 @param node reference to the node to be processed
3128 @type ast.Call
3129 """
3130 if not isinstance(node.func, ast.Attribute):
3131 return
3132 func = node.func
3133 if not isinstance(func.value, ast.Name) or func.value.id != "re":
3134 return
3135
3136 def check(numArgs, paramName):
3137 if len(node.args) > numArgs:
3138 arg = node.args[numArgs]
3139 self.violations.append((arg, "M-534", func.attr, paramName))
3140
3141 if func.attr in ("sub", "subn"):
3142 check(3, "count")
3143 elif func.attr == "split":
3144 check(2, "maxsplit")
3145
3146 def __checkForM535(self, node):
3147 """
3148 Private method to check that a static key isn't used in a dict comprehension.
3149
3150 Record a warning if a likely unchanging key is used - either a constant,
3151 or a variable that isn't coming from the generator expression.
3152
3153 @param node reference to the node to be processed
3154 @type ast.DictComp
3155 """
3156 if isinstance(node.key, ast.Constant):
3157 self.violations.append((node, "M-535", node.key.value))
3158 elif isinstance(
3159 node.key, ast.Name
3160 ) and node.key.id not in self.__getDictCompLoopAndNamedExprVarNames(node):
3161 self.violations.append((node, "M-535", node.key.id))
3162
3163 def __checkForM539(self, node):
3164 """
3165 Private method to check for correct ContextVar usage.
3166
3167 @param node reference to the node to be processed
3168 @type ast.Call
3169 """
3170 if not (
3171 (isinstance(node.func, ast.Name) and node.func.id == "ContextVar")
3172 or (
3173 isinstance(node.func, ast.Attribute)
3174 and node.func.attr == "ContextVar"
3175 and isinstance(node.func.value, ast.Name)
3176 and node.func.value.id == "contextvars"
3177 )
3178 ):
3179 return
3180
3181 # ContextVar only takes one kw currently, but better safe than sorry
3182 for kw in node.keywords:
3183 if kw.arg == "default":
3184 break
3185 else:
3186 return
3187
3188 visitor = FunctionDefDefaultsVisitor("M-539", "M-539")
3189 visitor.visit(kw.value)
3190 self.violations.extend(visitor.errors)
3191
3192 def __checkForM540AddNote(self, node):
3193 """
3194 Private method to check add_note usage.
3195
3196 @param node reference to the node to be processed
3197 @type ast.Attribute
3198 @return flag
3199 @rtype bool
3200 """
3201 if (
3202 node.attr == "add_note"
3203 and isinstance(node.value, ast.Name)
3204 and self.__M540CaughtException
3205 and node.value.id == self.__M540CaughtException.name
3206 ):
3207 self.__M540CaughtException.hasNote = True
3208 return True
3209
3210 return False
3211
3212 def __checkForM540Usage(self, node):
3213 """
3214 Private method to check the usage of exceptions with added note.
3215
3216 @param node reference to the node to be processed
3217 @type ast.expr or None
3218 """ # noqa: D-234y
3219
3220 def superwalk(node: ast.AST | list[ast.AST]):
3221 """
3222 Function to walk an AST node or a list of AST nodes.
3223
3224 @param node reference to the node or a list of nodes to be processed
3225 @type ast.AST or list[ast.AST]
3226 @yield next node to be processed
3227 @ytype ast.AST
3228 """
3229 if isinstance(node, list):
3230 for n in node:
3231 yield from ast.walk(n)
3232 else:
3233 yield from ast.walk(node)
3234
3235 if not self.__M540CaughtException or node is None:
3236 return
3237
3238 for n in superwalk(node):
3239 if isinstance(n, ast.Name) and n.id == self.__M540CaughtException.name:
3240 self.__M540CaughtException = None
3241 break
3242
3243 def __checkForM541(self, node):
3244 """
3245 Private method to check for duplicate key value pairs in a dictionary literal.
3246
3247 @param node reference to the node to be processed
3248 @type ast.Dict
3249 """ # noqa: D-234r
3250
3251 def convertToValue(item):
3252 """
3253 Function to extract the value of a given item.
3254
3255 @param item node to extract value from
3256 @type ast.Ast
3257 @return value of the node
3258 @rtype Any
3259 """
3260 if isinstance(item, ast.Constant):
3261 return item.value
3262 elif isinstance(item, ast.Tuple):
3263 return tuple(convertToValue(i) for i in item.elts)
3264 elif isinstance(item, ast.Name):
3265 return M541VariableKeyType(item.id)
3266 else:
3267 return M541UnhandledKeyType()
3268
3269 keys = [convertToValue(key) for key in node.keys]
3270 keyCounts = Counter(keys)
3271 duplicateKeys = [key for key, count in keyCounts.items() if count > 1]
3272 for key in duplicateKeys:
3273 keyIndices = [i for i, iKey in enumerate(keys) if iKey == key]
3274 seen = set()
3275 for index in keyIndices:
3276 value = convertToValue(node.values[index])
3277 if value in seen:
3278 keyNode = node.keys[index]
3279 self.violations.append((keyNode, "M-541"))
3280 seen.add(value)
3281
3282 def __checkForM569(self, node):
3283 """
3284 Private method to check for changes to a loop's mutable iterable.
3285
3286 @param node loop node to be checked
3287 @type ast.For
3288 """
3289 if isinstance(node.iter, ast.Name):
3290 name = self.toNameStr(node.iter)
3291 elif isinstance(node.iter, ast.Attribute):
3292 name = self.toNameStr(node.iter)
3293 else:
3294 return
3295 checker = M569Checker(name, self)
3296 checker.visit(node.body)
3297 for mutation in checker.mutations:
3298 self.violations.append((mutation, "M-569"))
3299
3300
3301 class M569Checker(ast.NodeVisitor):
3302 """
3303 Class traversing a 'for' loop body to check for modifications to a loop's
3304 mutable iterable.
3305 """
3306
3307 # https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types
3308 MUTATING_FUNCTIONS = (
3309 "append",
3310 "sort",
3311 "reverse",
3312 "remove",
3313 "clear",
3314 "extend",
3315 "insert",
3316 "pop",
3317 "popitem",
3318 )
3319
3320 def __init__(self, name, bugbear):
3321 """
3322 Constructor
3323
3324 @param name name of the iterator
3325 @type str
3326 @param bugbear reference to the bugbear visitor
3327 @type BugBearVisitor
3328 """
3329 self.__name = name
3330 self.__bb = bugbear
3331 self.mutations = []
3332
3333 def visit_Delete(self, node):
3334 """
3335 Public method handling 'Delete' nodes.
3336
3337 @param node reference to the node to be processed
3338 @type ast.Delete
3339 """
3340 for target in node.targets:
3341 if isinstance(target, ast.Subscript):
3342 name = self.__bb.toNameStr(target.value)
3343 elif isinstance(target, (ast.Attribute, ast.Name)):
3344 name = self.__bb.toNameStr(target)
3345 else:
3346 name = "" # fallback
3347 self.generic_visit(target)
3348
3349 if name == self.__name:
3350 self.mutations.append(node)
3351
3352 def visit_Call(self, node):
3353 """
3354 Public method handling 'Call' nodes.
3355
3356 @param node reference to the node to be processed
3357 @type ast.Call
3358 """
3359 if isinstance(node.func, ast.Attribute):
3360 name = self.__bb.toNameStr(node.func.value)
3361 functionObject = name
3362 functionName = node.func.attr
3363
3364 if (
3365 functionObject == self.__name
3366 and functionName in self.MUTATING_FUNCTIONS
3367 ):
3368 self.mutations.append(node)
3369
3370 self.generic_visit(node)
3371
3372 def visit(self, node):
3373 """
3374 Public method to inspect an ast node.
3375
3376 Like super-visit but supports iteration over lists.
3377
3378 @param node AST node to be traversed
3379 @type TYPE
3380 @return reference to the last processed node
3381 @rtype ast.Node
3382 """
3383 if not isinstance(node, list):
3384 return super().visit(node)
3385
3386 for elem in node:
3387 super().visit(elem)
3388 return node
3389
3390
3391 class ExceptBaseExceptionVisitor(ast.NodeVisitor):
3392 """
3393 Class to determine, if a 'BaseException' is re-raised.
3394 """
3395
3396 def __init__(self, exceptNode):
3397 """
3398 Constructor
3399
3400 @param exceptNode exception node to be inspected
3401 @type ast.ExceptHandler
3402 """
3403 super().__init__()
3404 self.__root = exceptNode
3405 self.__reRaised = False
3406
3407 def reRaised(self) -> bool:
3408 """
3409 Public method to check, if the exception is re-raised.
3410
3411 @return flag indicating a re-raised exception
3412 @rtype bool
3413 """
3414 self.visit(self.__root)
3415 return self.__reRaised
3416
3417 def visit_Raise(self, node):
3418 """
3419 Public method to handle 'Raise' nodes.
3420
3421 If we find a corresponding `raise` or `raise e` where e was from
3422 `except BaseException as e:` then we mark re_raised as True and can
3423 stop scanning.
3424
3425 @param node reference to the node to be processed
3426 @type ast.Raise
3427 """
3428 if node.exc is None or (
3429 isinstance(node.exc, ast.Name) and node.exc.id == self.__root.name
3430 ):
3431 self.__reRaised = True
3432 return
3433
3434 super().generic_visit(node)
3435
3436 def visit_ExceptHandler(self, node: ast.ExceptHandler):
3437 """
3438 Public method to handle 'ExceptHandler' nodes.
3439
3440 @param node reference to the node to be processed
3441 @type ast.ExceptHandler
3442 """
3443 if node is not self.__root:
3444 return # entered a nested except - stop searching
3445
3446 super().generic_visit(node)
3447
3448
3449 class NameFinder(ast.NodeVisitor):
3450 """
3451 Class to extract a name out of a tree of nodes.
3452 """
3453
3454 def __init__(self):
3455 """
3456 Constructor
3457 """
3458 super().__init__()
3459
3460 self.__names = {}
3461
3462 def visit_Name(self, node):
3463 """
3464 Public method to handle 'Name' nodes.
3465
3466 @param node reference to the node to be processed
3467 @type ast.Name
3468 """
3469 self.__names.setdefault(node.id, []).append(node)
3470
3471 def visit(self, node):
3472 """
3473 Public method to traverse a given AST node.
3474
3475 @param node AST node to be traversed
3476 @type ast.Node
3477 @return reference to the last processed node
3478 @rtype ast.Node
3479 """
3480 if isinstance(node, list):
3481 for elem in node:
3482 super().visit(elem)
3483 return node
3484 else:
3485 return super().visit(node)
3486
3487 def getNames(self):
3488 """
3489 Public method to return the extracted names and Name nodes.
3490
3491 @return dictionary containing the names as keys and the list of nodes
3492 @rtype dict
3493 """
3494 return self.__names
3495
3496
3497 class NamedExprFinder(ast.NodeVisitor):
3498 """
3499 Class to extract names defined through an ast.NamedExpr.
3500 """
3501
3502 def __init__(self):
3503 """
3504 Constructor
3505 """
3506 super().__init__()
3507
3508 self.__names = {}
3509
3510 def visit_NamedExpr(self, node: ast.NamedExpr):
3511 """
3512 Public method handling 'NamedExpr' nodes.
3513
3514 @param node reference to the node to be processed
3515 @type ast.NamedExpr
3516 """
3517 self.__names.setdefault(node.target.id, []).append(node.target)
3518
3519 self.generic_visit(node)
3520
3521 def visit(self, node):
3522 """
3523 Public method to traverse a given AST node.
3524
3525 Like super-visit but supports iteration over lists.
3526
3527 @param node AST node to be traversed
3528 @type TYPE
3529 @return reference to the last processed node
3530 @rtype ast.Node
3531 """
3532 if not isinstance(node, list):
3533 super().visit(node)
3534
3535 for elem in node:
3536 super().visit(elem)
3537
3538 return node
3539
3540 def getNames(self):
3541 """
3542 Public method to return the extracted names and Name nodes.
3543
3544 @return dictionary containing the names as keys and the list of nodes
3545 @rtype dict
3546 """
3547 return self.__names
3548
3549
3550 class FunctionDefDefaultsVisitor(ast.NodeVisitor):
3551 """
3552 Class used by M506, M508 and M539.
3553 """
3554
3555 def __init__(
3556 self,
3557 errorCodeCalls, # M506 or M539
3558 errorCodeLiterals, # M508 or M539
3559 ):
3560 """
3561 Constructor
3562
3563 @param errorCodeCalls error code for ast.Call nodes
3564 @type str
3565 @param errorCodeLiterals error code for literal nodes
3566 @type str
3567 """
3568 self.__errorCodeCalls = errorCodeCalls
3569 self.__errorCodeLiterals = errorCodeLiterals
3570 for nodeType in BugbearMutableLiterals + BugbearMutableComprehensions:
3571 setattr(
3572 self, f"visit_{nodeType}", self.__visitMutableLiteralOrComprehension
3573 )
3574 self.errors = []
3575 self.__argDepth = 0
3576
3577 super().__init__()
3578
3579 def __visitMutableLiteralOrComprehension(self, node):
3580 """
3581 Private method to flag mutable literals and comprehensions.
3582
3583 @param node AST node to be processed
3584 @type ast.Dict, ast.List, ast.Set, ast.ListComp, ast.DictComp or ast.SetComp
3585 """
3586 # Flag M506 if mutable literal/comprehension is not nested.
3587 # We only flag these at the top level of the expression as we
3588 # cannot easily guarantee that nested mutable structures are not
3589 # made immutable by outer operations, so we prefer no false positives.
3590 # e.g.
3591 # >>> def this_is_fine(a=frozenset({"a", "b", "c"})): ...
3592 #
3593 # >>> def this_is_not_fine_but_hard_to_detect(a=(lambda x: x)([1, 2, 3]))
3594 #
3595 # We do still search for cases of B008 within mutable structures though.
3596 if self.__argDepth == 1:
3597 self.errors.append((node, self.__errorCodeCalls))
3598
3599 # Check for nested functions.
3600 self.generic_visit(node)
3601
3602 def visit_Call(self, node):
3603 """
3604 Public method to process Call nodes.
3605
3606 @param node AST node to be processed
3607 @type ast.Call
3608 """
3609 callPath = ".".join(composeCallPath(node.func))
3610 if callPath in BugbearMutableCalls:
3611 self.errors.append((node, self.__errorCodeCalls))
3612 self.generic_visit(node)
3613 return
3614
3615 if callPath in BugbearImmutableCalls:
3616 self.generic_visit(node)
3617 return
3618
3619 # Check if function call is actually a float infinity/NaN literal
3620 if callPath == "float" and len(node.args) == 1:
3621 try:
3622 value = float(ast.literal_eval(node.args[0]))
3623 except Exception: # secok
3624 pass
3625 else:
3626 if math.isfinite(value):
3627 self.errors.append((node, self.__errorCodeLiterals))
3628 else:
3629 self.errors.append((node, self.__errorCodeLiterals))
3630
3631 # Check for nested functions.
3632 self.generic_visit(node)
3633
3634 def visit_Lambda(self, node):
3635 """
3636 Public method to process Lambda nodes.
3637
3638 @param node AST node to be processed
3639 @type ast.Lambda
3640 """
3641 # Don't recurse into lambda expressions
3642 # as they are evaluated at call time.
3643 pass
3644
3645 def visit(self, node):
3646 """
3647 Public method to traverse an AST node or a list of AST nodes.
3648
3649 This is an extended method that can also handle a list of AST nodes.
3650
3651 @param node AST node or list of AST nodes to be processed
3652 @type ast.AST or list of ast.AST
3653 """
3654 self.__argDepth += 1
3655 if isinstance(node, list):
3656 for elem in node:
3657 if elem is not None:
3658 super().visit(elem)
3659 else:
3660 super().visit(node)
3661 self.__argDepth -= 1
3662
3663
3664 class M520NameFinder(NameFinder):
3665 """
3666 Class to extract a name out of a tree of nodes ignoring names defined within the
3667 local scope of a comprehension.
3668 """
3669
3670 def visit_GeneratorExp(self, node):
3671 """
3672 Public method to handle a generator expressions.
3673
3674 @param node reference to the node to be processed
3675 @type ast.GeneratorExp
3676 """
3677 self.visit(node.generators)
3678
3679 def visit_ListComp(self, node):
3680 """
3681 Public method to handle a list comprehension.
3682
3683 @param node reference to the node to be processed
3684 @type TYPE
3685 """
3686 self.visit(node.generators)
3687
3688 def visit_DictComp(self, node):
3689 """
3690 Public method to handle a dictionary comprehension.
3691
3692 @param node reference to the node to be processed
3693 @type TYPE
3694 """
3695 self.visit(node.generators)
3696
3697 def visit_comprehension(self, node):
3698 """
3699 Public method to handle the 'for' of a comprehension.
3700
3701 @param node reference to the node to be processed
3702 @type ast.comprehension
3703 """
3704 self.visit(node.iter)
3705
3706 def visit_Lambda(self, node):
3707 """
3708 Public method to handle a Lambda function.
3709
3710 @param node reference to the node to be processed
3711 @type ast.Lambda
3712 """
3713 self.visit(node.body)
3714 for lambdaArg in node.args.args:
3715 self.getNames().pop(lambdaArg.arg, None)
3716
3717
3718 class ReturnVisitor(ast.NodeVisitor):
3719 """
3720 Class implementing a node visitor to check return statements.
3721 """
3722
3723 Assigns = "assigns"
3724 Refs = "refs"
3725 Returns = "returns"
3726
3727 def __init__(self):
3728 """
3729 Constructor
3730 """
3731 super().__init__()
3732
3733 self.__stack = []
3734 self.violations = []
3735 self.__loopCount = 0
3736
3737 @property
3738 def assigns(self):
3739 """
3740 Public method to get the Assign nodes.
3741
3742 @return dictionary containing the node name as key and line number
3743 as value
3744 @rtype dict
3745 """
3746 return self.__stack[-1][ReturnVisitor.Assigns]
3747
3748 @property
3749 def refs(self):
3750 """
3751 Public method to get the References nodes.
3752
3753 @return dictionary containing the node name as key and line number
3754 as value
3755 @rtype dict
3756 """
3757 return self.__stack[-1][ReturnVisitor.Refs]
3758
3759 @property
3760 def returns(self):
3761 """
3762 Public method to get the Return nodes.
3763
3764 @return dictionary containing the node name as key and line number
3765 as value
3766 @rtype dict
3767 """
3768 return self.__stack[-1][ReturnVisitor.Returns]
3769
3770 def visit_For(self, node):
3771 """
3772 Public method to handle a for loop.
3773
3774 @param node reference to the for node to handle
3775 @type ast.For
3776 """
3777 self.__visitLoop(node)
3778
3779 def visit_AsyncFor(self, node):
3780 """
3781 Public method to handle an async for loop.
3782
3783 @param node reference to the async for node to handle
3784 @type ast.AsyncFor
3785 """
3786 self.__visitLoop(node)
3787
3788 def visit_While(self, node):
3789 """
3790 Public method to handle a while loop.
3791
3792 @param node reference to the while node to handle
3793 @type ast.While
3794 """
3795 self.__visitLoop(node)
3796
3797 def __visitLoop(self, node):
3798 """
3799 Private method to handle loop nodes.
3800
3801 @param node reference to the loop node to handle
3802 @type ast.For, ast.AsyncFor or ast.While
3803 """
3804 self.__loopCount += 1
3805 self.generic_visit(node)
3806 self.__loopCount -= 1
3807
3808 def __visitWithStack(self, node):
3809 """
3810 Private method to traverse a given function node using a stack.
3811
3812 @param node AST node to be traversed
3813 @type ast.FunctionDef or ast.AsyncFunctionDef
3814 """
3815 self.__stack.append(
3816 {
3817 ReturnVisitor.Assigns: defaultdict(list),
3818 ReturnVisitor.Refs: defaultdict(list),
3819 ReturnVisitor.Returns: [],
3820 }
3821 )
3822
3823 self.generic_visit(node)
3824 self.__checkFunction(node)
3825 self.__stack.pop()
3826
3827 def visit_FunctionDef(self, node):
3828 """
3829 Public method to handle a function definition.
3830
3831 @param node reference to the node to handle
3832 @type ast.FunctionDef
3833 """
3834 self.__visitWithStack(node)
3835
3836 def visit_AsyncFunctionDef(self, node):
3837 """
3838 Public method to handle a function definition.
3839
3840 @param node reference to the node to handle
3841 @type ast.AsyncFunctionDef
3842 """
3843 self.__visitWithStack(node)
3844
3845 def visit_Return(self, node):
3846 """
3847 Public method to handle a return node.
3848
3849 @param node reference to the node to handle
3850 @type ast.Return
3851 """
3852 self.returns.append(node)
3853 self.generic_visit(node)
3854
3855 def visit_Assign(self, node):
3856 """
3857 Public method to handle an assign node.
3858
3859 @param node reference to the node to handle
3860 @type ast.Assign
3861 """
3862 if not self.__stack:
3863 return
3864
3865 self.generic_visit(node.value)
3866
3867 target = node.targets[0]
3868 if isinstance(target, ast.Tuple) and not isinstance(node.value, ast.Tuple):
3869 # skip unpacking assign
3870 return
3871
3872 self.__visitAssignTarget(target)
3873
3874 def visit_Name(self, node):
3875 """
3876 Public method to handle a name node.
3877
3878 @param node reference to the node to handle
3879 @type ast.Name
3880 """
3881 if self.__stack:
3882 self.refs[node.id].append(node.lineno)
3883
3884 def __visitAssignTarget(self, node):
3885 """
3886 Private method to handle an assign target node.
3887
3888 @param node reference to the node to handle
3889 @type ast.AST
3890 """
3891 if isinstance(node, ast.Tuple):
3892 for elt in node.elts:
3893 self.__visitAssignTarget(elt)
3894 return
3895
3896 if not self.__loopCount and isinstance(node, ast.Name):
3897 self.assigns[node.id].append(node.lineno)
3898 return
3899
3900 self.generic_visit(node)
3901
3902 def __checkFunction(self, node):
3903 """
3904 Private method to check a function definition node.
3905
3906 @param node reference to the node to check
3907 @type ast.AsyncFunctionDef or ast.FunctionDef
3908 """
3909 if not self.returns or not node.body:
3910 return
3911
3912 if len(node.body) == 1 and isinstance(node.body[-1], ast.Return):
3913 # skip functions that consist of `return None` only
3914 return
3915
3916 if not self.__resultExists():
3917 self.__checkUnnecessaryReturnNone()
3918 return
3919
3920 self.__checkImplicitReturnValue()
3921 self.__checkImplicitReturn(node.body[-1])
3922
3923 for n in self.returns:
3924 if n.value:
3925 self.__checkUnnecessaryAssign(n.value)
3926
3927 def __isNone(self, node):
3928 """
3929 Private method to check, if a node value is None.
3930
3931 @param node reference to the node to check
3932 @type ast.AST
3933 @return flag indicating the node contains a None value
3934 @rtype bool
3935 """
3936 return AstUtilities.isNameConstant(node) and AstUtilities.getValue(node) is None
3937
3938 def __isFalse(self, node):
3939 """
3940 Private method to check, if a node value is False.
3941
3942 @param node reference to the node to check
3943 @type ast.AST
3944 @return flag indicating the node contains a False value
3945 @rtype bool
3946 """
3947 return (
3948 AstUtilities.isNameConstant(node) and AstUtilities.getValue(node) is False
3949 )
3950
3951 def __resultExists(self):
3952 """
3953 Private method to check the existance of a return result.
3954
3955 @return flag indicating the existence of a return result
3956 @rtype bool
3957 """
3958 for node in self.returns:
3959 value = node.value
3960 if value and not self.__isNone(value):
3961 return True
3962
3963 return False
3964
3965 def __checkImplicitReturnValue(self):
3966 """
3967 Private method to check for implicit return values.
3968 """
3969 for node in self.returns:
3970 if not node.value:
3971 self.violations.append((node, "M-832"))
3972
3973 def __checkUnnecessaryReturnNone(self):
3974 """
3975 Private method to check for an unnecessary 'return None' statement.
3976 """
3977 for node in self.returns:
3978 if self.__isNone(node.value):
3979 self.violations.append((node, "M-831"))
3980
3981 def __checkImplicitReturn(self, node):
3982 """
3983 Private method to check for an implicit return statement.
3984
3985 @param node reference to the node to check
3986 @type ast.AST
3987 """
3988 if isinstance(node, ast.If):
3989 if not node.body or not node.orelse:
3990 self.violations.append((node, "M-833"))
3991 return
3992
3993 self.__checkImplicitReturn(node.body[-1])
3994 self.__checkImplicitReturn(node.orelse[-1])
3995 return
3996
3997 if isinstance(node, (ast.For, ast.AsyncFor)) and node.orelse:
3998 self.__checkImplicitReturn(node.orelse[-1])
3999 return
4000
4001 if isinstance(node, (ast.With, ast.AsyncWith)):
4002 self.__checkImplicitReturn(node.body[-1])
4003 return
4004
4005 if isinstance(node, ast.Assert) and self.__isFalse(node.test):
4006 return
4007
4008 try:
4009 okNodes = (ast.Return, ast.Raise, ast.While, ast.Try)
4010 except AttributeError:
4011 okNodes = (ast.Return, ast.Raise, ast.While)
4012 if not isinstance(node, okNodes):
4013 self.violations.append((node, "M-833"))
4014
4015 def __checkUnnecessaryAssign(self, node):
4016 """
4017 Private method to check for an unnecessary assign statement.
4018
4019 @param node reference to the node to check
4020 @type ast.AST
4021 """
4022 if not isinstance(node, ast.Name):
4023 return
4024
4025 varname = node.id
4026 returnLineno = node.lineno
4027
4028 if varname not in self.assigns:
4029 return
4030
4031 if varname not in self.refs:
4032 self.violations.append((node, "M-834"))
4033 return
4034
4035 if self.__hasRefsBeforeNextAssign(varname, returnLineno):
4036 return
4037
4038 self.violations.append((node, "M-834"))
4039
4040 def __hasRefsBeforeNextAssign(self, varname, returnLineno):
4041 """
4042 Private method to check for references before a following assign
4043 statement.
4044
4045 @param varname variable name to check for
4046 @type str
4047 @param returnLineno line number of the return statement
4048 @type int
4049 @return flag indicating the existence of references
4050 @rtype bool
4051 """
4052 beforeAssign = 0
4053 afterAssign = None
4054
4055 for lineno in sorted(self.assigns[varname]):
4056 if lineno > returnLineno:
4057 afterAssign = lineno
4058 break
4059
4060 if lineno <= returnLineno:
4061 beforeAssign = lineno
4062
4063 for lineno in self.refs[varname]:
4064 if lineno == returnLineno:
4065 continue
4066
4067 if afterAssign:
4068 if beforeAssign < lineno <= afterAssign:
4069 return True
4070
4071 elif beforeAssign < lineno:
4072 return True
4073
4074 return False
4075
4076
4077 class DateTimeVisitor(ast.NodeVisitor):
4078 """
4079 Class implementing a node visitor to check datetime function calls.
4080
4081 Note: This class is modeled after flake8_datetimez checker.
4082 """
4083
4084 def __init__(self):
4085 """
4086 Constructor
4087 """
4088 super().__init__()
4089
4090 self.violations = []
4091
4092 def __getFromKeywords(self, keywords, name):
4093 """
4094 Private method to get a keyword node given its name.
4095
4096 @param keywords list of keyword argument nodes
4097 @type list of ast.AST
4098 @param name name of the keyword node
4099 @type str
4100 @return keyword node
4101 @rtype ast.AST
4102 """
4103 for keyword in keywords:
4104 if keyword.arg == name:
4105 return keyword
4106
4107 return None
4108
4109 def visit_Call(self, node):
4110 """
4111 Public method to handle a function call.
4112
4113 Every datetime related function call is check for use of the naive
4114 variant (i.e. use without TZ info).
4115
4116 @param node reference to the node to be processed
4117 @type ast.Call
4118 """
4119 # datetime.something()
4120 isDateTimeClass = (
4121 isinstance(node.func, ast.Attribute)
4122 and isinstance(node.func.value, ast.Name)
4123 and node.func.value.id == "datetime"
4124 )
4125
4126 # datetime.datetime.something()
4127 isDateTimeModuleAndClass = (
4128 isinstance(node.func, ast.Attribute)
4129 and isinstance(node.func.value, ast.Attribute)
4130 and node.func.value.attr == "datetime"
4131 and isinstance(node.func.value.value, ast.Name)
4132 and node.func.value.value.id == "datetime"
4133 )
4134
4135 if isDateTimeClass:
4136 if node.func.attr == "datetime":
4137 # datetime.datetime(2000, 1, 1, 0, 0, 0, 0,
4138 # datetime.timezone.utc)
4139 isCase1 = len(node.args) >= 8 and not (
4140 AstUtilities.isNameConstant(node.args[7])
4141 and AstUtilities.getValue(node.args[7]) is None
4142 )
4143
4144 # datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
4145 tzinfoKeyword = self.__getFromKeywords(node.keywords, "tzinfo")
4146 isCase2 = tzinfoKeyword is not None and not (
4147 AstUtilities.isNameConstant(tzinfoKeyword.value)
4148 and AstUtilities.getValue(tzinfoKeyword.value) is None
4149 )
4150
4151 if not (isCase1 or isCase2):
4152 self.violations.append((node, "M-301"))
4153
4154 elif node.func.attr == "time":
4155 # time(12, 10, 45, 0, datetime.timezone.utc)
4156 isCase1 = len(node.args) >= 5 and not (
4157 AstUtilities.isNameConstant(node.args[4])
4158 and AstUtilities.getValue(node.args[4]) is None
4159 )
4160
4161 # datetime.time(12, 10, 45, tzinfo=datetime.timezone.utc)
4162 tzinfoKeyword = self.__getFromKeywords(node.keywords, "tzinfo")
4163 isCase2 = tzinfoKeyword is not None and not (
4164 AstUtilities.isNameConstant(tzinfoKeyword.value)
4165 and AstUtilities.getValue(tzinfoKeyword.value) is None
4166 )
4167
4168 if not (isCase1 or isCase2):
4169 self.violations.append((node, "M-321"))
4170
4171 elif node.func.attr == "date":
4172 self.violations.append((node, "M-311"))
4173
4174 if isDateTimeClass or isDateTimeModuleAndClass:
4175 if node.func.attr == "today":
4176 self.violations.append((node, "M-302"))
4177
4178 elif node.func.attr == "utcnow":
4179 self.violations.append((node, "M-303"))
4180
4181 elif node.func.attr == "utcfromtimestamp":
4182 self.violations.append((node, "M-304"))
4183
4184 elif node.func.attr in "now":
4185 # datetime.now(UTC)
4186 isCase1 = (
4187 len(node.args) == 1
4188 and len(node.keywords) == 0
4189 and not (
4190 AstUtilities.isNameConstant(node.args[0])
4191 and AstUtilities.getValue(node.args[0]) is None
4192 )
4193 )
4194
4195 # datetime.now(tz=UTC)
4196 tzKeyword = self.__getFromKeywords(node.keywords, "tz")
4197 isCase2 = tzKeyword is not None and not (
4198 AstUtilities.isNameConstant(tzKeyword.value)
4199 and AstUtilities.getValue(tzKeyword.value) is None
4200 )
4201
4202 if not (isCase1 or isCase2):
4203 self.violations.append((node, "M-305"))
4204
4205 elif node.func.attr == "fromtimestamp":
4206 # datetime.fromtimestamp(1234, UTC)
4207 isCase1 = (
4208 len(node.args) == 2
4209 and len(node.keywords) == 0
4210 and not (
4211 AstUtilities.isNameConstant(node.args[1])
4212 and AstUtilities.getValue(node.args[1]) is None
4213 )
4214 )
4215
4216 # datetime.fromtimestamp(1234, tz=UTC)
4217 tzKeyword = self.__getFromKeywords(node.keywords, "tz")
4218 isCase2 = tzKeyword is not None and not (
4219 AstUtilities.isNameConstant(tzKeyword.value)
4220 and AstUtilities.getValue(tzKeyword.value) is None
4221 )
4222
4223 if not (isCase1 or isCase2):
4224 self.violations.append((node, "M-306"))
4225
4226 elif node.func.attr == "strptime":
4227 # datetime.strptime(...).replace(tzinfo=UTC)
4228 parent = getattr(node, "_dtCheckerParent", None)
4229 pparent = getattr(parent, "_dtCheckerParent", None)
4230 if not (
4231 isinstance(parent, ast.Attribute) and parent.attr == "replace"
4232 ) or not isinstance(pparent, ast.Call):
4233 isCase1 = False
4234 else:
4235 tzinfoKeyword = self.__getFromKeywords(pparent.keywords, "tzinfo")
4236 isCase1 = tzinfoKeyword is not None and not (
4237 AstUtilities.isNameConstant(tzinfoKeyword.value)
4238 and AstUtilities.getValue(tzinfoKeyword.value) is None
4239 )
4240
4241 if not isCase1:
4242 self.violations.append((node, "M-307"))
4243
4244 elif node.func.attr == "fromordinal":
4245 self.violations.append((node, "M-308"))
4246
4247 # date.something()
4248 isDateClass = (
4249 isinstance(node.func, ast.Attribute)
4250 and isinstance(node.func.value, ast.Name)
4251 and node.func.value.id == "date"
4252 )
4253
4254 # datetime.date.something()
4255 isDateModuleAndClass = (
4256 isinstance(node.func, ast.Attribute)
4257 and isinstance(node.func.value, ast.Attribute)
4258 and node.func.value.attr == "date"
4259 and isinstance(node.func.value.value, ast.Name)
4260 and node.func.value.value.id == "datetime"
4261 )
4262
4263 if isDateClass or isDateModuleAndClass:
4264 if node.func.attr == "today":
4265 self.violations.append((node, "M-312"))
4266
4267 elif node.func.attr == "fromtimestamp":
4268 self.violations.append((node, "M-313"))
4269
4270 elif node.func.attr == "fromordinal":
4271 self.violations.append((node, "M-314"))
4272
4273 elif node.func.attr == "fromisoformat":
4274 self.violations.append((node, "M-315"))
4275
4276 self.generic_visit(node)
4277
4278
4279 class SysVersionVisitor(ast.NodeVisitor):
4280 """
4281 Class implementing a node visitor to check the use of sys.version and
4282 sys.version_info.
4283
4284 Note: This class is modeled after flake8-2020 v1.8.1.
4285 """
4286
4287 def __init__(self):
4288 """
4289 Constructor
4290 """
4291 super().__init__()
4292
4293 self.violations = []
4294 self.__fromImports = {}
4295
4296 def visit_ImportFrom(self, node):
4297 """
4298 Public method to handle a from ... import ... statement.
4299
4300 @param node reference to the node to be processed
4301 @type ast.ImportFrom
4302 """
4303 for alias in node.names:
4304 if node.module is not None and not alias.asname:
4305 self.__fromImports[alias.name] = node.module
4306
4307 self.generic_visit(node)
4308
4309 def __isSys(self, attr, node):
4310 """
4311 Private method to check for a reference to sys attribute.
4312
4313 @param attr attribute name
4314 @type str
4315 @param node reference to the node to be checked
4316 @type ast.Node
4317 @return flag indicating a match
4318 @rtype bool
4319 """
4320 match = False
4321 if (
4322 isinstance(node, ast.Attribute)
4323 and isinstance(node.value, ast.Name)
4324 and node.value.id == "sys"
4325 and node.attr == attr
4326 ) or (
4327 isinstance(node, ast.Name)
4328 and node.id == attr
4329 and self.__fromImports.get(node.id) == "sys"
4330 ):
4331 match = True
4332
4333 return match
4334
4335 def __isSysVersionUpperSlice(self, node, n):
4336 """
4337 Private method to check the upper slice of sys.version.
4338
4339 @param node reference to the node to be checked
4340 @type ast.Node
4341 @param n slice value to check against
4342 @type int
4343 @return flag indicating a match
4344 @rtype bool
4345 """
4346 return (
4347 self.__isSys("version", node.value)
4348 and isinstance(node.slice, ast.Slice)
4349 and node.slice.lower is None
4350 and AstUtilities.isNumber(node.slice.upper)
4351 and AstUtilities.getValue(node.slice.upper) == n
4352 and node.slice.step is None
4353 )
4354
4355 def visit_Subscript(self, node):
4356 """
4357 Public method to handle a subscript.
4358
4359 @param node reference to the node to be processed
4360 @type ast.Subscript
4361 """
4362 if self.__isSysVersionUpperSlice(node, 1):
4363 self.violations.append((node.value, "M-423"))
4364 elif self.__isSysVersionUpperSlice(node, 3):
4365 self.violations.append((node.value, "M-401"))
4366 elif (
4367 self.__isSys("version", node.value)
4368 and isinstance(node.slice, ast.Index)
4369 and AstUtilities.isNumber(node.slice.value)
4370 and AstUtilities.getValue(node.slice.value) == 2
4371 ):
4372 self.violations.append((node.value, "M-402"))
4373 elif (
4374 self.__isSys("version", node.value)
4375 and isinstance(node.slice, ast.Index)
4376 and AstUtilities.isNumber(node.slice.value)
4377 and AstUtilities.getValue(node.slice.value) == 0
4378 ):
4379 self.violations.append((node.value, "M-421"))
4380
4381 self.generic_visit(node)
4382
4383 def visit_Compare(self, node):
4384 """
4385 Public method to handle a comparison.
4386
4387 @param node reference to the node to be processed
4388 @type ast.Compare
4389 """
4390 if (
4391 isinstance(node.left, ast.Subscript)
4392 and self.__isSys("version_info", node.left.value)
4393 and isinstance(node.left.slice, ast.Index)
4394 and AstUtilities.isNumber(node.left.slice.value)
4395 and AstUtilities.getValue(node.left.slice.value) == 0
4396 and len(node.ops) == 1
4397 and isinstance(node.ops[0], ast.Eq)
4398 and AstUtilities.isNumber(node.comparators[0])
4399 and AstUtilities.getValue(node.comparators[0]) == 3
4400 ):
4401 self.violations.append((node.left, "M-411"))
4402 elif (
4403 self.__isSys("version", node.left)
4404 and len(node.ops) == 1
4405 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE))
4406 and AstUtilities.isString(node.comparators[0])
4407 ):
4408 if len(AstUtilities.getValue(node.comparators[0])) == 1:
4409 errorCode = "M-422"
4410 else:
4411 errorCode = "M-403"
4412 self.violations.append((node.left, errorCode))
4413 elif (
4414 isinstance(node.left, ast.Subscript)
4415 and self.__isSys("version_info", node.left.value)
4416 and isinstance(node.left.slice, ast.Index)
4417 and AstUtilities.isNumber(node.left.slice.value)
4418 and AstUtilities.getValue(node.left.slice.value) == 1
4419 and len(node.ops) == 1
4420 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE))
4421 and AstUtilities.isNumber(node.comparators[0])
4422 ):
4423 self.violations.append((node, "M-413"))
4424 elif (
4425 isinstance(node.left, ast.Attribute)
4426 and self.__isSys("version_info", node.left.value)
4427 and node.left.attr == "minor"
4428 and len(node.ops) == 1
4429 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE))
4430 and AstUtilities.isNumber(node.comparators[0])
4431 ):
4432 self.violations.append((node, "M-414"))
4433
4434 self.generic_visit(node)
4435
4436 def visit_Attribute(self, node):
4437 """
4438 Public method to handle an attribute.
4439
4440 @param node reference to the node to be processed
4441 @type ast.Attribute
4442 """
4443 if (
4444 isinstance(node.value, ast.Name)
4445 and node.value.id == "six"
4446 and node.attr == "PY3"
4447 ):
4448 self.violations.append((node, "M-412"))
4449
4450 self.generic_visit(node)
4451
4452 def visit_Name(self, node):
4453 """
4454 Public method to handle an name.
4455
4456 @param node reference to the node to be processed
4457 @type ast.Name
4458 """
4459 if node.id == "PY3" and self.__fromImports.get(node.id) == "six":
4460 self.violations.append((node, "M-412"))
4461
4462 self.generic_visit(node)
4463
4464
4465 class DefaultMatchCaseVisitor(ast.NodeVisitor):
4466 """
4467 Class implementing a node visitor to check default match cases.
4468
4469 Note: This class is modeled after flake8-spm v0.0.1.
4470 """
4471
4472 def __init__(self):
4473 """
4474 Constructor
4475 """
4476 super().__init__()
4477
4478 self.violations = []
4479
4480 def visit_Match(self, node):
4481 """
4482 Public method to handle Match nodes.
4483
4484 @param node reference to the node to be processed
4485 @type ast.Match
4486 """
4487 for badNode, issueCode in self.__badNodes(node):
4488 self.violations.append((badNode, issueCode))
4489
4490 self.generic_visit(node)
4491
4492 def __badNodes(self, node):
4493 """
4494 Private method to yield bad match nodes.
4495
4496 @param node reference to the node to be processed
4497 @type ast.Match
4498 @yield tuple containing a reference to bad match case node and the corresponding
4499 issue code
4500 @ytype tyuple of (ast.AST, str)
4501 """
4502 for case in node.cases:
4503 if self.__emptyMatchDefault(case):
4504 if self.__lastStatementDoesNotRaise(case):
4505 yield self.__findBadNode(case), "M-901"
4506 elif self.__returnPrecedesExceptionRaising(case):
4507 yield self.__findBadNode(case), "M-902"
4508
4509 def __emptyMatchDefault(self, case):
4510 """
4511 Private method to check for an empty default match case.
4512
4513 @param case reference to the node to be processed
4514 @type ast.match_case
4515 @return flag indicating an empty default match case
4516 @rtype bool
4517 """
4518 pattern = case.pattern
4519 return isinstance(pattern, ast.MatchAs) and (
4520 pattern.pattern is None
4521 or (
4522 isinstance(pattern.pattern, ast.MatchAs)
4523 and pattern.pattern.pattern is None
4524 )
4525 )
4526
4527 def __lastStatementDoesNotRaise(self, case):
4528 """
4529 Private method to check that the last case statement does not raise an
4530 exception.
4531
4532 @param case reference to the node to be processed
4533 @type ast.match_case
4534 @return flag indicating that the last case statement does not raise an
4535 exception
4536 @rtype bool
4537 """
4538 return not isinstance(case.body[-1], ast.Raise)
4539
4540 def __returnPrecedesExceptionRaising(self, case):
4541 """
4542 Private method to check that no return precedes an exception raising.
4543
4544 @param case reference to the node to be processed
4545 @type ast.match_case
4546 @return flag indicating that a return precedes an exception raising
4547 @rtype bool
4548 """
4549 returnIndex = -1
4550 raiseIndex = -1
4551 for index, body in enumerate(case.body):
4552 if isinstance(body, ast.Return):
4553 returnIndex = index
4554 elif isinstance(body, ast.Raise):
4555 raiseIndex = index
4556 return returnIndex >= 0 and returnIndex < raiseIndex
4557
4558 def __findBadNode(self, case) -> ast.AST:
4559 """
4560 Private method returning a reference to the bad node of a case node.
4561
4562 @param case reference to the node to be processed
4563 @type ast.match_case
4564 @return reference to the bad node
4565 @rtype ast.AST
4566 """
4567 for body in case.body:
4568 # Handle special case when return precedes exception raising.
4569 # In this case the bad node is that with the return statement.
4570 if isinstance(body, ast.Return):
4571 return body
4572
4573 return case.body[-1]
4574
4575
4576 #
4577 # eflag: noqa = M-891

eric ide

mercurial