1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
4
5"""Transform utilities (filters and decorator)."""
6
7from __future__ import annotations
8
9from collections import OrderedDict
10from collections.abc import Generator
11from typing import Any, TypeVar
12
13from astroid.context import InferenceContext
14from astroid.exceptions import InferenceOverwriteError, UseInferenceDefault
15from astroid.nodes import NodeNG
16from astroid.typing import (
17 InferenceResult,
18 InferFn,
19 TransformFn,
20)
21
22_cache: OrderedDict[
23 tuple[InferFn[Any], NodeNG, InferenceContext | None], list[InferenceResult]
24] = OrderedDict()
25
26_CURRENTLY_INFERRING: set[tuple[InferFn[Any], NodeNG]] = set()
27
28_NodesT = TypeVar("_NodesT", bound=NodeNG)
29
30
31def clear_inference_tip_cache() -> None:
32 """Clear the inference tips cache."""
33 _cache.clear()
34
35
36def _inference_tip_cached(func: InferFn[_NodesT]) -> InferFn[_NodesT]:
37 """Cache decorator used for inference tips."""
38
39 def inner(
40 node: _NodesT,
41 context: InferenceContext | None = None,
42 ) -> Generator[InferenceResult]:
43 partial_cache_key = (func, node)
44 if partial_cache_key in _CURRENTLY_INFERRING:
45 # If through recursion we end up trying to infer the same
46 # func + node we raise here.
47 _CURRENTLY_INFERRING.remove(partial_cache_key)
48 raise UseInferenceDefault
49 if context is not None and context.is_empty():
50 # Fresh, empty contexts will defeat the cache.
51 context = None
52 try:
53 yield from _cache[func, node, context]
54 return
55 except KeyError:
56 # Recursion guard with a partial cache key.
57 # Using the full key causes a recursion error on PyPy.
58 # It's a pragmatic compromise to avoid so much recursive inference
59 # with slightly different contexts while still passing the simple
60 # test cases included with this commit.
61 _CURRENTLY_INFERRING.add(partial_cache_key)
62 try:
63 # May raise UseInferenceDefault
64 result = _cache[func, node, context] = list(func(node, context))
65 except Exception as e:
66 # Suppress the KeyError from the cache miss.
67 raise e from None
68 finally:
69 # Remove recursion guard.
70 try:
71 _CURRENTLY_INFERRING.remove(partial_cache_key)
72 except KeyError:
73 pass # Recursion may beat us to the punch.
74
75 if len(_cache) > 64:
76 _cache.popitem(last=False)
77
78 # https://github.com/pylint-dev/pylint/issues/8686
79 yield from result # pylint: disable=used-before-assignment
80
81 return inner
82
83
84def inference_tip(
85 infer_function: InferFn[_NodesT], raise_on_overwrite: bool = False
86) -> TransformFn[_NodesT]:
87 """Given an instance specific inference function, return a function to be
88 given to AstroidManager().register_transform to set this inference function.
89
90 :param bool raise_on_overwrite: Raise an `InferenceOverwriteError`
91 if the inference tip will overwrite another. Used for debugging
92
93 Typical usage
94
95 .. sourcecode:: python
96
97 AstroidManager().register_transform(Call, inference_tip(infer_named_tuple),
98 predicate)
99
100 .. Note::
101
102 Using an inference tip will override
103 any previously set inference tip for the given
104 node. Use a predicate in the transform to prevent
105 excess overwrites.
106 """
107
108 def transform(
109 node: _NodesT, infer_function: InferFn[_NodesT] = infer_function
110 ) -> _NodesT:
111 if (
112 raise_on_overwrite
113 and node._explicit_inference is not None
114 and node._explicit_inference is not infer_function
115 ):
116 raise InferenceOverwriteError(
117 f"Inference already set to {infer_function}. "
118 f"Trying to overwrite with {node._explicit_inference} for {node}"
119 )
120 node._explicit_inference = _inference_tip_cached(infer_function)
121 return node
122
123 return transform