blob: 53b75344a23de1bc59d432b95c6e6d84c41793b1 [file] [log] [blame]
# Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
# Copyright (c) 2014-2015 Brett Cannon <brett@python.org>
# Copyright (c) 2015 Simu Toni <simutoni@gmail.com>
# Copyright (c) 2015 Pavel Roskin <proski@gnu.org>
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
# Copyright (c) 2015 Cosmin Poieana <cmin@ropython.org>
# Copyright (c) 2015 Viorel Stirbu <viorels@gmail.com>
# Copyright (c) 2016, 2018 Jakub Wilk <jwilk@jwilk.net>
# Copyright (c) 2016-2017 Roy Williams <roy.williams.iii@gmail.com>
# Copyright (c) 2016 Roy Williams <rwilliams@lyft.com>
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
# Copyright (c) 2016 Erik <erik.eriksson@yahoo.com>
# Copyright (c) 2017, 2020 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2017-2018 Ville Skyttä <ville.skytta@iki.fi>
# Copyright (c) 2017 Daniel Miller <millerdev@gmail.com>
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
# Copyright (c) 2018-2020 Anthony Sottile <asottile@umich.edu>
# Copyright (c) 2018 sbagan <pnlbagan@gmail.com>
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
# Copyright (c) 2018 Aivar Annamaa <aivarannamaa@users.noreply.github.com>
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
# Copyright (c) 2018 Ashley Whetter <ashley@awhetter.co.uk>
# Copyright (c) 2018 gaurikholkar <f2013002@goa.bits-pilani.ac.in>
# Copyright (c) 2019-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
# Copyright (c) 2019 Nick Drozd <nicholasdrozd@gmail.com>
# Copyright (c) 2019 Hugues Bruant <hugues.bruant@affirm.com>
# Copyright (c) 2019 Gabriel R Sezefredo <gabriel@sezefredo.com.br>
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
# Copyright (c) 2019 bluesheeptoken <louis.fruleux1@gmail.com>
# Copyright (c) 2020 Peter Kolbus <peter.kolbus@gmail.com>
# Copyright (c) 2020 谭九鼎 <109224573@qq.com>
# Copyright (c) 2020 Federico Bond <federicobond@gmail.com>
# Copyright (c) 2020 Athos Ribeiro <athoscr@fedoraproject.org>
# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
# Copyright (c) 2021 bot <bot@noreply.github.com>
# Copyright (c) 2021 Tiago Honorato <tiagohonorato1@gmail.com>
# Copyright (c) 2021 tiagohonorato <61059243+tiagohonorato@users.noreply.github.com>
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/LICENSE
"""Check Python 2 code for Python 2/3 source-compatible issues."""
import itertools
import re
import tokenize
from collections import namedtuple
import astroid
from pylint import checkers, interfaces
from pylint.checkers import utils
from pylint.checkers.utils import find_try_except_wrapper_node, node_ignores_exception
from pylint.constants import WarningScope
from pylint.interfaces import INFERENCE, INFERENCE_FAILURE
_ZERO = re.compile("^0+$")
def _is_old_octal(literal):
if _ZERO.match(literal):
return False
if re.match(r"0\d+", literal):
try:
int(literal, 8)
except ValueError:
return False
return True
return None
def _inferred_value_is_dict(value):
if isinstance(value, astroid.Dict):
return True
return isinstance(value, astroid.Instance) and "dict" in value.basenames
def _infer_if_relevant_attr(node, relevant_attrs):
return node.expr.infer() if node.attrname in relevant_attrs else []
def _is_builtin(node):
return getattr(node, "name", None) in ("__builtin__", "builtins")
_ACCEPTS_ITERATOR = {
"iter",
"list",
"tuple",
"sorted",
"set",
"sum",
"any",
"all",
"enumerate",
"dict",
"filter",
"reversed",
"max",
"min",
"frozenset",
"OrderedDict",
"zip",
"map",
}
ATTRIBUTES_ACCEPTS_ITERATOR = {"join", "from_iterable"}
_BUILTIN_METHOD_ACCEPTS_ITERATOR = {
"builtins.list.extend",
"builtins.dict.update",
"builtins.set.update",
}
DICT_METHODS = {"items", "keys", "values"}
def _in_iterating_context(node):
"""Check if the node is being used as an iterator.
Definition is taken from lib2to3.fixer_util.in_special_context().
"""
parent = node.parent
# Since a call can't be the loop variant we only need to know if the node's
# parent is a 'for' loop to know it's being used as the iterator for the
# loop.
if isinstance(parent, astroid.For):
return True
# Need to make sure the use of the node is in the iterator part of the
# comprehension.
if isinstance(parent, astroid.Comprehension):
if parent.iter == node:
return True
# Various built-ins can take in an iterable or list and lead to the same
# value.
elif isinstance(parent, astroid.Call):
if isinstance(parent.func, astroid.Name):
if parent.func.name in _ACCEPTS_ITERATOR:
return True
elif isinstance(parent.func, astroid.Attribute):
if parent.func.attrname in ATTRIBUTES_ACCEPTS_ITERATOR:
return True
inferred = utils.safe_infer(parent.func)
if inferred:
if inferred.qname() in _BUILTIN_METHOD_ACCEPTS_ITERATOR:
return True
root = inferred.root()
if root and root.name == "itertools":
return True
# If the call is in an unpacking, there's no need to warn,
# since it can be considered iterating.
elif isinstance(parent, astroid.Assign) and isinstance(
parent.targets[0], (astroid.List, astroid.Tuple)
):
if len(parent.targets[0].elts) > 1:
return True
# If the call is in a containment check, we consider that to
# be an iterating context
elif (
isinstance(parent, astroid.Compare)
and len(parent.ops) == 1
and parent.ops[0][0] in ["in", "not in"]
):
return True
# Also if it's an `yield from`, that's fair
elif isinstance(parent, astroid.YieldFrom):
return True
if isinstance(parent, astroid.Starred):
return True
return False
def _is_conditional_import(node):
"""Checks if an import node is in the context of a conditional."""
parent = node.parent
return isinstance(
parent, (astroid.TryExcept, astroid.ExceptHandler, astroid.If, astroid.IfExp)
)
Branch = namedtuple("Branch", ["node", "is_py2_only"])
class Python3Checker(checkers.BaseChecker):
__implements__ = interfaces.IAstroidChecker
enabled = False
name = "python3"
msgs = {
# Errors for what will syntactically break in Python 3, warnings for
# everything else.
"E1601": (
"print statement used",
"print-statement",
"Used when a print statement is used "
"(`print` is a function in Python 3)",
),
"E1602": (
"Parameter unpacking specified",
"parameter-unpacking",
"Used when parameter unpacking is specified for a function"
"(Python 3 doesn't allow it)",
),
"E1603": (
"Implicit unpacking of exceptions is not supported in Python 3",
"unpacking-in-except",
"Python3 will not allow implicit unpacking of "
"exceptions in except clauses. "
"See https://www.python.org/dev/peps/pep-3110/",
{"old_names": [("W0712", "old-unpacking-in-except")]},
),
"E1604": (
"Use raise ErrorClass(args) instead of raise ErrorClass, args.",
"old-raise-syntax",
"Used when the alternate raise syntax "
"'raise foo, bar' is used "
"instead of 'raise foo(bar)'.",
{"old_names": [("W0121", "old-old-raise-syntax")]},
),
"E1605": (
"Use of the `` operator",
"backtick",
'Used when the deprecated "``" (backtick) operator is used '
"instead of the str() function.",
{"scope": WarningScope.NODE, "old_names": [("W0333", "old-backtick")]},
),
"E1609": (
"Import * only allowed at module level",
"import-star-module-level",
"Used when the import star syntax is used somewhere "
"else than the module level.",
{"maxversion": (3, 0)},
),
"W1601": (
"apply built-in referenced",
"apply-builtin",
"Used when the apply built-in function is referenced "
"(missing from Python 3)",
),
"W1602": (
"basestring built-in referenced",
"basestring-builtin",
"Used when the basestring built-in function is referenced "
"(missing from Python 3)",
),
"W1603": (
"buffer built-in referenced",
"buffer-builtin",
"Used when the buffer built-in function is referenced "
"(missing from Python 3)",
),
"W1604": (
"cmp built-in referenced",
"cmp-builtin",
"Used when the cmp built-in function is referenced "
"(missing from Python 3)",
),
"W1605": (
"coerce built-in referenced",
"coerce-builtin",
"Used when the coerce built-in function is referenced "
"(missing from Python 3)",
),
"W1606": (
"execfile built-in referenced",
"execfile-builtin",
"Used when the execfile built-in function is referenced "
"(missing from Python 3)",
),
"W1607": (
"file built-in referenced",
"file-builtin",
"Used when the file built-in function is referenced "
"(missing from Python 3)",
),
"W1608": (
"long built-in referenced",
"long-builtin",
"Used when the long built-in function is referenced "
"(missing from Python 3)",
),
"W1609": (
"raw_input built-in referenced",
"raw_input-builtin",
"Used when the raw_input built-in function is referenced "
"(missing from Python 3)",
),
"W1610": (
"reduce built-in referenced",
"reduce-builtin",
"Used when the reduce built-in function is referenced "
"(missing from Python 3)",
),
"W1611": (
"StandardError built-in referenced",
"standarderror-builtin",
"Used when the StandardError built-in function is referenced "
"(missing from Python 3)",
),
"W1612": (
"unicode built-in referenced",
"unicode-builtin",
"Used when the unicode built-in function is referenced "
"(missing from Python 3)",
),
"W1613": (
"xrange built-in referenced",
"xrange-builtin",
"Used when the xrange built-in function is referenced "
"(missing from Python 3)",
),
"W1614": (
"__coerce__ method defined",
"coerce-method",
"Used when a __coerce__ method is defined "
"(method is not used by Python 3)",
),
"W1615": (
"__delslice__ method defined",
"delslice-method",
"Used when a __delslice__ method is defined "
"(method is not used by Python 3)",
),
"W1616": (
"__getslice__ method defined",
"getslice-method",
"Used when a __getslice__ method is defined "
"(method is not used by Python 3)",
),
"W1617": (
"__setslice__ method defined",
"setslice-method",
"Used when a __setslice__ method is defined "
"(method is not used by Python 3)",
),
"W1618": (
"import missing `from __future__ import absolute_import`",
"no-absolute-import",
"Used when an import is not accompanied by "
"``from __future__ import absolute_import`` "
"(default behaviour in Python 3)",
),
"W1619": (
"division w/o __future__ statement",
"old-division",
"Used for non-floor division w/o a float literal or "
"``from __future__ import division`` "
"(Python 3 returns a float for int division unconditionally)",
),
"W1620": (
"Calling a dict.iter*() method",
"dict-iter-method",
"Used for calls to dict.iterkeys(), itervalues() or iteritems() "
"(Python 3 lacks these methods)",
),
"W1621": (
"Calling a dict.view*() method",
"dict-view-method",
"Used for calls to dict.viewkeys(), viewvalues() or viewitems() "
"(Python 3 lacks these methods)",
),
"W1622": (
"Called a next() method on an object",
"next-method-called",
"Used when an object's next() method is called "
"(Python 3 uses the next() built-in function)",
),
"W1623": (
"Assigning to a class's __metaclass__ attribute",
"metaclass-assignment",
"Used when a metaclass is specified by assigning to __metaclass__ "
"(Python 3 specifies the metaclass as a class statement argument)",
),
"W1624": (
"Indexing exceptions will not work on Python 3",
"indexing-exception",
"Indexing exceptions will not work on Python 3. Use "
"`exception.args[index]` instead.",
{"old_names": [("W0713", "old-indexing-exception")]},
),
"W1625": (
"Raising a string exception",
"raising-string",
"Used when a string exception is raised. This will not "
"work on Python 3.",
{"old_names": [("W0701", "old-raising-string")]},
),
"W1626": (
"reload built-in referenced",
"reload-builtin",
"Used when the reload built-in function is referenced "
"(missing from Python 3). You can use instead imp.reload "
"or importlib.reload.",
),
"W1627": (
"__oct__ method defined",
"oct-method",
"Used when an __oct__ method is defined "
"(method is not used by Python 3)",
),
"W1628": (
"__hex__ method defined",
"hex-method",
"Used when a __hex__ method is defined (method is not used by Python 3)",
),
"W1629": (
"__nonzero__ method defined",
"nonzero-method",
"Used when a __nonzero__ method is defined "
"(method is not used by Python 3)",
),
"W1630": (
"__cmp__ method defined",
"cmp-method",
"Used when a __cmp__ method is defined (method is not used by Python 3)",
),
# 'W1631': replaced by W1636
"W1632": (
"input built-in referenced",
"input-builtin",
"Used when the input built-in is referenced "
"(backwards-incompatible semantics in Python 3)",
),
"W1633": (
"round built-in referenced",
"round-builtin",
"Used when the round built-in is referenced "
"(backwards-incompatible semantics in Python 3)",
),
"W1634": (
"intern built-in referenced",
"intern-builtin",
"Used when the intern built-in is referenced "
"(Moved to sys.intern in Python 3)",
),
"W1635": (
"unichr built-in referenced",
"unichr-builtin",
"Used when the unichr built-in is referenced (Use chr in Python 3)",
),
"W1636": (
"map built-in referenced when not iterating",
"map-builtin-not-iterating",
"Used when the map built-in is referenced in a non-iterating "
"context (returns an iterator in Python 3)",
{"old_names": [("W1631", "implicit-map-evaluation")]},
),
"W1637": (
"zip built-in referenced when not iterating",
"zip-builtin-not-iterating",
"Used when the zip built-in is referenced in a non-iterating "
"context (returns an iterator in Python 3)",
),
"W1638": (
"range built-in referenced when not iterating",
"range-builtin-not-iterating",
"Used when the range built-in is referenced in a non-iterating "
"context (returns a range in Python 3)",
),
"W1639": (
"filter built-in referenced when not iterating",
"filter-builtin-not-iterating",
"Used when the filter built-in is referenced in a non-iterating "
"context (returns an iterator in Python 3)",
),
"W1640": (
"Using the cmp argument for list.sort / sorted",
"using-cmp-argument",
"Using the cmp argument for list.sort or the sorted "
"builtin should be avoided, since it was removed in "
"Python 3. Using either `key` or `functools.cmp_to_key` "
"should be preferred.",
),
"W1641": (
"Implementing __eq__ without also implementing __hash__",
"eq-without-hash",
"Used when a class implements __eq__ but not __hash__. In Python 2, objects "
"get object.__hash__ as the default implementation, in Python 3 objects get "
"None as their default __hash__ implementation if they also implement __eq__.",
),
"W1642": (
"__div__ method defined",
"div-method",
"Used when a __div__ method is defined. Using `__truediv__` and setting"
"__div__ = __truediv__ should be preferred."
"(method is not used by Python 3)",
),
"W1643": (
"__idiv__ method defined",
"idiv-method",
"Used when an __idiv__ method is defined. Using `__itruediv__` and setting"
"__idiv__ = __itruediv__ should be preferred."
"(method is not used by Python 3)",
),
"W1644": (
"__rdiv__ method defined",
"rdiv-method",
"Used when a __rdiv__ method is defined. Using `__rtruediv__` and setting"
"__rdiv__ = __rtruediv__ should be preferred."
"(method is not used by Python 3)",
),
"W1645": (
"Exception.message removed in Python 3",
"exception-message-attribute",
"Used when the message attribute is accessed on an Exception. Use "
"str(exception) instead.",
),
"W1646": (
"non-text encoding used in str.decode",
"invalid-str-codec",
"Used when using str.encode or str.decode with a non-text encoding. Use "
"codecs module to handle arbitrary codecs.",
),
"W1647": (
"sys.maxint removed in Python 3",
"sys-max-int",
"Used when accessing sys.maxint. Use sys.maxsize instead.",
),
"W1648": (
"Module moved in Python 3",
"bad-python3-import",
"Used when importing a module that no longer exists in Python 3.",
),
"W1649": (
"Accessing a deprecated function on the string module",
"deprecated-string-function",
"Used when accessing a string function that has been deprecated in Python 3.",
),
"W1650": (
"Using str.translate with deprecated deletechars parameters",
"deprecated-str-translate-call",
"Used when using the deprecated deletechars parameters from str.translate. Use "
"re.sub to remove the desired characters ",
),
"W1651": (
"Accessing a deprecated function on the itertools module",
"deprecated-itertools-function",
"Used when accessing a function on itertools that has been removed in Python 3.",
),
"W1652": (
"Accessing a deprecated fields on the types module",
"deprecated-types-field",
"Used when accessing a field on types that has been removed in Python 3.",
),
"W1653": (
"next method defined",
"next-method-defined",
"Used when a next method is defined that would be an iterator in Python 2 but "
"is treated as a normal function in Python 3.",
),
"W1654": (
"dict.items referenced when not iterating",
"dict-items-not-iterating",
"Used when dict.items is referenced in a non-iterating "
"context (returns an iterator in Python 3)",
),
"W1655": (
"dict.keys referenced when not iterating",
"dict-keys-not-iterating",
"Used when dict.keys is referenced in a non-iterating "
"context (returns an iterator in Python 3)",
),
"W1656": (
"dict.values referenced when not iterating",
"dict-values-not-iterating",
"Used when dict.values is referenced in a non-iterating "
"context (returns an iterator in Python 3)",
),
"W1657": (
"Accessing a removed attribute on the operator module",
"deprecated-operator-function",
"Used when accessing a field on operator module that has been "
"removed in Python 3.",
),
"W1658": (
"Accessing a removed attribute on the urllib module",
"deprecated-urllib-function",
"Used when accessing a field on urllib module that has been "
"removed or moved in Python 3.",
),
"W1659": (
"Accessing a removed xreadlines attribute",
"xreadlines-attribute",
"Used when accessing the xreadlines() function on a file stream, "
"removed in Python 3.",
),
"W1660": (
"Accessing a removed attribute on the sys module",
"deprecated-sys-function",
"Used when accessing a field on sys module that has been "
"removed in Python 3.",
),
"W1661": (
"Using an exception object that was bound by an except handler",
"exception-escape",
"Emitted when using an exception, that was bound in an except "
"handler, outside of the except handler. On Python 3 these "
"exceptions will be deleted once they get out "
"of the except handler.",
),
"W1662": (
"Using a variable that was bound inside a comprehension",
"comprehension-escape",
"Emitted when using a variable, that was bound in a comprehension "
"handler, outside of the comprehension itself. On Python 3 these "
"variables will be deleted outside of the "
"comprehension.",
),
}
_bad_builtins = frozenset(
[
"apply",
"basestring",
"buffer",
"cmp",
"coerce",
"execfile",
"file",
"input", # Not missing, but incompatible semantics
"intern",
"long",
"raw_input",
"reduce",
"round", # Not missing, but incompatible semantics
"StandardError",
"unichr",
"unicode",
"xrange",
"reload",
]
)
_unused_magic_methods = frozenset(
[
"__coerce__",
"__delslice__",
"__getslice__",
"__setslice__",
"__oct__",
"__hex__",
"__nonzero__",
"__cmp__",
"__div__",
"__idiv__",
"__rdiv__",
]
)
_invalid_encodings = frozenset(
[
"base64_codec",
"base64",
"base_64",
"bz2_codec",
"bz2",
"hex_codec",
"hex",
"quopri_codec",
"quopri",
"quotedprintable",
"quoted_printable",
"uu_codec",
"uu",
"zlib_codec",
"zlib",
"zip",
"rot13",
"rot_13",
]
)
_bad_python3_module_map = {
"sys-max-int": {"sys": frozenset(["maxint"])},
"deprecated-itertools-function": {
"itertools": frozenset(
["izip", "ifilter", "imap", "izip_longest", "ifilterfalse"]
)
},
"deprecated-types-field": {
"types": frozenset(
[
"EllipsisType",
"XRangeType",
"ComplexType",
"StringType",
"TypeType",
"LongType",
"UnicodeType",
"ClassType",
"BufferType",
"StringTypes",
"NotImplementedType",
"NoneType",
"InstanceType",
"FloatType",
"SliceType",
"UnboundMethodType",
"ObjectType",
"IntType",
"TupleType",
"ListType",
"DictType",
"FileType",
"DictionaryType",
"BooleanType",
"DictProxyType",
]
)
},
"bad-python3-import": frozenset(
[
"anydbm",
"BaseHTTPServer",
"__builtin__",
"CGIHTTPServer",
"ConfigParser",
"copy_reg",
"cPickle",
"cStringIO",
"Cookie",
"cookielib",
"dbhash",
"dumbdbm",
"dumbdb",
"Dialog",
"DocXMLRPCServer",
"FileDialog",
"FixTk",
"gdbm",
"htmlentitydefs",
"HTMLParser",
"httplib",
"markupbase",
"Queue",
"repr",
"robotparser",
"ScrolledText",
"SimpleDialog",
"SimpleHTTPServer",
"SimpleXMLRPCServer",
"StringIO",
"dummy_thread",
"SocketServer",
"test.test_support",
"Tkinter",
"Tix",
"Tkconstants",
"tkColorChooser",
"tkCommonDialog",
"Tkdnd",
"tkFileDialog",
"tkFont",
"tkMessageBox",
"tkSimpleDialog",
"UserList",
"UserString",
"whichdb",
"_winreg",
"xmlrpclib",
"audiodev",
"Bastion",
"bsddb185",
"bsddb3",
"Canvas",
"cfmfile",
"cl",
"commands",
"compiler",
"dircache",
"dl",
"exception",
"fpformat",
"htmllib",
"ihooks",
"imageop",
"imputil",
"linuxaudiodev",
"md5",
"mhlib",
"mimetools",
"MimeWriter",
"mimify",
"multifile",
"mutex",
"new",
"popen2",
"posixfile",
"pure",
"rexec",
"rfc822",
"sets",
"sha",
"sgmllib",
"sre",
"stringold",
"sunaudio",
"sv",
"test.testall",
"thread",
"timing",
"toaiff",
"user",
"urllib2",
"urlparse",
]
),
"deprecated-string-function": {
"string": frozenset(
[
"maketrans",
"atof",
"atoi",
"atol",
"capitalize",
"expandtabs",
"find",
"rfind",
"index",
"rindex",
"count",
"lower",
"letters",
"split",
"rsplit",
"splitfields",
"join",
"joinfields",
"lstrip",
"rstrip",
"strip",
"swapcase",
"translate",
"upper",
"ljust",
"rjust",
"center",
"zfill",
"replace",
"lowercase",
"letters",
"uppercase",
"atol_error",
"atof_error",
"atoi_error",
"index_error",
]
)
},
"deprecated-operator-function": {"operator": frozenset({"div"})},
"deprecated-urllib-function": {
"urllib": frozenset(
{
"addbase",
"addclosehook",
"addinfo",
"addinfourl",
"always_safe",
"basejoin",
"ftpcache",
"ftperrors",
"ftpwrapper",
"getproxies",
"getproxies_environment",
"getproxies_macosx_sysconf",
"main",
"noheaders",
"pathname2url",
"proxy_bypass",
"proxy_bypass_environment",
"proxy_bypass_macosx_sysconf",
"quote",
"quote_plus",
"reporthook",
"splitattr",
"splithost",
"splitnport",
"splitpasswd",
"splitport",
"splitquery",
"splittag",
"splittype",
"splituser",
"splitvalue",
"unquote",
"unquote_plus",
"unwrap",
"url2pathname",
"urlcleanup",
"urlencode",
"urlopen",
"urlretrieve",
}
)
},
"deprecated-sys-function": {"sys": frozenset({"exc_clear"})},
}
_deprecated_attrs = frozenset(
itertools.chain.from_iterable(
attr
for module_map in _bad_python3_module_map.values()
if isinstance(module_map, dict)
for attr in module_map.values()
)
)
_relevant_call_attrs = (
DICT_METHODS | _deprecated_attrs | {"encode", "decode", "translate"}
)
_python_2_tests = frozenset(
astroid.extract_node(x).repr_tree()
for x in (
"sys.version_info[0] == 2",
"sys.version_info[0] < 3",
"sys.version_info == (2, 7)",
"sys.version_info <= (2, 7)",
"sys.version_info < (3, 0)",
)
)
def __init__(self, *args, **kwargs):
self._future_division = False
self._future_absolute_import = False
self._modules_warned_about = set()
self._branch_stack = []
super().__init__(*args, **kwargs)
# pylint: disable=keyword-arg-before-vararg, arguments-differ
def add_message(self, msg_id, always_warn=False, *args, **kwargs):
if always_warn or not (
self._branch_stack and self._branch_stack[-1].is_py2_only
):
super().add_message(msg_id, *args, **kwargs)
def _is_py2_test(self, node):
if isinstance(node.test, astroid.Attribute) and isinstance(
node.test.expr, astroid.Name
):
if node.test.expr.name == "six" and node.test.attrname == "PY2":
return True
elif (
isinstance(node.test, astroid.Compare)
and node.test.repr_tree() in self._python_2_tests
):
return True
return False
def visit_if(self, node):
self._branch_stack.append(Branch(node, self._is_py2_test(node)))
def leave_if(self, node):
new_node = self._branch_stack.pop().node
assert new_node == node
def visit_ifexp(self, node):
self._branch_stack.append(Branch(node, self._is_py2_test(node)))
def leave_ifexp(self, node):
new_node = self._branch_stack.pop()
assert new_node.node == node
def visit_module(self, node): # pylint: disable=unused-argument
"""Clear checker state after previous module."""
self._future_division = False
self._future_absolute_import = False
def visit_functiondef(self, node):
if node.is_method():
if node.name in self._unused_magic_methods:
method_name = node.name
if node.name.startswith("__"):
method_name = node.name[2:-2]
self.add_message(method_name + "-method", node=node)
elif node.name == "next":
# If there is a method named `next` declared, if it is invokable
# with zero arguments then it implements the Iterator protocol.
# This means if the method is an instance method or a
# classmethod 1 argument should cause a failure, if it is a
# staticmethod 0 arguments should cause a failure.
failing_arg_count = 1
if utils.decorated_with(
node, [astroid.bases.BUILTINS + ".staticmethod"]
):
failing_arg_count = 0
if len(node.args.args) == failing_arg_count:
self.add_message("next-method-defined", node=node)
@utils.check_messages("parameter-unpacking")
def visit_arguments(self, node):
for arg in node.args:
if isinstance(arg, astroid.Tuple):
self.add_message("parameter-unpacking", node=arg)
@utils.check_messages("comprehension-escape")
def visit_listcomp(self, node):
names = {
generator.target.name
for generator in node.generators
if isinstance(generator.target, astroid.AssignName)
}
scope = node.parent.scope()
scope_names = scope.nodes_of_class(astroid.Name, skip_klass=astroid.FunctionDef)
has_redefined_assign_name = any(
assign_name
for assign_name in scope.nodes_of_class(
astroid.AssignName, skip_klass=astroid.FunctionDef
)
if assign_name.name in names and assign_name.lineno > node.lineno
)
if has_redefined_assign_name:
return
emitted_for_names = set()
scope_names = list(scope_names)
for scope_name in scope_names:
if (
scope_name.name not in names
or scope_name.lineno <= node.lineno
or scope_name.name in emitted_for_names
or scope_name.scope() == node
):
continue
emitted_for_names.add(scope_name.name)
self.add_message("comprehension-escape", node=scope_name)
def visit_name(self, node):
"""Detect when a "bad" built-in is referenced."""
found_node, _ = node.lookup(node.name)
if not _is_builtin(found_node):
return
if node.name not in self._bad_builtins:
return
if node_ignores_exception(node) or isinstance(
find_try_except_wrapper_node(node), astroid.ExceptHandler
):
return
message = node.name.lower() + "-builtin"
self.add_message(message, node=node)
@utils.check_messages("print-statement")
def visit_print(self, node):
self.add_message("print-statement", node=node, always_warn=True)
def _warn_if_deprecated(self, node, module, attributes, report_on_modules=True):
for message, module_map in self._bad_python3_module_map.items():
if module in module_map and module not in self._modules_warned_about:
if isinstance(module_map, frozenset):
if report_on_modules:
self._modules_warned_about.add(module)
self.add_message(message, node=node)
elif attributes and module_map[module].intersection(attributes):
self.add_message(message, node=node)
def visit_importfrom(self, node):
if node.modname == "__future__":
for name, _ in node.names:
if name == "division":
self._future_division = True
elif name == "absolute_import":
self._future_absolute_import = True
else:
if not self._future_absolute_import:
if self.linter.is_message_enabled("no-absolute-import"):
self.add_message("no-absolute-import", node=node)
self._future_absolute_import = True
if not _is_conditional_import(node) and not node.level:
self._warn_if_deprecated(node, node.modname, {x[0] for x in node.names})
if node.names[0][0] == "*":
if self.linter.is_message_enabled("import-star-module-level"):
if not isinstance(node.scope(), astroid.Module):
self.add_message("import-star-module-level", node=node)
def visit_import(self, node):
if not self._future_absolute_import:
if self.linter.is_message_enabled("no-absolute-import"):
self.add_message("no-absolute-import", node=node)
self._future_absolute_import = True
if not _is_conditional_import(node):
for name, _ in node.names:
self._warn_if_deprecated(node, name, None)
@utils.check_messages("metaclass-assignment")
def visit_classdef(self, node):
if "__metaclass__" in node.locals:
self.add_message("metaclass-assignment", node=node)
locals_and_methods = set(node.locals).union(x.name for x in node.mymethods())
if "__eq__" in locals_and_methods and "__hash__" not in locals_and_methods:
self.add_message("eq-without-hash", node=node)
@utils.check_messages("old-division")
def visit_binop(self, node):
if not self._future_division and node.op == "/":
for arg in (node.left, node.right):
inferred = utils.safe_infer(arg)
# If we can infer the object and that object is not an int, bail out.
if inferred and not (
(
isinstance(inferred, astroid.Const)
and isinstance(inferred.value, int)
)
or (
isinstance(inferred, astroid.Instance)
and inferred.name == "int"
)
):
break
else:
self.add_message("old-division", node=node)
def _check_cmp_argument(self, node):
# Check that the `cmp` argument is used
kwargs = []
if isinstance(node.func, astroid.Attribute) and node.func.attrname == "sort":
inferred = utils.safe_infer(node.func.expr)
if not inferred:
return
builtins_list = f"{astroid.bases.BUILTINS}.list"
if isinstance(inferred, astroid.List) or inferred.qname() == builtins_list:
kwargs = node.keywords
elif isinstance(node.func, astroid.Name) and node.func.name == "sorted":
inferred = utils.safe_infer(node.func)
if not inferred:
return
builtins_sorted = f"{astroid.bases.BUILTINS}.sorted"
if inferred.qname() == builtins_sorted:
kwargs = node.keywords
for kwarg in kwargs or []:
if kwarg.arg == "cmp":
self.add_message("using-cmp-argument", node=node)
return
@staticmethod
def _is_constant_string_or_name(node):
if isinstance(node, astroid.Const):
return isinstance(node.value, str)
return isinstance(node, astroid.Name)
@staticmethod
def _is_none(node):
return isinstance(node, astroid.Const) and node.value is None
@staticmethod
def _has_only_n_positional_args(node, number_of_args):
return len(node.args) == number_of_args and all(node.args) and not node.keywords
@staticmethod
def _could_be_string(inferred_types):
confidence = INFERENCE if inferred_types else INFERENCE_FAILURE
for inferred_type in inferred_types:
if inferred_type is astroid.Uninferable:
confidence = INFERENCE_FAILURE
elif not (
isinstance(inferred_type, astroid.Const)
and isinstance(inferred_type.value, str)
):
return None
return confidence
def visit_call(self, node):
self._check_cmp_argument(node)
if isinstance(node.func, astroid.Attribute):
inferred_types = set()
try:
for inferred_receiver in _infer_if_relevant_attr(
node.func, self._relevant_call_attrs
):
if inferred_receiver is astroid.Uninferable:
continue
inferred_types.add(inferred_receiver)
if isinstance(inferred_receiver, astroid.Module):
self._warn_if_deprecated(
node,
inferred_receiver.name,
{node.func.attrname},
report_on_modules=False,
)
if (
_inferred_value_is_dict(inferred_receiver)
and node.func.attrname in DICT_METHODS
):
if not _in_iterating_context(node):
checker = f"dict-{node.func.attrname}-not-iterating"
self.add_message(checker, node=node)
except astroid.InferenceError:
pass
if node.args:
is_str_confidence = self._could_be_string(inferred_types)
if is_str_confidence:
if (
node.func.attrname in ("encode", "decode")
and len(node.args) >= 1
and node.args[0]
):
first_arg = node.args[0]
self._validate_encoding(first_arg, node)
if (
node.func.attrname == "translate"
and self._has_only_n_positional_args(node, 2)
and self._is_none(node.args[0])
and self._is_constant_string_or_name(node.args[1])
):
# The above statement looking for calls of the form:
#
# foo.translate(None, 'abc123')
#
# or
#
# foo.translate(None, some_variable)
#
# This check is somewhat broad and _may_ have some false positives, but
# after checking several large codebases it did not have any false
# positives while finding several real issues. This call pattern seems
# rare enough that the trade off is worth it.
self.add_message(
"deprecated-str-translate-call",
node=node,
confidence=is_str_confidence,
)
return
if node.keywords:
return
if node.func.attrname == "next":
self.add_message("next-method-called", node=node)
elif node.func.attrname in ("iterkeys", "itervalues", "iteritems"):
self.add_message("dict-iter-method", node=node)
elif node.func.attrname in ("viewkeys", "viewvalues", "viewitems"):
self.add_message("dict-view-method", node=node)
elif isinstance(node.func, astroid.Name):
found_node = node.func.lookup(node.func.name)[0]
if _is_builtin(found_node):
if node.func.name in ("filter", "map", "range", "zip"):
if not _in_iterating_context(node):
checker = f"{node.func.name}-builtin-not-iterating"
self.add_message(checker, node=node)
elif node.func.name == "open" and node.keywords:
kwargs = node.keywords
for kwarg in kwargs or []:
if kwarg.arg == "encoding":
self._validate_encoding(kwarg.value, node)
break
def _validate_encoding(self, encoding, node):
if isinstance(encoding, astroid.Const):
value = encoding.value
if value in self._invalid_encodings:
self.add_message("invalid-str-codec", node=node)
@utils.check_messages("indexing-exception")
def visit_subscript(self, node):
"""Look for indexing exceptions."""
try:
for inferred in node.value.infer():
if not isinstance(inferred, astroid.Instance):
continue
if utils.inherit_from_std_ex(inferred):
self.add_message("indexing-exception", node=node)
except astroid.InferenceError:
return
def visit_assignattr(self, node):
if isinstance(node.assign_type(), astroid.AugAssign):
self.visit_attribute(node)
def visit_delattr(self, node):
self.visit_attribute(node)
@utils.check_messages("exception-message-attribute", "xreadlines-attribute")
def visit_attribute(self, node):
"""Look for removed attributes"""
if node.attrname == "xreadlines":
self.add_message("xreadlines-attribute", node=node)
return
exception_message = "message"
try:
for inferred in _infer_if_relevant_attr(
node, self._deprecated_attrs | {exception_message}
):
if isinstance(inferred, astroid.Instance) and utils.inherit_from_std_ex(
inferred
):
if node.attrname == exception_message:
# Exceptions with .message clearly defined are an exception
if exception_message in inferred.instance_attrs:
continue
self.add_message("exception-message-attribute", node=node)
if isinstance(inferred, astroid.Module):
self._warn_if_deprecated(
node, inferred.name, {node.attrname}, report_on_modules=False
)
except astroid.InferenceError:
return
@utils.check_messages("unpacking-in-except", "comprehension-escape")
def visit_excepthandler(self, node):
"""Visit an except handler block and check for exception unpacking."""
def _is_used_in_except_block(node, block):
current = node
while current and current is not block:
current = current.parent
return current is not None
if isinstance(node.name, (astroid.Tuple, astroid.List)):
self.add_message("unpacking-in-except", node=node)
return
if not node.name:
return
# Find any names
scope = node.parent.scope()
scope_names = scope.nodes_of_class(astroid.Name, skip_klass=astroid.FunctionDef)
scope_names = list(scope_names)
potential_leaked_names = [
scope_name
for scope_name in scope_names
if scope_name.name == node.name.name
and scope_name.lineno > node.lineno
and not _is_used_in_except_block(scope_name, node)
]
reassignments_for_same_name = {
assign_name.lineno
for assign_name in scope.nodes_of_class(
astroid.AssignName, skip_klass=astroid.FunctionDef
)
if assign_name.name == node.name.name
}
for leaked_name in potential_leaked_names:
if any(
node.lineno < elem < leaked_name.lineno
for elem in reassignments_for_same_name
):
continue
self.add_message("exception-escape", node=leaked_name)
@utils.check_messages("backtick")
def visit_repr(self, node):
self.add_message("backtick", node=node)
@utils.check_messages("raising-string", "old-raise-syntax")
def visit_raise(self, node):
"""Visit a raise statement and check for raising
strings or old-raise-syntax.
"""
# Ignore empty raise.
if node.exc is None:
return
expr = node.exc
if self._check_raise_value(node, expr):
return
try:
value = next(astroid.unpack_infer(expr))
except astroid.InferenceError:
return
self._check_raise_value(node, value)
def _check_raise_value(self, node, expr):
if isinstance(expr, astroid.Const):
value = expr.value
if isinstance(value, str):
self.add_message("raising-string", node=node)
return True
return None
class Python3TokenChecker(checkers.BaseTokenChecker):
__implements__ = interfaces.ITokenChecker
name = "python3"
enabled = False
msgs = {
"E1606": (
"Use of long suffix",
"long-suffix",
'Used when "l" or "L" is used to mark a long integer. '
"This will not work in Python 3, since `int` and `long` "
"types have merged.",
{"maxversion": (3, 0)},
),
"E1607": (
"Use of the <> operator",
"old-ne-operator",
'Used when the deprecated "<>" operator is used instead '
'of "!=". This is removed in Python 3.',
{"maxversion": (3, 0), "old_names": [("W0331", "old-old-ne-operator")]},
),
"E1608": (
"Use of old octal literal",
"old-octal-literal",
"Used when encountering the old octal syntax, "
"removed in Python 3. To use the new syntax, "
"prepend 0o on the number.",
{"maxversion": (3, 0)},
),
"E1610": (
"Non-ascii bytes literals not supported in 3.x",
"non-ascii-bytes-literal",
"Used when non-ascii bytes literals are found in a program. "
"They are no longer supported in Python 3.",
{"maxversion": (3, 0)},
),
}
def process_tokens(self, tokens):
for idx, (tok_type, token, start, _, _) in enumerate(tokens):
if tok_type == tokenize.NUMBER:
if token.lower().endswith("l"):
# This has a different semantic than lowercase-l-suffix.
self.add_message("long-suffix", line=start[0])
elif _is_old_octal(token):
self.add_message("old-octal-literal", line=start[0])
if tokens[idx][1] == "<>":
self.add_message("old-ne-operator", line=tokens[idx][2][0])
if tok_type == tokenize.STRING and token.startswith("b"):
if any(elem for elem in token if ord(elem) > 127):
self.add_message("non-ascii-bytes-literal", line=start[0])
def register(linter):
linter.register_checker(Python3Checker(linter))
linter.register_checker(Python3TokenChecker(linter))