| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672 |
- # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
- # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
- # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
- """This module contains some base nodes that can be inherited for the different nodes.
- Previously these were called Mixin nodes.
- """
- from __future__ import annotations
- import itertools
- from collections.abc import Callable, Generator, Iterator
- from functools import cached_property, lru_cache, partial
- from typing import TYPE_CHECKING, Any, ClassVar
- from astroid import bases, nodes, util
- from astroid.context import (
- CallContext,
- InferenceContext,
- bind_context_to_node,
- )
- from astroid.exceptions import (
- AttributeInferenceError,
- InferenceError,
- )
- from astroid.interpreter import dunder_lookup
- from astroid.nodes.node_ng import NodeNG
- from astroid.typing import InferenceResult
- if TYPE_CHECKING:
- from astroid.nodes.node_classes import LocalsDictNodeNG
- GetFlowFactory = Callable[
- [
- InferenceResult,
- InferenceResult | None,
- nodes.AugAssign | nodes.BinOp,
- InferenceResult,
- InferenceResult | None,
- InferenceContext,
- InferenceContext,
- ],
- list[partial[Generator[InferenceResult]]],
- ]
- class Statement(NodeNG):
- """Statement node adding a few attributes.
- NOTE: This class is part of the public API of 'astroid.nodes'.
- """
- is_statement = True
- """Whether this node indicates a statement."""
- def next_sibling(self):
- """The next sibling statement node.
- :returns: The next sibling statement node.
- :rtype: NodeNG or None
- """
- stmts = self.parent.child_sequence(self)
- index = stmts.index(self)
- try:
- return stmts[index + 1]
- except IndexError:
- return None
- def previous_sibling(self):
- """The previous sibling statement.
- :returns: The previous sibling statement node.
- :rtype: NodeNG or None
- """
- stmts = self.parent.child_sequence(self)
- index = stmts.index(self)
- if index >= 1:
- return stmts[index - 1]
- return None
- class NoChildrenNode(NodeNG):
- """Base nodes for nodes with no children, e.g. Pass."""
- def get_children(self) -> Iterator[NodeNG]:
- yield from ()
- class FilterStmtsBaseNode(NodeNG):
- """Base node for statement filtering and assignment type."""
- def _get_filtered_stmts(self, _, node, _stmts, mystmt: Statement | None):
- """Method used in _filter_stmts to get statements and trigger break."""
- if self.statement() is mystmt:
- # original node's statement is the assignment, only keep
- # current node (gen exp, list comp)
- return [node], True
- return _stmts, False
- def assign_type(self):
- return self
- class AssignTypeNode(NodeNG):
- """Base node for nodes that can 'assign' such as AnnAssign."""
- def assign_type(self):
- return self
- def _get_filtered_stmts(self, lookup_node, node, _stmts, mystmt: Statement | None):
- """Method used in filter_stmts."""
- if self is mystmt:
- return _stmts, True
- if self.statement() is mystmt:
- # original node's statement is the assignment, only keep
- # current node (gen exp, list comp)
- return [node], True
- return _stmts, False
- class ParentAssignNode(AssignTypeNode):
- """Base node for nodes whose assign_type is determined by the parent node."""
- def assign_type(self):
- return self.parent.assign_type()
- class ImportNode(FilterStmtsBaseNode, NoChildrenNode, Statement):
- """Base node for From and Import Nodes."""
- modname: str | None
- """The module that is being imported from.
- This is ``None`` for relative imports.
- """
- names: list[tuple[str, str | None]]
- """What is being imported from the module.
- Each entry is a :class:`tuple` of the name being imported,
- and the alias that the name is assigned to (if any).
- """
- def _infer_name(self, frame, name):
- return name
- def do_import_module(self, modname: str | None = None) -> nodes.Module:
- """Return the ast for a module whose name is <modname> imported by <self>."""
- mymodule = self.root()
- level: int | None = getattr(self, "level", None) # Import has no level
- if modname is None:
- modname = self.modname
- # If the module ImportNode is importing is a module with the same name
- # as the file that contains the ImportNode we don't want to use the cache
- # to make sure we use the import system to get the correct module.
- if (
- modname
- # pylint: disable-next=no-member # pylint doesn't recognize type of mymodule
- and mymodule.relative_to_absolute_name(modname, level) == mymodule.name
- ):
- use_cache = False
- else:
- use_cache = True
- # pylint: disable-next=no-member # pylint doesn't recognize type of mymodule
- return mymodule.import_module(
- modname,
- level=level,
- relative_only=bool(level and level >= 1),
- use_cache=use_cache,
- )
- def real_name(self, asname: str) -> str:
- """Get name from 'as' name."""
- for name, _asname in self.names:
- if name == "*":
- return asname
- if not _asname:
- name = name.split(".", 1)[0]
- _asname = name
- if asname == _asname:
- return name
- raise AttributeInferenceError(
- "Could not find original name for {attribute} in {target!r}",
- target=self,
- attribute=asname,
- )
- class MultiLineBlockNode(NodeNG):
- """Base node for multi-line blocks, e.g. For and FunctionDef.
- Note that this does not apply to every node with a `body` field.
- For instance, an If node has a multi-line body, but the body of an
- IfExpr is not multi-line, and hence cannot contain Return nodes,
- Assign nodes, etc.
- """
- _multi_line_block_fields: ClassVar[tuple[str, ...]] = ()
- @cached_property
- def _multi_line_blocks(self):
- return tuple(getattr(self, field) for field in self._multi_line_block_fields)
- def _get_return_nodes_skip_functions(self):
- for block in self._multi_line_blocks:
- for child_node in block:
- if child_node.is_function:
- continue
- yield from child_node._get_return_nodes_skip_functions()
- def _get_yield_nodes_skip_functions(self):
- for block in self._multi_line_blocks:
- for child_node in block:
- if child_node.is_function:
- continue
- yield from child_node._get_yield_nodes_skip_functions()
- def _get_yield_nodes_skip_lambdas(self):
- for block in self._multi_line_blocks:
- for child_node in block:
- if child_node.is_lambda:
- continue
- yield from child_node._get_yield_nodes_skip_lambdas()
- @cached_property
- def _assign_nodes_in_scope(self) -> list[nodes.Assign]:
- children_assign_nodes = (
- child_node._assign_nodes_in_scope
- for block in self._multi_line_blocks
- for child_node in block
- )
- return list(itertools.chain.from_iterable(children_assign_nodes))
- class MultiLineWithElseBlockNode(MultiLineBlockNode):
- """Base node for multi-line blocks that can have else statements."""
- @cached_property
- def blockstart_tolineno(self):
- return self.lineno
- def _elsed_block_range(
- self, lineno: int, orelse: list[nodes.NodeNG], last: int | None = None
- ) -> tuple[int, int]:
- """Handle block line numbers range for try/finally, for, if and while
- statements.
- """
- if lineno == self.fromlineno:
- return lineno, lineno
- if orelse:
- if lineno >= orelse[0].fromlineno:
- return lineno, orelse[-1].tolineno
- return lineno, orelse[0].fromlineno - 1
- return lineno, last or self.tolineno
- class LookupMixIn(NodeNG):
- """Mixin to look up a name in the right scope."""
- @lru_cache # noqa
- def lookup(self, name: str) -> tuple[LocalsDictNodeNG, list[NodeNG]]:
- """Lookup where the given variable is assigned.
- The lookup starts from self's scope. If self is not a frame itself
- and the name is found in the inner frame locals, statements will be
- filtered to remove ignorable statements according to self's location.
- :param name: The name of the variable to find assignments for.
- :returns: The scope node and the list of assignments associated to the
- given name according to the scope where it has been found (locals,
- globals or builtin).
- """
- return self.scope().scope_lookup(self, name)
- def ilookup(self, name):
- """Lookup the inferred values of the given variable.
- :param name: The variable name to find values for.
- :type name: str
- :returns: The inferred values of the statements returned from
- :meth:`lookup`.
- :rtype: iterable
- """
- frame, stmts = self.lookup(name)
- context = InferenceContext()
- return bases._infer_stmts(stmts, context, frame)
- def _reflected_name(name) -> str:
- return "__r" + name[2:]
- def _augmented_name(name) -> str:
- return "__i" + name[2:]
- BIN_OP_METHOD = {
- "+": "__add__",
- "-": "__sub__",
- "/": "__truediv__",
- "//": "__floordiv__",
- "*": "__mul__",
- "**": "__pow__",
- "%": "__mod__",
- "&": "__and__",
- "|": "__or__",
- "^": "__xor__",
- "<<": "__lshift__",
- ">>": "__rshift__",
- "@": "__matmul__",
- }
- REFLECTED_BIN_OP_METHOD = {
- key: _reflected_name(value) for (key, value) in BIN_OP_METHOD.items()
- }
- AUGMENTED_OP_METHOD = {
- key + "=": _augmented_name(value) for (key, value) in BIN_OP_METHOD.items()
- }
- class OperatorNode(NodeNG):
- @staticmethod
- def _filter_operation_errors(
- infer_callable: Callable[
- [InferenceContext | None],
- Generator[InferenceResult | util.BadOperationMessage],
- ],
- context: InferenceContext | None,
- error: type[util.BadOperationMessage],
- ) -> Generator[InferenceResult]:
- for result in infer_callable(context):
- if isinstance(result, error):
- # For the sake of .infer(), we don't care about operation
- # errors, which is the job of a linter. So return something
- # which shows that we can't infer the result.
- yield util.Uninferable
- else:
- yield result
- @staticmethod
- def _is_not_implemented(const) -> bool:
- """Check if the given const node is NotImplemented."""
- return isinstance(const, nodes.Const) and const.value is NotImplemented
- @staticmethod
- def _infer_old_style_string_formatting(
- instance: nodes.Const, other: nodes.NodeNG, context: InferenceContext
- ) -> tuple[util.UninferableBase | nodes.Const]:
- """Infer the result of '"string" % ...'.
- TODO: Instead of returning Uninferable we should rely
- on the call to '%' to see if the result is actually uninferable.
- """
- if isinstance(other, nodes.Tuple):
- if util.Uninferable in other.elts:
- return (util.Uninferable,)
- inferred_positional = [util.safe_infer(i, context) for i in other.elts]
- if all(isinstance(i, nodes.Const) for i in inferred_positional):
- values = tuple(i.value for i in inferred_positional)
- else:
- values = None
- elif isinstance(other, nodes.Dict):
- values: dict[Any, Any] = {}
- for pair in other.items:
- key = util.safe_infer(pair[0], context)
- if not isinstance(key, nodes.Const):
- return (util.Uninferable,)
- value = util.safe_infer(pair[1], context)
- if not isinstance(value, nodes.Const):
- return (util.Uninferable,)
- values[key.value] = value.value
- elif isinstance(other, nodes.Const):
- values = other.value
- else:
- return (util.Uninferable,)
- try:
- return (nodes.const_factory(instance.value % values),)
- except (TypeError, KeyError, ValueError):
- return (util.Uninferable,)
- @staticmethod
- def _invoke_binop_inference(
- instance: InferenceResult,
- opnode: nodes.AugAssign | nodes.BinOp,
- op: str,
- other: InferenceResult,
- context: InferenceContext,
- method_name: str,
- ) -> Generator[InferenceResult]:
- """Invoke binary operation inference on the given instance."""
- methods = dunder_lookup.lookup(instance, method_name)
- context = bind_context_to_node(context, instance)
- method = methods[0]
- context.callcontext.callee = method
- if (
- isinstance(instance, nodes.Const)
- and isinstance(instance.value, str)
- and op == "%"
- ):
- return iter(
- OperatorNode._infer_old_style_string_formatting(
- instance, other, context
- )
- )
- try:
- inferred = next(method.infer(context=context))
- except StopIteration as e:
- raise InferenceError(node=method, context=context) from e
- if isinstance(inferred, util.UninferableBase):
- raise InferenceError
- if not isinstance(
- instance,
- (nodes.Const, nodes.Tuple, nodes.List, nodes.ClassDef, bases.Instance),
- ):
- raise InferenceError # pragma: no cover # Used as a failsafe
- return instance.infer_binary_op(opnode, op, other, context, inferred)
- @staticmethod
- def _aug_op(
- instance: InferenceResult,
- opnode: nodes.AugAssign,
- op: str,
- other: InferenceResult,
- context: InferenceContext,
- reverse: bool = False,
- ) -> partial[Generator[InferenceResult]]:
- """Get an inference callable for an augmented binary operation."""
- method_name = AUGMENTED_OP_METHOD[op]
- return partial(
- OperatorNode._invoke_binop_inference,
- instance=instance,
- op=op,
- opnode=opnode,
- other=other,
- context=context,
- method_name=method_name,
- )
- @staticmethod
- def _bin_op(
- instance: InferenceResult,
- opnode: nodes.AugAssign | nodes.BinOp,
- op: str,
- other: InferenceResult,
- context: InferenceContext,
- reverse: bool = False,
- ) -> partial[Generator[InferenceResult]]:
- """Get an inference callable for a normal binary operation.
- If *reverse* is True, then the reflected method will be used instead.
- """
- if reverse:
- method_name = REFLECTED_BIN_OP_METHOD[op]
- else:
- method_name = BIN_OP_METHOD[op]
- return partial(
- OperatorNode._invoke_binop_inference,
- instance=instance,
- op=op,
- opnode=opnode,
- other=other,
- context=context,
- method_name=method_name,
- )
- @staticmethod
- def _bin_op_or_union_type(
- left: bases.UnionType | nodes.ClassDef | nodes.Const,
- right: bases.UnionType | nodes.ClassDef | nodes.Const,
- ) -> Generator[InferenceResult]:
- """Create a new UnionType instance for binary or, e.g. int | str."""
- yield bases.UnionType(left, right)
- @staticmethod
- def _get_binop_contexts(context, left, right):
- """Get contexts for binary operations.
- This will return two inference contexts, the first one
- for x.__op__(y), the other one for y.__rop__(x), where
- only the arguments are inversed.
- """
- # The order is important, since the first one should be
- # left.__op__(right).
- for arg in (right, left):
- new_context = context.clone()
- new_context.callcontext = CallContext(args=[arg])
- new_context.boundnode = None
- yield new_context
- @staticmethod
- def _same_type(type1, type2) -> bool:
- """Check if type1 is the same as type2."""
- return type1.qname() == type2.qname()
- @staticmethod
- def _get_aug_flow(
- left: InferenceResult,
- left_type: InferenceResult | None,
- aug_opnode: nodes.AugAssign,
- right: InferenceResult,
- right_type: InferenceResult | None,
- context: InferenceContext,
- reverse_context: InferenceContext,
- ) -> list[partial[Generator[InferenceResult]]]:
- """Get the flow for augmented binary operations.
- The rules are a bit messy:
- * if left and right have the same type, then left.__augop__(right)
- is first tried and then left.__op__(right).
- * if left and right are unrelated typewise, then
- left.__augop__(right) is tried, then left.__op__(right)
- is tried and then right.__rop__(left) is tried.
- * if left is a subtype of right, then left.__augop__(right)
- is tried and then left.__op__(right).
- * if left is a supertype of right, then left.__augop__(right)
- is tried, then right.__rop__(left) and then
- left.__op__(right)
- """
- from astroid import helpers # pylint: disable=import-outside-toplevel
- bin_op = aug_opnode.op.strip("=")
- aug_op = aug_opnode.op
- if OperatorNode._same_type(left_type, right_type):
- methods = [
- OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
- OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
- ]
- elif helpers.is_subtype(left_type, right_type):
- methods = [
- OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
- OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
- ]
- elif helpers.is_supertype(left_type, right_type):
- methods = [
- OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
- OperatorNode._bin_op(
- right, aug_opnode, bin_op, left, reverse_context, reverse=True
- ),
- OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
- ]
- else:
- methods = [
- OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
- OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
- OperatorNode._bin_op(
- right, aug_opnode, bin_op, left, reverse_context, reverse=True
- ),
- ]
- return methods
- @staticmethod
- def _get_binop_flow(
- left: InferenceResult,
- left_type: InferenceResult | None,
- binary_opnode: nodes.AugAssign | nodes.BinOp,
- right: InferenceResult,
- right_type: InferenceResult | None,
- context: InferenceContext,
- reverse_context: InferenceContext,
- ) -> list[partial[Generator[InferenceResult]]]:
- """Get the flow for binary operations.
- The rules are a bit messy:
- * if left and right have the same type, then only one
- method will be called, left.__op__(right)
- * if left and right are unrelated typewise, then first
- left.__op__(right) is tried and if this does not exist
- or returns NotImplemented, then right.__rop__(left) is tried.
- * if left is a subtype of right, then only left.__op__(right)
- is tried.
- * if left is a supertype of right, then right.__rop__(left)
- is first tried and then left.__op__(right)
- """
- from astroid import helpers # pylint: disable=import-outside-toplevel
- op = binary_opnode.op
- if OperatorNode._same_type(left_type, right_type):
- methods = [OperatorNode._bin_op(left, binary_opnode, op, right, context)]
- elif helpers.is_subtype(left_type, right_type):
- methods = [OperatorNode._bin_op(left, binary_opnode, op, right, context)]
- elif helpers.is_supertype(left_type, right_type):
- methods = [
- OperatorNode._bin_op(
- right, binary_opnode, op, left, reverse_context, reverse=True
- ),
- OperatorNode._bin_op(left, binary_opnode, op, right, context),
- ]
- else:
- methods = [
- OperatorNode._bin_op(left, binary_opnode, op, right, context),
- OperatorNode._bin_op(
- right, binary_opnode, op, left, reverse_context, reverse=True
- ),
- ]
- # pylint: disable = too-many-boolean-expressions
- if (
- op == "|"
- and (
- isinstance(left, (bases.UnionType, nodes.ClassDef))
- or (isinstance(left, nodes.Const) and left.value is None)
- )
- and (
- isinstance(right, (bases.UnionType, nodes.ClassDef))
- or (isinstance(right, nodes.Const) and right.value is None)
- )
- ):
- methods.extend([partial(OperatorNode._bin_op_or_union_type, left, right)])
- return methods
- @staticmethod
- def _infer_binary_operation(
- left: InferenceResult,
- right: InferenceResult,
- binary_opnode: nodes.AugAssign | nodes.BinOp,
- context: InferenceContext,
- flow_factory: GetFlowFactory,
- ) -> Generator[InferenceResult | util.BadBinaryOperationMessage]:
- """Infer a binary operation between a left operand and a right operand.
- This is used by both normal binary operations and augmented binary
- operations, the only difference is the flow factory used.
- """
- from astroid import helpers # pylint: disable=import-outside-toplevel
- context, reverse_context = OperatorNode._get_binop_contexts(
- context, left, right
- )
- left_type = helpers.object_type(left)
- right_type = helpers.object_type(right)
- methods = flow_factory(
- left, left_type, binary_opnode, right, right_type, context, reverse_context
- )
- for method in methods:
- try:
- results = list(method())
- except AttributeError:
- continue
- except AttributeInferenceError:
- continue
- except InferenceError:
- yield util.Uninferable
- return
- else:
- if any(isinstance(result, util.UninferableBase) for result in results):
- yield util.Uninferable
- return
- if all(map(OperatorNode._is_not_implemented, results)):
- continue
- not_implemented = sum(
- 1 for result in results if OperatorNode._is_not_implemented(result)
- )
- if not_implemented and not_implemented != len(results):
- # Can't infer yet what this is.
- yield util.Uninferable
- return
- yield from results
- return
- # The operation doesn't seem to be supported so let the caller know about it
- yield util.BadBinaryOperationMessage(left_type, binary_opnode.op, right_type)
|