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