diffcone 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.
- diffcone/__init__.py +13 -0
- diffcone/cache.py +528 -0
- diffcone/check.py +399 -0
- diffcone/classify.py +255 -0
- diffcone/cli.py +886 -0
- diffcone/collect.py +846 -0
- diffcone/cython.py +757 -0
- diffcone/declarations.py +111 -0
- diffcone/discovery/__init__.py +121 -0
- diffcone/discovery/asv_static.py +494 -0
- diffcone/discovery/common.py +130 -0
- diffcone/discovery/pytest_static.py +2680 -0
- diffcone/evidence.py +738 -0
- diffcone/evidence_plan.py +1487 -0
- diffcone/execution.py +1681 -0
- diffcone/indexer/__init__.py +48 -0
- diffcone/indexer/core.py +286 -0
- diffcone/indexer/definitions.py +292 -0
- diffcone/indexer/dynamics.py +406 -0
- diffcone/indexer/facts.py +373 -0
- diffcone/indexer/literals.py +388 -0
- diffcone/indexer/references.py +895 -0
- diffcone/indexer/resolver.py +1001 -0
- diffcone/indexer/scopes.py +276 -0
- diffcone/indexer/state.py +83 -0
- diffcone/indexer/symbols.py +441 -0
- diffcone/indexer/syntax.py +173 -0
- diffcone/manifest.py +166 -0
- diffcone/model.py +191 -0
- diffcone/planner.py +1453 -0
- diffcone/report.py +256 -0
- diffcone/selection.py +133 -0
- diffcone/snapshot.py +739 -0
- diffcone/testing.py +200 -0
- diffcone-0.1.0.dist-info/METADATA +133 -0
- diffcone-0.1.0.dist-info/RECORD +39 -0
- diffcone-0.1.0.dist-info/WHEEL +4 -0
- diffcone-0.1.0.dist-info/entry_points.txt +3 -0
- diffcone-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""After pass 2: dynamic references whose name or receiver comes from a
|
|
2
|
+
parameter or an instance attribute, bounded by what every call site or
|
|
3
|
+
``__init__`` passes."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from diffcone.indexer.definitions import _ATTRIBUTE_HOOKS
|
|
11
|
+
from diffcone.indexer.facts import _AttrWrite, _ParamDynamic
|
|
12
|
+
from diffcone.indexer.resolver import Resolver
|
|
13
|
+
from diffcone.indexer.scopes import Node, Resolved, Unresolved
|
|
14
|
+
from diffcone.indexer.syntax import IMPORT_ATTRIBUTION_DEPTH
|
|
15
|
+
from diffcone.model import (
|
|
16
|
+
CLASS,
|
|
17
|
+
REFERENCES,
|
|
18
|
+
UNRESOLVED_ATTRIBUTE,
|
|
19
|
+
UNRESOLVED_DYNAMIC,
|
|
20
|
+
Edge,
|
|
21
|
+
UnresolvedReference,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DynamicBounds(Resolver):
|
|
26
|
+
"""Bounds on dynamic references, after pass 2."""
|
|
27
|
+
|
|
28
|
+
def _resolve_param_dynamics(self) -> None:
|
|
29
|
+
"""Expand ``getattr(x, p)`` / ``import_module(p)`` where ``p`` is a
|
|
30
|
+
parameter or an instance attribute, using the literal strings every
|
|
31
|
+
resolved call site passes (for an attribute: what ``__init__`` binds
|
|
32
|
+
it to, see _attribute_writes). A function that escapes (used as a
|
|
33
|
+
value, or whose name occurs as an unresolved reference so callers may
|
|
34
|
+
be unknown) or has an unbounded call site stays dynamic.
|
|
35
|
+
|
|
36
|
+
Expanding a ``getattr`` can itself make a function escape (its value
|
|
37
|
+
is used) or record a new name-bounded reference (a name that cannot
|
|
38
|
+
be resolved on its receiver), either of which can unbound another
|
|
39
|
+
expansion, so the candidates are recomputed until both the escape
|
|
40
|
+
set and the unresolved names are stable."""
|
|
41
|
+
unresolved_names = {
|
|
42
|
+
u.name for u in self.index.unresolved if u.name and not u.detail.startswith("super().")
|
|
43
|
+
}
|
|
44
|
+
# ``super().m`` that did not resolve in class K can still only reach
|
|
45
|
+
# an ``m`` after K in the MRO of K or of a subclass of K.
|
|
46
|
+
self._super_misses.clear()
|
|
47
|
+
for u in self.index.unresolved:
|
|
48
|
+
if u.name and u.detail.startswith("super()."):
|
|
49
|
+
source = self.index.symbols.get(u.symbol)
|
|
50
|
+
while source is not None and source.kind != CLASS:
|
|
51
|
+
source = self.index.symbols.get(source.container or "")
|
|
52
|
+
if source is None:
|
|
53
|
+
unresolved_names.add(u.name)
|
|
54
|
+
else:
|
|
55
|
+
self._super_misses[u.name].add(source.id)
|
|
56
|
+
writes: dict[tuple[str, str], list[_AttrWrite]] = defaultdict(list)
|
|
57
|
+
for w in self.out.attr_writes:
|
|
58
|
+
writes[(w.cls, w.attr)].append(w)
|
|
59
|
+
while True:
|
|
60
|
+
escapes = set(self.out.escapes)
|
|
61
|
+
names = set(unresolved_names)
|
|
62
|
+
planned: list[tuple[_ParamDynamic, list[str] | None]] = []
|
|
63
|
+
for pd in self.out.param_dynamics:
|
|
64
|
+
if pd.self_class:
|
|
65
|
+
values = self._attribute_strings(
|
|
66
|
+
pd.self_class, pd.param, writes, unresolved_names
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
values = self._param_values(pd.function, pd.param, unresolved_names)
|
|
70
|
+
planned.append((pd, values))
|
|
71
|
+
for pd, values in planned:
|
|
72
|
+
if values is None or pd.kind != "getattr":
|
|
73
|
+
continue
|
|
74
|
+
for name in dict.fromkeys(values):
|
|
75
|
+
if pd.base is None:
|
|
76
|
+
unresolved_names.add(name)
|
|
77
|
+
continue
|
|
78
|
+
node, rest = self.resolve_chain_names(pd.base + [name], pd.scope)
|
|
79
|
+
if isinstance(node, Resolved) and not node.detail:
|
|
80
|
+
self.escape(node)
|
|
81
|
+
elif isinstance(node, Unresolved) and node.name:
|
|
82
|
+
unresolved_names.add(node.name)
|
|
83
|
+
unresolved_names.update(rest)
|
|
84
|
+
if self.out.escapes == escapes and unresolved_names == names:
|
|
85
|
+
break
|
|
86
|
+
for pd, values in planned:
|
|
87
|
+
if pd.kind == "import" and self._import_per_caller(pd):
|
|
88
|
+
continue
|
|
89
|
+
if values is None:
|
|
90
|
+
# The name is unbounded. If the *receiver* is one the call
|
|
91
|
+
# sites name, the read is still bounded: it can only be an
|
|
92
|
+
# attribute of those classes, so depend on their members
|
|
93
|
+
# rather than on everything (see _receiver_classes).
|
|
94
|
+
classes = self._receiver_classes(pd, writes) if pd.kind == "getattr" else None
|
|
95
|
+
if classes:
|
|
96
|
+
for member in sorted(self._class_members(classes)):
|
|
97
|
+
self.out.edges.add(
|
|
98
|
+
Edge(pd.function, member, REFERENCES, "attribute read dynamically")
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
self.out.unresolved.add(
|
|
102
|
+
UnresolvedReference(pd.function, UNRESOLVED_DYNAMIC, "", pd.detail)
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
for name in dict.fromkeys(values):
|
|
106
|
+
if pd.kind == "import":
|
|
107
|
+
if name.startswith("."):
|
|
108
|
+
self.out.unresolved.add(
|
|
109
|
+
UnresolvedReference(pd.function, UNRESOLVED_DYNAMIC, "", pd.detail)
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
self._module_import_edge(pd.function, name)
|
|
113
|
+
elif pd.base is None:
|
|
114
|
+
self.out.unresolved.add(
|
|
115
|
+
UnresolvedReference(
|
|
116
|
+
pd.function, UNRESOLVED_ATTRIBUTE, name, f"getattr(..., {name!r})"
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
chain = ".".join(pd.base + [name])
|
|
121
|
+
node, rest = self.resolve_chain_names(pd.base + [name], pd.scope)
|
|
122
|
+
self._record(pd.function, node, chain=chain)
|
|
123
|
+
for extra in rest:
|
|
124
|
+
self.out.unresolved.add(
|
|
125
|
+
UnresolvedReference(pd.function, UNRESOLVED_ATTRIBUTE, extra, chain)
|
|
126
|
+
)
|
|
127
|
+
for ref in self.out.attr_refs:
|
|
128
|
+
bound = self._attribute_writes(ref.cls, ref.attr, writes, unresolved_names)
|
|
129
|
+
if bound is None or any(binding[0] != "symbol" for _, binding in bound):
|
|
130
|
+
continue
|
|
131
|
+
for _, binding in bound:
|
|
132
|
+
node: Node = Resolved(binding[1])
|
|
133
|
+
for attr in ref.rest:
|
|
134
|
+
node = self._step(node, attr)
|
|
135
|
+
if isinstance(node, Resolved) and not ref.rest and node.symbol != ref.source:
|
|
136
|
+
self.out.edges.add(
|
|
137
|
+
Edge(ref.source, node.symbol, REFERENCES, f"self.{ref.attr}")
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
self._record(ref.source, node, chain=ref.chain)
|
|
141
|
+
|
|
142
|
+
def _import_per_caller(self, pd: _ParamDynamic) -> bool:
|
|
143
|
+
"""``import_optional_dependency(name)``: resolve the parameter per call
|
|
144
|
+
site rather than once for the function.
|
|
145
|
+
|
|
146
|
+
A caller that passes a literal can only cause an import of *that*
|
|
147
|
+
module, so the edge belongs to it; one that passes something unbounded
|
|
148
|
+
keeps the dynamic reference, and only what reaches that caller is
|
|
149
|
+
selected conservatively. Attributing the import to the caller rather
|
|
150
|
+
than to the helper that runs it is deliberate: a target reaching the
|
|
151
|
+
caller reaches the import, and the helper's other callers did not name
|
|
152
|
+
that module. Returns False when the callers are not known, which
|
|
153
|
+
leaves the all-or-nothing treatment in place.
|
|
154
|
+
"""
|
|
155
|
+
attributed = self._attributed_imports(pd.function, pd.param, set())
|
|
156
|
+
if attributed is None:
|
|
157
|
+
return False
|
|
158
|
+
for caller, names in attributed:
|
|
159
|
+
if names is None:
|
|
160
|
+
self.out.unresolved.add(
|
|
161
|
+
UnresolvedReference(caller, UNRESOLVED_DYNAMIC, "", pd.detail)
|
|
162
|
+
)
|
|
163
|
+
continue
|
|
164
|
+
for name in dict.fromkeys(names):
|
|
165
|
+
self._module_import_edge(caller, name)
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
def _attributed_imports(
|
|
169
|
+
self, function: str, param: str, seen: set[tuple[str, str]], depth: int = 0
|
|
170
|
+
) -> list[tuple[str, tuple[str, ...] | None]] | None:
|
|
171
|
+
"""Per call site of ``function``, who imports what through ``param``.
|
|
172
|
+
|
|
173
|
+
A site that passes its own parameter answers one level further out --
|
|
174
|
+
pandas' ``skip_if_no(name)`` hands its parameter to the importer, and
|
|
175
|
+
its own callers name the module -- so the search follows it, with a
|
|
176
|
+
depth cap and a guard against a cycle. None when the callers cannot
|
|
177
|
+
be known at all."""
|
|
178
|
+
if (function, param) in seen or depth > IMPORT_ATTRIBUTION_DEPTH:
|
|
179
|
+
return None
|
|
180
|
+
seen.add((function, param))
|
|
181
|
+
info = self.out.func_params.get(function)
|
|
182
|
+
symbol = self.index.symbols.get(function)
|
|
183
|
+
sites = self.out.call_sites.get(function, [])
|
|
184
|
+
if info is None or symbol is None or not sites:
|
|
185
|
+
return None
|
|
186
|
+
if function in self.out.escapes or self._super_may_reach(symbol):
|
|
187
|
+
return None # it may be called from somewhere unseen
|
|
188
|
+
if not all(site.caller for site in sites):
|
|
189
|
+
return None # an older cache entry, without the caller recorded
|
|
190
|
+
found: list[tuple[str, tuple[str, ...] | None]] = []
|
|
191
|
+
for site in sites:
|
|
192
|
+
names = site.value_for(param, info)
|
|
193
|
+
if names is not None and not any(name.startswith(".") for name in names):
|
|
194
|
+
found.append((site.caller, tuple(names)))
|
|
195
|
+
continue
|
|
196
|
+
outer = site.param_for(param, info) if names is None else None
|
|
197
|
+
deeper = (
|
|
198
|
+
self._attributed_imports(site.caller, outer, seen, depth + 1)
|
|
199
|
+
if outer is not None
|
|
200
|
+
else None
|
|
201
|
+
)
|
|
202
|
+
found.extend(deeper if deeper is not None else [(site.caller, None)])
|
|
203
|
+
return found
|
|
204
|
+
|
|
205
|
+
def _class_members(self, classes: set[str]) -> set[str]:
|
|
206
|
+
"""Every symbol inside those classes and their in-scope subclasses: an
|
|
207
|
+
instance of one may be an instance of the other."""
|
|
208
|
+
family = set(classes)
|
|
209
|
+
for cls in classes:
|
|
210
|
+
family.update(self._descendants.get(cls, ()))
|
|
211
|
+
return {
|
|
212
|
+
symbol
|
|
213
|
+
for symbol in self.index.symbols
|
|
214
|
+
for cls in family
|
|
215
|
+
if symbol.startswith(cls + ".")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def _receiver_classes(
|
|
219
|
+
self, pd: _ParamDynamic, writes: dict[tuple[str, str], list[_AttrWrite]]
|
|
220
|
+
) -> set[str] | None:
|
|
221
|
+
"""The classes the receiver of ``getattr(receiver, <unbounded>)`` may
|
|
222
|
+
be an instance of, or None when nothing says.
|
|
223
|
+
|
|
224
|
+
Two shapes carry the answer. A receiver that is a parameter is
|
|
225
|
+
whatever the call sites pass (``invoke(Provider(), name)``). A
|
|
226
|
+
receiver that is ``self.<attr>`` is what ``__init__`` bound it to,
|
|
227
|
+
which is usually a parameter of its own, so the constructions answer
|
|
228
|
+
instead (structlog's ``getattr(self._logger, method_name)``)."""
|
|
229
|
+
if pd.base is None or pd.self_class:
|
|
230
|
+
return None
|
|
231
|
+
info = self.out.func_params.get(pd.function)
|
|
232
|
+
if info is None:
|
|
233
|
+
return None
|
|
234
|
+
if len(pd.base) == 1 and pd.base[0] in info.positional:
|
|
235
|
+
return self._passed_classes(pd.function, pd.base[0])
|
|
236
|
+
if (
|
|
237
|
+
len(pd.base) == 2
|
|
238
|
+
and pd.scope.self_class
|
|
239
|
+
and pd.base[0] == pd.scope.self_name
|
|
240
|
+
and not pd.scope.self_is_class
|
|
241
|
+
):
|
|
242
|
+
return self._attribute_classes(pd.scope.self_class, pd.base[1], writes)
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def _passed_classes(self, function: str, param: str) -> set[str] | None:
|
|
246
|
+
"""The classes every resolved call site passes for ``param``; None
|
|
247
|
+
when the function may be called from somewhere unseen or a site says
|
|
248
|
+
nothing about what it passes."""
|
|
249
|
+
info = self.out.func_params.get(function)
|
|
250
|
+
symbol = self.index.symbols.get(function)
|
|
251
|
+
sites = self.out.call_sites.get(function, [])
|
|
252
|
+
if (
|
|
253
|
+
info is None
|
|
254
|
+
or symbol is None
|
|
255
|
+
or not sites
|
|
256
|
+
or function in self.out.escapes
|
|
257
|
+
or self._super_may_reach(symbol)
|
|
258
|
+
):
|
|
259
|
+
return None
|
|
260
|
+
found: set[str] = set()
|
|
261
|
+
for site in sites:
|
|
262
|
+
cls = site.class_for(param, info)
|
|
263
|
+
if cls is None:
|
|
264
|
+
return None
|
|
265
|
+
found.add(cls)
|
|
266
|
+
return found or None
|
|
267
|
+
|
|
268
|
+
def _attribute_classes(
|
|
269
|
+
self, class_id: str, attr: str, writes: dict[tuple[str, str], list[_AttrWrite]]
|
|
270
|
+
) -> set[str] | None:
|
|
271
|
+
"""What ``self.<attr>`` holds, as classes: every write must assign a
|
|
272
|
+
class, or a parameter whose constructions all pass one.
|
|
273
|
+
|
|
274
|
+
The guards are this rule's own, not ``_attribute_writes``'s. Both
|
|
275
|
+
refuse when the attribute is written through a receiver whose type is
|
|
276
|
+
unknown, is a class-level name, or the class customises attribute
|
|
277
|
+
access. This one does not refuse merely because the class escapes:
|
|
278
|
+
an unseen subclass lives in code outside the source roots, and what
|
|
279
|
+
such code puts in the attribute comes from there too. A construction
|
|
280
|
+
we *can* see whose argument says nothing still gives up (below), which
|
|
281
|
+
is the case that matters -- a factory inside the project."""
|
|
282
|
+
unbound = self.out.attr_unbound
|
|
283
|
+
if ("", "*") in unbound or ("", attr) in unbound:
|
|
284
|
+
return None
|
|
285
|
+
family: set[str] = set()
|
|
286
|
+
for cid in (class_id, *self._descendants.get(class_id, ())):
|
|
287
|
+
family.update(self._mro(cid))
|
|
288
|
+
bound: list[tuple[_AttrWrite, list[Any]]] = []
|
|
289
|
+
for cid in sorted(family):
|
|
290
|
+
cscope = self.class_scopes.get(cid)
|
|
291
|
+
if cscope is None or cscope.opaque or (cid, "*") in unbound or (cid, attr) in unbound:
|
|
292
|
+
return None
|
|
293
|
+
if attr in cscope.members or attr in cscope.bindings:
|
|
294
|
+
return None
|
|
295
|
+
if _ATTRIBUTE_HOOKS & cscope.members.keys():
|
|
296
|
+
return None
|
|
297
|
+
for w in writes.get((cid, attr), ()):
|
|
298
|
+
if w.binding is None:
|
|
299
|
+
return None
|
|
300
|
+
bound.append((w, w.binding))
|
|
301
|
+
if not bound:
|
|
302
|
+
return None
|
|
303
|
+
found: set[str] = set()
|
|
304
|
+
for w, (kind, value) in bound:
|
|
305
|
+
if kind == "symbol":
|
|
306
|
+
symbol = self.index.symbols.get(value)
|
|
307
|
+
if symbol is None or symbol.kind != CLASS:
|
|
308
|
+
return None
|
|
309
|
+
found.add(value)
|
|
310
|
+
elif kind == "param":
|
|
311
|
+
passed = self._passed_classes(w.method, value)
|
|
312
|
+
if passed is None:
|
|
313
|
+
return None
|
|
314
|
+
found |= passed
|
|
315
|
+
else:
|
|
316
|
+
return None
|
|
317
|
+
return found or None
|
|
318
|
+
|
|
319
|
+
def _param_values(
|
|
320
|
+
self, function: str, param: str, unresolved_names: set[str]
|
|
321
|
+
) -> list[str] | None:
|
|
322
|
+
"""The literal strings every call site of ``function`` passes for
|
|
323
|
+
``param``, or None when some caller may be unseen or unbounded."""
|
|
324
|
+
info = self.out.func_params.get(function)
|
|
325
|
+
symbol = self.index.symbols.get(function)
|
|
326
|
+
sites = self.out.call_sites.get(function, [])
|
|
327
|
+
if (
|
|
328
|
+
info is None
|
|
329
|
+
or symbol is None
|
|
330
|
+
or function in self.out.escapes
|
|
331
|
+
or symbol.name in unresolved_names
|
|
332
|
+
or self._super_may_reach(symbol)
|
|
333
|
+
or not sites
|
|
334
|
+
or self._constructor_escapes(symbol, unresolved_names)
|
|
335
|
+
):
|
|
336
|
+
return None
|
|
337
|
+
values: list[str] = []
|
|
338
|
+
for site in sites:
|
|
339
|
+
found = site.value_for(param, info)
|
|
340
|
+
if found is None:
|
|
341
|
+
return None
|
|
342
|
+
values.extend(found)
|
|
343
|
+
return values
|
|
344
|
+
|
|
345
|
+
def _attribute_writes(
|
|
346
|
+
self,
|
|
347
|
+
class_id: str,
|
|
348
|
+
attr: str,
|
|
349
|
+
writes: dict[tuple[str, str], list[_AttrWrite]],
|
|
350
|
+
unresolved_names: set[str],
|
|
351
|
+
) -> list[tuple[_AttrWrite, list[Any]]] | None:
|
|
352
|
+
"""What ``self.<attr>`` may hold in a method of ``class_id``: its
|
|
353
|
+
bound writes with their bindings, or None when it cannot be bounded. The
|
|
354
|
+
instance may belong to any in-scope subclass, so every class in the
|
|
355
|
+
MRO of the class or of a subclass counts; each must be plain and
|
|
356
|
+
fully in scope, none may define the attribute at class level or
|
|
357
|
+
customise attribute access, and every write must be a bounded
|
|
358
|
+
``__init__`` assignment. A class that escapes, or whose name occurs
|
|
359
|
+
as an unresolved reference, may have subclasses the index cannot
|
|
360
|
+
see (``class S(Base)`` with ``Base = Foo if X else Bar``), whose
|
|
361
|
+
writes are unknown."""
|
|
362
|
+
unbound = self.out.attr_unbound
|
|
363
|
+
if ("", "*") in unbound or ("", attr) in unbound:
|
|
364
|
+
return None
|
|
365
|
+
family: set[str] = set()
|
|
366
|
+
for cid in (class_id, *self._descendants.get(class_id, ())):
|
|
367
|
+
family.update(self._mro(cid))
|
|
368
|
+
bound: list[tuple[_AttrWrite, list[Any]]] = []
|
|
369
|
+
for cid in sorted(family):
|
|
370
|
+
cscope = self.class_scopes[cid]
|
|
371
|
+
if not cscope.plain or cscope.opaque or (cid, "*") in unbound or (cid, attr) in unbound:
|
|
372
|
+
return None
|
|
373
|
+
if cid in self.out.escapes or self.index.symbols[cid].name in unresolved_names:
|
|
374
|
+
return None
|
|
375
|
+
if attr in cscope.members or attr in cscope.bindings:
|
|
376
|
+
return None
|
|
377
|
+
if _ATTRIBUTE_HOOKS & cscope.members.keys():
|
|
378
|
+
return None
|
|
379
|
+
for w in writes.get((cid, attr), ()):
|
|
380
|
+
if w.binding is None:
|
|
381
|
+
return None
|
|
382
|
+
bound.append((w, w.binding))
|
|
383
|
+
return bound or None
|
|
384
|
+
|
|
385
|
+
def _attribute_strings(
|
|
386
|
+
self,
|
|
387
|
+
class_id: str,
|
|
388
|
+
attr: str,
|
|
389
|
+
writes: dict[tuple[str, str], list[_AttrWrite]],
|
|
390
|
+
unresolved_names: set[str],
|
|
391
|
+
) -> list[str] | None:
|
|
392
|
+
bound = self._attribute_writes(class_id, attr, writes, unresolved_names)
|
|
393
|
+
if bound is None:
|
|
394
|
+
return None
|
|
395
|
+
values: list[str] = []
|
|
396
|
+
for w, (kind, value) in bound:
|
|
397
|
+
if kind == "strings":
|
|
398
|
+
values.extend(value)
|
|
399
|
+
elif kind == "param":
|
|
400
|
+
found = self._param_values(w.method, value, unresolved_names)
|
|
401
|
+
if found is None:
|
|
402
|
+
return None
|
|
403
|
+
values.extend(found)
|
|
404
|
+
else:
|
|
405
|
+
return None
|
|
406
|
+
return values
|