# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt

"""Basic checker for Python code."""

from __future__ import annotations

import collections
import itertools
from collections.abc import Iterator
from typing import TYPE_CHECKING, Literal, cast

import astroid
from astroid import bases, nodes, objects, util

from pylint import utils as lint_utils
from pylint.checkers import BaseChecker, utils
from pylint.interfaces import HIGH, INFERENCE, Confidence
from pylint.reporters.ureports import nodes as reporter_nodes
from pylint.utils import LinterStats

if TYPE_CHECKING:
    from pylint.lint.pylinter import PyLinter


class _BasicChecker(BaseChecker):
    """Permits separating multiple checks with the same checker name into
    classes/file.
    """

    name = "basic"


REVERSED_PROTOCOL_METHOD = "__reversed__"
SEQUENCE_PROTOCOL_METHODS = ("__getitem__", "__len__")
REVERSED_METHODS = (SEQUENCE_PROTOCOL_METHODS, (REVERSED_PROTOCOL_METHOD,))
# A mapping from qname -> symbol, to be used when generating messages
# about dangerous default values as arguments
DEFAULT_ARGUMENT_SYMBOLS = dict(
    zip(
        [".".join(["builtins", x]) for x in ("set", "dict", "list")],
        ["set()", "{}", "[]"],
    ),
    **{
        x: f"{x}()"
        for x in (
            "collections.deque",
            "collections.ChainMap",
            "collections.Counter",
            "collections.OrderedDict",
            "collections.defaultdict",
            "collections.UserDict",
            "collections.UserList",
        )
    },
)


def report_by_type_stats(
    sect: reporter_nodes.Section,
    stats: LinterStats,
    old_stats: LinterStats | None,
) -> None:
    """Make a report of.

    * percentage of different types documented
    * percentage of different types with a bad name
    """
    # percentage of different types documented and/or with a bad name
    nice_stats: dict[str, dict[str, str]] = {}
    for node_type in ("module", "class", "method", "function"):
        total = stats.get_node_count(node_type)
        nice_stats[node_type] = {}
        if total != 0:
            undocumented_node = stats.get_undocumented(node_type)
            documented = total - undocumented_node
            percent = (documented * 100.0) / total
            nice_stats[node_type]["percent_documented"] = f"{percent:.2f}"
            badname_node = stats.get_bad_names(node_type)
            percent = (badname_node * 100.0) / total
            nice_stats[node_type]["percent_badname"] = f"{percent:.2f}"
    lines = ["type", "number", "old number", "difference", "%documented", "%badname"]
    for node_type in ("module", "class", "method", "function"):
        node_type = cast(Literal["function", "class", "method", "module"], node_type)
        new = stats.get_node_count(node_type)
        old = old_stats.get_node_count(node_type) if old_stats else None
        diff_str = lint_utils.diff_string(old, new) if old else None
        lines += [
            node_type,
            str(new),
            str(old) if old else "NC",
            diff_str if diff_str else "NC",
            nice_stats[node_type].get("percent_documented", "0"),
            nice_stats[node_type].get("percent_badname", "0"),
        ]
    sect.append(reporter_nodes.Table(children=lines, cols=6, rheaders=1))


# pylint: disable-next = too-many-public-methods
class BasicChecker(_BasicChecker):
    """Basic checker.

    Checks for :
    * doc strings
    * number of arguments, local variables, branches, returns and statements in
    functions, methods
    * required module attributes
    * dangerous default values as arguments
    * redefinition of function / method / class
    * uses of the global statement
    """

    name = "basic"
    msgs = {
        "W0101": (
            "Unreachable code",
            "unreachable",
            'Used when there is some code behind a "return" or "raise" '
            "statement, which will never be accessed.",
        ),
        "W0102": (
            "Dangerous default value %s as argument",
            "dangerous-default-value",
            "Used when a mutable value as list or dictionary is detected in "
            "a default value for an argument.",
        ),
        "W0104": (
            "Statement seems to have no effect",
            "pointless-statement",
            "Used when a statement doesn't have (or at least seems to) any effect.",
        ),
        "W0105": (
            "String statement has no effect",
            "pointless-string-statement",
            "Used when a string is used as a statement (which of course "
            "has no effect). This is a particular case of W0104 with its "
            "own message so you can easily disable it if you're using "
            "those strings as documentation, instead of comments.",
        ),
        "W0106": (
            'Expression "%s" is assigned to nothing',
            "expression-not-assigned",
            "Used when an expression that is not a function call is assigned "
            "to nothing. Probably something else was intended.",
        ),
        "W0108": (
            "Lambda may not be necessary",
            "unnecessary-lambda",
            "Used when the body of a lambda expression is a function call "
            "on the same argument list as the lambda itself; such lambda "
            "expressions are in all but a few cases replaceable with the "
            "function being called in the body of the lambda.",
        ),
        "W0109": (
            "Duplicate key %r in dictionary",
            "duplicate-key",
            "Used when a dictionary expression binds the same key multiple times.",
        ),
        "W0122": (
            "Use of exec",
            "exec-used",
            "Raised when the 'exec' statement is used. It's dangerous to use this "
            "function for a user input, and it's also slower than actual code in "
            "general. This doesn't mean you should never use it, but you should "
            "consider alternatives first and restrict the functions available.",
        ),
        "W0123": (
            "Use of eval",
            "eval-used",
            'Used when you use the "eval" function, to discourage its '
            "usage. Consider using `ast.literal_eval` for safely evaluating "
            "strings containing Python expressions "
            "from untrusted sources.",
        ),
        "W0150": (
            "%s statement in finally block may swallow exception",
            "lost-exception",
            "Used when a break or a return statement is found inside the "
            "finally clause of a try...finally block: the exceptions raised "
            "in the try clause will be silently swallowed instead of being "
            "re-raised.",
        ),
        "W0199": (
            "Assert called on a populated tuple. Did you mean 'assert x,y'?",
            "assert-on-tuple",
            "A call of assert on a tuple will always evaluate to true if "
            "the tuple is not empty, and will always evaluate to false if "
            "it is.",
        ),
        "W0124": (
            'Following "as" with another context manager looks like a tuple.',
            "confusing-with-statement",
            "Emitted when a `with` statement component returns multiple values "
            "and uses name binding with `as` only for a part of those values, "
            "as in with ctx() as a, b. This can be misleading, since it's not "
            "clear if the context manager returns a tuple or if the node without "
            "a name binding is another context manager.",
        ),
        "W0125": (
            "Using a conditional statement with a constant value",
            "using-constant-test",
            "Emitted when a conditional statement (If or ternary if) "
            "uses a constant value for its test. This might not be what "
            "the user intended to do.",
        ),
        "W0126": (
            "Using a conditional statement with potentially wrong function or method call due to "
            "missing parentheses",
            "missing-parentheses-for-call-in-test",
            "Emitted when a conditional statement (If or ternary if) "
            "seems to wrongly call a function due to missing parentheses",
        ),
        "W0127": (
            "Assigning the same variable %r to itself",
            "self-assigning-variable",
            "Emitted when we detect that a variable is assigned to itself",
        ),
        "W0128": (
            "Redeclared variable %r in assignment",
            "redeclared-assigned-name",
            "Emitted when we detect that a variable was redeclared in the same assignment.",
        ),
        "E0111": (
            "The first reversed() argument is not a sequence",
            "bad-reversed-sequence",
            "Used when the first argument to reversed() builtin "
            "isn't a sequence (does not implement __reversed__, "
            "nor __getitem__ and __len__",
        ),
        "E0119": (
            "format function is not called on str",
            "misplaced-format-function",
            "Emitted when format function is not called on str object. "
            'e.g doing print("value: {}").format(123) instead of '
            'print("value: {}".format(123)). This might not be what the user '
            "intended to do.",
        ),
        "W0129": (
            "Assert statement has a string literal as its first argument. The assert will %s fail.",
            "assert-on-string-literal",
            "Used when an assert statement has a string literal as its first argument, which will "
            "cause the assert to always pass.",
        ),
        "W0130": (
            "Duplicate value %r in set",
            "duplicate-value",
            "This message is emitted when a set contains the same value two or more times.",
        ),
        "W0131": (
            "Named expression used without context",
            "named-expr-without-context",
            "Emitted if named expression is used to do a regular assignment "
            "outside a context like if, for, while, or a comprehension.",
        ),
        "W0133": (
            "Exception statement has no effect",
            "pointless-exception-statement",
            "Used when an exception is created without being assigned, raised or returned "
            "for subsequent use elsewhere.",
        ),
        "W0134": (
            "'return' shadowed by the 'finally' clause.",
            "return-in-finally",
            "Emitted when a 'return' statement is found in a 'finally' block. This will overwrite "
            "the return value of a function and should be avoided.",
        ),
    }

    reports = (("RP0101", "Statistics by type", report_by_type_stats),)

    def __init__(self, linter: PyLinter) -> None:
        super().__init__(linter)
        self._trys: list[nodes.Try]

    def open(self) -> None:
        """Initialize visit variables and statistics."""
        py_version = self.linter.config.py_version
        self._py38_plus = py_version >= (3, 8)
        self._trys = []
        self.linter.stats.reset_node_count()

    @utils.only_required_for_messages(
        "using-constant-test", "missing-parentheses-for-call-in-test"
    )
    def visit_if(self, node: nodes.If) -> None:
        self._check_using_constant_test(node, node.test)

    @utils.only_required_for_messages(
        "using-constant-test", "missing-parentheses-for-call-in-test"
    )
    def visit_ifexp(self, node: nodes.IfExp) -> None:
        self._check_using_constant_test(node, node.test)

    @utils.only_required_for_messages(
        "using-constant-test", "missing-parentheses-for-call-in-test"
    )
    def visit_comprehension(self, node: nodes.Comprehension) -> None:
        if node.ifs:
            for if_test in node.ifs:
                self._check_using_constant_test(node, if_test)

    def _check_using_constant_test(
        self,
        node: nodes.If | nodes.IfExp | nodes.Comprehension,
        test: nodes.NodeNG | None,
    ) -> None:
        const_nodes = (
            nodes.Module,
            nodes.GeneratorExp,
            nodes.Lambda,
            nodes.FunctionDef,
            nodes.ClassDef,
            bases.Generator,
            astroid.UnboundMethod,
            astroid.BoundMethod,
            nodes.Module,
        )
        structs = (nodes.Dict, nodes.Tuple, nodes.Set, nodes.List)

        # These nodes are excepted, since they are not constant
        # values, requiring a computation to happen.
        except_nodes = (
            nodes.Call,
            nodes.BinOp,
            nodes.BoolOp,
            nodes.UnaryOp,
            nodes.Subscript,
        )
        inferred = None
        emit = isinstance(test, (nodes.Const, *structs, *const_nodes))
        maybe_generator_call = None
        if not isinstance(test, except_nodes):
            inferred = utils.safe_infer(test)
            if isinstance(inferred, util.UninferableBase) and isinstance(
                test, nodes.Name
            ):
                emit, maybe_generator_call = BasicChecker._name_holds_generator(test)

        # Emit if calling a function that only returns GeneratorExp (always tests True)
        elif isinstance(test, nodes.Call):
            maybe_generator_call = test
        if maybe_generator_call:
            inferred_call = utils.safe_infer(maybe_generator_call.func)
            if isinstance(inferred_call, nodes.FunctionDef):
                # Can't use all(x) or not any(not x) for this condition, because it
                # will return True for empty generators, which is not what we want.
                all_returns_were_generator = None
                for return_node in inferred_call._get_return_nodes_skip_functions():
                    if not isinstance(return_node.value, nodes.GeneratorExp):
                        all_returns_were_generator = False
                        break
                    all_returns_were_generator = True
                if all_returns_were_generator:
                    self.add_message(
                        "using-constant-test", node=node, confidence=INFERENCE
                    )
                    return

        if emit:
            self.add_message("using-constant-test", node=test, confidence=INFERENCE)
        elif isinstance(inferred, const_nodes):
            # If the constant node is a FunctionDef or Lambda then
            # it may be an illicit function call due to missing parentheses
            call_inferred = None
            try:
                # Just forcing the generator to infer all elements.
                # astroid.exceptions.InferenceError are false positives
                # see https://github.com/pylint-dev/pylint/pull/8185
                if isinstance(inferred, (nodes.FunctionDef, nodes.Lambda)):
                    call_inferred = list(inferred.infer_call_result(node))
            except astroid.InferenceError:
                call_inferred = None
            if call_inferred:
                self.add_message(
                    "missing-parentheses-for-call-in-test",
                    node=test,
                    confidence=INFERENCE,
                )
            self.add_message("using-constant-test", node=test, confidence=INFERENCE)

    @staticmethod
    def _name_holds_generator(test: nodes.Name) -> tuple[bool, nodes.Call | None]:
        """Return whether `test` tests a name certain to hold a generator, or optionally
        a call that should be then tested to see if *it* returns only generators.
        """
        assert isinstance(test, nodes.Name)
        emit = False
        maybe_generator_call = None
        lookup_result = test.frame().lookup(test.name)
        if not lookup_result:
            return emit, maybe_generator_call
        maybe_generator_assigned = (
            isinstance(assign_name.parent.value, nodes.GeneratorExp)
            for assign_name in lookup_result[1]
            if isinstance(assign_name.parent, nodes.Assign)
        )
        first_item = next(maybe_generator_assigned, None)
        if first_item is not None:
            # Emit if this variable is certain to hold a generator
            if all(itertools.chain((first_item,), maybe_generator_assigned)):
                emit = True
            # If this variable holds the result of a call, save it for next test
            elif (
                len(lookup_result[1]) == 1
                and isinstance(lookup_result[1][0].parent, nodes.Assign)
                and isinstance(lookup_result[1][0].parent.value, nodes.Call)
            ):
                maybe_generator_call = lookup_result[1][0].parent.value
        return emit, maybe_generator_call

    def visit_module(self, _: nodes.Module) -> None:
        """Check module name, docstring and required arguments."""
        self.linter.stats.node_count["module"] += 1

    def visit_classdef(self, _: nodes.ClassDef) -> None:
        """Check module name, docstring and redefinition
        increment branch counter.
        """
        self.linter.stats.node_count["klass"] += 1

    @utils.only_required_for_messages(
        "pointless-statement",
        "pointless-exception-statement",
        "pointless-string-statement",
        "expression-not-assigned",
        "named-expr-without-context",
    )
    def visit_expr(self, node: nodes.Expr) -> None:
        """Check for various kind of statements without effect."""
        expr = node.value
        if isinstance(expr, nodes.Const) and isinstance(expr.value, str):
            # treat string statement in a separated message
            # Handle PEP-257 attribute docstrings.
            # An attribute docstring is defined as being a string right after
            # an assignment at the module level, class level or __init__ level.
            scope = expr.scope()
            if isinstance(scope, (nodes.ClassDef, nodes.Module, nodes.FunctionDef)):
                if isinstance(scope, nodes.FunctionDef) and scope.name != "__init__":
                    pass
                else:
                    sibling = expr.previous_sibling()
                    if (
                        sibling is not None
                        and sibling.scope() is scope
                        and isinstance(
                            sibling, (nodes.Assign, nodes.AnnAssign, nodes.TypeAlias)
                        )
                    ):
                        return
            self.add_message("pointless-string-statement", node=node)
            return

        # Warn W0133 for exceptions that are used as statements
        if isinstance(expr, nodes.Call):
            name = ""
            if isinstance(expr.func, nodes.Name):
                name = expr.func.name
            elif isinstance(expr.func, nodes.Attribute):
                name = expr.func.attrname

            # Heuristic: only run inference for names that begin with an uppercase char
            # This reduces W0133's coverage, but retains acceptable runtime performance
            # For more details, see: https://github.com/pylint-dev/pylint/issues/8073
            inferred = utils.safe_infer(expr) if name[:1].isupper() else None
            if isinstance(inferred, objects.ExceptionInstance):
                self.add_message(
                    "pointless-exception-statement", node=node, confidence=INFERENCE
                )
            return

        # Ignore if this is :
        # * the unique child of a try/except body
        # * a yield statement
        # * an ellipsis (which can be used on Python 3 instead of pass)
        # warn W0106 if we have any underlying function call (we can't predict
        # side effects), else pointless-statement
        if (
            isinstance(expr, (nodes.Yield, nodes.Await))
            or (
                isinstance(node.parent, (nodes.Try, nodes.TryStar))
                and node.parent.body == [node]
            )
            or (isinstance(expr, nodes.Const) and expr.value is Ellipsis)
        ):
            return
        if isinstance(expr, nodes.NamedExpr):
            self.add_message("named-expr-without-context", node=node, confidence=HIGH)
        elif any(expr.nodes_of_class(nodes.Call)):
            self.add_message(
                "expression-not-assigned", node=node, args=expr.as_string()
            )
        else:
            self.add_message("pointless-statement", node=node)

    @staticmethod
    def _filter_vararg(
        node: nodes.Lambda, call_args: list[nodes.NodeNG]
    ) -> Iterator[nodes.NodeNG]:
        # Return the arguments for the given call which are
        # not passed as vararg.
        for arg in call_args:
            if isinstance(arg, nodes.Starred):
                if (
                    isinstance(arg.value, nodes.Name)
                    and arg.value.name != node.args.vararg
                ):
                    yield arg
            else:
                yield arg

    @staticmethod
    def _has_variadic_argument(
        args: list[nodes.Starred | nodes.Keyword], variadic_name: str
    ) -> bool:
        return not args or any(
            (isinstance(a.value, nodes.Name) and a.value.name != variadic_name)
            or not isinstance(a.value, nodes.Name)
            for a in args
        )

    @utils.only_required_for_messages("unnecessary-lambda")
    def visit_lambda(self, node: nodes.Lambda) -> None:
        """Check whether the lambda is suspicious."""
        # if the body of the lambda is a call expression with the same
        # argument list as the lambda itself, then the lambda is
        # possibly unnecessary and at least suspicious.
        if node.args.defaults:
            # If the arguments of the lambda include defaults, then a
            # judgment cannot be made because there is no way to check
            # that the defaults defined by the lambda are the same as
            # the defaults defined by the function called in the body
            # of the lambda.
            return
        call = node.body
        if not isinstance(call, nodes.Call):
            # The body of the lambda must be a function call expression
            # for the lambda to be unnecessary.
            return
        match call.func:
            case nodes.Attribute(expr=nodes.Call()):
                # Chained call, the intermediate call might
                # return something else (but we don't check that, yet).
                return

        ordinary_args = list(node.args.args)
        new_call_args = list(self._filter_vararg(node, call.args))
        if node.args.kwarg:
            if self._has_variadic_argument(call.keywords, node.args.kwarg):
                return
        elif call.keywords:
            return

        if node.args.vararg:
            if self._has_variadic_argument(call.starargs, node.args.vararg):
                return
        elif call.starargs:
            return

        # The "ordinary" arguments must be in a correspondence such that:
        # ordinary_args[i].name == call.args[i].name.
        if len(ordinary_args) != len(new_call_args):
            return
        for arg, passed_arg in zip(ordinary_args, new_call_args):
            if not isinstance(passed_arg, nodes.Name):
                return
            if arg.name != passed_arg.name:
                return

        # The lambda is necessary if it uses its parameter in the function it is
        # calling in the lambda's body
        # e.g. lambda foo: (func1 if foo else func2)(foo)
        for name in call.func.nodes_of_class(nodes.Name):
            if name.lookup(name.name)[0] is node:
                return

        self.add_message("unnecessary-lambda", line=node.fromlineno, node=node)

    @utils.only_required_for_messages("dangerous-default-value")
    def visit_functiondef(self, node: nodes.FunctionDef) -> None:
        """Check function name, docstring, arguments, redefinition,
        variable names, max locals.
        """
        if node.is_method():
            self.linter.stats.node_count["method"] += 1
        else:
            self.linter.stats.node_count["function"] += 1
        self._check_dangerous_default(node)

    visit_asyncfunctiondef = visit_functiondef

    def _check_dangerous_default(self, node: nodes.FunctionDef) -> None:
        """Check for dangerous default values as arguments."""

        def is_iterable(internal_node: nodes.NodeNG) -> bool:
            return isinstance(internal_node, (nodes.List, nodes.Set, nodes.Dict))

        defaults = (node.args.defaults or []) + (node.args.kw_defaults or [])
        for default in defaults:
            if not default:
                continue
            try:
                value = next(default.infer())
            except astroid.InferenceError:
                continue

            if (
                isinstance(value, astroid.Instance)
                and value.qname() in DEFAULT_ARGUMENT_SYMBOLS
            ):
                if value is default:
                    msg = DEFAULT_ARGUMENT_SYMBOLS[value.qname()]
                elif isinstance(value, astroid.Instance) or is_iterable(value):
                    # We are here in the following situation(s):
                    #   * a dict/set/list/tuple call which wasn't inferred
                    #     to a syntax node ({}, () etc.). This can happen
                    #     when the arguments are invalid or unknown to
                    #     the inference.
                    #   * a variable from somewhere else, which turns out to be a list
                    #     or a dict.
                    if is_iterable(default):
                        msg = value.pytype()
                    elif isinstance(default, nodes.Call):
                        msg = f"{value.name}() ({value.qname()})"
                    else:
                        msg = f"{default.as_string()} ({value.qname()})"
                else:
                    # this argument is a name
                    msg = f"{default.as_string()} ({DEFAULT_ARGUMENT_SYMBOLS[value.qname()]})"
                self.add_message("dangerous-default-value", node=node, args=(msg,))

    @utils.only_required_for_messages("unreachable", "lost-exception")
    def visit_return(self, node: nodes.Return) -> None:
        """Return node visitor.

        1 - check if the node has a right sibling (if so, that's some
        unreachable code)
        2 - check if the node is inside the 'finally' clause of a 'try...finally'
        block
        """
        self._check_unreachable(node)
        # Is it inside final body of a try...finally block ?
        self._check_not_in_finally(node, "return", (nodes.FunctionDef,))

    @utils.only_required_for_messages("unreachable")
    def visit_continue(self, node: nodes.Continue) -> None:
        """Check is the node has a right sibling (if so, that's some unreachable
        code).
        """
        self._check_unreachable(node)

    @utils.only_required_for_messages("unreachable", "lost-exception")
    def visit_break(self, node: nodes.Break) -> None:
        """Break node visitor.

        1 - check if the node has a right sibling (if so, that's some
        unreachable code)
        2 - check if the node is inside the 'finally' clause of a 'try...finally'
        block
        """
        # 1 - Is it right sibling ?
        self._check_unreachable(node)
        # 2 - Is it inside final body of a try...finally block ?
        self._check_not_in_finally(node, "break", (nodes.For, nodes.While))

    @utils.only_required_for_messages("unreachable")
    def visit_raise(self, node: nodes.Raise) -> None:
        """Check if the node has a right sibling (if so, that's some unreachable
        code).
        """
        self._check_unreachable(node)

    def _check_misplaced_format_function(self, call_node: nodes.Call) -> None:
        if not isinstance(call_node.func, nodes.Attribute):
            return
        if call_node.func.attrname != "format":
            return

        expr = utils.safe_infer(call_node.func.expr)
        if isinstance(expr, util.UninferableBase):
            return
        if not expr:
            # we are doubtful on inferred type of node, so here just check if format
            # was called on print()
            match call_node.func.expr:
                case nodes.Call(func=nodes.Name(name="print")):
                    self.add_message("misplaced-format-function", node=call_node)

    @utils.only_required_for_messages(
        "eval-used",
        "exec-used",
        "bad-reversed-sequence",
        "misplaced-format-function",
        "unreachable",
    )
    def visit_call(self, node: nodes.Call) -> None:
        """Visit a Call node."""
        if utils.is_terminating_func(node):
            self._check_unreachable(node, confidence=INFERENCE)
        self._check_misplaced_format_function(node)
        if isinstance(node.func, nodes.Name):
            name = node.func.name
            # ignore the name if it's not a builtin (i.e. not defined in the
            # locals nor globals scope)
            if not (name in node.frame() or name in node.root()):
                match name:
                    case "exec":
                        self.add_message("exec-used", node=node)
                    case "reversed":
                        self._check_reversed(node)
                    case "eval":
                        self.add_message("eval-used", node=node)

    @utils.only_required_for_messages("assert-on-tuple", "assert-on-string-literal")
    def visit_assert(self, node: nodes.Assert) -> None:
        """Check whether assert is used on a tuple or string literal."""
        match node.test:
            case nodes.Tuple(elts=elts) if len(elts) > 0:
                self.add_message("assert-on-tuple", node=node, confidence=HIGH)
            case nodes.Const(value=str() as val):
                when = "never" if val else "always"
                self.add_message("assert-on-string-literal", node=node, args=(when,))

    @utils.only_required_for_messages("duplicate-key")
    def visit_dict(self, node: nodes.Dict) -> None:
        """Check duplicate key in dictionary."""
        keys = set()
        for k, _ in node.items:
            match k:
                case nodes.Const():
                    key = k.value
                case nodes.Attribute():
                    key = k.as_string()
                case _:
                    continue
            if key in keys:
                self.add_message("duplicate-key", node=node, args=key)
            keys.add(key)

    @utils.only_required_for_messages("duplicate-value")
    def visit_set(self, node: nodes.Set) -> None:
        """Check duplicate value in set."""
        values = set()
        for v in node.elts:
            if isinstance(v, nodes.Const):
                value = v.value
            else:
                continue
            if value in values:
                self.add_message(
                    "duplicate-value", node=node, args=value, confidence=HIGH
                )
            values.add(value)

    def visit_try(self, node: nodes.Try) -> None:
        """Update try block flag."""
        self._trys.append(node)

        for final_node in node.finalbody:
            for return_node in final_node.nodes_of_class(nodes.Return):
                self.add_message("return-in-finally", node=return_node, confidence=HIGH)

    def leave_try(self, _: nodes.Try) -> None:
        """Update try block flag."""
        self._trys.pop()

    def _check_unreachable(
        self,
        node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise | nodes.Call,
        confidence: Confidence = HIGH,
    ) -> None:
        """Check unreachable code."""
        unreachable_statement = node.next_sibling()
        if unreachable_statement is not None:
            if (
                isinstance(node, nodes.Return)
                and isinstance(unreachable_statement, nodes.Expr)
                and isinstance(unreachable_statement.value, nodes.Yield)
            ):
                # Don't add 'unreachable' for empty generators.
                # Only add warning if 'yield' is followed by another node.
                unreachable_statement = unreachable_statement.next_sibling()
                if unreachable_statement is None:
                    return
            self.add_message(
                "unreachable", node=unreachable_statement, confidence=confidence
            )

    def _check_not_in_finally(
        self,
        node: nodes.Break | nodes.Return,
        node_name: str,
        breaker_classes: tuple[nodes.NodeNG, ...] = (),
    ) -> None:
        """Check that a node is not inside a 'finally' clause of a
        'try...finally' statement.

        If we find a parent which type is in breaker_classes before
        a 'try...finally' block we skip the whole check.
        """
        # if self._trys is empty, we're not an in try block
        if not self._trys:
            return
        # the node could be a grand-grand...-child of the 'try...finally'
        _parent = node.parent
        _node = node
        while _parent and not isinstance(_parent, breaker_classes):
            if hasattr(_parent, "finalbody") and _node in _parent.finalbody:
                self.add_message("lost-exception", node=node, args=node_name)
                return
            _node = _parent
            _parent = _node.parent

    def _check_reversed(self, node: nodes.Call) -> None:
        """Check that the argument to `reversed` is a sequence."""
        try:
            argument = utils.safe_infer(utils.get_argument_from_call(node, position=0))
        except utils.NoSuchArgumentError:
            pass
        else:
            match argument:
                case util.UninferableBase():
                    return
                case None:
                    # Nothing was inferred.
                    # Try to see if we have iter().
                    if isinstance(node.args[0], nodes.Call):
                        try:
                            func = next(node.args[0].func.infer())
                        except astroid.InferenceError:
                            return
                        if getattr(
                            func, "name", None
                        ) == "iter" and utils.is_builtin_object(func):
                            self.add_message("bad-reversed-sequence", node=node)
                    return

                case nodes.List() | nodes.Tuple():
                    return

                case astroid.Instance() if not self._py38_plus:
                    # dicts are reversible, but only from Python 3.8 onward. Prior to
                    # that, any class based on dict must explicitly provide a
                    # __reversed__ method
                    if any(
                        ancestor.name == "dict" and utils.is_builtin_object(ancestor)
                        for ancestor in itertools.chain(
                            (argument._proxied,), argument._proxied.ancestors()
                        )
                    ):
                        try:
                            argument.locals[REVERSED_PROTOCOL_METHOD]
                        except KeyError:
                            self.add_message("bad-reversed-sequence", node=node)
                        return

            if hasattr(argument, "getattr"):
                # everything else is not a proper sequence for reversed()
                for methods in REVERSED_METHODS:
                    for meth in methods:
                        try:
                            argument.getattr(meth)
                        except astroid.NotFoundError:
                            break
                    else:
                        break
                else:
                    self.add_message("bad-reversed-sequence", node=node)
            else:
                self.add_message("bad-reversed-sequence", node=node)

    @utils.only_required_for_messages("confusing-with-statement")
    def visit_with(self, node: nodes.With) -> None:
        # a "with" statement with multiple managers corresponds
        # to one AST "With" node with multiple items
        pairs = node.items
        if pairs:
            for prev_pair, pair in itertools.pairwise(pairs):
                if isinstance(prev_pair[1], nodes.AssignName) and (
                    pair[1] is None and not isinstance(pair[0], nodes.Call)
                ):
                    # Don't emit a message if the second is a function call
                    # there's no way that can be mistaken for a name assignment.
                    # If the line number doesn't match
                    # we assume it's a nested "with".
                    self.add_message("confusing-with-statement", node=node)

    def _check_self_assigning_variable(self, node: nodes.Assign) -> None:
        # Detect assigning to the same variable.

        scope = node.scope()
        scope_locals = scope.locals

        rhs_names = []
        targets = node.targets
        if isinstance(targets[0], nodes.Tuple):
            if len(targets) != 1:
                # A complex assignment, so bail out early.
                return
            targets = targets[0].elts
            if len(targets) == 1:
                # Unpacking a variable into the same name.
                return

        match node.value:
            case nodes.Name():
                if len(targets) != 1:
                    return
                rhs_names = [node.value]
            case nodes.Tuple():
                rhs_count = len(node.value.elts)
                if len(targets) != rhs_count or rhs_count == 1:
                    return
                rhs_names = node.value.elts

        for target, lhs_name in zip(targets, rhs_names):
            if not isinstance(lhs_name, nodes.Name):
                continue
            if not isinstance(target, nodes.AssignName):
                continue
            # Check that the scope is different from a class level, which is usually
            # a pattern to expose module level attributes as class level ones.
            if isinstance(scope, nodes.ClassDef) and target.name in scope_locals:
                continue
            if target.name == lhs_name.name:
                self.add_message(
                    "self-assigning-variable", args=(target.name,), node=target
                )

    def _check_redeclared_assign_name(self, targets: list[nodes.NodeNG | None]) -> None:
        dummy_variables_rgx = self.linter.config.dummy_variables_rgx

        for target in targets:
            if not isinstance(target, nodes.Tuple):
                continue

            found_names = []
            for element in target.elts:
                if isinstance(element, nodes.Tuple):
                    self._check_redeclared_assign_name([element])
                elif isinstance(element, nodes.AssignName) and element.name != "_":
                    if dummy_variables_rgx and dummy_variables_rgx.match(element.name):
                        return
                    found_names.append(element.name)

            names = collections.Counter(found_names)
            for name, count in names.most_common():
                if count > 1:
                    self.add_message(
                        "redeclared-assigned-name", args=(name,), node=target
                    )

    @utils.only_required_for_messages(
        "self-assigning-variable", "redeclared-assigned-name"
    )
    def visit_assign(self, node: nodes.Assign) -> None:
        self._check_self_assigning_variable(node)
        self._check_redeclared_assign_name(node.targets)

    @utils.only_required_for_messages("redeclared-assigned-name")
    def visit_for(self, node: nodes.For) -> None:
        self._check_redeclared_assign_name([node.target])
