| # 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 |
| |
| """Various context related utilities, including inference and call contexts.""" |
| |
| from __future__ import annotations |
| |
| import contextlib |
| import pprint |
| from collections.abc import Iterator |
| from typing import TYPE_CHECKING, Dict, Optional, Sequence, Tuple |
| |
| from astroid.typing import InferenceResult, SuccessfulInferenceResult |
| |
| if TYPE_CHECKING: |
| from astroid import constraint, nodes |
| from astroid.nodes.node_classes import Keyword, NodeNG |
| |
| _InferenceCache = Dict[ |
| Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"] |
| ] |
| |
| _INFERENCE_CACHE: _InferenceCache = {} |
| |
| |
| def _invalidate_cache() -> None: |
| _INFERENCE_CACHE.clear() |
| |
| |
| class InferenceContext: |
| """Provide context for inference. |
| |
| Store already inferred nodes to save time |
| Account for already visited nodes to stop infinite recursion |
| """ |
| |
| __slots__ = ( |
| "path", |
| "lookupname", |
| "callcontext", |
| "boundnode", |
| "extra_context", |
| "constraints", |
| "_nodes_inferred", |
| ) |
| |
| max_inferred = 100 |
| |
| def __init__( |
| self, |
| path: set[tuple[nodes.NodeNG, str | None]] | None = None, |
| nodes_inferred: list[int] | None = None, |
| ) -> None: |
| if nodes_inferred is None: |
| self._nodes_inferred = [0] |
| else: |
| self._nodes_inferred = nodes_inferred |
| |
| self.path = path or set() |
| """Path of visited nodes and their lookupname. |
| |
| Currently this key is ``(node, context.lookupname)`` |
| """ |
| self.lookupname: str | None = None |
| """The original name of the node. |
| |
| e.g. |
| foo = 1 |
| The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo' |
| """ |
| self.callcontext: CallContext | None = None |
| """The call arguments and keywords for the given context.""" |
| self.boundnode: SuccessfulInferenceResult | None = None |
| """The bound node of the given context. |
| |
| e.g. the bound node of object.__new__(cls) is the object node |
| """ |
| self.extra_context: dict[SuccessfulInferenceResult, InferenceContext] = {} |
| """Context that needs to be passed down through call stacks for call arguments.""" |
| |
| self.constraints: dict[str, dict[nodes.If, set[constraint.Constraint]]] = {} |
| """The constraints on nodes.""" |
| |
| @property |
| def nodes_inferred(self) -> int: |
| """ |
| Number of nodes inferred in this context and all its clones/descendents. |
| |
| Wrap inner value in a mutable cell to allow for mutating a class |
| variable in the presence of __slots__ |
| """ |
| return self._nodes_inferred[0] |
| |
| @nodes_inferred.setter |
| def nodes_inferred(self, value: int) -> None: |
| self._nodes_inferred[0] = value |
| |
| @property |
| def inferred(self) -> _InferenceCache: |
| """ |
| Inferred node contexts to their mapped results. |
| |
| Currently the key is ``(node, lookupname, callcontext, boundnode)`` |
| and the value is tuple of the inferred results |
| """ |
| return _INFERENCE_CACHE |
| |
| def push(self, node: nodes.NodeNG) -> bool: |
| """Push node into inference path. |
| |
| Allows one to see if the given node has already |
| been looked at for this inference context |
| """ |
| name = self.lookupname |
| if (node, name) in self.path: |
| return True |
| |
| self.path.add((node, name)) |
| return False |
| |
| def clone(self) -> InferenceContext: |
| """Clone inference path. |
| |
| For example, each side of a binary operation (BinOp) |
| starts with the same context but diverge as each side is inferred |
| so the InferenceContext will need be cloned |
| """ |
| # XXX copy lookupname/callcontext ? |
| clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred) |
| clone.callcontext = self.callcontext |
| clone.boundnode = self.boundnode |
| clone.extra_context = self.extra_context |
| clone.constraints = self.constraints.copy() |
| return clone |
| |
| @contextlib.contextmanager |
| def restore_path(self) -> Iterator[None]: |
| path = set(self.path) |
| yield |
| self.path = path |
| |
| def is_empty(self) -> bool: |
| return ( |
| not self.path |
| and not self.nodes_inferred |
| and not self.callcontext |
| and not self.boundnode |
| and not self.lookupname |
| and not self.callcontext |
| and not self.extra_context |
| and not self.constraints |
| ) |
| |
| def __str__(self) -> str: |
| state = ( |
| f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}" |
| for field in self.__slots__ |
| ) |
| return "{}({})".format(type(self).__name__, ",\n ".join(state)) |
| |
| |
| class CallContext: |
| """Holds information for a call site.""" |
| |
| __slots__ = ("args", "keywords", "callee") |
| |
| def __init__( |
| self, |
| args: list[NodeNG], |
| keywords: list[Keyword] | None = None, |
| callee: InferenceResult | None = None, |
| ): |
| self.args = args # Call positional arguments |
| if keywords: |
| arg_value_pairs = [(arg.arg, arg.value) for arg in keywords] |
| else: |
| arg_value_pairs = [] |
| self.keywords = arg_value_pairs # Call keyword arguments |
| self.callee = callee # Function being called |
| |
| |
| def copy_context(context: InferenceContext | None) -> InferenceContext: |
| """Clone a context if given, or return a fresh context.""" |
| if context is not None: |
| return context.clone() |
| |
| return InferenceContext() |
| |
| |
| def bind_context_to_node( |
| context: InferenceContext | None, node: SuccessfulInferenceResult |
| ) -> InferenceContext: |
| """Give a context a boundnode |
| to retrieve the correct function name or attribute value |
| with from further inference. |
| |
| Do not use an existing context since the boundnode could then |
| be incorrectly propagated higher up in the call stack. |
| """ |
| context = copy_context(context) |
| context.boundnode = node |
| return context |