1# Copyright (c) 2015-2016, 2018-2020 Claudiu Popa <pcmanticore@gmail.com>
2# Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com>
3# Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
4# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
5# Copyright (c) 2019-2021 hippo91 <guillaume.peillex@gmail.com>
6# Copyright (c) 2020 Bryce Guinta <bryce.guinta@protonmail.com>
7# Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
8# Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
9# Copyright (c) 2021 David Liu <david@cs.toronto.edu>
10# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
11# Copyright (c) 2021 Andrew Haigh <hello@nelf.in>
12
13# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
14# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
15
16"""Various context related utilities, including inference and call contexts."""
17import contextlib
18import pprint
19from typing import TYPE_CHECKING, List, MutableMapping, Optional, Sequence, Tuple
20
21if TYPE_CHECKING:
22    from astroid.nodes.node_classes import Keyword, NodeNG
23
24
25_INFERENCE_CACHE = {}
26
27
28def _invalidate_cache():
29    _INFERENCE_CACHE.clear()
30
31
32class InferenceContext:
33    """Provide context for inference
34
35    Store already inferred nodes to save time
36    Account for already visited nodes to stop infinite recursion
37    """
38
39    __slots__ = (
40        "path",
41        "lookupname",
42        "callcontext",
43        "boundnode",
44        "extra_context",
45        "_nodes_inferred",
46    )
47
48    max_inferred = 100
49
50    def __init__(self, path=None, nodes_inferred=None):
51        if nodes_inferred is None:
52            self._nodes_inferred = [0]
53        else:
54            self._nodes_inferred = nodes_inferred
55        self.path = path or set()
56        """
57        :type: set(tuple(NodeNG, optional(str)))
58
59        Path of visited nodes and their lookupname
60
61        Currently this key is ``(node, context.lookupname)``
62        """
63        self.lookupname = None
64        """
65        :type: optional[str]
66
67        The original name of the node
68
69        e.g.
70        foo = 1
71        The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo'
72        """
73        self.callcontext = None
74        """
75        :type: optional[CallContext]
76
77        The call arguments and keywords for the given context
78        """
79        self.boundnode = None
80        """
81        :type: optional[NodeNG]
82
83        The bound node of the given context
84
85        e.g. the bound node of object.__new__(cls) is the object node
86        """
87        self.extra_context = {}
88        """
89        :type: dict(NodeNG, Context)
90
91        Context that needs to be passed down through call stacks
92        for call arguments
93        """
94
95    @property
96    def nodes_inferred(self):
97        """
98        Number of nodes inferred in this context and all its clones/decendents
99
100        Wrap inner value in a mutable cell to allow for mutating a class
101        variable in the presence of __slots__
102        """
103        return self._nodes_inferred[0]
104
105    @nodes_inferred.setter
106    def nodes_inferred(self, value):
107        self._nodes_inferred[0] = value
108
109    @property
110    def inferred(
111        self,
112    ) -> MutableMapping[
113        Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"]
114    ]:
115        """
116        Inferred node contexts to their mapped results
117
118        Currently the key is ``(node, lookupname, callcontext, boundnode)``
119        and the value is tuple of the inferred results
120        """
121        return _INFERENCE_CACHE
122
123    def push(self, node):
124        """Push node into inference path
125
126        :return: True if node is already in context path else False
127        :rtype: bool
128
129        Allows one to see if the given node has already
130        been looked at for this inference context"""
131        name = self.lookupname
132        if (node, name) in self.path:
133            return True
134
135        self.path.add((node, name))
136        return False
137
138    def clone(self):
139        """Clone inference path
140
141        For example, each side of a binary operation (BinOp)
142        starts with the same context but diverge as each side is inferred
143        so the InferenceContext will need be cloned"""
144        # XXX copy lookupname/callcontext ?
145        clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred)
146        clone.callcontext = self.callcontext
147        clone.boundnode = self.boundnode
148        clone.extra_context = self.extra_context
149        return clone
150
151    @contextlib.contextmanager
152    def restore_path(self):
153        path = set(self.path)
154        yield
155        self.path = path
156
157    def __str__(self):
158        state = (
159            f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
160            for field in self.__slots__
161        )
162        return "{}({})".format(type(self).__name__, ",\n    ".join(state))
163
164
165class CallContext:
166    """Holds information for a call site."""
167
168    __slots__ = ("args", "keywords", "callee")
169
170    def __init__(
171        self,
172        args: List["NodeNG"],
173        keywords: Optional[List["Keyword"]] = None,
174        callee: Optional["NodeNG"] = None,
175    ):
176        self.args = args  # Call positional arguments
177        if keywords:
178            keywords = [(arg.arg, arg.value) for arg in keywords]
179        else:
180            keywords = []
181        self.keywords = keywords  # Call keyword arguments
182        self.callee = callee  # Function being called
183
184
185def copy_context(context: Optional[InferenceContext]) -> InferenceContext:
186    """Clone a context if given, or return a fresh contexxt"""
187    if context is not None:
188        return context.clone()
189
190    return InferenceContext()
191
192
193def bind_context_to_node(context, node):
194    """Give a context a boundnode
195    to retrieve the correct function name or attribute value
196    with from further inference.
197
198    Do not use an existing context since the boundnode could then
199    be incorrectly propagated higher up in the call stack.
200
201    :param context: Context to use
202    :type context: Optional(context)
203
204    :param node: Node to do name lookups from
205    :type node NodeNG:
206
207    :returns: A new context
208    :rtype: InferenceContext
209    """
210    context = copy_context(context)
211    context.boundnode = node
212    return context
213