# Copyright (c) 2016-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
# Copyright (c) 2019, 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
# Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>

# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/LICENSE

import astroid

from pylint.checkers import BaseChecker
from pylint.checkers.utils import check_messages, is_none, node_type
from pylint.interfaces import IAstroidChecker

BUILTINS = "builtins"


class MultipleTypesChecker(BaseChecker):
    """Checks for variable type redefinitions (NoneType excepted)

    At a function, method, class or module scope

    This rule could be improved:

    - Currently, if an attribute is set to different types in 2 methods of a
      same class, it won't be detected (see functional test)
    - One could improve the support for inference on assignment with tuples,
      ifexpr, etc. Also it would be great to have support for inference on
      str.split()
    """

    __implements__ = IAstroidChecker

    name = "multiple_types"
    msgs = {
        "R0204": (
            "Redefinition of %s type from %s to %s",
            "redefined-variable-type",
            "Used when the type of a variable changes inside a "
            "method or a function.",
        )
    }

    def visit_classdef(self, _):
        self._assigns.append({})

    @check_messages("redefined-variable-type")
    def leave_classdef(self, _):
        self._check_and_add_messages()

    visit_functiondef = visit_classdef
    leave_functiondef = leave_module = leave_classdef

    def visit_module(self, _):
        self._assigns = [{}]

    def _check_and_add_messages(self):
        assigns = self._assigns.pop()
        for name, args in assigns.items():
            if len(args) <= 1:
                continue
            orig_node, orig_type = args[0]
            # Check if there is a type in the following nodes that would be
            # different from orig_type.
            for redef_node, redef_type in args[1:]:
                if redef_type == orig_type:
                    continue
                # if a variable is defined to several types in an if node,
                # this is not actually redefining.
                orig_parent = orig_node.parent
                redef_parent = redef_node.parent
                if isinstance(orig_parent, astroid.If):
                    if orig_parent == redef_parent:
                        if (
                            redef_node in orig_parent.orelse
                            and orig_node not in orig_parent.orelse
                        ):
                            orig_node, orig_type = redef_node, redef_type
                            continue
                    elif isinstance(
                        redef_parent, astroid.If
                    ) and redef_parent in orig_parent.nodes_of_class(astroid.If):
                        orig_node, orig_type = redef_node, redef_type
                        continue
                orig_type = orig_type.replace(BUILTINS + ".", "")
                redef_type = redef_type.replace(BUILTINS + ".", "")
                self.add_message(
                    "redefined-variable-type",
                    node=redef_node,
                    args=(name, orig_type, redef_type),
                )
                break

    def visit_assign(self, node):
        # we don't handle multiple assignment nor slice assignment
        target = node.targets[0]
        if isinstance(target, (astroid.Tuple, astroid.Subscript)):
            return
        # ignore NoneType
        if is_none(node):
            return
        _type = node_type(node.value)
        if _type:
            self._assigns[-1].setdefault(target.as_string(), []).append(
                (node, _type.pytype())
            )


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

    :param linter: Main interface object for Pylint plugins
    :type linter: Pylint object
    """
    linter.register_checker(MultipleTypesChecker(linter))
