| # 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 |
| |
| """Variables checkers for Python code.""" |
| |
| from __future__ import annotations |
| |
| import collections |
| import copy |
| import itertools |
| import math |
| import os |
| import re |
| from collections import defaultdict |
| from collections.abc import Generator, Iterable, Iterator |
| from enum import Enum |
| from functools import cached_property |
| from typing import TYPE_CHECKING, Any, NamedTuple |
| |
| import astroid |
| import astroid.exceptions |
| from astroid import bases, extract_node, nodes, util |
| from astroid.nodes import _base_nodes |
| from astroid.typing import InferenceResult |
| |
| from pylint.checkers import BaseChecker, utils |
| from pylint.checkers.utils import ( |
| in_type_checking_block, |
| is_module_ignored, |
| is_postponed_evaluation_enabled, |
| is_sys_guard, |
| overridden_method, |
| ) |
| from pylint.constants import PY39_PLUS, TYPING_NEVER, TYPING_NORETURN |
| from pylint.interfaces import CONTROL_FLOW, HIGH, INFERENCE, INFERENCE_FAILURE |
| from pylint.typing import MessageDefinitionTuple |
| |
| if TYPE_CHECKING: |
| from pylint.lint import PyLinter |
| |
| SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") |
| FUTURE = "__future__" |
| # regexp for ignored argument name |
| IGNORED_ARGUMENT_NAMES = re.compile("_.*|^ignored_|^unused_") |
| # In Python 3.7 abc has a Python implementation which is preferred |
| # by astroid. Unfortunately this also messes up our explicit checks |
| # for `abc` |
| METACLASS_NAME_TRANSFORMS = {"_py_abc": "abc"} |
| BUILTIN_RANGE = "builtins.range" |
| TYPING_MODULE = "typing" |
| TYPING_NAMES = frozenset( |
| { |
| "Any", |
| "Callable", |
| "ClassVar", |
| "Generic", |
| "Optional", |
| "Tuple", |
| "Type", |
| "TypeVar", |
| "Union", |
| "AbstractSet", |
| "ByteString", |
| "Container", |
| "ContextManager", |
| "Hashable", |
| "ItemsView", |
| "Iterable", |
| "Iterator", |
| "KeysView", |
| "Mapping", |
| "MappingView", |
| "MutableMapping", |
| "MutableSequence", |
| "MutableSet", |
| "Sequence", |
| "Sized", |
| "ValuesView", |
| "Awaitable", |
| "AsyncIterator", |
| "AsyncIterable", |
| "Coroutine", |
| "Collection", |
| "AsyncGenerator", |
| "AsyncContextManager", |
| "Reversible", |
| "SupportsAbs", |
| "SupportsBytes", |
| "SupportsComplex", |
| "SupportsFloat", |
| "SupportsInt", |
| "SupportsRound", |
| "Counter", |
| "Deque", |
| "Dict", |
| "DefaultDict", |
| "List", |
| "Set", |
| "FrozenSet", |
| "NamedTuple", |
| "Generator", |
| "AnyStr", |
| "Text", |
| "Pattern", |
| "BinaryIO", |
| } |
| ) |
| |
| DICT_TYPES = ( |
| astroid.objects.DictValues, |
| astroid.objects.DictKeys, |
| astroid.objects.DictItems, |
| astroid.nodes.node_classes.Dict, |
| ) |
| |
| NODES_WITH_VALUE_ATTR = ( |
| nodes.Assign, |
| nodes.AnnAssign, |
| nodes.AugAssign, |
| nodes.Expr, |
| nodes.Return, |
| nodes.Match, |
| nodes.TypeAlias, |
| ) |
| |
| |
| class VariableVisitConsumerAction(Enum): |
| """Reported by _check_consumer() and its sub-methods to determine the |
| subsequent action to take in _undefined_and_used_before_checker(). |
| |
| Continue -> continue loop to next consumer |
| Return -> return and thereby break the loop |
| """ |
| |
| CONTINUE = 0 |
| RETURN = 1 |
| |
| |
| def _is_from_future_import(stmt: nodes.ImportFrom, name: str) -> bool | None: |
| """Check if the name is a future import from another module.""" |
| try: |
| module = stmt.do_import_module(stmt.modname) |
| except astroid.AstroidBuildingError: |
| return None |
| |
| for local_node in module.locals.get(name, []): |
| if isinstance(local_node, nodes.ImportFrom) and local_node.modname == FUTURE: |
| return True |
| return None |
| |
| |
| def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) -> str: |
| """Return extra information to add to the message for unpacking-non-sequence |
| and unbalanced-tuple/dict-unpacking errors. |
| """ |
| more = "" |
| if isinstance(inferred, DICT_TYPES): |
| if isinstance(node, nodes.Assign): |
| more = node.value.as_string() |
| elif isinstance(node, nodes.For): |
| more = node.iter.as_string() |
| return more |
| |
| inferred_module = inferred.root().name |
| if node.root().name == inferred_module: |
| if node.lineno == inferred.lineno: |
| more = f"'{inferred.as_string()}'" |
| elif inferred.lineno: |
| more = f"defined at line {inferred.lineno}" |
| elif inferred.lineno: |
| more = f"defined at line {inferred.lineno} of {inferred_module}" |
| return more |
| |
| |
| def _detect_global_scope( |
| node: nodes.Name, frame: nodes.LocalsDictNodeNG, defframe: nodes.LocalsDictNodeNG |
| ) -> bool: |
| """Detect that the given frames share a global scope. |
| |
| Two frames share a global scope when neither |
| of them are hidden under a function scope, as well |
| as any parent scope of them, until the root scope. |
| In this case, depending from something defined later on |
| will only work if guarded by a nested function definition. |
| |
| Example: |
| class A: |
| # B has the same global scope as `C`, leading to a NameError. |
| # Return True to indicate a shared scope. |
| class B(C): ... |
| class C: ... |
| |
| Whereas this does not lead to a NameError: |
| class A: |
| def guard(): |
| # Return False to indicate no scope sharing. |
| class B(C): ... |
| class C: ... |
| """ |
| def_scope = scope = None |
| if frame and frame.parent: |
| scope = frame.parent.scope() |
| if defframe and defframe.parent: |
| def_scope = defframe.parent.scope() |
| if ( |
| isinstance(frame, nodes.ClassDef) |
| and scope is not def_scope |
| and scope is utils.get_node_first_ancestor_of_type(node, nodes.FunctionDef) |
| ): |
| # If the current node's scope is a class nested under a function, |
| # and the def_scope is something else, then they aren't shared. |
| return False |
| if isinstance(frame, nodes.FunctionDef): |
| # If the parent of the current node is a |
| # function, then it can be under its scope (defined in); or |
| # the `->` part of annotations. The same goes |
| # for annotations of function arguments, they'll have |
| # their parent the Arguments node. |
| if frame.parent_of(defframe): |
| return node.lineno < defframe.lineno # type: ignore[no-any-return] |
| if not isinstance(node.parent, (nodes.FunctionDef, nodes.Arguments)): |
| return False |
| |
| break_scopes = [] |
| for current_scope in (scope or frame, def_scope): |
| # Look for parent scopes. If there is anything different |
| # than a module or a class scope, then the frames don't |
| # share a global scope. |
| parent_scope = current_scope |
| while parent_scope: |
| if not isinstance(parent_scope, (nodes.ClassDef, nodes.Module)): |
| break_scopes.append(parent_scope) |
| break |
| if parent_scope.parent: |
| parent_scope = parent_scope.parent.scope() |
| else: |
| break |
| if len(set(break_scopes)) > 1: |
| # Store different scopes than expected. |
| # If the stored scopes are, in fact, the very same, then it means |
| # that the two frames (frame and defframe) share the same scope, |
| # and we could apply our lineno analysis over them. |
| # For instance, this works when they are inside a function, the node |
| # that uses a definition and the definition itself. |
| return False |
| # At this point, we are certain that frame and defframe share a scope |
| # and the definition of the first depends on the second. |
| return frame.lineno < defframe.lineno # type: ignore[no-any-return] |
| |
| |
| def _infer_name_module( |
| node: nodes.Import, name: str |
| ) -> Generator[InferenceResult, None, None]: |
| context = astroid.context.InferenceContext() |
| context.lookupname = name |
| return node.infer(context, asname=False) # type: ignore[no-any-return] |
| |
| |
| def _fix_dot_imports( |
| not_consumed: dict[str, list[nodes.NodeNG]] |
| ) -> list[tuple[str, _base_nodes.ImportNode]]: |
| """Try to fix imports with multiple dots, by returning a dictionary |
| with the import names expanded. |
| |
| The function unflattens root imports, |
| like 'xml' (when we have both 'xml.etree' and 'xml.sax'), to 'xml.etree' |
| and 'xml.sax' respectively. |
| """ |
| names: dict[str, _base_nodes.ImportNode] = {} |
| for name, stmts in not_consumed.items(): |
| if any( |
| isinstance(stmt, nodes.AssignName) |
| and isinstance(stmt.assign_type(), nodes.AugAssign) |
| for stmt in stmts |
| ): |
| continue |
| for stmt in stmts: |
| if not isinstance(stmt, (nodes.ImportFrom, nodes.Import)): |
| continue |
| for imports in stmt.names: |
| second_name = None |
| import_module_name = imports[0] |
| if import_module_name == "*": |
| # In case of wildcard imports, |
| # pick the name from inside the imported module. |
| second_name = name |
| else: |
| name_matches_dotted_import = False |
| if ( |
| import_module_name.startswith(name) |
| and import_module_name.find(".") > -1 |
| ): |
| name_matches_dotted_import = True |
| |
| if name_matches_dotted_import or name in imports: |
| # Most likely something like 'xml.etree', |
| # which will appear in the .locals as 'xml'. |
| # Only pick the name if it wasn't consumed. |
| second_name = import_module_name |
| if second_name and second_name not in names: |
| names[second_name] = stmt |
| return sorted(names.items(), key=lambda a: a[1].fromlineno) |
| |
| |
| def _find_frame_imports(name: str, frame: nodes.LocalsDictNodeNG) -> bool: |
| """Detect imports in the frame, with the required *name*. |
| |
| Such imports can be considered assignments if they are not globals. |
| Returns True if an import for the given name was found. |
| """ |
| if name in _flattened_scope_names(frame.nodes_of_class(nodes.Global)): |
| return False |
| |
| imports = frame.nodes_of_class((nodes.Import, nodes.ImportFrom)) |
| for import_node in imports: |
| for import_name, import_alias in import_node.names: |
| # If the import uses an alias, check only that. |
| # Otherwise, check only the import name. |
| if import_alias: |
| if import_alias == name: |
| return True |
| elif import_name and import_name == name: |
| return True |
| return False |
| |
| |
| def _import_name_is_global( |
| stmt: nodes.Global | _base_nodes.ImportNode, global_names: set[str] |
| ) -> bool: |
| for import_name, import_alias in stmt.names: |
| # If the import uses an alias, check only that. |
| # Otherwise, check only the import name. |
| if import_alias: |
| if import_alias in global_names: |
| return True |
| elif import_name in global_names: |
| return True |
| return False |
| |
| |
| def _flattened_scope_names( |
| iterator: Iterator[nodes.Global | nodes.Nonlocal], |
| ) -> set[str]: |
| values = (set(stmt.names) for stmt in iterator) |
| return set(itertools.chain.from_iterable(values)) |
| |
| |
| def _assigned_locally(name_node: nodes.Name) -> bool: |
| """Checks if name_node has corresponding assign statement in same scope.""" |
| name_node_scope = name_node.scope() |
| assign_stmts = name_node_scope.nodes_of_class(nodes.AssignName) |
| return any(a.name == name_node.name for a in assign_stmts) or _find_frame_imports( |
| name_node.name, name_node_scope |
| ) |
| |
| |
| def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> bool: |
| skip_nodes = ( |
| nodes.FunctionDef, |
| nodes.ClassDef, |
| nodes.Import, |
| nodes.ImportFrom, |
| ) |
| for call in scope.nodes_of_class(nodes.Call, skip_klass=skip_nodes): |
| inferred = utils.safe_infer(call.func) |
| if ( |
| utils.is_builtin_object(inferred) |
| and getattr(inferred, "name", None) == "locals" |
| ): |
| if stmt.lineno < call.lineno: |
| return True |
| return False |
| |
| |
| MSGS: dict[str, MessageDefinitionTuple] = { |
| "E0601": ( |
| "Using variable %r before assignment", |
| "used-before-assignment", |
| "Emitted when a local variable is accessed before its assignment took place. " |
| "Assignments in try blocks are assumed not to have occurred when evaluating " |
| "associated except/finally blocks. Assignments in except blocks are assumed " |
| "not to have occurred when evaluating statements outside the block, except " |
| "when the associated try block contains a return statement.", |
| ), |
| "E0602": ( |
| "Undefined variable %r", |
| "undefined-variable", |
| "Used when an undefined variable is accessed.", |
| ), |
| "E0603": ( |
| "Undefined variable name %r in __all__", |
| "undefined-all-variable", |
| "Used when an undefined variable name is referenced in __all__.", |
| ), |
| "E0604": ( |
| "Invalid object %r in __all__, must contain only strings", |
| "invalid-all-object", |
| "Used when an invalid (non-string) object occurs in __all__.", |
| ), |
| "E0605": ( |
| "Invalid format for __all__, must be tuple or list", |
| "invalid-all-format", |
| "Used when __all__ has an invalid format.", |
| ), |
| "E0606": ( |
| "Possibly using variable %r before assignment", |
| "possibly-used-before-assignment", |
| "Emitted when a local variable is accessed before its assignment took place " |
| "in both branches of an if/else switch.", |
| ), |
| "E0611": ( |
| "No name %r in module %r", |
| "no-name-in-module", |
| "Used when a name cannot be found in a module.", |
| ), |
| "W0601": ( |
| "Global variable %r undefined at the module level", |
| "global-variable-undefined", |
| 'Used when a variable is defined through the "global" statement ' |
| "but the variable is not defined in the module scope.", |
| ), |
| "W0602": ( |
| "Using global for %r but no assignment is done", |
| "global-variable-not-assigned", |
| "When a variable defined in the global scope is modified in an inner scope, " |
| "the 'global' keyword is required in the inner scope only if there is an " |
| "assignment operation done in the inner scope.", |
| ), |
| "W0603": ( |
| "Using the global statement", # W0121 |
| "global-statement", |
| 'Used when you use the "global" statement to update a global ' |
| "variable. Pylint discourages its usage. That doesn't mean you cannot " |
| "use it!", |
| ), |
| "W0604": ( |
| "Using the global statement at the module level", # W0103 |
| "global-at-module-level", |
| 'Used when you use the "global" statement at the module level ' |
| "since it has no effect.", |
| ), |
| "W0611": ( |
| "Unused %s", |
| "unused-import", |
| "Used when an imported module or variable is not used.", |
| ), |
| "W0612": ( |
| "Unused variable %r", |
| "unused-variable", |
| "Used when a variable is defined but not used.", |
| ), |
| "W0613": ( |
| "Unused argument %r", |
| "unused-argument", |
| "Used when a function or method argument is not used.", |
| ), |
| "W0614": ( |
| "Unused import(s) %s from wildcard import of %s", |
| "unused-wildcard-import", |
| "Used when an imported module or variable is not used from a " |
| "`'from X import *'` style import.", |
| ), |
| "W0621": ( |
| "Redefining name %r from outer scope (line %s)", |
| "redefined-outer-name", |
| "Used when a variable's name hides a name defined in an outer scope or except handler.", |
| ), |
| "W0622": ( |
| "Redefining built-in %r", |
| "redefined-builtin", |
| "Used when a variable or function override a built-in.", |
| ), |
| "W0631": ( |
| "Using possibly undefined loop variable %r", |
| "undefined-loop-variable", |
| "Used when a loop variable (i.e. defined by a for loop or " |
| "a list comprehension or a generator expression) is used outside " |
| "the loop.", |
| ), |
| "W0632": ( |
| "Possible unbalanced tuple unpacking with sequence %s: left side has %d " |
| "label%s, right side has %d value%s", |
| "unbalanced-tuple-unpacking", |
| "Used when there is an unbalanced tuple unpacking in assignment", |
| {"old_names": [("E0632", "old-unbalanced-tuple-unpacking")]}, |
| ), |
| "E0633": ( |
| "Attempting to unpack a non-sequence%s", |
| "unpacking-non-sequence", |
| "Used when something which is not a sequence is used in an unpack assignment", |
| {"old_names": [("W0633", "old-unpacking-non-sequence")]}, |
| ), |
| "W0640": ( |
| "Cell variable %s defined in loop", |
| "cell-var-from-loop", |
| "A variable used in a closure is defined in a loop. " |
| "This will result in all closures using the same value for " |
| "the closed-over variable.", |
| ), |
| "W0641": ( |
| "Possibly unused variable %r", |
| "possibly-unused-variable", |
| "Used when a variable is defined but might not be used. " |
| "The possibility comes from the fact that locals() might be used, " |
| "which could consume or not the said variable", |
| ), |
| "W0642": ( |
| "Invalid assignment to %s in method", |
| "self-cls-assignment", |
| "Invalid assignment to self or cls in instance or class method " |
| "respectively.", |
| ), |
| "E0643": ( |
| "Invalid index for iterable length", |
| "potential-index-error", |
| "Emitted when an index used on an iterable goes beyond the length of that " |
| "iterable.", |
| ), |
| "W0644": ( |
| "Possible unbalanced dict unpacking with %s: " |
| "left side has %d label%s, right side has %d value%s", |
| "unbalanced-dict-unpacking", |
| "Used when there is an unbalanced dict unpacking in assignment or for loop", |
| ), |
| } |
| |
| |
| class ScopeConsumer(NamedTuple): |
| """Store nodes and their consumption states.""" |
| |
| to_consume: dict[str, list[nodes.NodeNG]] |
| consumed: dict[str, list[nodes.NodeNG]] |
| consumed_uncertain: defaultdict[str, list[nodes.NodeNG]] |
| scope_type: str |
| |
| |
| class NamesConsumer: |
| """A simple class to handle consumed, to consume and scope type info of node locals.""" |
| |
| def __init__(self, node: nodes.NodeNG, scope_type: str) -> None: |
| self._atomic = ScopeConsumer( |
| copy.copy(node.locals), {}, collections.defaultdict(list), scope_type |
| ) |
| self.node = node |
| self.names_under_always_false_test: set[str] = set() |
| self.names_defined_under_one_branch_only: set[str] = set() |
| |
| def __repr__(self) -> str: |
| _to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()] |
| _consumed = [f"{k}->{v}" for k, v in self._atomic.consumed.items()] |
| _consumed_uncertain = [ |
| f"{k}->{v}" for k, v in self._atomic.consumed_uncertain.items() |
| ] |
| to_consumes = ", ".join(_to_consumes) |
| consumed = ", ".join(_consumed) |
| consumed_uncertain = ", ".join(_consumed_uncertain) |
| return f""" |
| to_consume : {to_consumes} |
| consumed : {consumed} |
| consumed_uncertain: {consumed_uncertain} |
| scope_type : {self._atomic.scope_type} |
| """ |
| |
| def __iter__(self) -> Iterator[Any]: |
| return iter(self._atomic) |
| |
| @property |
| def to_consume(self) -> dict[str, list[nodes.NodeNG]]: |
| return self._atomic.to_consume |
| |
| @property |
| def consumed(self) -> dict[str, list[nodes.NodeNG]]: |
| return self._atomic.consumed |
| |
| @property |
| def consumed_uncertain(self) -> defaultdict[str, list[nodes.NodeNG]]: |
| """Retrieves nodes filtered out by get_next_to_consume() that may not |
| have executed. |
| |
| These include nodes such as statements in except blocks, or statements |
| in try blocks (when evaluating their corresponding except and finally |
| blocks). Checkers that want to treat the statements as executed |
| (e.g. for unused-variable) may need to add them back. |
| """ |
| return self._atomic.consumed_uncertain |
| |
| @property |
| def scope_type(self) -> str: |
| return self._atomic.scope_type |
| |
| def mark_as_consumed(self, name: str, consumed_nodes: list[nodes.NodeNG]) -> None: |
| """Mark the given nodes as consumed for the name. |
| |
| If all of the nodes for the name were consumed, delete the name from |
| the to_consume dictionary |
| """ |
| unconsumed = [n for n in self.to_consume[name] if n not in set(consumed_nodes)] |
| self.consumed[name] = consumed_nodes |
| |
| if unconsumed: |
| self.to_consume[name] = unconsumed |
| else: |
| del self.to_consume[name] |
| |
| def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: |
| """Return a list of the nodes that define `node` from this scope. |
| |
| If it is uncertain whether a node will be consumed, such as for statements in |
| except blocks, add it to self.consumed_uncertain instead of returning it. |
| Return None to indicate a special case that needs to be handled by the caller. |
| """ |
| name = node.name |
| parent_node = node.parent |
| found_nodes = self.to_consume.get(name) |
| node_statement = node.statement() |
| if ( |
| found_nodes |
| and isinstance(parent_node, nodes.Assign) |
| and parent_node == found_nodes[0].parent |
| ): |
| lhs = found_nodes[0].parent.targets[0] |
| if ( |
| isinstance(lhs, nodes.AssignName) and lhs.name == name |
| ): # this name is defined in this very statement |
| found_nodes = None |
| |
| if ( |
| found_nodes |
| and isinstance(parent_node, nodes.For) |
| and parent_node.iter == node |
| and parent_node.target in found_nodes |
| ): |
| found_nodes = None |
| |
| # Before filtering, check that this node's name is not a nonlocal |
| if any( |
| isinstance(child, nodes.Nonlocal) and node.name in child.names |
| for child in node.frame().get_children() |
| ): |
| return found_nodes |
| |
| # And no comprehension is under the node's frame |
| if VariablesChecker._comprehension_between_frame_and_node(node): |
| return found_nodes |
| |
| # Filter out assignments in ExceptHandlers that node is not contained in |
| if found_nodes: |
| found_nodes = [ |
| n |
| for n in found_nodes |
| if not isinstance(n.statement(), nodes.ExceptHandler) |
| or n.statement().parent_of(node) |
| ] |
| |
| # Filter out assignments guarded by always false conditions |
| if found_nodes: |
| uncertain_nodes = self._uncertain_nodes_if_tests(found_nodes, node) |
| self.consumed_uncertain[node.name] += uncertain_nodes |
| uncertain_nodes_set = set(uncertain_nodes) |
| found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] |
| |
| # Filter out assignments in an Except clause that the node is not |
| # contained in, assuming they may fail |
| if found_nodes: |
| uncertain_nodes = self._uncertain_nodes_in_except_blocks( |
| found_nodes, node, node_statement |
| ) |
| self.consumed_uncertain[node.name] += uncertain_nodes |
| uncertain_nodes_set = set(uncertain_nodes) |
| found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] |
| |
| # If this node is in a Finally block of a Try/Finally, |
| # filter out assignments in the try portion, assuming they may fail |
| if found_nodes: |
| uncertain_nodes = ( |
| self._uncertain_nodes_in_try_blocks_when_evaluating_finally_blocks( |
| found_nodes, node_statement, name |
| ) |
| ) |
| self.consumed_uncertain[node.name] += uncertain_nodes |
| uncertain_nodes_set = set(uncertain_nodes) |
| found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] |
| |
| # If this node is in an ExceptHandler, |
| # filter out assignments in the try portion, assuming they may fail |
| if found_nodes: |
| uncertain_nodes = ( |
| self._uncertain_nodes_in_try_blocks_when_evaluating_except_blocks( |
| found_nodes, node_statement |
| ) |
| ) |
| self.consumed_uncertain[node.name] += uncertain_nodes |
| uncertain_nodes_set = set(uncertain_nodes) |
| found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] |
| |
| return found_nodes |
| |
| def _inferred_to_define_name_raise_or_return( |
| self, name: str, node: nodes.NodeNG |
| ) -> bool: |
| """Return True if there is a path under this `if_node` |
| that is inferred to define `name`, raise, or return. |
| """ |
| # Handle try and with |
| if isinstance(node, nodes.Try): |
| # Allow either a path through try/else/finally OR a path through ALL except handlers |
| try_except_node = node |
| if node.finalbody: |
| try_except_node = next( |
| (child for child in node.nodes_of_class(nodes.Try)), |
| None, |
| ) |
| handlers = try_except_node.handlers if try_except_node else [] |
| return NamesConsumer._defines_name_raises_or_returns_recursive( |
| name, node |
| ) or all( |
| NamesConsumer._defines_name_raises_or_returns_recursive(name, handler) |
| for handler in handlers |
| ) |
| |
| if isinstance(node, (nodes.With, nodes.For, nodes.While)): |
| return NamesConsumer._defines_name_raises_or_returns_recursive(name, node) |
| |
| if not isinstance(node, nodes.If): |
| return False |
| |
| # Be permissive if there is a break or a continue |
| if any(node.nodes_of_class(nodes.Break, nodes.Continue)): |
| return True |
| |
| # Is there an assignment in this node itself, e.g. in named expression? |
| if NamesConsumer._defines_name_raises_or_returns(name, node): |
| return True |
| |
| test = node.test.value if isinstance(node.test, nodes.NamedExpr) else node.test |
| all_inferred = utils.infer_all(test) |
| only_search_if = False |
| only_search_else = True |
| |
| for inferred in all_inferred: |
| if not isinstance(inferred, nodes.Const): |
| only_search_else = False |
| continue |
| val = inferred.value |
| only_search_if = only_search_if or (val != NotImplemented and val) |
| only_search_else = only_search_else and not val |
| |
| # Only search else branch when test condition is inferred to be false |
| if all_inferred and only_search_else: |
| self.names_under_always_false_test.add(name) |
| return self._branch_handles_name(name, node.orelse) |
| # Search both if and else branches |
| if_branch_handles = self._branch_handles_name(name, node.body) |
| else_branch_handles = self._branch_handles_name(name, node.orelse) |
| if if_branch_handles ^ else_branch_handles: |
| self.names_defined_under_one_branch_only.add(name) |
| elif name in self.names_defined_under_one_branch_only: |
| self.names_defined_under_one_branch_only.remove(name) |
| return if_branch_handles and else_branch_handles |
| |
| def _branch_handles_name(self, name: str, body: Iterable[nodes.NodeNG]) -> bool: |
| return any( |
| NamesConsumer._defines_name_raises_or_returns(name, if_body_stmt) |
| or isinstance( |
| if_body_stmt, |
| ( |
| nodes.If, |
| nodes.Try, |
| nodes.With, |
| nodes.For, |
| nodes.While, |
| ), |
| ) |
| and self._inferred_to_define_name_raise_or_return(name, if_body_stmt) |
| for if_body_stmt in body |
| ) |
| |
| def _uncertain_nodes_if_tests( |
| self, found_nodes: list[nodes.NodeNG], node: nodes.NodeNG |
| ) -> list[nodes.NodeNG]: |
| """Identify nodes of uncertain execution because they are defined under if |
| tests. |
| |
| Don't identify a node if there is a path that is inferred to |
| define the name, raise, or return (e.g. any executed if/elif/else branch). |
| """ |
| uncertain_nodes = [] |
| for other_node in found_nodes: |
| if isinstance(other_node, nodes.AssignName): |
| name = other_node.name |
| elif isinstance(other_node, (nodes.Import, nodes.ImportFrom)): |
| name = node.name |
| else: |
| continue |
| |
| all_if = [ |
| n |
| for n in other_node.node_ancestors() |
| if isinstance(n, nodes.If) and not n.parent_of(node) |
| ] |
| if not all_if: |
| continue |
| |
| closest_if = all_if[0] |
| if ( |
| isinstance(node, nodes.AssignName) |
| and node.frame() is not closest_if.frame() |
| ): |
| continue |
| if closest_if.parent_of(node): |
| continue |
| |
| outer_if = all_if[-1] |
| if NamesConsumer._node_guarded_by_same_test(node, outer_if): |
| continue |
| |
| # Name defined in the if/else control flow |
| if self._inferred_to_define_name_raise_or_return(name, outer_if): |
| continue |
| |
| uncertain_nodes.append(other_node) |
| |
| return uncertain_nodes |
| |
| @staticmethod |
| def _node_guarded_by_same_test(node: nodes.NodeNG, other_if: nodes.If) -> bool: |
| """Identify if `node` is guarded by an equivalent test as `other_if`. |
| |
| Two tests are equivalent if their string representations are identical |
| or if their inferred values consist only of constants and those constants |
| are identical, and the if test guarding `node` is not a Name. |
| """ |
| other_if_test_as_string = other_if.test.as_string() |
| other_if_test_all_inferred = utils.infer_all(other_if.test) |
| for ancestor in node.node_ancestors(): |
| if not isinstance(ancestor, nodes.If): |
| continue |
| if ancestor.test.as_string() == other_if_test_as_string: |
| return True |
| if isinstance(ancestor.test, nodes.Name): |
| continue |
| all_inferred = utils.infer_all(ancestor.test) |
| if len(all_inferred) == len(other_if_test_all_inferred): |
| if any( |
| not isinstance(test, nodes.Const) |
| for test in (*all_inferred, *other_if_test_all_inferred) |
| ): |
| continue |
| if {test.value for test in all_inferred} != { |
| test.value for test in other_if_test_all_inferred |
| }: |
| continue |
| return True |
| |
| return False |
| |
| @staticmethod |
| def _uncertain_nodes_in_except_blocks( |
| found_nodes: list[nodes.NodeNG], |
| node: nodes.NodeNG, |
| node_statement: _base_nodes.Statement, |
| ) -> list[nodes.NodeNG]: |
| """Return any nodes in ``found_nodes`` that should be treated as uncertain |
| because they are in an except block. |
| """ |
| uncertain_nodes = [] |
| for other_node in found_nodes: |
| other_node_statement = other_node.statement() |
| # Only testing for statements in the except block of Try |
| closest_except_handler = utils.get_node_first_ancestor_of_type( |
| other_node_statement, nodes.ExceptHandler |
| ) |
| if not closest_except_handler: |
| continue |
| # If the other node is in the same scope as this node, assume it executes |
| if closest_except_handler.parent_of(node): |
| continue |
| closest_try_except: nodes.Try = closest_except_handler.parent |
| # If the try or else blocks return, assume the except blocks execute. |
| try_block_returns = any( |
| isinstance(try_statement, nodes.Return) |
| for try_statement in closest_try_except.body |
| ) |
| else_block_returns = any( |
| isinstance(else_statement, nodes.Return) |
| for else_statement in closest_try_except.orelse |
| ) |
| else_block_exits = any( |
| isinstance(else_statement, nodes.Expr) |
| and isinstance(else_statement.value, nodes.Call) |
| and utils.is_terminating_func(else_statement.value) |
| for else_statement in closest_try_except.orelse |
| ) |
| else_block_continues = any( |
| isinstance(else_statement, nodes.Continue) |
| for else_statement in closest_try_except.orelse |
| ) |
| if ( |
| else_block_continues |
| and isinstance(node_statement.parent, (nodes.For, nodes.While)) |
| and closest_try_except.parent.parent_of(node_statement) |
| ): |
| continue |
| |
| if try_block_returns or else_block_returns or else_block_exits: |
| # Exception: if this node is in the final block of the other_node_statement, |
| # it will execute before returning. Assume the except statements are uncertain. |
| if ( |
| isinstance(node_statement.parent, nodes.Try) |
| and node_statement in node_statement.parent.finalbody |
| and closest_try_except.parent.parent_of(node_statement) |
| ): |
| uncertain_nodes.append(other_node) |
| # Or the node_statement is in the else block of the relevant Try |
| elif ( |
| isinstance(node_statement.parent, nodes.Try) |
| and node_statement in node_statement.parent.orelse |
| and closest_try_except.parent.parent_of(node_statement) |
| ): |
| uncertain_nodes.append(other_node) |
| # Assume the except blocks execute, so long as each handler |
| # defines the name, raises, or returns. |
| elif all( |
| NamesConsumer._defines_name_raises_or_returns_recursive( |
| node.name, handler |
| ) |
| for handler in closest_try_except.handlers |
| ): |
| continue |
| |
| if NamesConsumer._check_loop_finishes_via_except(node, closest_try_except): |
| continue |
| |
| # Passed all tests for uncertain execution |
| uncertain_nodes.append(other_node) |
| return uncertain_nodes |
| |
| @staticmethod |
| 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.AnnAssign) |
| and node.value |
| and isinstance(node.target, nodes.AssignName) |
| and node.target.name == name |
| ): |
| return True |
| 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 |
| if isinstance(node, nodes.If): |
| if 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 |
| return False |
| |
| @staticmethod |
| def _defines_name_raises_or_returns_recursive( |
| name: str, node: nodes.NodeNG |
| ) -> bool: |
| """Return True if 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( |
| 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 |
| return False |
| |
| @staticmethod |
| def _check_loop_finishes_via_except( |
| node: nodes.NodeNG, other_node_try_except: nodes.Try |
| ) -> bool: |
| """Check for a specific control flow scenario. |
| |
| Described in https://github.com/pylint-dev/pylint/issues/5683. |
| |
| A scenario where the only non-break exit from a loop consists of the very |
| except handler we are examining, such that code in the `else` branch of |
| the loop can depend on it being assigned. |
| |
| Example: |
| for _ in range(3): |
| try: |
| do_something() |
| except: |
| name = 1 <-- only non-break exit from loop |
| else: |
| break |
| else: |
| print(name) |
| """ |
| if not other_node_try_except.orelse: |
| return False |
| closest_loop: None | (nodes.For | nodes.While) = ( |
| utils.get_node_first_ancestor_of_type(node, (nodes.For, nodes.While)) |
| ) |
| if closest_loop is None: |
| return False |
| if not any( |
| else_statement is node or else_statement.parent_of(node) |
| for else_statement in closest_loop.orelse |
| ): |
| # `node` not guarded by `else` |
| return False |
| for inner_else_statement in other_node_try_except.orelse: |
| if isinstance(inner_else_statement, nodes.Break): |
| break_stmt = inner_else_statement |
| break |
| else: |
| # No break statement |
| return False |
| |
| def _try_in_loop_body( |
| other_node_try_except: nodes.Try, loop: nodes.For | nodes.While |
| ) -> bool: |
| """Return True if `other_node_try_except` is a descendant of `loop`.""" |
| return any( |
| loop_body_statement is other_node_try_except |
| or loop_body_statement.parent_of(other_node_try_except) |
| for loop_body_statement in loop.body |
| ) |
| |
| if not _try_in_loop_body(other_node_try_except, closest_loop): |
| for ancestor in closest_loop.node_ancestors(): |
| if isinstance(ancestor, (nodes.For, nodes.While)): |
| if _try_in_loop_body(other_node_try_except, ancestor): |
| break |
| else: |
| # `other_node_try_except` didn't have a shared ancestor loop |
| return False |
| |
| for loop_stmt in closest_loop.body: |
| if NamesConsumer._recursive_search_for_continue_before_break( |
| loop_stmt, break_stmt |
| ): |
| break |
| else: |
| # No continue found, so we arrived at our special case! |
| return True |
| return False |
| |
| @staticmethod |
| def _recursive_search_for_continue_before_break( |
| stmt: _base_nodes.Statement, break_stmt: nodes.Break |
| ) -> bool: |
| """Return True if any Continue node can be found in descendants of `stmt` |
| before encountering `break_stmt`, ignoring any nested loops. |
| """ |
| if stmt is break_stmt: |
| return False |
| if isinstance(stmt, nodes.Continue): |
| return True |
| for child in stmt.get_children(): |
| if isinstance(stmt, (nodes.For, nodes.While)): |
| continue |
| if NamesConsumer._recursive_search_for_continue_before_break( |
| child, break_stmt |
| ): |
| return True |
| return False |
| |
| @staticmethod |
| def _uncertain_nodes_in_try_blocks_when_evaluating_except_blocks( |
| found_nodes: list[nodes.NodeNG], node_statement: _base_nodes.Statement |
| ) -> list[nodes.NodeNG]: |
| """Return any nodes in ``found_nodes`` that should be treated as uncertain. |
| |
| Nodes are uncertain when they are in a try block and the ``node_statement`` |
| being evaluated is in one of its except handlers. |
| """ |
| uncertain_nodes: list[nodes.NodeNG] = [] |
| closest_except_handler = utils.get_node_first_ancestor_of_type( |
| node_statement, nodes.ExceptHandler |
| ) |
| if closest_except_handler is None: |
| return uncertain_nodes |
| for other_node in found_nodes: |
| other_node_statement = other_node.statement() |
| # If the other statement is the except handler guarding `node`, it executes |
| if other_node_statement is closest_except_handler: |
| continue |
| # Ensure other_node is in a try block |
| ( |
| other_node_try_ancestor, |
| other_node_try_ancestor_visited_child, |
| ) = utils.get_node_first_ancestor_of_type_and_its_child( |
| other_node_statement, nodes.Try |
| ) |
| if other_node_try_ancestor is None: |
| continue |
| if ( |
| other_node_try_ancestor_visited_child |
| not in other_node_try_ancestor.body |
| ): |
| continue |
| # Make sure nesting is correct -- there should be at least one |
| # except handler that is a sibling attached to the try ancestor, |
| # or is an ancestor of the try ancestor. |
| if not any( |
| closest_except_handler in other_node_try_ancestor.handlers |
| or other_node_try_ancestor_except_handler |
| in closest_except_handler.node_ancestors() |
| for other_node_try_ancestor_except_handler in other_node_try_ancestor.handlers |
| ): |
| continue |
| # Passed all tests for uncertain execution |
| uncertain_nodes.append(other_node) |
| return uncertain_nodes |
| |
| @staticmethod |
| def _uncertain_nodes_in_try_blocks_when_evaluating_finally_blocks( |
| found_nodes: list[nodes.NodeNG], |
| node_statement: _base_nodes.Statement, |
| name: str, |
| ) -> list[nodes.NodeNG]: |
| uncertain_nodes: list[nodes.NodeNG] = [] |
| ( |
| closest_try_finally_ancestor, |
| child_of_closest_try_finally_ancestor, |
| ) = utils.get_node_first_ancestor_of_type_and_its_child( |
| node_statement, nodes.Try |
| ) |
| if closest_try_finally_ancestor is None: |
| return uncertain_nodes |
| if ( |
| child_of_closest_try_finally_ancestor |
| not in closest_try_finally_ancestor.finalbody |
| ): |
| return uncertain_nodes |
| for other_node in found_nodes: |
| other_node_statement = other_node.statement() |
| ( |
| other_node_try_finally_ancestor, |
| child_of_other_node_try_finally_ancestor, |
| ) = utils.get_node_first_ancestor_of_type_and_its_child( |
| other_node_statement, nodes.Try |
| ) |
| if other_node_try_finally_ancestor is None: |
| continue |
| # other_node needs to descend from the try of a try/finally. |
| if ( |
| child_of_other_node_try_finally_ancestor |
| not in other_node_try_finally_ancestor.body |
| ): |
| continue |
| # If the two try/finally ancestors are not the same, then |
| # node_statement's closest try/finally ancestor needs to be in |
| # the final body of other_node's try/finally ancestor, or |
| # descend from one of the statements in that final body. |
| if ( |
| other_node_try_finally_ancestor is not closest_try_finally_ancestor |
| and not any( |
| other_node_final_statement is closest_try_finally_ancestor |
| or other_node_final_statement.parent_of( |
| closest_try_finally_ancestor |
| ) |
| for other_node_final_statement in other_node_try_finally_ancestor.finalbody |
| ) |
| ): |
| continue |
| # Is the name defined in all exception clauses? |
| if other_node_try_finally_ancestor.handlers and all( |
| NamesConsumer._defines_name_raises_or_returns_recursive(name, handler) |
| for handler in other_node_try_finally_ancestor.handlers |
| ): |
| continue |
| # Passed all tests for uncertain execution |
| uncertain_nodes.append(other_node) |
| return uncertain_nodes |
| |
| |
| # pylint: disable=too-many-public-methods |
| class VariablesChecker(BaseChecker): |
| """BaseChecker for variables. |
| |
| Checks for |
| * unused variables / imports |
| * undefined variables |
| * redefinition of variable from builtins or from an outer scope or except handler |
| * use of variable before assignment |
| * __all__ consistency |
| * self/cls assignment |
| """ |
| |
| name = "variables" |
| msgs = MSGS |
| options = ( |
| ( |
| "init-import", |
| { |
| "default": False, |
| "type": "yn", |
| "metavar": "<y or n>", |
| "help": "Tells whether we should check for unused import in " |
| "__init__ files.", |
| }, |
| ), |
| ( |
| "dummy-variables-rgx", |
| { |
| "default": "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_", |
| "type": "regexp", |
| "metavar": "<regexp>", |
| "help": "A regular expression matching the name of dummy " |
| "variables (i.e. expected to not be used).", |
| }, |
| ), |
| ( |
| "additional-builtins", |
| { |
| "default": (), |
| "type": "csv", |
| "metavar": "<comma separated list>", |
| "help": "List of additional names supposed to be defined in " |
| "builtins. Remember that you should avoid defining new builtins " |
| "when possible.", |
| }, |
| ), |
| ( |
| "callbacks", |
| { |
| "default": ("cb_", "_cb"), |
| "type": "csv", |
| "metavar": "<callbacks>", |
| "help": "List of strings which can identify a callback " |
| "function by name. A callback name must start or " |
| "end with one of those strings.", |
| }, |
| ), |
| ( |
| "redefining-builtins-modules", |
| { |
| "default": ( |
| "six.moves", |
| "past.builtins", |
| "future.builtins", |
| "builtins", |
| "io", |
| ), |
| "type": "csv", |
| "metavar": "<comma separated list>", |
| "help": "List of qualified module names which can have objects " |
| "that can redefine builtins.", |
| }, |
| ), |
| ( |
| "ignored-argument-names", |
| { |
| "default": IGNORED_ARGUMENT_NAMES, |
| "type": "regexp", |
| "metavar": "<regexp>", |
| "help": "Argument names that match this expression will be ignored.", |
| }, |
| ), |
| ( |
| "allow-global-unused-variables", |
| { |
| "default": True, |
| "type": "yn", |
| "metavar": "<y or n>", |
| "help": "Tells whether unused global variables should be treated as a violation.", |
| }, |
| ), |
| ( |
| "allowed-redefined-builtins", |
| { |
| "default": (), |
| "type": "csv", |
| "metavar": "<comma separated list>", |
| "help": "List of names allowed to shadow builtins", |
| }, |
| ), |
| ) |
| |
| def __init__(self, linter: PyLinter) -> None: |
| super().__init__(linter) |
| self._to_consume: list[NamesConsumer] = [] |
| self._type_annotation_names: list[str] = [] |
| self._except_handler_names_queue: list[ |
| tuple[nodes.ExceptHandler, nodes.AssignName] |
| ] = [] |
| """This is a queue, last in first out.""" |
| self._evaluated_type_checking_scopes: dict[ |
| str, list[nodes.LocalsDictNodeNG] |
| ] = {} |
| self._postponed_evaluation_enabled = False |
| |
| @utils.only_required_for_messages( |
| "unbalanced-dict-unpacking", |
| ) |
| def visit_for(self, node: nodes.For) -> None: |
| if not isinstance(node.target, nodes.Tuple): |
| return |
| |
| targets = node.target.elts |
| |
| inferred = utils.safe_infer(node.iter) |
| if not isinstance(inferred, DICT_TYPES): |
| return |
| |
| values = self._nodes_to_unpack(inferred) |
| if not values: |
| # no dict items returned |
| return |
| |
| if isinstance(inferred, astroid.objects.DictItems): |
| # dict.items() is a bit special because values will be a tuple |
| # So as long as there are always 2 targets and values each are |
| # a tuple with two items, this will unpack correctly. |
| # Example: `for key, val in {1: 2, 3: 4}.items()` |
| if len(targets) == 2 and all(len(x.elts) == 2 for x in values): |
| return |
| |
| # Starred nodes indicate ambiguous unpacking |
| # if `dict.items()` is used so we won't flag them. |
| if any(isinstance(target, nodes.Starred) for target in targets): |
| return |
| |
| if isinstance(inferred, nodes.Dict): |
| if isinstance(node.iter, nodes.Name): |
| # If this a case of 'dict-items-missing-iter', we don't want to |
| # report it as an 'unbalanced-dict-unpacking' as well |
| # TODO (performance), merging both checks would streamline this |
| if len(targets) == 2: |
| return |
| |
| else: |
| is_starred_targets = any( |
| isinstance(target, nodes.Starred) for target in targets |
| ) |
| for value in values: |
| value_length = self._get_value_length(value) |
| is_valid_star_unpack = is_starred_targets and value_length >= len( |
| targets |
| ) |
| if len(targets) != value_length and not is_valid_star_unpack: |
| details = _get_unpacking_extra_info(node, inferred) |
| self._report_unbalanced_unpacking( |
| node, inferred, targets, value_length, details |
| ) |
| break |
| |
| def leave_for(self, node: nodes.For) -> None: |
| self._store_type_annotation_names(node) |
| |
| def visit_module(self, node: nodes.Module) -> None: |
| """Visit module : update consumption analysis variable |
| checks globals doesn't overrides builtins. |
| """ |
| self._to_consume = [NamesConsumer(node, "module")] |
| self._postponed_evaluation_enabled = is_postponed_evaluation_enabled(node) |
| |
| for name, stmts in node.locals.items(): |
| if utils.is_builtin(name): |
| if self._should_ignore_redefined_builtin(stmts[0]) or name == "__doc__": |
| continue |
| self.add_message("redefined-builtin", args=name, node=stmts[0]) |
| |
| @utils.only_required_for_messages( |
| "unused-import", |
| "unused-wildcard-import", |
| "redefined-builtin", |
| "undefined-all-variable", |
| "invalid-all-object", |
| "invalid-all-format", |
| "unused-variable", |
| "undefined-variable", |
| ) |
| def leave_module(self, node: nodes.Module) -> None: |
| """Leave module: check globals.""" |
| assert len(self._to_consume) == 1 |
| |
| self._check_metaclasses(node) |
| not_consumed = self._to_consume.pop().to_consume |
| # attempt to check for __all__ if defined |
| if "__all__" in node.locals: |
| self._check_all(node, not_consumed) |
| |
| # check for unused globals |
| self._check_globals(not_consumed) |
| |
| # don't check unused imports in __init__ files |
| if not self.linter.config.init_import and node.package: |
| return |
| |
| self._check_imports(not_consumed) |
| self._type_annotation_names = [] |
| |
| def visit_classdef(self, node: nodes.ClassDef) -> None: |
| """Visit class: update consumption analysis variable.""" |
| self._to_consume.append(NamesConsumer(node, "class")) |
| |
| def leave_classdef(self, node: nodes.ClassDef) -> None: |
| """Leave class: update consumption analysis variable.""" |
| # Check for hidden ancestor names |
| # e.g. "six" in: Class X(six.with_metaclass(ABCMeta, object)): |
| for name_node in node.nodes_of_class(nodes.Name): |
| if ( |
| isinstance(name_node.parent, nodes.Call) |
| and isinstance(name_node.parent.func, nodes.Attribute) |
| and isinstance(name_node.parent.func.expr, nodes.Name) |
| ): |
| hidden_name_node = name_node.parent.func.expr |
| for consumer in self._to_consume: |
| if hidden_name_node.name in consumer.to_consume: |
| consumer.mark_as_consumed( |
| hidden_name_node.name, |
| consumer.to_consume[hidden_name_node.name], |
| ) |
| break |
| self._to_consume.pop() |
| |
| def visit_lambda(self, node: nodes.Lambda) -> None: |
| """Visit lambda: update consumption analysis variable.""" |
| self._to_consume.append(NamesConsumer(node, "lambda")) |
| |
| def leave_lambda(self, _: nodes.Lambda) -> None: |
| """Leave lambda: update consumption analysis variable.""" |
| # do not check for not used locals here |
| self._to_consume.pop() |
| |
| def visit_generatorexp(self, node: nodes.GeneratorExp) -> None: |
| """Visit genexpr: update consumption analysis variable.""" |
| self._to_consume.append(NamesConsumer(node, "comprehension")) |
| |
| def leave_generatorexp(self, _: nodes.GeneratorExp) -> None: |
| """Leave genexpr: update consumption analysis variable.""" |
| # do not check for not used locals here |
| self._to_consume.pop() |
| |
| def visit_dictcomp(self, node: nodes.DictComp) -> None: |
| """Visit dictcomp: update consumption analysis variable.""" |
| self._to_consume.append(NamesConsumer(node, "comprehension")) |
| |
| def leave_dictcomp(self, _: nodes.DictComp) -> None: |
| """Leave dictcomp: update consumption analysis variable.""" |
| # do not check for not used locals here |
| self._to_consume.pop() |
| |
| def visit_setcomp(self, node: nodes.SetComp) -> None: |
| """Visit setcomp: update consumption analysis variable.""" |
| self._to_consume.append(NamesConsumer(node, "comprehension")) |
| |
| def leave_setcomp(self, _: nodes.SetComp) -> None: |
| """Leave setcomp: update consumption analysis variable.""" |
| # do not check for not used locals here |
| self._to_consume.pop() |
| |
| def visit_functiondef(self, node: nodes.FunctionDef) -> None: |
| """Visit function: update consumption analysis variable and check locals.""" |
| self._to_consume.append(NamesConsumer(node, "function")) |
| if not ( |
| self.linter.is_message_enabled("redefined-outer-name") |
| or self.linter.is_message_enabled("redefined-builtin") |
| ): |
| return |
| globs = node.root().globals |
| for name, stmt in node.items(): |
| if name in globs and not isinstance(stmt, nodes.Global): |
| definition = globs[name][0] |
| if ( |
| isinstance(definition, nodes.ImportFrom) |
| and definition.modname == FUTURE |
| ): |
| # It is a __future__ directive, not a symbol. |
| continue |
| |
| # Do not take in account redefined names for the purpose |
| # of type checking.: |
| if any( |
| in_type_checking_block(definition) for definition in globs[name] |
| ): |
| continue |
| |
| line = definition.fromlineno |
| if not self._is_name_ignored(stmt, name): |
| self.add_message( |
| "redefined-outer-name", args=(name, line), node=stmt |
| ) |
| |
| elif ( |
| utils.is_builtin(name) |
| and not self._allowed_redefined_builtin(name) |
| and not self._should_ignore_redefined_builtin(stmt) |
| ): |
| # do not print Redefining builtin for additional builtins |
| self.add_message("redefined-builtin", args=name, node=stmt) |
| |
| def leave_functiondef(self, node: nodes.FunctionDef) -> None: |
| """Leave function: check function's locals are consumed.""" |
| self._check_metaclasses(node) |
| |
| if node.type_comment_returns: |
| self._store_type_annotation_node(node.type_comment_returns) |
| if node.type_comment_args: |
| for argument_annotation in node.type_comment_args: |
| self._store_type_annotation_node(argument_annotation) |
| |
| not_consumed = self._to_consume.pop().to_consume |
| if not ( |
| self.linter.is_message_enabled("unused-variable") |
| or self.linter.is_message_enabled("possibly-unused-variable") |
| or self.linter.is_message_enabled("unused-argument") |
| ): |
| return |
| |
| # Don't check arguments of function which are only raising an exception. |
| if utils.is_error(node): |
| return |
| |
| # Don't check arguments of abstract methods or within an interface. |
| is_method = node.is_method() |
| if is_method and node.is_abstract(): |
| return |
| |
| global_names = _flattened_scope_names(node.nodes_of_class(nodes.Global)) |
| nonlocal_names = _flattened_scope_names(node.nodes_of_class(nodes.Nonlocal)) |
| comprehension_target_names: set[str] = set() |
| |
| for comprehension_scope in node.nodes_of_class(nodes.ComprehensionScope): |
| for generator in comprehension_scope.generators: |
| for name in utils.find_assigned_names_recursive(generator.target): |
| comprehension_target_names.add(name) |
| |
| for name, stmts in not_consumed.items(): |
| self._check_is_unused( |
| name, |
| node, |
| stmts[0], |
| global_names, |
| nonlocal_names, |
| comprehension_target_names, |
| ) |
| |
| visit_asyncfunctiondef = visit_functiondef |
| leave_asyncfunctiondef = leave_functiondef |
| |
| @utils.only_required_for_messages( |
| "global-variable-undefined", |
| "global-variable-not-assigned", |
| "global-statement", |
| "global-at-module-level", |
| "redefined-builtin", |
| ) |
| def visit_global(self, node: nodes.Global) -> None: |
| """Check names imported exists in the global scope.""" |
| frame = node.frame() |
| if isinstance(frame, nodes.Module): |
| self.add_message("global-at-module-level", node=node, confidence=HIGH) |
| return |
| |
| module = frame.root() |
| default_message = True |
| locals_ = node.scope().locals |
| for name in node.names: |
| try: |
| assign_nodes = module.getattr(name) |
| except astroid.NotFoundError: |
| # unassigned global, skip |
| assign_nodes = [] |
| |
| not_defined_locally_by_import = not any( |
| isinstance(local, (nodes.Import, nodes.ImportFrom)) |
| for local in locals_.get(name, ()) |
| ) |
| if ( |
| not utils.is_reassigned_after_current(node, name) |
| and not utils.is_deleted_after_current(node, name) |
| and not_defined_locally_by_import |
| ): |
| self.add_message( |
| "global-variable-not-assigned", |
| args=name, |
| node=node, |
| confidence=HIGH, |
| ) |
| default_message = False |
| continue |
| |
| for anode in assign_nodes: |
| if ( |
| isinstance(anode, nodes.AssignName) |
| and anode.name in module.special_attributes |
| ): |
| self.add_message("redefined-builtin", args=name, node=node) |
| break |
| if anode.frame() is module: |
| # module level assignment |
| break |
| if ( |
| isinstance(anode, (nodes.ClassDef, nodes.FunctionDef)) |
| and anode.parent is module |
| ): |
| # module level function assignment |
| break |
| else: |
| if not_defined_locally_by_import: |
| # global undefined at the module scope |
| self.add_message( |
| "global-variable-undefined", |
| args=name, |
| node=node, |
| confidence=HIGH, |
| ) |
| default_message = False |
| |
| if default_message: |
| self.add_message("global-statement", node=node, confidence=HIGH) |
| |
| def visit_assignname(self, node: nodes.AssignName) -> None: |
| if isinstance(node.assign_type(), nodes.AugAssign): |
| self.visit_name(node) |
| |
| def visit_delname(self, node: nodes.DelName) -> None: |
| self.visit_name(node) |
| |
| def visit_name(self, node: nodes.Name | nodes.AssignName | nodes.DelName) -> None: |
| """Don't add the 'utils.only_required_for_messages' decorator here! |
| |
| It's important that all 'Name' nodes are visited, otherwise the |
| 'NamesConsumers' won't be correct. |
| """ |
| stmt = node.statement() |
| if stmt.fromlineno is None: |
| # name node from an astroid built from live code, skip |
| assert not stmt.root().file.endswith(".py") |
| return |
| |
| self._undefined_and_used_before_checker(node, stmt) |
| self._loopvar_name(node) |
| |
| @utils.only_required_for_messages("redefined-outer-name") |
| def visit_excepthandler(self, node: nodes.ExceptHandler) -> None: |
| if not node.name or not isinstance(node.name, nodes.AssignName): |
| return |
| |
| for outer_except, outer_except_assign_name in self._except_handler_names_queue: |
| if node.name.name == outer_except_assign_name.name: |
| self.add_message( |
| "redefined-outer-name", |
| args=(outer_except_assign_name.name, outer_except.fromlineno), |
| node=node, |
| ) |
| break |
| |
| self._except_handler_names_queue.append((node, node.name)) |
| |
| @utils.only_required_for_messages("redefined-outer-name") |
| def leave_excepthandler(self, node: nodes.ExceptHandler) -> None: |
| if not node.name or not isinstance(node.name, nodes.AssignName): |
| return |
| self._except_handler_names_queue.pop() |
| |
| def _undefined_and_used_before_checker( |
| self, node: nodes.Name, stmt: nodes.NodeNG |
| ) -> None: |
| frame = stmt.scope() |
| start_index = len(self._to_consume) - 1 |
| |
| # iterates through parent scopes, from the inner to the outer |
| base_scope_type = self._to_consume[start_index].scope_type |
| |
| for i in range(start_index, -1, -1): |
| current_consumer = self._to_consume[i] |
| |
| # Certain nodes shouldn't be checked as they get checked another time |
| if self._should_node_be_skipped(node, current_consumer, i == start_index): |
| continue |
| |
| action, nodes_to_consume = self._check_consumer( |
| node, stmt, frame, current_consumer, base_scope_type |
| ) |
| if nodes_to_consume: |
| # Any nodes added to consumed_uncertain by get_next_to_consume() |
| # should be added back so that they are marked as used. |
| # They will have already had a chance to emit used-before-assignment. |
| # We check here instead of before every single return in _check_consumer() |
| nodes_to_consume += current_consumer.consumed_uncertain[node.name] |
| current_consumer.mark_as_consumed(node.name, nodes_to_consume) |
| if action is VariableVisitConsumerAction.CONTINUE: |
| continue |
| if action is VariableVisitConsumerAction.RETURN: |
| return |
| |
| # we have not found the name, if it isn't a builtin, that's an |
| # undefined name ! |
| if not ( |
| node.name in nodes.Module.scope_attrs |
| or utils.is_builtin(node.name) |
| or node.name in self.linter.config.additional_builtins |
| or ( |
| node.name == "__class__" |
| and any( |
| i.is_method() |
| for i in node.node_ancestors() |
| if isinstance(i, nodes.FunctionDef) |
| ) |
| ) |
| ) and not utils.node_ignores_exception(node, NameError): |
| self.add_message("undefined-variable", args=node.name, node=node) |
| |
| def _should_node_be_skipped( |
| self, node: nodes.Name, consumer: NamesConsumer, is_start_index: bool |
| ) -> bool: |
| """Tests a consumer and node for various conditions in which the node shouldn't |
| be checked for the undefined-variable and used-before-assignment checks. |
| """ |
| if consumer.scope_type == "class": |
| # The list of base classes in the class definition is not part |
| # of the class body. |
| # If the current scope is a class scope but it's not the inner |
| # 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 |
| |
| # 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 |
| |
| elif consumer.scope_type == "lambda" and utils.is_default_argument( |
| node, consumer.node |
| ): |
| return True |
| |
| return False |
| |
| # pylint: disable = too-many-return-statements, too-many-branches |
| def _check_consumer( |
| self, |
| node: nodes.Name, |
| stmt: nodes.NodeNG, |
| frame: nodes.LocalsDictNodeNG, |
| current_consumer: NamesConsumer, |
| base_scope_type: str, |
| ) -> 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 |
| # variable used outside the loop. |
| if node.name in current_consumer.consumed: |
| # Avoid the case where there are homonyms inside function scope and |
| # comprehension current scope (avoid bug #1731) |
| if utils.is_func_decorator(current_consumer.node) or not isinstance( |
| node, nodes.ComprehensionScope |
| ): |
| self._check_late_binding_closure(node) |
| return (VariableVisitConsumerAction.RETURN, None) |
| |
| found_nodes = current_consumer.get_next_to_consume(node) |
| if found_nodes is None: |
| return (VariableVisitConsumerAction.CONTINUE, None) |
| if not found_nodes: |
| self._report_unfound_name_definition(node, current_consumer) |
| # Mark for consumption any nodes added to consumed_uncertain by |
| # get_next_to_consume() because they might not have executed. |
| nodes_to_consume = current_consumer.consumed_uncertain[node.name] |
| nodes_to_consume = self._filter_type_checking_import_from_consumption( |
| node, nodes_to_consume |
| ) |
| return ( |
| VariableVisitConsumerAction.RETURN, |
| nodes_to_consume, |
| ) |
| |
| self._check_late_binding_closure(node) |
| |
| defnode = utils.assign_parent(found_nodes[0]) |
| defstmt = defnode.statement() |
| defframe = defstmt.frame() |
| |
| # The class reuses itself in the class scope. |
| is_recursive_klass: bool = ( |
| frame is defframe |
| and defframe.parent_of(node) |
| and isinstance(defframe, nodes.ClassDef) |
| and node.name == defframe.name |
| ) |
| |
| if ( |
| is_recursive_klass |
| and utils.get_node_first_ancestor_of_type(node, nodes.Lambda) |
| and ( |
| not utils.is_default_argument(node) |
| or node.scope().parent.scope() is not defframe |
| ) |
| ): |
| # Self-referential class references are fine in lambda's -- |
| # As long as they are not part of the default argument directly |
| # under the scope of the parent self-referring class. |
| # Example of valid default argument: |
| # class MyName3: |
| # myattr = 1 |
| # mylambda3 = lambda: lambda a=MyName3: a |
| # Example of invalid default argument: |
| # class MyName4: |
| # myattr = 1 |
| # mylambda4 = lambda a=MyName4: lambda: a |
| |
| # If the above conditional is True, |
| # there is no possibility of undefined-variable |
| # Also do not consume class name |
| # (since consuming blocks subsequent checks) |
| # -- quit |
| return (VariableVisitConsumerAction.RETURN, None) |
| |
| ( |
| maybe_before_assign, |
| annotation_return, |
| use_outer_definition, |
| ) = self._is_variable_violation( |
| node, |
| defnode, |
| stmt, |
| defstmt, |
| frame, |
| defframe, |
| base_scope_type, |
| is_recursive_klass, |
| ) |
| |
| if use_outer_definition: |
| return (VariableVisitConsumerAction.CONTINUE, None) |
| |
| if ( |
| maybe_before_assign |
| and not utils.is_defined_before(node) |
| and not astroid.are_exclusive(stmt, defstmt, ("NameError",)) |
| ): |
| # Used and defined in the same place, e.g `x += 1` and `del x` |
| defined_by_stmt = defstmt is stmt and isinstance( |
| node, (nodes.DelName, nodes.AssignName) |
| ) |
| if ( |
| is_recursive_klass |
| or defined_by_stmt |
| or annotation_return |
| or isinstance(defstmt, nodes.Delete) |
| ): |
| if not utils.node_ignores_exception(node, NameError): |
| # Handle postponed evaluation of annotations |
| if not ( |
| self._postponed_evaluation_enabled |
| and isinstance( |
| stmt, |
| ( |
| nodes.AnnAssign, |
| nodes.FunctionDef, |
| nodes.Arguments, |
| ), |
| ) |
| and node.name in node.root().locals |
| ): |
| if defined_by_stmt: |
| return (VariableVisitConsumerAction.CONTINUE, [node]) |
| return (VariableVisitConsumerAction.CONTINUE, None) |
| |
| elif base_scope_type != "lambda": |
| # E0601 may *not* occurs in lambda scope. |
| |
| # Skip postponed evaluation of annotations |
| # and unevaluated annotations inside a function body |
| if not ( |
| self._postponed_evaluation_enabled |
| and isinstance(stmt, (nodes.AnnAssign, nodes.FunctionDef)) |
| ) and not ( |
| isinstance(stmt, nodes.AnnAssign) |
| and utils.get_node_first_ancestor_of_type(stmt, nodes.FunctionDef) |
| ): |
| self.add_message( |
| "used-before-assignment", |
| args=node.name, |
| node=node, |
| confidence=HIGH, |
| ) |
| return (VariableVisitConsumerAction.RETURN, found_nodes) |
| |
| elif base_scope_type == "lambda": |
| # E0601 can occur in class-level scope in lambdas, as in |
| # the following example: |
| # class A: |
| # x = lambda attr: f + attr |
| # f = 42 |
| # We check lineno because doing the following is fine: |
| # class A: |
| # x = 42 |
| # y = lambda attr: x + attr |
| if ( |
| isinstance(frame, nodes.ClassDef) |
| and node.name in frame.locals |
| and stmt.fromlineno <= defstmt.fromlineno |
| ): |
| self.add_message( |
| "used-before-assignment", |
| args=node.name, |
| node=node, |
| confidence=HIGH, |
| ) |
| |
| elif not self._is_builtin(node.name) and self._is_only_type_assignment( |
| node, defstmt |
| ): |
| if node.scope().locals.get(node.name): |
| self.add_message( |
| "used-before-assignment", args=node.name, node=node, confidence=HIGH |
| ) |
| else: |
| self.add_message( |
| "undefined-variable", args=node.name, node=node, confidence=HIGH |
| ) |
| return (VariableVisitConsumerAction.RETURN, found_nodes) |
| |
| elif isinstance(defstmt, nodes.ClassDef): |
| return self._is_first_level_self_reference(node, defstmt, found_nodes) |
| |
| elif isinstance(defnode, nodes.NamedExpr): |
| if isinstance(defnode.parent, nodes.IfExp): |
| if self._is_never_evaluated(defnode, defnode.parent): |
| self.add_message( |
| "undefined-variable", |
| args=node.name, |
| node=node, |
| confidence=INFERENCE, |
| ) |
| return (VariableVisitConsumerAction.RETURN, found_nodes) |
| |
| return (VariableVisitConsumerAction.RETURN, found_nodes) |
| |
| def _report_unfound_name_definition( |
| self, node: nodes.NodeNG, current_consumer: NamesConsumer |
| ) -> None: |
| """Reports used-before-assignment when all name definition nodes |
| get filtered out by NamesConsumer. |
| """ |
| if ( |
| self._postponed_evaluation_enabled |
| and utils.is_node_in_type_annotation_context(node) |
| ): |
| return |
| if self._is_builtin(node.name): |
| return |
| if self._is_variable_annotation_in_function(node): |
| return |
| if ( |
| node.name in self._evaluated_type_checking_scopes |
| and node.scope() in self._evaluated_type_checking_scopes[node.name] |
| ): |
| return |
| |
| confidence = HIGH |
| if node.name in current_consumer.names_under_always_false_test: |
| confidence = INFERENCE |
| elif node.name in current_consumer.consumed_uncertain: |
| confidence = CONTROL_FLOW |
| |
| if node.name in current_consumer.names_defined_under_one_branch_only: |
| msg = "possibly-used-before-assignment" |
| else: |
| msg = "used-before-assignment" |
| |
| self.add_message( |
| msg, |
| args=node.name, |
| node=node, |
| confidence=confidence, |
| ) |
| |
| def _filter_type_checking_import_from_consumption( |
| self, node: nodes.NodeNG, nodes_to_consume: list[nodes.NodeNG] |
| ) -> list[nodes.NodeNG]: |
| """Do not consume type-checking import node as used-before-assignment |
| may invoke in different scopes. |
| """ |
| type_checking_import = next( |
| ( |
| n |
| for n in nodes_to_consume |
| if isinstance(n, (nodes.Import, nodes.ImportFrom)) |
| and in_type_checking_block(n) |
| ), |
| None, |
| ) |
| # If used-before-assignment reported for usage of type checking import |
| # keep track of its scope |
| if type_checking_import and not self._is_variable_annotation_in_function(node): |
| self._evaluated_type_checking_scopes.setdefault(node.name, []).append( |
| node.scope() |
| ) |
| nodes_to_consume = [n for n in nodes_to_consume if n != type_checking_import] |
| return nodes_to_consume |
| |
| @utils.only_required_for_messages("no-name-in-module") |
| def visit_import(self, node: nodes.Import) -> None: |
| """Check modules attribute accesses.""" |
| if not self._analyse_fallback_blocks and utils.is_from_fallback_block(node): |
| # No need to verify this, since ImportError is already |
| # handled by the client code. |
| return |
| # Don't verify import if part of guarded import block |
| if in_type_checking_block(node): |
| return |
| if isinstance(node.parent, nodes.If) and is_sys_guard(node.parent): |
| return |
| |
| for name, _ in node.names: |
| parts = name.split(".") |
| try: |
| module = next(_infer_name_module(node, parts[0])) |
| except astroid.ResolveError: |
| continue |
| if not isinstance(module, nodes.Module): |
| continue |
| self._check_module_attrs(node, module, parts[1:]) |
| |
| @utils.only_required_for_messages("no-name-in-module") |
| def visit_importfrom(self, node: nodes.ImportFrom) -> None: |
| """Check modules attribute accesses.""" |
| if not self._analyse_fallback_blocks and utils.is_from_fallback_block(node): |
| # No need to verify this, since ImportError is already |
| # handled by the client code. |
| return |
| # Don't verify import if part of guarded import block |
| # I.e. `sys.version_info` or `typing.TYPE_CHECKING` |
| if in_type_checking_block(node): |
| return |
| if isinstance(node.parent, nodes.If) and is_sys_guard(node.parent): |
| return |
| |
| name_parts = node.modname.split(".") |
| try: |
| module = node.do_import_module(name_parts[0]) |
| except astroid.AstroidBuildingError: |
| return |
| module = self._check_module_attrs(node, module, name_parts[1:]) |
| if not module: |
| return |
| for name, _ in node.names: |
| if name == "*": |
| continue |
| self._check_module_attrs(node, module, name.split(".")) |
| |
| @utils.only_required_for_messages( |
| "unbalanced-tuple-unpacking", |
| "unpacking-non-sequence", |
| "self-cls-assignment", |
| "unbalanced_dict_unpacking", |
| ) |
| def visit_assign(self, node: nodes.Assign) -> None: |
| """Check unbalanced tuple unpacking for assignments and unpacking |
| non-sequences as well as in case self/cls get assigned. |
| """ |
| self._check_self_cls_assign(node) |
| if not isinstance(node.targets[0], (nodes.Tuple, nodes.List)): |
| return |
| |
| targets = node.targets[0].itered() |
| |
| # Check if we have starred nodes. |
| if any(isinstance(target, nodes.Starred) for target in targets): |
| return |
| |
| try: |
| inferred = utils.safe_infer(node.value) |
| if inferred is not None: |
| self._check_unpacking(inferred, node, targets) |
| except astroid.InferenceError: |
| return |
| |
| # listcomp have now also their scope |
| def visit_listcomp(self, node: nodes.ListComp) -> None: |
| """Visit listcomp: update consumption analysis variable.""" |
| self._to_consume.append(NamesConsumer(node, "comprehension")) |
| |
| def leave_listcomp(self, _: nodes.ListComp) -> None: |
| """Leave listcomp: update consumption analysis variable.""" |
| # do not check for not used locals here |
| self._to_consume.pop() |
| |
| def leave_assign(self, node: nodes.Assign) -> None: |
| self._store_type_annotation_names(node) |
| |
| def leave_with(self, node: nodes.With) -> None: |
| self._store_type_annotation_names(node) |
| |
| def visit_arguments(self, node: nodes.Arguments) -> None: |
| for annotation in node.type_comment_args: |
| self._store_type_annotation_node(annotation) |
| |
| # Relying on other checker's options, which might not have been initialized yet. |
| @cached_property |
| def _analyse_fallback_blocks(self) -> bool: |
| return bool(self.linter.config.analyse_fallback_blocks) |
| |
| @cached_property |
| def _ignored_modules(self) -> Iterable[str]: |
| return self.linter.config.ignored_modules # type: ignore[no-any-return] |
| |
| @cached_property |
| def _allow_global_unused_variables(self) -> bool: |
| return bool(self.linter.config.allow_global_unused_variables) |
| |
| @staticmethod |
| def _defined_in_function_definition( |
| node: nodes.NodeNG, frame: nodes.NodeNG |
| ) -> bool: |
| in_annotation_or_default_or_decorator = False |
| if isinstance(frame, nodes.FunctionDef) and node.statement() is frame: |
| in_annotation_or_default_or_decorator = ( |
| ( |
| node in frame.args.annotations |
| or node in frame.args.posonlyargs_annotations |
| or node in frame.args.kwonlyargs_annotations |
| or node is frame.args.varargannotation |
| or node is frame.args.kwargannotation |
| ) |
| or frame.args.parent_of(node) |
| or (frame.decorators and frame.decorators.parent_of(node)) |
| or ( |
| frame.returns |
| and (node is frame.returns or frame.returns.parent_of(node)) |
| ) |
| ) |
| return in_annotation_or_default_or_decorator |
| |
| @staticmethod |
| def _in_lambda_or_comprehension_body( |
| node: nodes.NodeNG, frame: nodes.NodeNG |
| ) -> bool: |
| """Return True if node within a lambda/comprehension body (or similar) and thus |
| should not have access to class attributes in frame. |
| """ |
| child = node |
| parent = node.parent |
| while parent is not None: |
| if parent is frame: |
| return False |
| if isinstance(parent, nodes.Lambda) and child is not parent.args: |
| # Body of lambda should not have access to class attributes. |
| return True |
| if isinstance(parent, nodes.Comprehension) and child is not parent.iter: |
| # Only iter of list/set/dict/generator comprehension should have access. |
| return True |
| if isinstance(parent, nodes.ComprehensionScope) and not ( |
| parent.generators and child is parent.generators[0] |
| ): |
| # Body of list/set/dict/generator comprehension should not have access to class attributes. |
| # Furthermore, only the first generator (if multiple) in comprehension should have access. |
| return True |
| child = parent |
| parent = parent.parent |
| return False |
| |
| @staticmethod |
| def _is_variable_violation( |
| node: nodes.Name, |
| defnode: nodes.NodeNG, |
| stmt: _base_nodes.Statement, |
| defstmt: _base_nodes.Statement, |
| frame: nodes.LocalsDictNodeNG, # scope of statement of node |
| defframe: nodes.LocalsDictNodeNG, |
| base_scope_type: str, |
| is_recursive_klass: bool, |
| ) -> tuple[bool, bool, bool]: |
| maybe_before_assign = True |
| annotation_return = False |
| use_outer_definition = False |
| if frame is not defframe: |
| maybe_before_assign = _detect_global_scope(node, frame, defframe) |
| elif defframe.parent is None: |
| # we are at the module level, check the name is not |
| # defined in builtins |
| if ( |
| node.name in defframe.scope_attrs |
| or astroid.builtin_lookup(node.name)[1] |
| ): |
| maybe_before_assign = False |
| else: |
| # we are in a local scope, check the name is not |
| # defined in global or builtin scope |
| # skip this lookup if name is assigned later in function scope/lambda |
| # Note: the node.frame() is not the same as the `frame` argument which is |
| # equivalent to frame.statement().scope() |
| forbid_lookup = ( |
| isinstance(frame, nodes.FunctionDef) |
| or isinstance(node.frame(), nodes.Lambda) |
| ) and _assigned_locally(node) |
| if not forbid_lookup and defframe.root().lookup(node.name)[1]: |
| maybe_before_assign = False |
| use_outer_definition = stmt == defstmt and not isinstance( |
| defnode, nodes.Comprehension |
| ) |
| # check if we have a nonlocal |
| elif node.name in defframe.locals: |
| maybe_before_assign = not any( |
| isinstance(child, nodes.Nonlocal) and node.name in child.names |
| for child in defframe.get_children() |
| ) |
| |
| if ( |
| base_scope_type == "lambda" |
| and isinstance(frame, nodes.ClassDef) |
| and node.name in frame.locals |
| ): |
| # This rule verifies that if the definition node of the |
| # checked name is an Arguments node and if the name |
| # is used a default value in the arguments defaults |
| # and the actual definition of the variable label |
| # is happening before the Arguments definition. |
| # |
| # bar = None |
| # foo = lambda bar=bar: bar |
| # |
| # In this case, maybe_before_assign should be False, otherwise |
| # it should be True. |
| maybe_before_assign = not ( |
| isinstance(defnode, nodes.Arguments) |
| and node in defnode.defaults |
| and frame.locals[node.name][0].fromlineno < defstmt.fromlineno |
| ) |
| elif isinstance(defframe, nodes.ClassDef) and isinstance( |
| frame, nodes.FunctionDef |
| ): |
| # Special rule for function return annotations, |
| # using a name defined earlier in the class containing the function. |
| if node is frame.returns and defframe.parent_of(frame.returns): |
| annotation_return = True |
| if ( |
| frame.returns.name in defframe.locals |
| and defframe.locals[node.name][0].lineno < frame.lineno |
| ): |
| # Detect class assignments with a name defined earlier in the |
| # class. In this case, no warning should be raised. |
| maybe_before_assign = False |
| else: |
| maybe_before_assign = True |
| if isinstance(node.parent, nodes.Arguments): |
| maybe_before_assign = stmt.fromlineno <= defstmt.fromlineno |
| elif is_recursive_klass: |
| maybe_before_assign = True |
| else: |
| maybe_before_assign = ( |
| maybe_before_assign and stmt.fromlineno <= defstmt.fromlineno |
| ) |
| if maybe_before_assign and stmt.fromlineno == defstmt.fromlineno: |
| if ( |
| isinstance(defframe, nodes.FunctionDef) |
| and frame is defframe |
| and defframe.parent_of(node) |
| and ( |
| defnode in defframe.type_params |
| # Single statement function, with the statement on the |
| # same line as the function definition |
| or stmt is not defstmt |
| ) |
| ): |
| maybe_before_assign = False |
| elif ( |
| isinstance(defstmt, NODES_WITH_VALUE_ATTR) |
| and VariablesChecker._maybe_used_and_assigned_at_once(defstmt) |
| and frame is defframe |
| and defframe.parent_of(node) |
| and stmt is defstmt |
| ): |
| # Single statement if, with assignment expression on same |
| # line as assignment |
| # x = b if (b := True) else False |
| maybe_before_assign = False |
| elif ( |
| isinstance( # pylint: disable=too-many-boolean-expressions |
| defnode, nodes.NamedExpr |
| ) |
| and frame is defframe |
| and defframe.parent_of(stmt) |
| and stmt is defstmt |
| and ( |
| ( |
| defnode.lineno == node.lineno |
| and defnode.col_offset < node.col_offset |
| ) |
| or (defnode.lineno < node.lineno) |
| or ( |
| # Issue in the `ast` module until py39 |
| # Nodes in a multiline string have the same lineno |
| # Could be false-positive without check |
| not PY39_PLUS |
| and defnode.lineno == node.lineno |
| and isinstance( |
| defstmt, |
| ( |
| nodes.Assign, |
| nodes.AnnAssign, |
| nodes.AugAssign, |
| nodes.Return, |
| ), |
| ) |
| and isinstance(defstmt.value, nodes.JoinedStr) |
| ) |
| ) |
| ): |
| # Relation of a name to the same name in a named expression |
| # Could be used before assignment if self-referencing: |
| # (b := b) |
| # Otherwise, safe if used after assignment: |
| # (b := 2) and b |
| maybe_before_assign = defnode.value is node or any( |
| anc is defnode.value for anc in node.node_ancestors() |
| ) |
| |
| return maybe_before_assign, annotation_return, use_outer_definition |
| |
| @staticmethod |
| def _maybe_used_and_assigned_at_once(defstmt: _base_nodes.Statement) -> bool: |
| """Check if `defstmt` has the potential to use and assign a name in the |
| same statement. |
| """ |
| if isinstance(defstmt, nodes.Match): |
| return any(case.guard for case in defstmt.cases) |
| if isinstance(defstmt, nodes.IfExp): |
| return True |
| if isinstance(defstmt, nodes.TypeAlias): |
| return True |
| if isinstance(defstmt.value, nodes.BaseContainer): |
| return any( |
| VariablesChecker._maybe_used_and_assigned_at_once(elt) |
| for elt in defstmt.value.elts |
| if isinstance(elt, (*NODES_WITH_VALUE_ATTR, nodes.IfExp, nodes.Match)) |
| ) |
| value = defstmt.value |
| if isinstance(value, nodes.IfExp): |
| return True |
| if isinstance(value, nodes.Lambda) and isinstance(value.body, nodes.IfExp): |
| return True |
| if isinstance(value, nodes.Dict) and any( |
| isinstance(item[0], nodes.IfExp) or isinstance(item[1], nodes.IfExp) |
| for item in value.items |
| ): |
| return True |
| if not isinstance(value, nodes.Call): |
| return False |
| return any( |
| any(isinstance(kwarg.value, nodes.IfExp) for kwarg in call.keywords) |
| or any(isinstance(arg, nodes.IfExp) for arg in call.args) |
| or ( |
| isinstance(call.func, nodes.Attribute) |
| and isinstance(call.func.expr, nodes.IfExp) |
| ) |
| for call in value.nodes_of_class(klass=nodes.Call) |
| ) |
| |
| def _is_builtin(self, name: str) -> bool: |
| return name in self.linter.config.additional_builtins or utils.is_builtin(name) |
| |
| @staticmethod |
| def _is_only_type_assignment( |
| node: nodes.Name, defstmt: _base_nodes.Statement |
| ) -> bool: |
| """Check if variable only gets assigned a type and never a value.""" |
| if not isinstance(defstmt, nodes.AnnAssign) or defstmt.value: |
| return False |
| |
| defstmt_frame = defstmt.frame() |
| node_frame = node.frame() |
| |
| parent = node |
| while parent is not defstmt_frame.parent: |
| parent_scope = parent.scope() |
| |
| # Find out if any nonlocals receive values in nested functions |
| for inner_func in parent_scope.nodes_of_class(nodes.FunctionDef): |
| if inner_func is parent_scope: |
| continue |
| if any( |
| node.name in nl.names |
| for nl in inner_func.nodes_of_class(nodes.Nonlocal) |
| ) and any( |
| node.name == an.name |
| for an in inner_func.nodes_of_class(nodes.AssignName) |
| ): |
| return False |
| |
| local_refs = parent_scope.locals.get(node.name, []) |
| for ref_node in local_refs: |
| # If local ref is in the same frame as our node, but on a later lineno |
| # we don't actually care about this local ref. |
| # Local refs are ordered, so we break. |
| # print(var) |
| # var = 1 # <- irrelevant |
| if defstmt_frame == node_frame and ref_node.lineno > node.lineno: |
| break |
| |
| # If the parent of the local reference is anything but an AnnAssign |
| # Or if the AnnAssign adds a value the variable will now have a value |
| # var = 1 # OR |
| # var: int = 1 |
| if ( |
| not isinstance(ref_node.parent, nodes.AnnAssign) |
| or ref_node.parent.value |
| ) and not ( |
| # EXCEPTION: will not have a value if a self-referencing named expression |
| # var: int |
| # if (var := var * var) <-- "var" still undefined |
| isinstance(ref_node.parent, nodes.NamedExpr) |
| and any( |
| anc is ref_node.parent.value for anc in node.node_ancestors() |
| ) |
| ): |
| return False |
| parent = parent_scope.parent |
| return True |
| |
| @staticmethod |
| def _is_first_level_self_reference( |
| node: nodes.Name, defstmt: nodes.ClassDef, found_nodes: list[nodes.NodeNG] |
| ) -> tuple[VariableVisitConsumerAction, list[nodes.NodeNG] | None]: |
| """Check if a first level method's annotation or default values |
| refers to its own class, and return a consumer action. |
| """ |
| if node.frame().parent == defstmt and node.statement() == node.frame(): |
| # Check if used as type annotation |
| # Break if postponed evaluation is enabled |
| if utils.is_node_in_type_annotation_context(node): |
| if not utils.is_postponed_evaluation_enabled(node): |
| return (VariableVisitConsumerAction.CONTINUE, None) |
| return (VariableVisitConsumerAction.RETURN, None) |
| # Check if used as default value by calling the class |
| if isinstance(node.parent, nodes.Call) and isinstance( |
| node.parent.parent, nodes.Arguments |
| ): |
| return (VariableVisitConsumerAction.CONTINUE, None) |
| return (VariableVisitConsumerAction.RETURN, found_nodes) |
| |
| @staticmethod |
| def _is_never_evaluated( |
| defnode: nodes.NamedExpr, defnode_parent: nodes.IfExp |
| ) -> bool: |
| """Check if a NamedExpr is inside a side of if ... else that never |
| gets evaluated. |
| """ |
| inferred_test = utils.safe_infer(defnode_parent.test) |
| if isinstance(inferred_test, nodes.Const): |
| if inferred_test.value is True and defnode == defnode_parent.orelse: |
| return True |
| if inferred_test.value is False and defnode == defnode_parent.body: |
| return True |
| return False |
| |
| @staticmethod |
| def _is_variable_annotation_in_function(node: nodes.NodeNG) -> bool: |
| is_annotation = utils.get_node_first_ancestor_of_type(node, nodes.AnnAssign) |
| return ( |
| is_annotation |
| and utils.get_node_first_ancestor_of_type( # type: ignore[return-value] |
| is_annotation, nodes.FunctionDef |
| ) |
| ) |
| |
| def _ignore_class_scope(self, node: nodes.NodeNG) -> bool: |
| """Return True if the node is in a local class scope, as an assignment. |
| |
| Detect if we are in a local class scope, as an assignment. |
| For example, the following is fair game. |
| |
| class A: |
| b = 1 |
| c = lambda b=b: b * b |
| |
| class B: |
| tp = 1 |
| def func(self, arg: tp): |
| ... |
| class C: |
| tp = 2 |
| def func(self, arg=tp): |
| ... |
| class C: |
| class Tp: |
| pass |
| class D(Tp): |
| ... |
| """ |
| name = node.name |
| frame = node.statement().scope() |
| in_annotation_or_default_or_decorator = self._defined_in_function_definition( |
| node, frame |
| ) |
| in_ancestor_list = utils.is_ancestor_name(frame, node) |
| if in_annotation_or_default_or_decorator or in_ancestor_list: |
| frame_locals = frame.parent.scope().locals |
| else: |
| frame_locals = frame.locals |
| return not ( |
| (isinstance(frame, nodes.ClassDef) or in_annotation_or_default_or_decorator) |
| and not self._in_lambda_or_comprehension_body(node, frame) |
| and name in frame_locals |
| ) |
| |
| # pylint: disable-next=too-many-branches,too-many-statements |
| def _loopvar_name(self, node: astroid.Name) -> None: |
| # filter variables according to node's scope |
| astmts = [s for s in node.lookup(node.name)[1] if hasattr(s, "assign_type")] |
| # If this variable usage exists inside a function definition |
| # that exists in the same loop, |
| # the usage is safe because the function will not be defined either if |
| # the variable is not defined. |
| scope = node.scope() |
| if isinstance(scope, (nodes.Lambda, nodes.FunctionDef)) and any( |
| asmt.scope().parent_of(scope) for asmt in astmts |
| ): |
| return |
| # Filter variables according to their respective scope. Test parent |
| # and statement to avoid #74747. This is not a total fix, which would |
| # introduce a mechanism similar to special attribute lookup in |
| # modules. Also, in order to get correct inference in this case, the |
| # scope lookup rules would need to be changed to return the initial |
| # assignment (which does not exist in code per se) as well as any later |
| # modifications. |
| if ( |
| not astmts # pylint: disable=too-many-boolean-expressions |
| or ( |
| astmts[0].parent == astmts[0].root() |
| and astmts[0].parent.parent_of(node) |
| ) |
| or ( |
| astmts[0].is_statement |
| or not isinstance(astmts[0].parent, nodes.Module) |
| and astmts[0].statement().parent_of(node) |
| ) |
| ): |
| _astmts = [] |
| else: |
| _astmts = astmts[:1] |
| for i, stmt in enumerate(astmts[1:]): |
| try: |
| astmt_statement = astmts[i].statement() |
| except astroid.exceptions.ParentMissingError: |
| continue |
| if astmt_statement.parent_of(stmt) and not utils.in_for_else_branch( |
| astmt_statement, stmt |
| ): |
| continue |
| _astmts.append(stmt) |
| astmts = _astmts |
| if len(astmts) != 1: |
| return |
| |
| assign = astmts[0].assign_type() |
| if not ( |
| isinstance(assign, (nodes.For, nodes.Comprehension, nodes.GeneratorExp)) |
| and assign.statement() is not node.statement() |
| ): |
| return |
| |
| if not isinstance(assign, nodes.For): |
| self.add_message("undefined-loop-variable", args=node.name, node=node) |
| return |
| for else_stmt in assign.orelse: |
| if isinstance( |
| else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue) |
| ): |
| return |
| # TODO: 4.0: Consider using RefactoringChecker._is_function_def_never_returning |
| if isinstance(else_stmt, nodes.Expr) and isinstance( |
| else_stmt.value, nodes.Call |
| ): |
| inferred_func = utils.safe_infer(else_stmt.value.func) |
| if ( |
| isinstance(inferred_func, nodes.FunctionDef) |
| and inferred_func.returns |
| ): |
| inferred_return = utils.safe_infer(inferred_func.returns) |
| if isinstance( |
| inferred_return, nodes.FunctionDef |
| ) and inferred_return.qname() in { |
| *TYPING_NORETURN, |
| *TYPING_NEVER, |
| "typing._SpecialForm", |
| }: |
| return |
| # typing_extensions.NoReturn returns a _SpecialForm |
| if ( |
| isinstance(inferred_return, bases.Instance) |
| and inferred_return.qname() == "typing._SpecialForm" |
| ): |
| return |
| |
| maybe_walrus = utils.get_node_first_ancestor_of_type(node, nodes.NamedExpr) |
| if maybe_walrus: |
| maybe_comprehension = utils.get_node_first_ancestor_of_type( |
| maybe_walrus, nodes.Comprehension |
| ) |
| if maybe_comprehension: |
| comprehension_scope = utils.get_node_first_ancestor_of_type( |
| maybe_comprehension, nodes.ComprehensionScope |
| ) |
| if comprehension_scope is None: |
| # Should not be possible. |
| pass |
| elif ( |
| comprehension_scope.parent.scope() is scope |
| and node.name in comprehension_scope.locals |
| ): |
| return |
| |
| # For functions we can do more by inferring the length of the itered object |
| try: |
| inferred = next(assign.iter.infer()) |
| # Prefer the target of enumerate() rather than the enumerate object itself |
| if ( |
| isinstance(inferred, astroid.Instance) |
| and inferred.qname() == "builtins.enumerate" |
| ): |
| likely_call = assign.iter |
| if isinstance(assign.iter, nodes.IfExp): |
| likely_call = assign.iter.body |
| if isinstance(likely_call, nodes.Call): |
| inferred = next(likely_call.args[0].infer()) |
| except astroid.InferenceError: |
| self.add_message("undefined-loop-variable", args=node.name, node=node) |
| else: |
| if ( |
| isinstance(inferred, astroid.Instance) |
| and inferred.qname() == BUILTIN_RANGE |
| ): |
| # Consider range() objects safe, even if they might not yield any results. |
| return |
| |
| # Consider sequences. |
| sequences = ( |
| nodes.List, |
| nodes.Tuple, |
| nodes.Dict, |
| nodes.Set, |
| astroid.objects.FrozenSet, |
| ) |
| if not isinstance(inferred, sequences): |
| self.add_message("undefined-loop-variable", args=node.name, node=node) |
| return |
| |
| elements = getattr(inferred, "elts", getattr(inferred, "items", [])) |
| if not elements: |
| self.add_message("undefined-loop-variable", args=node.name, node=node) |
| |
| # pylint: disable = too-many-branches |
| def _check_is_unused( |
| self, |
| name: str, |
| node: nodes.FunctionDef, |
| stmt: nodes.NodeNG, |
| global_names: set[str], |
| nonlocal_names: Iterable[str], |
| comprehension_target_names: Iterable[str], |
| ) -> None: |
| # Ignore some special names specified by user configuration. |
| if self._is_name_ignored(stmt, name): |
| return |
| # Ignore names that were added dynamically to the Function scope |
| if ( |
| isinstance(node, nodes.FunctionDef) |
| and name == "__class__" |
| and len(node.locals["__class__"]) == 1 |
| and isinstance(node.locals["__class__"][0], nodes.ClassDef) |
| ): |
| return |
| |
| # Ignore names imported by the global statement. |
| if isinstance(stmt, (nodes.Global, nodes.Import, nodes.ImportFrom)): |
| # Detect imports, assigned to global statements. |
| if global_names and _import_name_is_global(stmt, global_names): |
| return |
| |
| # Ignore names in comprehension targets |
| if name in comprehension_target_names: |
| return |
| |
| # Ignore names in string literal type annotation. |
| if name in self._type_annotation_names: |
| return |
| |
| argnames = node.argnames() |
| # Care about functions with unknown argument (builtins) |
| if name in argnames: |
| if node.name == "__new__": |
| is_init_def = False |
| # Look for the `__init__` method in all the methods of the same class. |
| for n in node.parent.get_children(): |
| is_init_def = hasattr(n, "name") and (n.name == "__init__") |
| if is_init_def: |
| break |
| # Ignore unused arguments check for `__new__` if `__init__` is defined. |
| if is_init_def: |
| return |
| self._check_unused_arguments(name, node, stmt, argnames, nonlocal_names) |
| else: |
| if stmt.parent and isinstance( |
| stmt.parent, (nodes.Assign, nodes.AnnAssign, nodes.Tuple, nodes.For) |
| ): |
| if name in nonlocal_names: |
| return |
| |
| qname = asname = None |
| if isinstance(stmt, (nodes.Import, nodes.ImportFrom)): |
| # Need the complete name, which we don't have in .locals. |
| if len(stmt.names) > 1: |
| import_names = next( |
| (names for names in stmt.names if name in names), None |
| ) |
| else: |
| import_names = stmt.names[0] |
| if import_names: |
| qname, asname = import_names |
| name = asname or qname |
| |
| if _has_locals_call_after_node(stmt, node.scope()): |
| message_name = "possibly-unused-variable" |
| else: |
| if isinstance(stmt, nodes.Import): |
| if asname is not None: |
| msg = f"{qname} imported as {asname}" |
| else: |
| msg = f"import {name}" |
| self.add_message("unused-import", args=msg, node=stmt) |
| return |
| if isinstance(stmt, nodes.ImportFrom): |
| if asname is not None: |
| msg = f"{qname} imported from {stmt.modname} as {asname}" |
| else: |
| msg = f"{name} imported from {stmt.modname}" |
| self.add_message("unused-import", args=msg, node=stmt) |
| return |
| message_name = "unused-variable" |
| |
| if isinstance(stmt, nodes.FunctionDef) and stmt.decorators: |
| return |
| |
| # Don't check function stubs created only for type information |
| if utils.is_overload_stub(node): |
| return |
| |
| # Special case for exception variable |
| if isinstance(stmt.parent, nodes.ExceptHandler) and any( |
| n.name == name for n in stmt.parent.nodes_of_class(nodes.Name) |
| ): |
| return |
| |
| self.add_message(message_name, args=name, node=stmt) |
| |
| def _is_name_ignored( |
| self, stmt: nodes.NodeNG, name: str |
| ) -> re.Pattern[str] | re.Match[str] | None: |
| authorized_rgx = self.linter.config.dummy_variables_rgx |
| if ( |
| isinstance(stmt, nodes.AssignName) |
| and isinstance(stmt.parent, nodes.Arguments) |
| or isinstance(stmt, nodes.Arguments) |
| ): |
| regex: re.Pattern[str] = self.linter.config.ignored_argument_names |
| else: |
| regex = authorized_rgx |
| # See https://stackoverflow.com/a/47007761/2519059 to |
| # understand what this function return. Please do NOT use |
| # this elsewhere, this is confusing for no benefit |
| return regex and regex.match(name) |
| |
| def _check_unused_arguments( |
| self, |
| name: str, |
| node: nodes.FunctionDef, |
| stmt: nodes.NodeNG, |
| argnames: list[str], |
| nonlocal_names: Iterable[str], |
| ) -> None: |
| is_method = node.is_method() |
| klass = node.parent.frame() |
| if is_method and isinstance(klass, nodes.ClassDef): |
| confidence = ( |
| INFERENCE if utils.has_known_bases(klass) else INFERENCE_FAILURE |
| ) |
| else: |
| confidence = HIGH |
| |
| if is_method: |
| # Don't warn for the first argument of a (non static) method |
| if node.type != "staticmethod" and name == argnames[0]: |
| return |
| # Don't warn for argument of an overridden method |
| overridden = overridden_method(klass, node.name) |
| if overridden is not None and name in overridden.argnames(): |
| return |
| if node.name in utils.PYMETHODS and node.name not in ( |
| "__init__", |
| "__new__", |
| ): |
| return |
| # Don't check callback arguments |
| if any( |
| node.name.startswith(cb) or node.name.endswith(cb) |
| for cb in self.linter.config.callbacks |
| ): |
| return |
| # Don't check arguments of singledispatch.register function. |
| if utils.is_registered_in_singledispatch_function(node): |
| return |
| |
| # Don't check function stubs created only for type information |
| if utils.is_overload_stub(node): |
| return |
| |
| # Don't check protocol classes |
| if utils.is_protocol_class(klass): |
| return |
| |
| if name in nonlocal_names: |
| return |
| |
| self.add_message("unused-argument", args=name, node=stmt, confidence=confidence) |
| |
| def _check_late_binding_closure(self, node: nodes.Name) -> None: |
| """Check whether node is a cell var that is assigned within a containing loop. |
| |
| Special cases where we don't care about the error: |
| 1. When the node's function is immediately called, e.g. (lambda: i)() |
| 2. When the node's function is returned from within the loop, e.g. return lambda: i |
| """ |
| if not self.linter.is_message_enabled("cell-var-from-loop"): |
| return |
| |
| node_scope = node.frame() |
| |
| # If node appears in a default argument expression, |
| # look at the next enclosing frame instead |
| if utils.is_default_argument(node, node_scope): |
| node_scope = node_scope.parent.frame() |
| |
| # Check if node is a cell var |
| if ( |
| not isinstance(node_scope, (nodes.Lambda, nodes.FunctionDef)) |
| or node.name in node_scope.locals |
| ): |
| return |
| |
| assign_scope, stmts = node.lookup(node.name) |
| if not stmts or not assign_scope.parent_of(node_scope): |
| return |
| |
| if utils.is_comprehension(assign_scope): |
| self.add_message("cell-var-from-loop", node=node, args=node.name) |
| else: |
| # Look for an enclosing For loop. |
| # Currently, we only consider the first assignment |
| assignment_node = stmts[0] |
| |
| maybe_for = assignment_node |
| while maybe_for and not isinstance(maybe_for, nodes.For): |
| if maybe_for is assign_scope: |
| break |
| maybe_for = maybe_for.parent |
| else: |
| if ( |
| maybe_for |
| and maybe_for.parent_of(node_scope) |
| and not utils.is_being_called(node_scope) |
| and node_scope.parent |
| and not isinstance(node_scope.statement(), nodes.Return) |
| ): |
| self.add_message("cell-var-from-loop", node=node, args=node.name) |
| |
| def _should_ignore_redefined_builtin(self, stmt: nodes.NodeNG) -> bool: |
| if not isinstance(stmt, nodes.ImportFrom): |
| return False |
| return stmt.modname in self.linter.config.redefining_builtins_modules |
| |
| def _allowed_redefined_builtin(self, name: str) -> bool: |
| return name in self.linter.config.allowed_redefined_builtins |
| |
| @staticmethod |
| def _comprehension_between_frame_and_node(node: nodes.Name) -> bool: |
| """Return True if a ComprehensionScope intervenes between `node` and its |
| frame. |
| """ |
| closest_comprehension_scope = utils.get_node_first_ancestor_of_type( |
| node, nodes.ComprehensionScope |
| ) |
| return closest_comprehension_scope is not None and node.frame().parent_of( |
| closest_comprehension_scope |
| ) |
| |
| def _store_type_annotation_node(self, type_annotation: nodes.NodeNG) -> None: |
| """Given a type annotation, store all the name nodes it refers to.""" |
| if isinstance(type_annotation, nodes.Name): |
| self._type_annotation_names.append(type_annotation.name) |
| return |
| |
| if isinstance(type_annotation, nodes.Attribute): |
| self._store_type_annotation_node(type_annotation.expr) |
| return |
| |
| if not isinstance(type_annotation, nodes.Subscript): |
| return |
| |
| if ( |
| isinstance(type_annotation.value, nodes.Attribute) |
| and isinstance(type_annotation.value.expr, nodes.Name) |
| and type_annotation.value.expr.name == TYPING_MODULE |
| ): |
| self._type_annotation_names.append(TYPING_MODULE) |
| return |
| |
| self._type_annotation_names.extend( |
| annotation.name for annotation in type_annotation.nodes_of_class(nodes.Name) |
| ) |
| |
| def _store_type_annotation_names( |
| self, node: nodes.For | nodes.Assign | nodes.With |
| ) -> None: |
| type_annotation = node.type_annotation |
| if not type_annotation: |
| return |
| self._store_type_annotation_node(node.type_annotation) |
| |
| def _check_self_cls_assign(self, node: nodes.Assign) -> None: |
| """Check that self/cls don't get assigned.""" |
| assign_names: set[str | None] = set() |
| for target in node.targets: |
| if isinstance(target, nodes.AssignName): |
| assign_names.add(target.name) |
| elif isinstance(target, nodes.Tuple): |
| assign_names.update( |
| elt.name for elt in target.elts if isinstance(elt, nodes.AssignName) |
| ) |
| scope = node.scope() |
| nonlocals_with_same_name = node.scope().parent and any( |
| child for child in scope.body if isinstance(child, nodes.Nonlocal) |
| ) |
| if nonlocals_with_same_name: |
| scope = node.scope().parent.scope() |
| |
| if not ( |
| isinstance(scope, nodes.FunctionDef) |
| and scope.is_method() |
| and "builtins.staticmethod" not in scope.decoratornames() |
| ): |
| return |
| argument_names = scope.argnames() |
| if not argument_names: |
| return |
| self_cls_name = argument_names[0] |
| if self_cls_name in assign_names: |
| self.add_message("self-cls-assignment", node=node, args=(self_cls_name,)) |
| |
| def _check_unpacking( |
| self, inferred: InferenceResult, node: nodes.Assign, targets: list[nodes.NodeNG] |
| ) -> None: |
| """Check for unbalanced tuple unpacking |
| and unpacking non sequences. |
| """ |
| if utils.is_inside_abstract_class(node): |
| return |
| if utils.is_comprehension(node): |
| return |
| if isinstance(inferred, util.UninferableBase): |
| return |
| if ( |
| isinstance(inferred.parent, nodes.Arguments) |
| and isinstance(node.value, nodes.Name) |
| and node.value.name == inferred.parent.vararg |
| ): |
| # Variable-length argument, we can't determine the length. |
| return |
| |
| # Attempt to check unpacking is properly balanced |
| values = self._nodes_to_unpack(inferred) |
| details = _get_unpacking_extra_info(node, inferred) |
| |
| if values is not None: |
| if len(targets) != len(values): |
| self._report_unbalanced_unpacking( |
| node, inferred, targets, len(values), details |
| ) |
| # attempt to check unpacking may be possible (i.e. RHS is iterable) |
| elif not utils.is_iterable(inferred): |
| self._report_unpacking_non_sequence(node, details) |
| |
| @staticmethod |
| def _get_value_length(value_node: nodes.NodeNG) -> int: |
| value_subnodes = VariablesChecker._nodes_to_unpack(value_node) |
| if value_subnodes is not None: |
| return len(value_subnodes) |
| if isinstance(value_node, nodes.Const) and isinstance( |
| value_node.value, (str, bytes) |
| ): |
| return len(value_node.value) |
| if isinstance(value_node, nodes.Subscript): |
| step = value_node.slice.step or 1 |
| splice_range = value_node.slice.upper.value - value_node.slice.lower.value |
| splice_length = int(math.ceil(splice_range / step)) |
| return splice_length |
| return 1 |
| |
| @staticmethod |
| def _nodes_to_unpack(node: nodes.NodeNG) -> list[nodes.NodeNG] | None: |
| """Return the list of values of the `Assign` node.""" |
| if isinstance(node, (nodes.Tuple, nodes.List, nodes.Set, *DICT_TYPES)): |
| return node.itered() # type: ignore[no-any-return] |
| if isinstance(node, astroid.Instance) and any( |
| ancestor.qname() == "typing.NamedTuple" for ancestor in node.ancestors() |
| ): |
| return [i for i in node.values() if isinstance(i, nodes.AssignName)] |
| return None |
| |
| def _report_unbalanced_unpacking( |
| self, |
| node: nodes.NodeNG, |
| inferred: InferenceResult, |
| targets: list[nodes.NodeNG], |
| values_count: int, |
| details: str, |
| ) -> None: |
| args = ( |
| details, |
| len(targets), |
| "" if len(targets) == 1 else "s", |
| values_count, |
| "" if values_count == 1 else "s", |
| ) |
| |
| symbol = ( |
| "unbalanced-dict-unpacking" |
| if isinstance(inferred, DICT_TYPES) |
| else "unbalanced-tuple-unpacking" |
| ) |
| self.add_message(symbol, node=node, args=args, confidence=INFERENCE) |
| |
| def _report_unpacking_non_sequence(self, node: nodes.NodeNG, details: str) -> None: |
| if details and not details.startswith(" "): |
| details = f" {details}" |
| self.add_message("unpacking-non-sequence", node=node, args=details) |
| |
| def _check_module_attrs( |
| self, |
| node: _base_nodes.ImportNode, |
| module: nodes.Module, |
| module_names: list[str], |
| ) -> nodes.Module | None: |
| """Check that module_names (list of string) are accessible through the |
| given module, if the latest access name corresponds to a module, return it. |
| """ |
| while module_names: |
| name = module_names.pop(0) |
| if name == "__dict__": |
| module = None |
| break |
| try: |
| module = module.getattr(name)[0] |
| if not isinstance(module, nodes.Module): |
| module = next(module.infer()) |
| if not isinstance(module, nodes.Module): |
| return None |
| except astroid.NotFoundError: |
| # Unable to import `name` from `module`. Since `name` may itself be a |
| # module, we first check if it matches the ignored modules. |
| if is_module_ignored(f"{module.qname()}.{name}", self._ignored_modules): |
| return None |
| self.add_message( |
| "no-name-in-module", args=(name, module.name), node=node |
| ) |
| return None |
| except astroid.InferenceError: |
| return None |
| if module_names: |
| modname = module.name if module else "__dict__" |
| self.add_message( |
| "no-name-in-module", node=node, args=(".".join(module_names), modname) |
| ) |
| return None |
| if isinstance(module, nodes.Module): |
| return module |
| return None |
| |
| def _check_all( |
| self, node: nodes.Module, not_consumed: dict[str, list[nodes.NodeNG]] |
| ) -> None: |
| try: |
| assigned = next(node.igetattr("__all__")) |
| except astroid.InferenceError: |
| return |
| if isinstance(assigned, util.UninferableBase): |
| return |
| if assigned.pytype() not in {"builtins.list", "builtins.tuple"}: |
| line, col = assigned.tolineno, assigned.col_offset |
| self.add_message("invalid-all-format", line=line, col_offset=col, node=node) |
| return |
| for elt in getattr(assigned, "elts", ()): |
| try: |
| elt_name = next(elt.infer()) |
| except astroid.InferenceError: |
| continue |
| if isinstance(elt_name, util.UninferableBase): |
| continue |
| if not elt_name.parent: |
| continue |
| |
| if not isinstance(elt_name, nodes.Const) or not isinstance( |
| elt_name.value, str |
| ): |
| self.add_message("invalid-all-object", args=elt.as_string(), node=elt) |
| continue |
| |
| elt_name = elt_name.value |
| # If elt is in not_consumed, remove it from not_consumed |
| if elt_name in not_consumed: |
| del not_consumed[elt_name] |
| continue |
| |
| if elt_name not in node.locals: |
| if not node.package: |
| self.add_message( |
| "undefined-all-variable", args=(elt_name,), node=elt |
| ) |
| else: |
| basename = os.path.splitext(node.file)[0] |
| if os.path.basename(basename) == "__init__": |
| name = node.name + "." + elt_name |
| try: |
| astroid.modutils.file_from_modpath(name.split(".")) |
| except ImportError: |
| self.add_message( |
| "undefined-all-variable", args=(elt_name,), node=elt |
| ) |
| except SyntaxError: |
| # don't yield a syntax-error warning, |
| # because it will be later yielded |
| # when the file will be checked |
| pass |
| |
| def _check_globals(self, not_consumed: dict[str, nodes.NodeNG]) -> None: |
| if self._allow_global_unused_variables: |
| return |
| for name, node_lst in not_consumed.items(): |
| for node in node_lst: |
| if in_type_checking_block(node): |
| continue |
| self.add_message("unused-variable", args=(name,), node=node) |
| |
| # pylint: disable = too-many-branches |
| def _check_imports(self, not_consumed: dict[str, list[nodes.NodeNG]]) -> None: |
| local_names = _fix_dot_imports(not_consumed) |
| checked = set() |
| unused_wildcard_imports: defaultdict[ |
| tuple[str, nodes.ImportFrom], list[str] |
| ] = collections.defaultdict(list) |
| for name, stmt in local_names: |
| for imports in stmt.names: |
| real_name = imported_name = imports[0] |
| if imported_name == "*": |
| real_name = name |
| as_name = imports[1] |
| if real_name in checked: |
| continue |
| if name not in (real_name, as_name): |
| continue |
| checked.add(real_name) |
| |
| is_type_annotation_import = ( |
| imported_name in self._type_annotation_names |
| or as_name in self._type_annotation_names |
| ) |
| |
| is_dummy_import = ( |
| as_name |
| and self.linter.config.dummy_variables_rgx |
| and self.linter.config.dummy_variables_rgx.match(as_name) |
| ) |
| |
| if isinstance(stmt, nodes.Import) or ( |
| isinstance(stmt, nodes.ImportFrom) and not stmt.modname |
| ): |
| if isinstance(stmt, nodes.ImportFrom) and SPECIAL_OBJ.search( |
| imported_name |
| ): |
| # Filter special objects (__doc__, __all__) etc., |
| # because they can be imported for exporting. |
| continue |
| |
| if is_type_annotation_import or is_dummy_import: |
| # Most likely a typing import if it wasn't used so far. |
| # Also filter dummy variables. |
| continue |
| |
| if as_name is None: |
| msg = f"import {imported_name}" |
| else: |
| msg = f"{imported_name} imported as {as_name}" |
| if not in_type_checking_block(stmt): |
| self.add_message("unused-import", args=msg, node=stmt) |
| elif isinstance(stmt, nodes.ImportFrom) and stmt.modname != FUTURE: |
| if SPECIAL_OBJ.search(imported_name): |
| # Filter special objects (__doc__, __all__) etc., |
| # because they can be imported for exporting. |
| continue |
| |
| if _is_from_future_import(stmt, name): |
| # Check if the name is in fact loaded from a |
| # __future__ import in another module. |
| continue |
| |
| if is_type_annotation_import or is_dummy_import: |
| # Most likely a typing import if it wasn't used so far. |
| # Also filter dummy variables. |
| continue |
| |
| if imported_name == "*": |
| unused_wildcard_imports[(stmt.modname, stmt)].append(name) |
| else: |
| if as_name is None: |
| msg = f"{imported_name} imported from {stmt.modname}" |
| else: |
| msg = f"{imported_name} imported from {stmt.modname} as {as_name}" |
| if not in_type_checking_block(stmt): |
| self.add_message("unused-import", args=msg, node=stmt) |
| |
| # Construct string for unused-wildcard-import message |
| for module, unused_list in unused_wildcard_imports.items(): |
| if len(unused_list) == 1: |
| arg_string = unused_list[0] |
| else: |
| arg_string = ( |
| f"{', '.join(i for i in unused_list[:-1])} and {unused_list[-1]}" |
| ) |
| self.add_message( |
| "unused-wildcard-import", args=(arg_string, module[0]), node=module[1] |
| ) |
| del self._to_consume |
| |
| def _check_metaclasses(self, node: nodes.Module | nodes.FunctionDef) -> None: |
| """Update consumption analysis for metaclasses.""" |
| consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] |
| |
| for child_node in node.get_children(): |
| if isinstance(child_node, nodes.ClassDef): |
| consumed.extend(self._check_classdef_metaclasses(child_node, node)) |
| |
| # Pop the consumed items, in order to avoid having |
| # unused-import and unused-variable false positives |
| for scope_locals, name in consumed: |
| scope_locals.pop(name, None) |
| |
| def _check_classdef_metaclasses( |
| self, klass: nodes.ClassDef, parent_node: nodes.Module | nodes.FunctionDef |
| ) -> list[tuple[dict[str, list[nodes.NodeNG]], str]]: |
| if not klass._metaclass: |
| # Skip if this class doesn't use explicitly a metaclass, but inherits it from ancestors |
| return [] |
| |
| consumed: list[tuple[dict[str, list[nodes.NodeNG]], str]] = [] |
| metaclass = klass.metaclass() |
| name = "" |
| if isinstance(klass._metaclass, nodes.Name): |
| name = klass._metaclass.name |
| elif isinstance(klass._metaclass, nodes.Attribute) and klass._metaclass.expr: |
| attr = klass._metaclass.expr |
| while not isinstance(attr, nodes.Name): |
| attr = attr.expr |
| name = attr.name |
| elif isinstance(klass._metaclass, nodes.Call) and isinstance( |
| klass._metaclass.func, nodes.Name |
| ): |
| name = klass._metaclass.func.name |
| elif metaclass: |
| name = metaclass.root().name |
| |
| found = False |
| name = METACLASS_NAME_TRANSFORMS.get(name, name) |
| if name: |
| # check enclosing scopes starting from most local |
| for scope_locals, _, _, _ in self._to_consume[::-1]: |
| found_nodes = scope_locals.get(name, []) |
| for found_node in found_nodes: |
| if found_node.lineno <= klass.lineno: |
| consumed.append((scope_locals, name)) |
| found = True |
| break |
| # Check parent scope |
| nodes_in_parent_scope = parent_node.locals.get(name, []) |
| for found_node_parent in nodes_in_parent_scope: |
| if found_node_parent.lineno <= klass.lineno: |
| found = True |
| break |
| if ( |
| not found |
| and not metaclass |
| and not ( |
| name in nodes.Module.scope_attrs |
| or utils.is_builtin(name) |
| or name in self.linter.config.additional_builtins |
| ) |
| ): |
| self.add_message("undefined-variable", node=klass, args=(name,)) |
| |
| return consumed |
| |
| def visit_subscript(self, node: nodes.Subscript) -> None: |
| inferred_slice = utils.safe_infer(node.slice) |
| |
| self._check_potential_index_error(node, inferred_slice) |
| |
| def _check_potential_index_error( |
| self, node: nodes.Subscript, inferred_slice: nodes.NodeNG | None |
| ) -> None: |
| """Check for the potential-index-error message.""" |
| # Currently we only check simple slices of a single integer |
| if not isinstance(inferred_slice, nodes.Const) or not isinstance( |
| inferred_slice.value, int |
| ): |
| return |
| |
| # If the node.value is a Tuple or List without inference it is defined in place |
| if isinstance(node.value, (nodes.Tuple, nodes.List)): |
| # Add 1 because iterables are 0-indexed |
| if len(node.value.elts) < inferred_slice.value + 1: |
| self.add_message( |
| "potential-index-error", node=node, confidence=INFERENCE |
| ) |
| return |
| |
| @utils.only_required_for_messages( |
| "unused-import", |
| "unused-variable", |
| ) |
| def visit_const(self, node: nodes.Const) -> None: |
| """Take note of names that appear inside string literal type annotations |
| unless the string is a parameter to `typing.Literal` or `typing.Annotation`. |
| """ |
| if node.pytype() != "builtins.str": |
| return |
| if not utils.is_node_in_type_annotation_context(node): |
| return |
| |
| # Check if parent's or grandparent's first child is typing.Literal |
| parent = node.parent |
| if isinstance(parent, nodes.Tuple): |
| parent = parent.parent |
| if isinstance(parent, nodes.Subscript): |
| origin = next(parent.get_children(), None) |
| if origin is not None and utils.is_typing_member( |
| origin, ("Annotated", "Literal") |
| ): |
| return |
| |
| try: |
| annotation = extract_node(node.value) |
| self._store_type_annotation_node(annotation) |
| except ValueError: |
| # e.g. node.value is white space |
| pass |
| except astroid.AstroidSyntaxError: |
| # e.g. "?" or ":" in typing.Literal["?", ":"] |
| pass |
| |
| |
| def register(linter: PyLinter) -> None: |
| linter.register_checker(VariablesChecker(linter)) |