diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index b3fd71b2e8..01d8cd3189 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: from collections.abc import Generator, Iterable, Iterator + from typing import Literal from astroid.nodes import _base_nodes from astroid.typing import InferenceResult @@ -42,6 +43,8 @@ Consumption = dict[str, list[nodes.NodeNG]] + Scope = Literal["class", "comprehension", "function", "lambda", "module"] + SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") FUTURE = "__future__" @@ -478,7 +481,7 @@ class NamesConsumer: """A simple class to handle consumed, to consume and scope type info of node locals.""" node: nodes.NodeNG - scope_type: str + scope_type: Scope to_consume: Consumption consumed: Consumption @@ -492,7 +495,7 @@ class NamesConsumer: (e.g. for unused-variable) may need to add them back. """ - def __init__(self, node: nodes.NodeNG, scope_type: str): + def __init__(self, node: nodes.NodeNG, scope_type: Scope): self.node = node self.scope_type = scope_type @@ -872,52 +875,63 @@ def _uncertain_nodes_in_except_blocks( def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool: if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return, nodes.Continue)): return True - if isinstance(node, nodes.Expr) and isinstance(node.value, nodes.Call): - if utils.is_terminating_func(node.value): - return True - if ( - isinstance(node.value.func, nodes.Name) - and node.value.func.name == "assert_never" - ): - return True - if ( - isinstance(node, nodes.AnnAssign) - and node.value - and isinstance(node.target, nodes.AssignName) - and node.target.name == name - ): - return True + + if isinstance(node, nodes.Expr): + return isinstance(node.value, nodes.Call) and ( + ( + isinstance(node.value.func, nodes.Name) + and node.value.func.name == "assert_never" + ) + or utils.is_terminating_func(node.value) + ) + + if isinstance(node, nodes.AnnAssign): + return ( + node.value + and isinstance(node.target, nodes.AssignName) + and node.target.name == name + ) + if isinstance(node, nodes.Assign): - for target in node.targets: - for elt in utils.get_all_elements(target): - if isinstance(elt, nodes.Starred): - elt = elt.value - if isinstance(elt, nodes.AssignName) and elt.name == name: - return True + return any( + isinstance( + elt_or_val := elt.value if isinstance(elt, nodes.Starred) else elt, + nodes.AssignName, + ) + and elt_or_val.name == name + for target in node.targets + for elt in utils.get_all_elements(target) + ) + if isinstance(node, nodes.If): - if any( + return any( child_named_expr.target.name == name for child_named_expr in node.nodes_of_class(nodes.NamedExpr) - ): - return True - if isinstance(node, (nodes.Import, nodes.ImportFrom)) and any( - (node_name[1] and node_name[1] == name) or (node_name[0] == name) - for node_name in node.names - ): - return True - if isinstance(node, nodes.With) and any( - isinstance(item[1], nodes.AssignName) and item[1].name == name - for item in node.items - ): - return True - if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)) and node.name == name: - return True - if ( - isinstance(node, nodes.ExceptHandler) - and node.name - and node.name.name == name - ): - return True + ) + + if isinstance(node, (nodes.Import, nodes.ImportFrom)): + return any( + (alias and alias == name) or (node_name == name) + for node_name, alias in node.names + ) + + if isinstance(node, nodes.With): + return any( + isinstance(item[1], nodes.AssignName) and item[1].name == name + for item in node.items + ) + + if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)): + assert isinstance(node.name, str) + return node.name == name + + if isinstance(node, nodes.ExceptHandler): + return ( + node.name + and isinstance(node_name := node.name.name, str) + and node_name == name + ) + return False @staticmethod @@ -925,24 +939,27 @@ def _defines_name_raises_or_returns_recursive( name: str, node: nodes.NodeNG, ) -> bool: - """Return True if some child of `node` defines the name `name`, + """Return whether some child of `node` defines the name `name`, raises, or returns. """ for stmt in node.get_children(): if NamesConsumer._defines_name_raises_or_returns(name, stmt): return True + if isinstance(stmt, (nodes.If, nodes.With)): - if any( + return any( NamesConsumer._defines_name_raises_or_returns(name, nested_stmt) for nested_stmt in stmt.get_children() - ): - return True - if ( - isinstance(stmt, nodes.Try) - and not stmt.finalbody - and NamesConsumer._defines_name_raises_or_returns_recursive(name, stmt) - ): - return True + ) + + if isinstance(stmt, nodes.Try): + return ( + not stmt.finalbody + and NamesConsumer._defines_name_raises_or_returns_recursive( + name, stmt + ) + ) + return False @staticmethod @@ -1709,32 +1726,29 @@ def _should_node_be_skipped( # scope, ignore it. This prevents to access this scope instead of # the globals one in function members when there are some common # names. - if utils.is_ancestor_name(consumer.node, node) or ( - not is_start_index and self._ignore_class_scope(node) - ): - return True - - # Ignore inner class scope for keywords in class definition - if isinstance(node.parent, nodes.Keyword) and isinstance( - node.parent.parent, nodes.ClassDef - ): - return True - - elif consumer.scope_type == "function" and self._defined_in_function_definition( - node, consumer.node - ): - if any(node.name == param.name.name for param in consumer.node.type_params): - return False + return ( + utils.is_ancestor_name(consumer.node, node) + or (not is_start_index and self._ignore_class_scope(node)) + # Ignore inner class scope for keywords in class definition + or ( + isinstance(node.parent, nodes.Keyword) + and isinstance(node.parent.parent, nodes.ClassDef) + ) + ) + if consumer.scope_type == "function": # If the name node is used as a function default argument's value or as # a decorator, then start from the parent frame of the function instead # of the function frame - and thus open an inner class scope - return True + return self._defined_in_function_definition( + node, + consumer.node, + ) and not any( + node.name == param.name.name for param in consumer.node.type_params + ) - elif consumer.scope_type == "lambda" and utils.is_default_argument( - node, consumer.node - ): - return True + if consumer.scope_type == "lambda": + return utils.is_default_argument(node, consumer.node) return False @@ -1745,7 +1759,7 @@ def _check_consumer( stmt: nodes.NodeNG, frame: nodes.LocalsDictNodeNG, current_consumer: NamesConsumer, - base_scope_type: str, + base_scope_type: Scope, ) -> tuple[VariableVisitConsumerAction, list[nodes.NodeNG] | None]: """Checks a consumer for conditions that should trigger messages.""" # If the name has already been consumed, only check it's not a loop @@ -2176,7 +2190,7 @@ def _is_variable_violation( defstmt: _base_nodes.Statement, frame: nodes.LocalsDictNodeNG, # scope of statement of node defframe: nodes.LocalsDictNodeNG, - base_scope_type: str, + base_scope_type: Scope, is_recursive_klass: bool, ) -> tuple[bool, bool, bool]: maybe_before_assign = True