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 |
|