# -*- coding: utf-8 -*-
# Copyright (c) 2014-2018 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 Ville Skyttä <ville.skytta@iki.fi>
# Copyright (c) 2017 Daniel Miller <millerdev@gmail.com>
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
# Copyright (c) 2018 Anthony Sottile <asottile@umich.edu>
# Copyright (c) 2018 Ashley Whetter <ashley@awhetter.co.uk>
# Copyright (c) 2018 Ville Skyttä <ville.skytta@upcloud.com>
# Copyright (c) 2018 gaurikholkar <f2013002@goa.bits-pilani.ac.in>
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING

"""Check Python 2 code for Python 2/3 source-compatible issues."""
from __future__ import absolute_import, print_function

from collections import namedtuple
import re
import sys
import tokenize
from typing import FrozenSet

import astroid
from astroid import bases

from pylint import checkers, interfaces
from pylint.checkers.utils import node_ignores_exception, find_try_except_wrapper_node
from pylint.interfaces import INFERENCE_FAILURE, INFERENCE
from pylint.utils import WarningScope
from pylint.checkers import utils


_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 _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",
}
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):
            parent_scope = parent.func.lookup(parent.func.name)[0]
            if _is_builtin(parent_scope) and 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"
    ):
        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 http://www.python.org/dev/peps/pep-3110/",
            {"old_names": [("W0712", "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-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", "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", "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", "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 an iterator 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"})},
    }

    if (3, 4) <= sys.version_info < (3, 4, 4):
        # Python 3.4.0 -> 3.4.3 has a bug which breaks `repr_tree()`:
        # https://bugs.python.org/issue23572
        _python_2_tests = frozenset()  # type: FrozenSet[str]
    else:
        _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(Python3Checker, self).__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(Python3Checker, self).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):
        assert self._branch_stack.pop().node == node

    def visit_ifexp(self, node):
        self._branch_stack.append(Branch(node, self._is_py2_test(node)))

    def leave_ifexp(self, node):
        assert self._branch_stack.pop().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, [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):
                if isinstance(arg, astroid.Const) and isinstance(arg.value, float):
                    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 = "{}.list".format(bases.BUILTINS)
            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 = "{}.sorted".format(bases.BUILTINS)
            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 node.func.expr.infer():
                    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 = "dict-{}-not-iterating".format(node.func.attrname)
                            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)
            else:
                if 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 = "{}-builtin-not-iterating".format(node.func.name)
                        self.add_message(checker, node=node)
                if 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 node.expr.infer():
                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):
            scope = node.scope()
            current = node
            while (
                current
                and current != scope
                and not isinstance(current, astroid.ExceptHandler)
            ):
                current = current.parent
            return isinstance(current, astroid.ExceptHandler) and current.type != node

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