apisec-code-bolt 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apisec_code_bolt/__init__.py +42 -0
- apisec_code_bolt/__main__.py +11 -0
- apisec_code_bolt/analysis/__init__.py +96 -0
- apisec_code_bolt/analysis/analyzer.py +2309 -0
- apisec_code_bolt/analysis/binding_tracker.py +341 -0
- apisec_code_bolt/analysis/call_graph.py +1197 -0
- apisec_code_bolt/analysis/call_graph_types.py +332 -0
- apisec_code_bolt/analysis/call_resolver.py +988 -0
- apisec_code_bolt/analysis/capability_tagger.py +322 -0
- apisec_code_bolt/analysis/config_scanner.py +197 -0
- apisec_code_bolt/analysis/data_flow.py +1883 -0
- apisec_code_bolt/analysis/dependency_extractor.py +959 -0
- apisec_code_bolt/analysis/flow_analysis.py +1406 -0
- apisec_code_bolt/analysis/hof_catalog.py +61 -0
- apisec_code_bolt/analysis/integration_detector.py +1399 -0
- apisec_code_bolt/analysis/literal_scanner.py +300 -0
- apisec_code_bolt/analysis/path_normalizer.py +55 -0
- apisec_code_bolt/analysis/read_site_detector.py +310 -0
- apisec_code_bolt/analysis/request_patterns.py +162 -0
- apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
- apisec_code_bolt/analysis/sink_evidence.py +333 -0
- apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
- apisec_code_bolt/cli/__init__.py +5 -0
- apisec_code_bolt/cli/exit_codes.py +17 -0
- apisec_code_bolt/cli/main.py +1069 -0
- apisec_code_bolt/cloud/__init__.py +1 -0
- apisec_code_bolt/cloud/apisec_client.py +118 -0
- apisec_code_bolt/cloud/client.py +255 -0
- apisec_code_bolt/core/__init__.py +75 -0
- apisec_code_bolt/core/config.py +528 -0
- apisec_code_bolt/core/credentials.py +65 -0
- apisec_code_bolt/core/discovery.py +433 -0
- apisec_code_bolt/core/log_format.py +115 -0
- apisec_code_bolt/core/manifest.py +1009 -0
- apisec_code_bolt/core/repo.py +280 -0
- apisec_code_bolt/core/state.py +59 -0
- apisec_code_bolt/core/telemetry.py +451 -0
- apisec_code_bolt/core/types.py +587 -0
- apisec_code_bolt/fingerprinting/__init__.py +1 -0
- apisec_code_bolt/frameworks/__init__.py +29 -0
- apisec_code_bolt/frameworks/_jwt_common.py +50 -0
- apisec_code_bolt/frameworks/auth_helpers.py +437 -0
- apisec_code_bolt/frameworks/base.py +608 -0
- apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
- apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
- apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
- apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
- apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
- apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
- apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
- apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
- apisec_code_bolt/frameworks/java/__init__.py +6 -0
- apisec_code_bolt/frameworks/java/_annotations.py +167 -0
- apisec_code_bolt/frameworks/java/_constraints.py +128 -0
- apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
- apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
- apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
- apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
- apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
- apisec_code_bolt/frameworks/js/__init__.py +8 -0
- apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
- apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
- apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
- apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
- apisec_code_bolt/frameworks/python/__init__.py +19 -0
- apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
- apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
- apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
- apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
- apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
- apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
- apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
- apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
- apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
- apisec_code_bolt/parsing/__init__.py +62 -0
- apisec_code_bolt/parsing/base.py +554 -0
- apisec_code_bolt/parsing/csharp/__init__.py +5 -0
- apisec_code_bolt/parsing/csharp/language_services.py +203 -0
- apisec_code_bolt/parsing/csharp/literals.py +72 -0
- apisec_code_bolt/parsing/csharp/parser.py +1158 -0
- apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
- apisec_code_bolt/parsing/js/__init__.py +5 -0
- apisec_code_bolt/parsing/js/language_services.py +118 -0
- apisec_code_bolt/parsing/js/parser.py +622 -0
- apisec_code_bolt/parsing/jvm/__init__.py +7 -0
- apisec_code_bolt/parsing/jvm/language_services.py +270 -0
- apisec_code_bolt/parsing/jvm/parser.py +774 -0
- apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
- apisec_code_bolt/parsing/python/__init__.py +150 -0
- apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
- apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
- apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
- apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
- apisec_code_bolt/parsing/python/expression_utils.py +221 -0
- apisec_code_bolt/parsing/python/extraction_types.py +271 -0
- apisec_code_bolt/parsing/python/language_services.py +487 -0
- apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
- apisec_code_bolt/parsing/python/parser.py +719 -0
- apisec_code_bolt/parsing/python/path_resolver.py +576 -0
- apisec_code_bolt/parsing/python/router_registry.py +806 -0
- apisec_code_bolt/parsing/python/type_resolver.py +730 -0
- apisec_code_bolt/parsing/python/visitors.py +1544 -0
- apisec_code_bolt/parsing/services.py +544 -0
- apisec_code_bolt/query/__init__.py +1 -0
- apisec_code_bolt/query/ast_cache.py +182 -0
- apisec_code_bolt/query/executor.py +283 -0
- apisec_code_bolt/query/handlers.py +832 -0
- apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
- apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
- apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
- apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyCG-style variable-to-type binding tracker with fixed-point propagation.
|
|
3
|
+
|
|
4
|
+
Tracks what types each variable could be bound to so that method calls on
|
|
5
|
+
dynamically-typed receivers can be resolved to concrete targets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .call_graph_types import TypeBinding
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BindingTracker:
|
|
16
|
+
"""
|
|
17
|
+
Tracks variable-to-type bindings for dynamic dispatch resolution.
|
|
18
|
+
|
|
19
|
+
This implements the core of the PyCG algorithm: we track what types
|
|
20
|
+
each variable could be bound to, so we can resolve method calls.
|
|
21
|
+
|
|
22
|
+
KEY IMPROVEMENT: Uses fixed-point iteration to propagate bindings
|
|
23
|
+
transitively (x = y means x has all types that y has).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
# Per-function bindings: (func_qname, var_name) -> TypeBinding
|
|
28
|
+
self._local_bindings: dict[tuple[str, str], TypeBinding] = {}
|
|
29
|
+
|
|
30
|
+
# Module-level bindings: (file_path, var_name) -> TypeBinding
|
|
31
|
+
self._module_bindings: dict[tuple[Path, str], TypeBinding] = {}
|
|
32
|
+
|
|
33
|
+
# Class attribute bindings: (class_qname, attr_name) -> TypeBinding
|
|
34
|
+
self._class_bindings: dict[tuple[str, str], TypeBinding] = {}
|
|
35
|
+
|
|
36
|
+
# Return value bindings: func_qname -> possible return types/callables
|
|
37
|
+
self._return_bindings: dict[str, TypeBinding] = {}
|
|
38
|
+
|
|
39
|
+
# Protocol/ABC implementations: protocol_name -> [implementing_classes]
|
|
40
|
+
self._protocol_implementations: dict[str, set[str]] = {}
|
|
41
|
+
|
|
42
|
+
# Propagation tracking
|
|
43
|
+
self._propagation_complete = False
|
|
44
|
+
|
|
45
|
+
def add_assignment(
|
|
46
|
+
self,
|
|
47
|
+
variable: str,
|
|
48
|
+
assigned_type: str | None,
|
|
49
|
+
file_path: Path,
|
|
50
|
+
line: int,
|
|
51
|
+
scope_function: str | None = None,
|
|
52
|
+
scope_class: str | None = None,
|
|
53
|
+
source_variable: str | None = None,
|
|
54
|
+
is_callable: bool = False,
|
|
55
|
+
callable_target: str | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Record an assignment that binds a variable to a type."""
|
|
58
|
+
self._propagation_complete = False
|
|
59
|
+
location = (file_path, line)
|
|
60
|
+
|
|
61
|
+
if scope_function:
|
|
62
|
+
key = (scope_function, variable)
|
|
63
|
+
if key not in self._local_bindings:
|
|
64
|
+
self._local_bindings[key] = TypeBinding(variable=variable)
|
|
65
|
+
binding = self._local_bindings[key]
|
|
66
|
+
elif scope_class:
|
|
67
|
+
key = (scope_class, variable)
|
|
68
|
+
if key not in self._class_bindings:
|
|
69
|
+
self._class_bindings[key] = TypeBinding(variable=variable)
|
|
70
|
+
binding = self._class_bindings[key]
|
|
71
|
+
else:
|
|
72
|
+
key = (file_path, variable)
|
|
73
|
+
if key not in self._module_bindings:
|
|
74
|
+
self._module_bindings[key] = TypeBinding(variable=variable)
|
|
75
|
+
binding = self._module_bindings[key]
|
|
76
|
+
|
|
77
|
+
if assigned_type:
|
|
78
|
+
binding.possible_types.add(assigned_type)
|
|
79
|
+
if source_variable:
|
|
80
|
+
binding.source_variables.add(source_variable)
|
|
81
|
+
if is_callable:
|
|
82
|
+
binding.holds_callable = True
|
|
83
|
+
if callable_target:
|
|
84
|
+
binding.callable_targets.add(callable_target)
|
|
85
|
+
|
|
86
|
+
binding.assignment_locations.append(location)
|
|
87
|
+
|
|
88
|
+
def add_self_binding(
|
|
89
|
+
self,
|
|
90
|
+
function_qname: str,
|
|
91
|
+
class_qname: str,
|
|
92
|
+
param_name: str = "self",
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Bind self/cls parameter to its enclosing class."""
|
|
95
|
+
self._propagation_complete = False
|
|
96
|
+
key = (function_qname, param_name)
|
|
97
|
+
binding = TypeBinding(
|
|
98
|
+
variable=param_name,
|
|
99
|
+
is_self=param_name == "self",
|
|
100
|
+
is_cls=param_name == "cls",
|
|
101
|
+
enclosing_class=class_qname,
|
|
102
|
+
)
|
|
103
|
+
binding.possible_types.add(class_qname)
|
|
104
|
+
self._local_bindings[key] = binding
|
|
105
|
+
|
|
106
|
+
def add_parameter(
|
|
107
|
+
self,
|
|
108
|
+
param_name: str,
|
|
109
|
+
function_qname: str,
|
|
110
|
+
type_annotation: str | None = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Record a function parameter (could be any compatible type)."""
|
|
113
|
+
self._propagation_complete = False
|
|
114
|
+
key = (function_qname, param_name)
|
|
115
|
+
binding = TypeBinding(
|
|
116
|
+
variable=param_name,
|
|
117
|
+
is_parameter=True,
|
|
118
|
+
parameter_type_annotation=type_annotation,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# If we have a type annotation, that's the possible type
|
|
122
|
+
if type_annotation:
|
|
123
|
+
binding.possible_types.add(type_annotation)
|
|
124
|
+
# Check if it's a protocol/ABC
|
|
125
|
+
if type_annotation in self._protocol_implementations:
|
|
126
|
+
binding.possible_types.update(self._protocol_implementations[type_annotation])
|
|
127
|
+
|
|
128
|
+
self._local_bindings[key] = binding
|
|
129
|
+
|
|
130
|
+
def add_import(
|
|
131
|
+
self,
|
|
132
|
+
local_name: str,
|
|
133
|
+
import_source: str,
|
|
134
|
+
file_path: Path,
|
|
135
|
+
line: int,
|
|
136
|
+
is_star: bool = False,
|
|
137
|
+
star_exports: list[str] | None = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Record an import binding."""
|
|
140
|
+
self._propagation_complete = False
|
|
141
|
+
key = (file_path, local_name)
|
|
142
|
+
binding = TypeBinding(
|
|
143
|
+
variable=local_name,
|
|
144
|
+
is_import=True,
|
|
145
|
+
import_source=import_source,
|
|
146
|
+
)
|
|
147
|
+
binding.assignment_locations.append((file_path, line))
|
|
148
|
+
self._module_bindings[key] = binding
|
|
149
|
+
|
|
150
|
+
# Handle star imports
|
|
151
|
+
if is_star and star_exports:
|
|
152
|
+
for name in star_exports:
|
|
153
|
+
star_key = (file_path, name)
|
|
154
|
+
star_binding = TypeBinding(
|
|
155
|
+
variable=name,
|
|
156
|
+
is_import=True,
|
|
157
|
+
import_source=f"{import_source}.{name}",
|
|
158
|
+
)
|
|
159
|
+
self._module_bindings[star_key] = star_binding
|
|
160
|
+
|
|
161
|
+
def add_return_binding(
|
|
162
|
+
self,
|
|
163
|
+
function_qname: str,
|
|
164
|
+
return_type: str | None = None,
|
|
165
|
+
returns_callable: bool = False,
|
|
166
|
+
callable_target: str | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Track what a function returns."""
|
|
169
|
+
self._propagation_complete = False
|
|
170
|
+
if function_qname not in self._return_bindings:
|
|
171
|
+
self._return_bindings[function_qname] = TypeBinding(
|
|
172
|
+
variable=f"<return:{function_qname}>"
|
|
173
|
+
)
|
|
174
|
+
binding = self._return_bindings[function_qname]
|
|
175
|
+
|
|
176
|
+
if return_type:
|
|
177
|
+
binding.possible_types.add(return_type)
|
|
178
|
+
if returns_callable:
|
|
179
|
+
binding.holds_callable = True
|
|
180
|
+
if callable_target:
|
|
181
|
+
binding.callable_targets.add(callable_target)
|
|
182
|
+
|
|
183
|
+
def add_protocol_implementation(
|
|
184
|
+
self,
|
|
185
|
+
protocol: str,
|
|
186
|
+
implementer: str,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Record that a class implements a protocol/ABC."""
|
|
189
|
+
self._propagation_complete = False
|
|
190
|
+
if protocol not in self._protocol_implementations:
|
|
191
|
+
self._protocol_implementations[protocol] = set()
|
|
192
|
+
self._protocol_implementations[protocol].add(implementer)
|
|
193
|
+
|
|
194
|
+
def propagate(self, max_iterations: int = 100) -> int:
|
|
195
|
+
"""
|
|
196
|
+
Perform fixed-point iteration to propagate bindings transitively.
|
|
197
|
+
|
|
198
|
+
If x = y, then x should have all types that y has.
|
|
199
|
+
|
|
200
|
+
Returns: Number of iterations performed
|
|
201
|
+
"""
|
|
202
|
+
if self._propagation_complete:
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
iterations = 0
|
|
206
|
+
changed = True
|
|
207
|
+
|
|
208
|
+
while changed and iterations < max_iterations:
|
|
209
|
+
changed = False
|
|
210
|
+
iterations += 1
|
|
211
|
+
|
|
212
|
+
# Propagate in local bindings
|
|
213
|
+
for key, binding in list(self._local_bindings.items()):
|
|
214
|
+
func_qname, var_name = key
|
|
215
|
+
for source_var in list(binding.source_variables):
|
|
216
|
+
source_key = (func_qname, source_var)
|
|
217
|
+
if source_key in self._local_bindings:
|
|
218
|
+
source_binding = self._local_bindings[source_key]
|
|
219
|
+
old_size = len(binding.possible_types)
|
|
220
|
+
binding.possible_types.update(source_binding.possible_types)
|
|
221
|
+
binding.callable_targets.update(source_binding.callable_targets)
|
|
222
|
+
if source_binding.holds_callable:
|
|
223
|
+
binding.holds_callable = True
|
|
224
|
+
if len(binding.possible_types) > old_size:
|
|
225
|
+
changed = True
|
|
226
|
+
|
|
227
|
+
# Propagate in module bindings
|
|
228
|
+
for key, binding in list(self._module_bindings.items()):
|
|
229
|
+
file_path, var_name = key
|
|
230
|
+
for source_var in list(binding.source_variables):
|
|
231
|
+
source_key = (file_path, source_var)
|
|
232
|
+
if source_key in self._module_bindings:
|
|
233
|
+
source_binding = self._module_bindings[source_key]
|
|
234
|
+
old_size = len(binding.possible_types)
|
|
235
|
+
binding.possible_types.update(source_binding.possible_types)
|
|
236
|
+
binding.callable_targets.update(source_binding.callable_targets)
|
|
237
|
+
if source_binding.holds_callable:
|
|
238
|
+
binding.holds_callable = True
|
|
239
|
+
if len(binding.possible_types) > old_size:
|
|
240
|
+
changed = True
|
|
241
|
+
|
|
242
|
+
# Propagate return values for call assignments
|
|
243
|
+
# If x = func(), x should have the return types of func
|
|
244
|
+
for _key, binding in list(self._local_bindings.items()):
|
|
245
|
+
for callable_name in list(binding.source_variables):
|
|
246
|
+
if callable_name in self._return_bindings:
|
|
247
|
+
return_binding = self._return_bindings[callable_name]
|
|
248
|
+
old_size = len(binding.possible_types)
|
|
249
|
+
binding.possible_types.update(return_binding.possible_types)
|
|
250
|
+
binding.callable_targets.update(return_binding.callable_targets)
|
|
251
|
+
if return_binding.holds_callable:
|
|
252
|
+
binding.holds_callable = True
|
|
253
|
+
if len(binding.possible_types) > old_size:
|
|
254
|
+
changed = True
|
|
255
|
+
|
|
256
|
+
self._propagation_complete = True
|
|
257
|
+
return iterations
|
|
258
|
+
|
|
259
|
+
def get_possible_types(
|
|
260
|
+
self,
|
|
261
|
+
variable: str,
|
|
262
|
+
file_path: Path,
|
|
263
|
+
scope_function: str | None = None,
|
|
264
|
+
) -> set[str]:
|
|
265
|
+
"""Get possible types for a variable in a given scope."""
|
|
266
|
+
if not self._propagation_complete:
|
|
267
|
+
self.propagate()
|
|
268
|
+
|
|
269
|
+
types: set[str] = set()
|
|
270
|
+
|
|
271
|
+
# Check local scope first
|
|
272
|
+
if scope_function:
|
|
273
|
+
key = (scope_function, variable)
|
|
274
|
+
if key in self._local_bindings:
|
|
275
|
+
types.update(self._local_bindings[key].possible_types)
|
|
276
|
+
|
|
277
|
+
# Check module scope
|
|
278
|
+
key = (file_path, variable)
|
|
279
|
+
if key in self._module_bindings:
|
|
280
|
+
binding = self._module_bindings[key]
|
|
281
|
+
types.update(binding.possible_types)
|
|
282
|
+
if binding.is_import and binding.import_source:
|
|
283
|
+
types.add(binding.import_source)
|
|
284
|
+
|
|
285
|
+
return types
|
|
286
|
+
|
|
287
|
+
def get_callable_targets(
|
|
288
|
+
self,
|
|
289
|
+
variable: str,
|
|
290
|
+
file_path: Path,
|
|
291
|
+
scope_function: str | None = None,
|
|
292
|
+
) -> set[str]:
|
|
293
|
+
"""Get callable targets for a variable that holds a function."""
|
|
294
|
+
if not self._propagation_complete:
|
|
295
|
+
self.propagate()
|
|
296
|
+
|
|
297
|
+
targets: set[str] = set()
|
|
298
|
+
|
|
299
|
+
if scope_function:
|
|
300
|
+
key = (scope_function, variable)
|
|
301
|
+
if key in self._local_bindings:
|
|
302
|
+
binding = self._local_bindings[key]
|
|
303
|
+
targets.update(binding.callable_targets)
|
|
304
|
+
|
|
305
|
+
key = (file_path, variable)
|
|
306
|
+
if key in self._module_bindings:
|
|
307
|
+
binding = self._module_bindings[key]
|
|
308
|
+
targets.update(binding.callable_targets)
|
|
309
|
+
|
|
310
|
+
return targets
|
|
311
|
+
|
|
312
|
+
def get_binding(
|
|
313
|
+
self,
|
|
314
|
+
variable: str,
|
|
315
|
+
file_path: Path,
|
|
316
|
+
scope_function: str | None = None,
|
|
317
|
+
) -> TypeBinding | None:
|
|
318
|
+
"""Get the binding for a variable."""
|
|
319
|
+
if scope_function:
|
|
320
|
+
key = (scope_function, variable)
|
|
321
|
+
if key in self._local_bindings:
|
|
322
|
+
return self._local_bindings[key]
|
|
323
|
+
|
|
324
|
+
key = (file_path, variable)
|
|
325
|
+
return self._module_bindings.get(key)
|
|
326
|
+
|
|
327
|
+
def get_return_types(self, function_qname: str) -> set[str]:
|
|
328
|
+
"""Get the return types of a function."""
|
|
329
|
+
if function_qname in self._return_bindings:
|
|
330
|
+
return self._return_bindings[function_qname].possible_types
|
|
331
|
+
return set()
|
|
332
|
+
|
|
333
|
+
def get_returned_callables(self, function_qname: str) -> set[str]:
|
|
334
|
+
"""Get callables returned by a function."""
|
|
335
|
+
if function_qname in self._return_bindings:
|
|
336
|
+
return self._return_bindings[function_qname].callable_targets
|
|
337
|
+
return set()
|
|
338
|
+
|
|
339
|
+
def get_protocol_implementers(self, protocol: str) -> set[str]:
|
|
340
|
+
"""Get all classes that implement a protocol/ABC."""
|
|
341
|
+
return self._protocol_implementations.get(protocol, set())
|