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

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

import astroid
from astroid import bases

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 _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 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"})},
    }

    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):
                inferred = utils.safe_infer(arg)
                # If we can infer the object and that object is not a numeric one, bail out.
                if inferred and not (
                    isinstance(inferred, astroid.Const)
                    and isinstance(inferred.value, (int, float))
                ):
                    break
                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))


# -*- coding: utf-8 -*-
# Copyright (c) 2006-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
# Copyright (c) 2008 pyves@crater.logilab.fr <pyves@crater.logilab.fr>
# Copyright (c) 2010 Julien Jehannet <julien.jehannet@logilab.fr>
# Copyright (c) 2013 Google, Inc.
# Copyright (c) 2013 John McGehee <jmcgehee@altera.com>
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
# Copyright (c) 2014 Brett Cannon <brett@python.org>
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
# Copyright (c) 2015 John Kirkham <jakirkham@gmail.com>
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
# Copyright (c) 2016 Erik <erik.eriksson@yahoo.com>
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
# Copyright (c) 2017-2018 Ville Skyttä <ville.skytta@iki.fi>
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
# Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
# Copyright (c) 2018 Anthony Sottile <asottile@umich.edu>
# Copyright (c) 2018 Gary Tyler McLeod <mail@garytyler.com>
# Copyright (c) 2018 Konstantin <Github@pheanex.de>
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.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/COPYING

"""utilities for Pylint configuration :

* pylintrc
* pylint.d (PYLINTHOME)
"""
from __future__ import print_function

import collections
import configparser
import contextlib
import copy
import io
import optparse
import os
import pickle
import re
import sys
import time

from pylint import utils

# TODO(cpopa): this module contains the logic for the
# configuration parser and for the command line parser,
# but it's really coupled to optparse's internals.
# The code was copied almost verbatim from logilab.common,
# in order to not depend on it anymore and it will definitely
# need a cleanup. It could be completely reengineered as well.

USER_HOME = os.path.expanduser("~")
if "PYLINTHOME" in os.environ:
    PYLINT_HOME = os.environ["PYLINTHOME"]
    if USER_HOME == "~":
        USER_HOME = os.path.dirname(PYLINT_HOME)
elif USER_HOME == "~":
    PYLINT_HOME = ".pylint.d"
else:
    PYLINT_HOME = os.path.join(USER_HOME, ".pylint.d")

def _get_pdata_path(base_name, recurs):
    base_name = base_name.replace(os.sep, "_")
    return os.path.join(PYLINT_HOME, "%s%s%s" % (base_name, recurs, ".stats"))

def load_results(base):
    data_file = _get_pdata_path(base, 1)
    try:
        with open(data_file, _PICK_LOAD) as stream:
            return pickle.load(stream)
    except Exception:  # pylint: disable=broad-except
        return {}

if sys.version_info < (3, 0):
    _PICK_DUMP, _PICK_LOAD = "w", "r"
else:
    _PICK_DUMP, _PICK_LOAD = "wb", "rb"

def save_results(results, base):
    if not os.path.exists(PYLINT_HOME):
        try:
            os.mkdir(PYLINT_HOME)
        except OSError:
            print("Unable to create directory %s" % PYLINT_HOME, file=sys.stderr)
    data_file = _get_pdata_path(base, 1)
    try:
        with open(data_file, _PICK_DUMP) as stream:
            pickle.dump(results, stream)
    except (IOError, OSError) as ex:
        print("Unable to create file %s: %s" % (data_file, ex), file=sys.stderr)

def find_pylintrc():
    """search the pylint rc file and return its path if it find it, else None
    """
    # is there a pylint rc file in the current directory ?
    if os.path.exists("pylintrc"):
        return os.path.abspath("pylintrc")
    if os.path.exists(".pylintrc"):
        return os.path.abspath(".pylintrc")
    if os.path.isfile("__init__.py"):
        curdir = os.path.abspath(os.getcwd())
        while os.path.isfile(os.path.join(curdir, "__init__.py")):
            curdir = os.path.abspath(os.path.join(curdir, ".."))
            if os.path.isfile(os.path.join(curdir, "pylintrc")):
                return os.path.join(curdir, "pylintrc")
            if os.path.isfile(os.path.join(curdir, ".pylintrc")):
                return os.path.join(curdir, ".pylintrc")
    if "PYLINTRC" in os.environ and os.path.exists(os.environ["PYLINTRC"]):
        pylintrc = os.environ["PYLINTRC"]
    else:
        user_home = os.path.expanduser("~")
        if user_home in ("~", "/root"):
            pylintrc = ".pylintrc"
        else:
            pylintrc = os.path.join(user_home, ".pylintrc")
            if not os.path.isfile(pylintrc):
                pylintrc = os.path.join(user_home, ".config", "pylintrc")
    if not os.path.isfile(pylintrc):
        if os.path.isfile("/etc/pylintrc"):
            pylintrc = "/etc/pylintrc"
        else:
            pylintrc = None
    return pylintrc

PYLINTRC = find_pylintrc()

ENV_HELP = (
    """
The following environment variables are used:
    * PYLINTHOME
    Path to the directory where persistent data for the run will be stored. If
not found, it defaults to ~/.pylint.d/ or .pylint.d (in the current working
directory).
    * PYLINTRC
    Path to the configuration file. See the documentation for the method used
to search for configuration file.
"""
    % globals()  # type: ignore
)

class UnsupportedAction(Exception):
    """raised by set_option when it doesn't know what to do for an action"""

def _multiple_choice_validator(choices, name, value):
    values = utils._check_csv(value)
    for csv_value in values:
        if csv_value not in choices:
            msg = "option %s: invalid value: %r, should be in %s"
            raise optparse.OptionValueError(msg % (name, csv_value, choices))
    return values

def _choice_validator(choices, name, value):
    if value not in choices:
        msg = "option %s: invalid value: %r, should be in %s"
        raise optparse.OptionValueError(msg % (name, value, choices))
    return value

# pylint: disable=unused-argument
def _csv_validator(_, name, value):
    return utils._check_csv(value)

# pylint: disable=unused-argument
def _regexp_validator(_, name, value):
    if hasattr(value, "pattern"):
        return value
    return re.compile(value)

# pylint: disable=unused-argument
def _regexp_csv_validator(_, name, value):
    return [_regexp_validator(_, name, val) for val in _csv_validator(_, name, value)]

def _yn_validator(opt, _, value):
    if isinstance(value, int):
        return bool(value)
    if value in ("y", "yes"):
        return True
    if value in ("n", "no"):
        return False
    msg = "option %s: invalid yn value %r, should be in (y, yes, n, no)"
    raise optparse.OptionValueError(msg % (opt, value))

def _non_empty_string_validator(opt, _, value):
    if not value:
        msg = "indent string can't be empty."
        raise optparse.OptionValueError(msg)
    return utils._unquote(value)

VALIDATORS = {
    "string": utils._unquote,
    "int": int,
    "regexp": re.compile,
    "regexp_csv": _regexp_csv_validator,
    "csv": _csv_validator,
    "yn": _yn_validator,
    "choice": lambda opt, name, value: _choice_validator(opt["choices"], name, value),
    "multiple_choice": lambda opt, name, value: _multiple_choice_validator(
        opt["choices"], name, value
    ),
    "non_empty_string": _non_empty_string_validator,
}

def _call_validator(opttype, optdict, option, value):
    if opttype not in VALIDATORS:
        raise Exception('Unsupported type "%s"' % opttype)
    try:
        return VALIDATORS[opttype](optdict, option, value)
    except TypeError:
        try:
            return VALIDATORS[opttype](value)
        except Exception:
            raise optparse.OptionValueError(
                "%s value (%r) should be of type %s" % (option, value, opttype)
            )

def _validate(value, optdict, name=""):
    """return a validated value for an option according to its type

    optional argument name is only used for error message formatting
    """
    try:
        _type = optdict["type"]
    except KeyError:
        # FIXME
        return value
    return _call_validator(_type, optdict, name, value)

def _level_options(group, outputlevel):
    return [
        option
        for option in group.option_list
        if (getattr(option, "level", 0) or 0) <= outputlevel
        and option.help is not optparse.SUPPRESS_HELP
    ]

def _expand_default(self, option):
    """Patch OptionParser.expand_default with custom behaviour

    This will handle defaults to avoid overriding values in the
    configuration file.
    """
    if self.parser is None or not self.default_tag:
        return option.help
    optname = option._long_opts[0][2:]
    try:
        provider = self.parser.options_manager._all_options[optname]
    except KeyError:
        value = None
    else:
        optdict = provider.get_option_def(optname)
        optname = provider.option_attrname(optname, optdict)
        value = getattr(provider.config, optname, optdict)
        value = utils._format_option_value(optdict, value)
    if value is optparse.NO_DEFAULT or not value:
        value = self.NO_DEFAULT_VALUE
    return option.help.replace(self.default_tag, str(value))

@contextlib.contextmanager
def _patch_optparse():
    orig_default = optparse.HelpFormatter
    try:
        optparse.HelpFormatter.expand_default = _expand_default
        yield
    finally:
        optparse.HelpFormatter.expand_default = orig_default

def _multiple_choices_validating_option(opt, name, value):
    return _multiple_choice_validator(opt.choices, name, value)

# pylint: disable=no-member
class Option(optparse.Option):
    TYPES = optparse.Option.TYPES + (
        "regexp",
        "regexp_csv",
        "csv",
        "yn",
        "multiple_choice",
        "non_empty_string",
    )
    ATTRS = optparse.Option.ATTRS + ["hide", "level"]
    TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
    TYPE_CHECKER["regexp"] = _regexp_validator
    TYPE_CHECKER["regexp_csv"] = _regexp_csv_validator
    TYPE_CHECKER["csv"] = _csv_validator
    TYPE_CHECKER["yn"] = _yn_validator
    TYPE_CHECKER["multiple_choice"] = _multiple_choices_validating_option
    TYPE_CHECKER["non_empty_string"] = _non_empty_string_validator

    def __init__(self, *opts, **attrs):
        optparse.Option.__init__(self, *opts, **attrs)
        if hasattr(self, "hide") and self.hide:
            self.help = optparse.SUPPRESS_HELP

    def _check_choice(self):
        if self.type in ("choice", "multiple_choice"):
            if self.choices is None:
                raise optparse.OptionError(
                    "must supply a list of choices for type 'choice'", self
                )
            if not isinstance(self.choices, (tuple, list)):
                raise optparse.OptionError(
                    "choices must be a list of strings ('%s' supplied)"
                    % str(type(self.choices)).split("'")[1],
                    self,
                )
        elif self.choices is not None:
            raise optparse.OptionError(
                "must not supply choices for type %r" % self.type, self
            )

    # pylint: disable=unsupported-assignment-operation
    optparse.Option.CHECK_METHODS[2] = _check_choice  # type: ignore

    def process(self, opt, value, values, parser):
        # First, convert the value(s) to the right type.  Howl if any
        # value(s) are bogus.
        value = self.convert_value(opt, value)
        if self.type == "named":
            existent = getattr(values, self.dest)
            if existent:
                existent.update(value)
                value = existent
        # And then take whatever action is expected of us.
        # This is a separate method to make life easier for
        # subclasses to add new actions.
        return self.take_action(self.action, self.dest, opt, value, values, parser)

class OptionParser(optparse.OptionParser):
    def __init__(self, option_class, *args, **kwargs):
        optparse.OptionParser.__init__(self, option_class=Option, *args, **kwargs)

    def format_option_help(self, formatter=None):
        if formatter is None:
            formatter = self.formatter
        outputlevel = getattr(formatter, "output_level", 0)
        formatter.store_option_strings(self)
        result = []
        result.append(formatter.format_heading("Options"))
        formatter.indent()
        if self.option_list:
            result.append(optparse.OptionContainer.format_option_help(self, formatter))
            result.append("\n")
        for group in self.option_groups:
            if group.level <= outputlevel and (
                group.description or _level_options(group, outputlevel)
            ):
                result.append(group.format_help(formatter))
                result.append("\n")
        formatter.dedent()
        # Drop the last "\n", or the header if no options or option groups:
        return "".join(result[:-1])

    def _match_long_opt(self, opt):
        """Disable abbreviations."""
        if opt not in self._long_opt:
            raise optparse.BadOptionError(opt)
        return opt

# pylint: disable=abstract-method; by design?
class _ManHelpFormatter(optparse.HelpFormatter):
    def __init__(
        self, indent_increment=0, max_help_position=24, width=79, short_first=0
    ):
        optparse.HelpFormatter.__init__(
            self, indent_increment, max_help_position, width, short_first
        )

    def format_heading(self, heading):
        return ".SH %s\n" % heading.upper()

    def format_description(self, description):
        return description

    def format_option(self, option):
        try:
            optstring = option.option_strings
        except AttributeError:
            optstring = self.format_option_strings(option)
        if option.help:
            help_text = self.expand_default(option)
            help_string = " ".join([l.strip() for l in help_text.splitlines()])
            help_string = help_string.replace("\\", "\\\\")
            help_string = help_string.replace("[current:", "[default:")
        else:
            help_string = ""
        return """.IP "%s"
%s
""" % (
            optstring,
            help_string,
        )

    def format_head(self, optparser, pkginfo, section=1):
        long_desc = ""
        try:
            pgm = optparser._get_prog_name()
        except AttributeError:
            # py >= 2.4.X (dunno which X exactly, at least 2)
            pgm = optparser.get_prog_name()
        short_desc = self.format_short_description(pgm, pkginfo.description)
        if hasattr(pkginfo, "long_desc"):
            long_desc = self.format_long_description(pgm, pkginfo.long_desc)
        return "%s\n%s\n%s\n%s" % (
            self.format_title(pgm, section),
            short_desc,
            self.format_synopsis(pgm),
            long_desc,
        )

    @staticmethod
    def format_title(pgm, section):
        date = "%d-%02d-%02d" % time.localtime()[:3]
        return '.TH %s %s "%s" %s' % (pgm, section, date, pgm)

    @staticmethod
    def format_short_description(pgm, short_desc):
        return """.SH NAME
.B %s
\\- %s
""" % (
            pgm,
            short_desc.strip(),
        )

    @staticmethod
    def format_synopsis(pgm):
        return (
            """.SH SYNOPSIS
.B  %s
[
.I OPTIONS
] [
.I <arguments>
]
"""
            % pgm
        )

    @staticmethod
    def format_long_description(pgm, long_desc):
        long_desc = "\n".join(line.lstrip() for line in long_desc.splitlines())
        long_desc = long_desc.replace("\n.\n", "\n\n")
        if long_desc.lower().startswith(pgm):
            long_desc = long_desc[len(pgm) :]
        return """.SH DESCRIPTION
.B %s
%s
""" % (
            pgm,
            long_desc.strip(),
        )

    @staticmethod
    def format_tail(pkginfo):
        tail = """.SH SEE ALSO
/usr/share/doc/pythonX.Y-%s/

.SH BUGS
Please report bugs on the project\'s mailing list:
%s

.SH AUTHOR
%s <%s>
""" % (
            getattr(pkginfo, "debian_name", pkginfo.modname),
            pkginfo.mailinglist,
            pkginfo.author,
            pkginfo.author_email,
        )

        if hasattr(pkginfo, "copyright"):
            tail += (
                """
.SH COPYRIGHT
%s
"""
                % pkginfo.copyright
            )

        return tail

class OptionsManagerMixIn:
    """Handle configuration from both a configuration file and command line options"""

    def __init__(self, usage, config_file=None, version=None):
        self.config_file = config_file
        self.reset_parsers(usage, version=version)
        # list of registered options providers
        self.options_providers = []
        # dictionary associating option name to checker
        self._all_options = collections.OrderedDict()
        self._short_options = {}
        self._nocallback_options = {}
        self._mygroups = {}
        # verbosity
        self._maxlevel = 0

    def reset_parsers(self, usage="", version=None):
        # configuration file parser
        self.cfgfile_parser = configparser.ConfigParser(
            inline_comment_prefixes=("#", ";")
        )
        # command line parser
        self.cmdline_parser = OptionParser(Option, usage=usage, version=version)
        self.cmdline_parser.options_manager = self
        self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS)

    def register_options_provider(self, provider, own_group=True):
        """register an options provider"""
        assert provider.priority <= 0, "provider's priority can't be >= 0"
        for i in range(len(self.options_providers)):
            if provider.priority > self.options_providers[i].priority:
                self.options_providers.insert(i, provider)
                break
        else:
            self.options_providers.append(provider)
        non_group_spec_options = [
            option for option in provider.options if "group" not in option[1]
        ]
        groups = getattr(provider, "option_groups", ())
        if own_group and non_group_spec_options:
            self.add_option_group(
                provider.name.upper(),
                provider.__doc__,
                non_group_spec_options,
                provider,
            )
        else:
            for opt, optdict in non_group_spec_options:
                self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
        for gname, gdoc in groups:
            gname = gname.upper()
            goptions = [
                option
                for option in provider.options
                if option[1].get("group", "").upper() == gname
            ]
            self.add_option_group(gname, gdoc, goptions, provider)

    def add_option_group(self, group_name, _, options, provider):
        # add option group to the command line parser
        if group_name in self._mygroups:
            group = self._mygroups[group_name]
        else:
            group = optparse.OptionGroup(
                self.cmdline_parser, title=group_name.capitalize()
            )
            self.cmdline_parser.add_option_group(group)
            group.level = provider.level
            self._mygroups[group_name] = group
            # add section to the config file
            if (
                group_name != "DEFAULT"
                and group_name not in self.cfgfile_parser._sections
            ):
                self.cfgfile_parser.add_section(group_name)
        # add provider's specific options
        for opt, optdict in options:
            self.add_optik_option(provider, group, opt, optdict)

    def add_optik_option(self, provider, optikcontainer, opt, optdict):
        args, optdict = self.optik_option(provider, opt, optdict)
        option = optikcontainer.add_option(*args, **optdict)
        self._all_options[opt] = provider
        self._maxlevel = max(self._maxlevel, option.level or 0)

    def optik_option(self, provider, opt, optdict):
        """get our personal option definition and return a suitable form for
        use with optik/optparse
        """
        optdict = copy.copy(optdict)
        if "action" in optdict:
            self._nocallback_options[provider] = opt
        else:
            optdict["action"] = "callback"
            optdict["callback"] = self.cb_set_provider_option
        # default is handled here and *must not* be given to optik if you
        # want the whole machinery to work
        if "default" in optdict:
            if (
                "help" in optdict
                and optdict.get("default") is not None
                and optdict["action"] not in ("store_true", "store_false")
            ):
                optdict["help"] += " [current: %default]"
            del optdict["default"]
        args = ["--" + str(opt)]
        if "short" in optdict:
            self._short_options[optdict["short"]] = opt
            args.append("-" + optdict["short"])
            del optdict["short"]
        # cleanup option definition dict before giving it to optik
        for key in list(optdict.keys()):
            if key not in self._optik_option_attrs:
                optdict.pop(key)
        return args, optdict

    def cb_set_provider_option(self, option, opt, value, parser):
        """optik callback for option setting"""
        if opt.startswith("--"):
            # remove -- on long option
            opt = opt[2:]
        else:
            # short option, get its long equivalent
            opt = self._short_options[opt[1:]]
        # trick since we can't set action='store_true' on options
        if value is None:
            value = 1
        self.global_set_option(opt, value)

    def global_set_option(self, opt, value):
        """set option on the correct option provider"""
        self._all_options[opt].set_option(opt, value)

    def generate_config(self, stream=None, skipsections=(), encoding=None):
        """write a configuration file according to the current configuration
        into the given stream or stdout
        """
        options_by_section = {}
        sections = []
        for provider in self.options_providers:
            for section, options in provider.options_by_section():
                if section is None:
                    section = provider.name
                if section in skipsections:
                    continue
                options = [
                    (n, d, v)
                    for (n, d, v) in options
                    if d.get("type") is not None and not d.get("deprecated")
                ]
                if not options:
                    continue
                if section not in sections:
                    sections.append(section)
                alloptions = options_by_section.setdefault(section, [])
                alloptions += options
        stream = stream or sys.stdout
        printed = False
        for section in sections:
            if printed:
                print("\n", file=stream)
            utils.format_section(
                stream, section.upper(), sorted(options_by_section[section])
            )
            printed = True

    def generate_manpage(self, pkginfo, section=1, stream=None):
        with _patch_optparse():
            _generate_manpage(
                self.cmdline_parser,
                pkginfo,
                section,
                stream=stream or sys.stdout,
                level=self._maxlevel,
            )

    def load_provider_defaults(self):
        """initialize configuration using default values"""
        for provider in self.options_providers:
            provider.load_defaults()

    def read_config_file(self, config_file=None, verbose=None):
        """read the configuration file but do not load it (i.e. dispatching
        values to each options provider)
        """
        helplevel = 1
        while helplevel <= self._maxlevel:
            opt = "-".join(["long"] * helplevel) + "-help"
            if opt in self._all_options:
                break  # already processed
            # pylint: disable=unused-argument
            def helpfunc(option, opt, val, p, level=helplevel):
                print(self.help(level))
                sys.exit(0)

            helpmsg = "%s verbose help." % " ".join(["more"] * helplevel)
            optdict = {"action": "callback", "callback": helpfunc, "help": helpmsg}
            provider = self.options_providers[0]
            self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
            provider.options += ((opt, optdict),)
            helplevel += 1
        if config_file is None:
            config_file = self.config_file
        if config_file is not None:
            config_file = os.path.expanduser(config_file)
            if not os.path.exists(config_file):
                raise IOError("The config file {:s} doesn't exist!".format(config_file))

        use_config_file = config_file and os.path.exists(config_file)
        if use_config_file:
            parser = self.cfgfile_parser

            # Use this encoding in order to strip the BOM marker, if any.
            with io.open(config_file, "r", encoding="utf_8_sig") as fp:
                parser.read_file(fp)

            # normalize sections'title
            for sect, values in list(parser._sections.items()):
                if not sect.isupper() and values:
                    parser._sections[sect.upper()] = values

        if not verbose:
            return

        if use_config_file:
            msg = "Using config file {}".format(os.path.abspath(config_file))
        else:
            msg = "No config file found, using default configuration"
        print(msg, file=sys.stderr)

    def load_config_file(self):
        """dispatch values previously read from a configuration file to each
        options provider)
        """
        parser = self.cfgfile_parser
        for section in parser.sections():
            for option, value in parser.items(section):
                try:
                    self.global_set_option(option, value)
                except (KeyError, optparse.OptionError):
                    # TODO handle here undeclared options appearing in the config file
                    continue

    def load_configuration(self, **kwargs):
        """override configuration according to given parameters"""
        return self.load_configuration_from_config(kwargs)

    def load_configuration_from_config(self, config):
        for opt, opt_value in config.items():
            opt = opt.replace("_", "-")
            provider = self._all_options[opt]
            provider.set_option(opt, opt_value)

    def load_command_line_configuration(self, args=None):
        """Override configuration according to command line parameters

        return additional arguments
        """
        with _patch_optparse():
            if args is None:
                args = sys.argv[1:]
            else:
                args = list(args)
            (options, args) = self.cmdline_parser.parse_args(args=args)
            for provider in self._nocallback_options:
                config = provider.config
                for attr in config.__dict__.keys():
                    value = getattr(options, attr, None)
                    if value is None:
                        continue
                    setattr(config, attr, value)
            return args

    def add_help_section(self, title, description, level=0):
        """add a dummy option section for help purpose """
        group = optparse.OptionGroup(
            self.cmdline_parser, title=title.capitalize(), description=description
        )
        group.level = level
        self._maxlevel = max(self._maxlevel, level)
        self.cmdline_parser.add_option_group(group)

    def help(self, level=0):
        """return the usage string for available options """
        self.cmdline_parser.formatter.output_level = level
        with _patch_optparse():
            return self.cmdline_parser.format_help()

class OptionsProviderMixIn:
    """Mixin to provide options to an OptionsManager"""

    # those attributes should be overridden
    priority = -1
    name = "default"
    options = ()
    level = 0

    def __init__(self):
        self.config = optparse.Values()
        self.load_defaults()

    def load_defaults(self):
        """initialize the provider using default values"""
        for opt, optdict in self.options:
            action = optdict.get("action")
            if action != "callback":
                # callback action have no default
                if optdict is None:
                    optdict = self.get_option_def(opt)
                default = optdict.get("default")
                self.set_option(opt, default, action, optdict)

    def option_attrname(self, opt, optdict=None):
        """get the config attribute corresponding to opt"""
        if optdict is None:
            optdict = self.get_option_def(opt)
        return optdict.get("dest", opt.replace("-", "_"))

    def option_value(self, opt):
        """get the current value for the given option"""
        return getattr(self.config, self.option_attrname(opt), None)

    def set_option(self, optname, value, action=None, optdict=None):
        """method called to set an option (registered in the options list)"""
        if optdict is None:
            optdict = self.get_option_def(optname)
        if value is not None:
            value = _validate(value, optdict, optname)
        if action is None:
            action = optdict.get("action", "store")
        if action == "store":
            setattr(self.config, self.option_attrname(optname, optdict), value)
        elif action in ("store_true", "count"):
            setattr(self.config, self.option_attrname(optname, optdict), 0)
        elif action == "store_false":
            setattr(self.config, self.option_attrname(optname, optdict), 1)
        elif action == "append":
            optname = self.option_attrname(optname, optdict)
            _list = getattr(self.config, optname, None)
            if _list is None:
                if isinstance(value, (list, tuple)):
                    _list = value
                elif value is not None:
                    _list = []
                    _list.append(value)
                setattr(self.config, optname, _list)
            elif isinstance(_list, tuple):
                setattr(self.config, optname, _list + (value,))
            else:
                _list.append(value)
        elif action == "callback":
            optdict["callback"](None, optname, value, None)
        else:
            raise UnsupportedAction(action)

    def get_option_def(self, opt):
        """return the dictionary defining an option given its name"""
        assert self.options
        for option in self.options:
            if option[0] == opt:
                return option[1]
        raise optparse.OptionError(
            "no such option %s in section %r" % (opt, self.name), opt
        )

    def options_by_section(self):
        """return an iterator on options grouped by section

        (section, [list of (optname, optdict, optvalue)])
        """
        sections = {}
        for optname, optdict in self.options:
            sections.setdefault(optdict.get("group"), []).append(
                (optname, optdict, self.option_value(optname))
            )
        if None in sections:
            yield None, sections.pop(None)
        for section, options in sorted(sections.items()):
            yield section.upper(), options

    def options_and_values(self, options=None):
        if options is None:
            options = self.options
        for optname, optdict in options:
            yield (optname, optdict, self.option_value(optname))

class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn):
    """basic mixin for simple configurations which don't need the
    manager / providers model
    """

    def __init__(self, *args, **kwargs):
        if not args:
            kwargs.setdefault("usage", "")
        OptionsManagerMixIn.__init__(self, *args, **kwargs)
        OptionsProviderMixIn.__init__(self)
        if not getattr(self, "option_groups", None):
            self.option_groups = []
            for _, optdict in self.options:
                try:
                    gdef = (optdict["group"].upper(), "")
                except KeyError:
                    continue
                if gdef not in self.option_groups:
                    self.option_groups.append(gdef)
        self.register_options_provider(self, own_group=False)

def _generate_manpage(optparser, pkginfo, section=1, stream=sys.stdout, level=0):
    formatter = _ManHelpFormatter()
    formatter.output_level = level
    formatter.parser = optparser
    print(formatter.format_head(optparser, pkginfo, section), file=stream)
    print(optparser.format_option_help(formatter), file=stream)
    print(formatter.format_tail(pkginfo), file=stream)


# -*- coding: utf-8 -*-
# Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
# Copyright (c) 2009, 2012, 2014 Google, Inc.
# Copyright (c) 2012 Mike Bryant <leachim@leachim.info>
# Copyright (c) 2014 Brett Cannon <brett@python.org>
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
# Copyright (c) 2016 Chris Murray <chris@chrismurray.scot>
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
# Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col>
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
# Copyright (c) 2018 Mariatta Wijaya <mariatta@python.org>
# 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

"""checker for use of Python logging
"""
import string

import astroid

from pylint import checkers, interfaces
from pylint.checkers import utils
from pylint.checkers.utils import check_messages

MSGS = {
    "W1201": (
        "Specify string format arguments as logging function parameters",
        "logging-not-lazy",
        "Used when a logging statement has a call form of "
        '"logging.<logging method>(format_string % (format_args...))". '
        "Such calls should leave string interpolation to the logging "
        "method itself and be written "
        '"logging.<logging method>(format_string, format_args...)" '
        "so that the program may avoid incurring the cost of the "
        "interpolation in those cases in which no message will be "
        "logged. For more, see "
        "http://www.python.org/dev/peps/pep-0282/.",
    ),
    "W1202": (
        "Use % formatting in logging functions and pass the % "
        "parameters as arguments",
        "logging-format-interpolation",
        "Used when a logging statement has a call form of "
        '"logging.<logging method>(format_string.format(format_args...))"'
        ". Such calls should use % formatting instead, but leave "
        "interpolation to the logging function by passing the parameters "
        "as arguments.",
    ),
    "W1203": (
        "Use % formatting in logging functions and pass the % "
        "parameters as arguments",
        "logging-fstring-interpolation",
        "Used when a logging statement has a call form of "
        '"logging.method(f"..."))"'
        ". Such calls should use % formatting instead, but leave "
        "interpolation to the logging function by passing the parameters "
        "as arguments.",
    ),
    "E1200": (
        "Unsupported logging format character %r (%#02x) at index %d",
        "logging-unsupported-format",
        "Used when an unsupported format character is used in a logging "
        "statement format string.",
    ),
    "E1201": (
        "Logging format string ends in middle of conversion specifier",
        "logging-format-truncated",
        "Used when a logging statement format string terminates before "
        "the end of a conversion specifier.",
    ),
    "E1205": (
        "Too many arguments for logging format string",
        "logging-too-many-args",
        "Used when a logging format string is given too many arguments.",
    ),
    "E1206": (
        "Not enough arguments for logging format string",
        "logging-too-few-args",
        "Used when a logging format string is given too few arguments.",
    ),
}

CHECKED_CONVENIENCE_FUNCTIONS = {
    "critical",
    "debug",
    "error",
    "exception",
    "fatal",
    "info",
    "warn",
    "warning",
}

def is_method_call(func, types=(), methods=()):
    """Determines if a BoundMethod node represents a method call.

    Args:
      func (astroid.BoundMethod): The BoundMethod AST node to check.
      types (Optional[String]): Optional sequence of caller type names to restrict check.
      methods (Optional[String]): Optional sequence of method names to restrict check.

    Returns:
      bool: true if the node represents a method call for the given type and
      method names, False otherwise.
    """
    return (
        isinstance(func, astroid.BoundMethod)
        and isinstance(func.bound, astroid.Instance)
        and (func.bound.name in types if types else True)
        and (func.name in methods if methods else True)
    )

class LoggingChecker(checkers.BaseChecker):
    """Checks use of the logging module."""

    __implements__ = interfaces.IAstroidChecker
    name = "logging"
    msgs = MSGS

    options = (
        (
            "logging-modules",
            {
                "default": ("logging",),
                "type": "csv",
                "metavar": "<comma separated list>",
                "help": "Logging modules to check that the string format "
                "arguments are in logging function parameter format.",
            },
        ),
        (
            "logging-format-style",
            {
                "default": "old",
                "type": "choice",
                "metavar": "<old (%) or new ({)>",
                "choices": ["old", "new"],
                "help": "Format style used to check logging format string. "
                "`old` means using % formatting, while `new` is for `{}` formatting.",
            },
        ),
    )

    def visit_module(self, node):  # pylint: disable=unused-argument
        """Clears any state left in this checker from last module checked."""
        # The code being checked can just as easily "import logging as foo",
        # so it is necessary to process the imports and store in this field
        # what name the logging module is actually given.
        self._logging_names = set()
        logging_mods = self.config.logging_modules

        self._format_style = self.config.logging_format_style
        self._logging_modules = set(logging_mods)
        self._from_imports = {}
        for logging_mod in logging_mods:
            parts = logging_mod.rsplit(".", 1)
            if len(parts) > 1:
                self._from_imports[parts[0]] = parts[1]

    def visit_importfrom(self, node):
        """Checks to see if a module uses a non-Python logging module."""
        try:
            logging_name = self._from_imports[node.modname]
            for module, as_name in node.names:
                if module == logging_name:
                    self._logging_names.add(as_name or module)
        except KeyError:
            pass

    def visit_import(self, node):
        """Checks to see if this module uses Python's built-in logging."""
        for module, as_name in node.names:
            if module in self._logging_modules:
                self._logging_names.add(as_name or module)

    @check_messages(*MSGS)
    def visit_call(self, node):
        """Checks calls to logging methods."""

        def is_logging_name():
            return (
                isinstance(node.func, astroid.Attribute)
                and isinstance(node.func.expr, astroid.Name)
                and node.func.expr.name in self._logging_names
            )

        def is_logger_class():
            try:
                for inferred in node.func.infer():
                    if isinstance(inferred, astroid.BoundMethod):
                        parent = inferred._proxied.parent
                        if isinstance(parent, astroid.ClassDef) and (
                            parent.qname() == "logging.Logger"
                            or any(
                                ancestor.qname() == "logging.Logger"
                                for ancestor in parent.ancestors()
                            )
                        ):
                            return True, inferred._proxied.name
            except astroid.exceptions.InferenceError:
                pass
            return False, None

        if is_logging_name():
            name = node.func.attrname
        else:
            result, name = is_logger_class()
            if not result:
                return
        self._check_log_method(node, name)

    def _check_log_method(self, node, name):
        """Checks calls to logging.log(level, format, *format_args)."""
        if name == "log":
            if node.starargs or node.kwargs or len(node.args) < 2:
                # Either a malformed call, star args, or double-star args. Beyond
                # the scope of this checker.
                return
            format_pos = 1
        elif name in CHECKED_CONVENIENCE_FUNCTIONS:
            if node.starargs or node.kwargs or not node.args:
                # Either no args, star args, or double-star args. Beyond the
                # scope of this checker.
                return
            format_pos = 0
        else:
            return

        if isinstance(node.args[format_pos], astroid.BinOp):
            binop = node.args[format_pos]
            emit = binop.op == "%"
            if binop.op == "+":
                total_number_of_strings = sum(
                    1
                    for operand in (binop.left, binop.right)
                    if self._is_operand_literal_str(utils.safe_infer(operand))
                )
                emit = total_number_of_strings > 0
            if emit:
                self.add_message("logging-not-lazy", node=node)
        elif isinstance(node.args[format_pos], astroid.Call):
            self._check_call_func(node.args[format_pos])
        elif isinstance(node.args[format_pos], astroid.Const):
            self._check_format_string(node, format_pos)
        elif isinstance(
            node.args[format_pos], (astroid.FormattedValue, astroid.JoinedStr)
        ):
            self.add_message("logging-fstring-interpolation", node=node)

    @staticmethod
    def _is_operand_literal_str(operand):
        """
        Return True if the operand in argument is a literal string
        """
        return isinstance(operand, astroid.Const) and operand.name == "str"

    def _check_call_func(self, node):
        """Checks that function call is not format_string.format().

        Args:
          node (astroid.node_classes.Call):
            Call AST node to be checked.
        """
        func = utils.safe_infer(node.func)
        types = ("str", "unicode")
        methods = ("format",)
        if is_method_call(func, types, methods) and not is_complex_format_str(
            func.bound
        ):
            self.add_message("logging-format-interpolation", node=node)

    def _check_format_string(self, node, format_arg):
        """Checks that format string tokens match the supplied arguments.

        Args:
          node (astroid.node_classes.NodeNG): AST node to be checked.
          format_arg (int): Index of the format string in the node arguments.
        """
        num_args = _count_supplied_tokens(node.args[format_arg + 1 :])
        if not num_args:
            # If no args were supplied the string is not interpolated and can contain
            # formatting characters - it's used verbatim. Don't check any further.
            return
        format_string = node.args[format_arg].value
        if not isinstance(format_string, str):
            # If the log format is constant non-string (e.g. logging.debug(5)),
            # ensure there are no arguments.
            required_num_args = 0
        else:
            try:
                if self._format_style == "old":
                    keyword_args, required_num_args, _, _ = utils.parse_format_string(
                        format_string
                    )
                    if keyword_args:
                        # Keyword checking on logging strings is complicated by
                        # special keywords - out of scope.
                        return
                elif self._format_style == "new":
                    keyword_arguments, implicit_pos_args, explicit_pos_args = utils.parse_format_method_string(
                        format_string
                    )

                    keyword_args_cnt = len(
                        set(k for k, l in keyword_arguments if not isinstance(k, int))
                    )
                    required_num_args = (
                        keyword_args_cnt + implicit_pos_args + explicit_pos_args
                    )
            except utils.UnsupportedFormatCharacter as ex:
                char = format_string[ex.index]
                self.add_message(
                    "logging-unsupported-format",
                    node=node,
                    args=(char, ord(char), ex.index),
                )
                return
            except utils.IncompleteFormatString:
                self.add_message("logging-format-truncated", node=node)
                return
        if num_args > required_num_args:
            self.add_message("logging-too-many-args", node=node)
        elif num_args < required_num_args:
            self.add_message("logging-too-few-args", node=node)

def is_complex_format_str(node):
    """Checks if node represents a string with complex formatting specs.

    Args:
        node (astroid.node_classes.NodeNG): AST node to check
    Returns:
        bool: True if inferred string uses complex formatting, False otherwise
    """
    inferred = utils.safe_infer(node)
    if inferred is None or not isinstance(inferred.value, str):
        return True
    try:
        parsed = list(string.Formatter().parse(inferred.value))
    except ValueError:
        # This format string is invalid
        return False
    for _, _, format_spec, _ in parsed:
        if format_spec:
            return True
    return False

def _count_supplied_tokens(args):
    """Counts the number of tokens in an args list.

    The Python log functions allow for special keyword arguments: func,
    exc_info and extra. To handle these cases correctly, we only count
    arguments that aren't keywords.

    Args:
      args (list): AST nodes that are arguments for a log format string.

    Returns:
      int: Number of AST nodes that aren't keywords.
    """
    return sum(1 for arg in args if not isinstance(arg, astroid.Keyword))

def register(linter):
    """Required method to auto-register this checker."""
    linter.register_checker(LoggingChecker(linter))