context.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
  2. # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
  4. """Various context related utilities, including inference and call contexts."""
  5. from __future__ import annotations
  6. import contextlib
  7. import pprint
  8. from typing import TYPE_CHECKING, Dict, Optional, Sequence, Tuple
  9. if TYPE_CHECKING:
  10. from astroid import constraint, nodes
  11. from astroid.nodes.node_classes import Keyword, NodeNG
  12. _InferenceCache = Dict[
  13. Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"]
  14. ]
  15. _INFERENCE_CACHE: _InferenceCache = {}
  16. def _invalidate_cache() -> None:
  17. _INFERENCE_CACHE.clear()
  18. class InferenceContext:
  19. """Provide context for inference.
  20. Store already inferred nodes to save time
  21. Account for already visited nodes to stop infinite recursion
  22. """
  23. __slots__ = (
  24. "path",
  25. "lookupname",
  26. "callcontext",
  27. "boundnode",
  28. "extra_context",
  29. "constraints",
  30. "_nodes_inferred",
  31. )
  32. max_inferred = 100
  33. def __init__(
  34. self,
  35. path=None,
  36. nodes_inferred: list[int] | None = None,
  37. ):
  38. if nodes_inferred is None:
  39. self._nodes_inferred = [0]
  40. else:
  41. self._nodes_inferred = nodes_inferred
  42. self.path = path or set()
  43. """
  44. :type: set(tuple(NodeNG, optional(str)))
  45. Path of visited nodes and their lookupname
  46. Currently this key is ``(node, context.lookupname)``
  47. """
  48. self.lookupname: str | None = None
  49. """The original name of the node.
  50. e.g.
  51. foo = 1
  52. The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo'
  53. """
  54. self.callcontext: CallContext | None = None
  55. """The call arguments and keywords for the given context."""
  56. self.boundnode = None
  57. """
  58. :type: optional[NodeNG]
  59. The bound node of the given context
  60. e.g. the bound node of object.__new__(cls) is the object node
  61. """
  62. self.extra_context = {}
  63. """
  64. :type: dict(NodeNG, Context)
  65. Context that needs to be passed down through call stacks
  66. for call arguments
  67. """
  68. self.constraints: dict[str, dict[nodes.If, set[constraint.Constraint]]] = {}
  69. """The constraints on nodes."""
  70. @property
  71. def nodes_inferred(self) -> int:
  72. """
  73. Number of nodes inferred in this context and all its clones/descendents.
  74. Wrap inner value in a mutable cell to allow for mutating a class
  75. variable in the presence of __slots__
  76. """
  77. return self._nodes_inferred[0]
  78. @nodes_inferred.setter
  79. def nodes_inferred(self, value: int) -> None:
  80. self._nodes_inferred[0] = value
  81. @property
  82. def inferred(self) -> _InferenceCache:
  83. """
  84. Inferred node contexts to their mapped results.
  85. Currently the key is ``(node, lookupname, callcontext, boundnode)``
  86. and the value is tuple of the inferred results
  87. """
  88. return _INFERENCE_CACHE
  89. def push(self, node) -> bool:
  90. """Push node into inference path.
  91. :return: Whether node is already in context path.
  92. Allows one to see if the given node has already
  93. been looked at for this inference context
  94. """
  95. name = self.lookupname
  96. if (node, name) in self.path:
  97. return True
  98. self.path.add((node, name))
  99. return False
  100. def clone(self) -> InferenceContext:
  101. """Clone inference path.
  102. For example, each side of a binary operation (BinOp)
  103. starts with the same context but diverge as each side is inferred
  104. so the InferenceContext will need be cloned
  105. """
  106. # XXX copy lookupname/callcontext ?
  107. clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred)
  108. clone.callcontext = self.callcontext
  109. clone.boundnode = self.boundnode
  110. clone.extra_context = self.extra_context
  111. clone.constraints = self.constraints.copy()
  112. return clone
  113. @contextlib.contextmanager
  114. def restore_path(self):
  115. path = set(self.path)
  116. yield
  117. self.path = path
  118. def __str__(self) -> str:
  119. state = (
  120. f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
  121. for field in self.__slots__
  122. )
  123. return "{}({})".format(type(self).__name__, ",\n ".join(state))
  124. class CallContext:
  125. """Holds information for a call site."""
  126. __slots__ = ("args", "keywords", "callee")
  127. def __init__(
  128. self,
  129. args: list[NodeNG],
  130. keywords: list[Keyword] | None = None,
  131. callee: NodeNG | None = None,
  132. ):
  133. self.args = args # Call positional arguments
  134. if keywords:
  135. arg_value_pairs = [(arg.arg, arg.value) for arg in keywords]
  136. else:
  137. arg_value_pairs = []
  138. self.keywords = arg_value_pairs # Call keyword arguments
  139. self.callee = callee # Function being called
  140. def copy_context(context: InferenceContext | None) -> InferenceContext:
  141. """Clone a context if given, or return a fresh context."""
  142. if context is not None:
  143. return context.clone()
  144. return InferenceContext()
  145. def bind_context_to_node(context: InferenceContext | None, node) -> InferenceContext:
  146. """Give a context a boundnode
  147. to retrieve the correct function name or attribute value
  148. with from further inference.
  149. Do not use an existing context since the boundnode could then
  150. be incorrectly propagated higher up in the call stack.
  151. :param node: Node to do name lookups from
  152. :type node NodeNG:
  153. :returns: A new context
  154. :rtype: InferenceContext
  155. """
  156. context = copy_context(context)
  157. context.boundnode = node
  158. return context