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,388 @@
|
|
|
1
|
+
"""What literal strings a name or expression can hold (``getattr(x, NAME)``
|
|
2
|
+
with ``NAME`` from a literal table), and which names a scope binds or
|
|
3
|
+
mutates in place."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from diffcone.indexer.syntax import DEF_NODES, FUNC_NODES
|
|
11
|
+
|
|
12
|
+
# Synthetic key in a literal table: what *indexing* the literal bound to NAME
|
|
13
|
+
# yields -- a dict display's values, a sequence display's elements -- kept
|
|
14
|
+
# beside what iterating it yields (stored under NAME itself). No identifier
|
|
15
|
+
# contains a bracket, so the two can never collide.
|
|
16
|
+
INDEXED = "[]"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _constant_string(expr: ast.expr) -> str | None:
|
|
20
|
+
"""The string ``expr`` is, when it is written out in the source."""
|
|
21
|
+
if isinstance(expr, ast.Constant) and isinstance(expr.value, str):
|
|
22
|
+
return expr.value
|
|
23
|
+
if isinstance(expr, ast.JoinedStr):
|
|
24
|
+
parts = [v.value for v in expr.values if isinstance(v, ast.Constant)]
|
|
25
|
+
if len(parts) == len(expr.values):
|
|
26
|
+
return "".join(str(part) for part in parts)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _literal_strings(expr: ast.expr) -> tuple[str, ...] | None:
|
|
31
|
+
constant = _constant_string(expr)
|
|
32
|
+
if constant is not None:
|
|
33
|
+
return (constant,)
|
|
34
|
+
if isinstance(expr, ast.Dict):
|
|
35
|
+
# Iterating a dict yields its keys; only all-literal string keys count.
|
|
36
|
+
keys: list[str] = []
|
|
37
|
+
for key in expr.keys:
|
|
38
|
+
if key is None or (name := _constant_string(key)) is None:
|
|
39
|
+
return None
|
|
40
|
+
keys.append(name)
|
|
41
|
+
return tuple(keys)
|
|
42
|
+
if isinstance(expr, (ast.Tuple, ast.List, ast.Set)):
|
|
43
|
+
out: list[str] = []
|
|
44
|
+
for elt in expr.elts:
|
|
45
|
+
values = _literal_strings(elt)
|
|
46
|
+
if values is None:
|
|
47
|
+
return None
|
|
48
|
+
out.extend(values)
|
|
49
|
+
return tuple(out)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _string_candidates(
|
|
54
|
+
expr: ast.expr,
|
|
55
|
+
local_literals: dict[str, tuple[str, ...] | None],
|
|
56
|
+
module_literals: dict[str, tuple[str, ...] | None],
|
|
57
|
+
) -> tuple[str, ...] | None:
|
|
58
|
+
direct = _literal_strings(expr)
|
|
59
|
+
if direct is not None:
|
|
60
|
+
return direct
|
|
61
|
+
if isinstance(expr, ast.Name):
|
|
62
|
+
if expr.id in local_literals:
|
|
63
|
+
return local_literals[expr.id]
|
|
64
|
+
return module_literals.get(expr.id)
|
|
65
|
+
# ``D.keys()`` over a literal-keyed dict yields its keys. (``D.items()``
|
|
66
|
+
# yields pairs and is handled only for ``for key, value in`` targets.)
|
|
67
|
+
if (receiver := _dict_method_receiver(expr, "keys")) is not None:
|
|
68
|
+
return _string_candidates(receiver, local_literals, module_literals)
|
|
69
|
+
# ``D.values()`` over a dict literal with string values yields them.
|
|
70
|
+
if (receiver := _dict_method_receiver(expr, "values")) is not None:
|
|
71
|
+
return _indexed(receiver, local_literals, module_literals)
|
|
72
|
+
# ``D[key]`` / ``L[i]``: one of the literal's values (a slice is not one).
|
|
73
|
+
if isinstance(expr, ast.Subscript) and not isinstance(expr.slice, ast.Slice):
|
|
74
|
+
return _indexed(expr.value, local_literals, module_literals)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _constant_strings(exprs: Sequence[ast.expr | None]) -> tuple[str, ...] | None:
|
|
79
|
+
"""The strings ``exprs`` are, or None when any of them is not written out
|
|
80
|
+
as one (a ``*``/``**`` unpacking, whose element is None, included)."""
|
|
81
|
+
out: list[str] = []
|
|
82
|
+
for expr in exprs:
|
|
83
|
+
if expr is None or (string := _constant_string(expr)) is None:
|
|
84
|
+
return None
|
|
85
|
+
out.append(string)
|
|
86
|
+
return tuple(out)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _indexed(
|
|
90
|
+
expr: ast.expr,
|
|
91
|
+
local_literals: dict[str, tuple[str, ...] | None],
|
|
92
|
+
module_literals: dict[str, tuple[str, ...] | None],
|
|
93
|
+
) -> tuple[str, ...] | None:
|
|
94
|
+
"""Every string indexing ``expr`` may yield, or None when unbounded: a
|
|
95
|
+
dict display's values, a sequence display's elements. A name is looked up
|
|
96
|
+
under its synthetic ``INDEXED`` key, which every binding of that name
|
|
97
|
+
writes, so a local shadowing a module-level table is unbounded rather
|
|
98
|
+
than that table's contents."""
|
|
99
|
+
if isinstance(expr, ast.Dict):
|
|
100
|
+
# ``{**other}`` has a None key and hides what it contributes.
|
|
101
|
+
if any(key is None for key in expr.keys):
|
|
102
|
+
return None
|
|
103
|
+
return _constant_strings(expr.values)
|
|
104
|
+
if isinstance(expr, (ast.Tuple, ast.List, ast.Set)):
|
|
105
|
+
return _constant_strings(expr.elts)
|
|
106
|
+
if isinstance(expr, ast.Name):
|
|
107
|
+
key = expr.id + INDEXED
|
|
108
|
+
if key in local_literals:
|
|
109
|
+
return local_literals[key]
|
|
110
|
+
return module_literals.get(key)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _dict_method_receiver(expr: ast.expr, method: str) -> ast.expr | None:
|
|
115
|
+
"""``D`` when ``expr`` is ``D.<method>()`` with no arguments."""
|
|
116
|
+
if (
|
|
117
|
+
isinstance(expr, ast.Call)
|
|
118
|
+
and not expr.args
|
|
119
|
+
and not expr.keywords
|
|
120
|
+
and isinstance(expr.func, ast.Attribute)
|
|
121
|
+
and expr.func.attr == method
|
|
122
|
+
):
|
|
123
|
+
return expr.func.value
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _collect_literal_bindings(
|
|
128
|
+
node: ast.AST, module_literals: dict[str, tuple[str, ...] | None]
|
|
129
|
+
) -> dict[str, tuple[str, ...] | None]:
|
|
130
|
+
"""Names bound in ``node``'s scope to string literals, tuples of them, or
|
|
131
|
+
loop variables over such tuples. A name with any other binding maps to
|
|
132
|
+
None (unbounded); nested scopes are not entered. A name bound to a dict
|
|
133
|
+
or sequence display also gets what indexing it yields under
|
|
134
|
+
``name + INDEXED``."""
|
|
135
|
+
found: dict[str, tuple[str, ...] | None] = {}
|
|
136
|
+
|
|
137
|
+
def merge(name: str, values: tuple[str, ...] | None) -> None:
|
|
138
|
+
known = found.get(name)
|
|
139
|
+
if known is not None and values is not None:
|
|
140
|
+
found[name] = tuple(dict.fromkeys(known + values))
|
|
141
|
+
else:
|
|
142
|
+
found[name] = None if (name in found and found[name] is None) else values
|
|
143
|
+
|
|
144
|
+
def bind(
|
|
145
|
+
name: str, values: tuple[str, ...] | None, indexed: tuple[str, ...] | None = None
|
|
146
|
+
) -> None:
|
|
147
|
+
merge(name, values)
|
|
148
|
+
merge(name + INDEXED, indexed)
|
|
149
|
+
|
|
150
|
+
def unbind(name: str) -> None:
|
|
151
|
+
found[name] = found[name + INDEXED] = None
|
|
152
|
+
|
|
153
|
+
# Name stores the forms below bind; every other store of a name
|
|
154
|
+
# (``+=``, walrus, ``with ... as``, unpacking, ``except ... as``, an
|
|
155
|
+
# import, a match capture, ``global``/``nonlocal``) leaves it unbounded.
|
|
156
|
+
handled: set[int] = set()
|
|
157
|
+
|
|
158
|
+
# Source order matters: ``names = {...}`` must be seen before the loop
|
|
159
|
+
# that iterates it, so children are pushed reversed onto the LIFO stack.
|
|
160
|
+
stack: list[ast.AST] = list(reversed(list(ast.iter_child_nodes(node))))
|
|
161
|
+
while stack:
|
|
162
|
+
n = stack.pop()
|
|
163
|
+
if isinstance(n, NESTED_SCOPES):
|
|
164
|
+
# Not entered, but it may rebind names of this scope: ``nonlocal``
|
|
165
|
+
# or ``global`` declarations, a walrus inside a comprehension.
|
|
166
|
+
for inner in ast.walk(n):
|
|
167
|
+
if isinstance(inner, (ast.Global, ast.Nonlocal)):
|
|
168
|
+
for name in inner.names:
|
|
169
|
+
unbind(name)
|
|
170
|
+
elif isinstance(inner, ast.NamedExpr) and isinstance(inner.target, ast.Name):
|
|
171
|
+
unbind(inner.target.id)
|
|
172
|
+
continue
|
|
173
|
+
if isinstance(n, ast.Assign):
|
|
174
|
+
values = _string_candidates(n.value, found, module_literals)
|
|
175
|
+
items = _indexed(n.value, found, module_literals)
|
|
176
|
+
for target in n.targets:
|
|
177
|
+
if isinstance(target, ast.Name):
|
|
178
|
+
handled.add(id(target))
|
|
179
|
+
bind(target.id, values, items)
|
|
180
|
+
elif isinstance(n, ast.AnnAssign) and n.value is not None:
|
|
181
|
+
if isinstance(n.target, ast.Name):
|
|
182
|
+
handled.add(id(n.target))
|
|
183
|
+
bind(
|
|
184
|
+
n.target.id,
|
|
185
|
+
_string_candidates(n.value, found, module_literals),
|
|
186
|
+
_indexed(n.value, found, module_literals),
|
|
187
|
+
)
|
|
188
|
+
elif isinstance(n, (ast.For, ast.AsyncFor)) and isinstance(n.target, ast.Name):
|
|
189
|
+
handled.add(id(n.target))
|
|
190
|
+
bind(n.target.id, _string_candidates(n.iter, found, module_literals))
|
|
191
|
+
elif isinstance(n, (ast.For, ast.AsyncFor)) and isinstance(n.target, ast.Tuple):
|
|
192
|
+
# ``for key, value in D.items()``: over a dict display both the
|
|
193
|
+
# keys and the values are bounded, anything else is not.
|
|
194
|
+
elts = n.target.elts
|
|
195
|
+
keys = items = None
|
|
196
|
+
receiver = _dict_method_receiver(n.iter, "items")
|
|
197
|
+
if receiver is not None and len(elts) == 2:
|
|
198
|
+
keys = _string_candidates(receiver, found, module_literals)
|
|
199
|
+
items = _indexed(receiver, found, module_literals)
|
|
200
|
+
for i, elt in enumerate(elts):
|
|
201
|
+
if isinstance(elt, ast.Name):
|
|
202
|
+
handled.add(id(elt))
|
|
203
|
+
bind(elt.id, keys if i == 0 else items)
|
|
204
|
+
elif isinstance(n, ast.Name) and isinstance(n.ctx, ast.Store):
|
|
205
|
+
if id(n) not in handled:
|
|
206
|
+
unbind(n.id)
|
|
207
|
+
elif isinstance(n, ast.Name) and isinstance(n.ctx, ast.Del):
|
|
208
|
+
# ``del name`` binds no new value (a later read fails): it only
|
|
209
|
+
# leaves a name that had no binding yet unbounded.
|
|
210
|
+
if n.id not in found:
|
|
211
|
+
unbind(n.id)
|
|
212
|
+
elif isinstance(n, ast.ExceptHandler) and n.name:
|
|
213
|
+
unbind(n.name)
|
|
214
|
+
elif isinstance(n, ast.alias):
|
|
215
|
+
unbind(n.asname or n.name.split(".")[0])
|
|
216
|
+
elif isinstance(n, (ast.MatchAs, ast.MatchStar)) and n.name:
|
|
217
|
+
unbind(n.name)
|
|
218
|
+
elif isinstance(n, ast.MatchMapping) and n.rest:
|
|
219
|
+
unbind(n.rest)
|
|
220
|
+
elif isinstance(n, (ast.Global, ast.Nonlocal)):
|
|
221
|
+
for name in n.names:
|
|
222
|
+
unbind(name)
|
|
223
|
+
elif (mutated := _mutated_name(n)) is not None:
|
|
224
|
+
# ``d[k] = v`` / ``d.append(x)``: the literal is not what it was.
|
|
225
|
+
unbind(mutated)
|
|
226
|
+
stack.extend(reversed(list(ast.iter_child_nodes(n))))
|
|
227
|
+
return found
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
COMPREHENSIONS = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
NESTED_SCOPES = DEF_NODES + COMPREHENSIONS + (ast.Lambda,)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Methods that change a container in place (see also
|
|
237
|
+
# _ReferenceCollector.MUTATING_METHODS, which records writer edges).
|
|
238
|
+
_MUTATING_METHODS = frozenset(
|
|
239
|
+
{
|
|
240
|
+
"append",
|
|
241
|
+
"extend",
|
|
242
|
+
"insert",
|
|
243
|
+
"pop",
|
|
244
|
+
"popitem",
|
|
245
|
+
"remove",
|
|
246
|
+
"clear",
|
|
247
|
+
"update",
|
|
248
|
+
"setdefault",
|
|
249
|
+
"add",
|
|
250
|
+
"discard",
|
|
251
|
+
"sort",
|
|
252
|
+
"reverse",
|
|
253
|
+
"__setitem__",
|
|
254
|
+
"__delitem__",
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _mutated_name(node: ast.AST) -> str | None:
|
|
260
|
+
"""The name a statement or call mutates in place (``d[k] = v``,
|
|
261
|
+
``d.append(x)``, ``del d[k]``), if it is a plain name."""
|
|
262
|
+
targets: list[ast.expr] = []
|
|
263
|
+
if isinstance(node, ast.Assign):
|
|
264
|
+
targets = list(node.targets)
|
|
265
|
+
elif isinstance(node, (ast.AugAssign, ast.AnnAssign)):
|
|
266
|
+
targets = [node.target]
|
|
267
|
+
elif isinstance(node, ast.Delete):
|
|
268
|
+
targets = list(node.targets)
|
|
269
|
+
elif (
|
|
270
|
+
isinstance(node, ast.Call)
|
|
271
|
+
and isinstance(node.func, ast.Attribute)
|
|
272
|
+
and node.func.attr in _MUTATING_METHODS
|
|
273
|
+
):
|
|
274
|
+
targets = [node.func.value]
|
|
275
|
+
for target in targets:
|
|
276
|
+
base = target
|
|
277
|
+
while isinstance(base, (ast.Subscript, ast.Attribute)):
|
|
278
|
+
base = base.value
|
|
279
|
+
if isinstance(base, ast.Name) and base is not target:
|
|
280
|
+
return base.id
|
|
281
|
+
if isinstance(base, ast.Name) and isinstance(node, ast.Call):
|
|
282
|
+
return base.id
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _module_mutations(tree: ast.Module) -> set[str]:
|
|
287
|
+
"""Every name the module mutates in place, anywhere in it (nested scopes
|
|
288
|
+
included): such a container's contents are not the literal it was
|
|
289
|
+
assigned."""
|
|
290
|
+
names: set[str] = set()
|
|
291
|
+
for node in ast.walk(tree):
|
|
292
|
+
mutated = _mutated_name(node)
|
|
293
|
+
if mutated is not None:
|
|
294
|
+
names.add(mutated)
|
|
295
|
+
return names
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _collect_store_names(stmt: ast.AST) -> set[str]:
|
|
299
|
+
"""Names bound by a statement in *its own* scope (nested scopes excluded)."""
|
|
300
|
+
names: set[str] = set()
|
|
301
|
+
stack: list[ast.AST] = [stmt]
|
|
302
|
+
while stack:
|
|
303
|
+
node = stack.pop()
|
|
304
|
+
if isinstance(node, NESTED_SCOPES) and node is not stmt:
|
|
305
|
+
if isinstance(node, DEF_NODES):
|
|
306
|
+
names.add(node.name)
|
|
307
|
+
continue
|
|
308
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
309
|
+
names.add(node.id)
|
|
310
|
+
elif isinstance(node, ast.ExceptHandler) and node.name:
|
|
311
|
+
names.add(node.name)
|
|
312
|
+
elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name:
|
|
313
|
+
names.add(node.name)
|
|
314
|
+
stack.extend(ast.iter_child_nodes(node))
|
|
315
|
+
return names
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class _LocalBindings(ast.NodeVisitor):
|
|
319
|
+
"""Collect the names a function, lambda or class body binds in its own
|
|
320
|
+
scope. Nested functions, lambdas and comprehensions get their own scope;
|
|
321
|
+
only their names (for defs) are bound here. ``global`` and ``nonlocal``
|
|
322
|
+
names are excluded.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
def __init__(self) -> None:
|
|
326
|
+
self.names: set[str] = set()
|
|
327
|
+
self.globals: set[str] = set()
|
|
328
|
+
|
|
329
|
+
def collect(self, node: ast.AST) -> set[str]:
|
|
330
|
+
if isinstance(node, FUNC_NODES + (ast.Lambda,)):
|
|
331
|
+
self.visit(node.args)
|
|
332
|
+
body = getattr(node, "body", [])
|
|
333
|
+
for stmt in body if isinstance(body, list) else [body]:
|
|
334
|
+
self.visit(stmt)
|
|
335
|
+
return self.names - self.globals
|
|
336
|
+
|
|
337
|
+
def visit_arg(self, node: ast.arg) -> None:
|
|
338
|
+
self.names.add(node.arg)
|
|
339
|
+
|
|
340
|
+
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
def _visit_comprehension(self, node: ast.AST) -> None:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
visit_ListComp = visit_SetComp = visit_DictComp = visit_GeneratorExp = _visit_comprehension
|
|
347
|
+
|
|
348
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
349
|
+
if isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
350
|
+
self.names.add(node.id)
|
|
351
|
+
|
|
352
|
+
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
353
|
+
if node.name:
|
|
354
|
+
self.names.add(node.name)
|
|
355
|
+
self.generic_visit(node)
|
|
356
|
+
|
|
357
|
+
def visit_MatchAs(self, node: ast.MatchAs) -> None:
|
|
358
|
+
if node.name:
|
|
359
|
+
self.names.add(node.name)
|
|
360
|
+
self.generic_visit(node)
|
|
361
|
+
|
|
362
|
+
def visit_MatchStar(self, node: ast.MatchStar) -> None:
|
|
363
|
+
if node.name:
|
|
364
|
+
self.names.add(node.name)
|
|
365
|
+
|
|
366
|
+
def visit_Global(self, node: ast.Global) -> None:
|
|
367
|
+
self.globals.update(node.names)
|
|
368
|
+
|
|
369
|
+
def visit_Nonlocal(self, node: ast.Nonlocal) -> None:
|
|
370
|
+
self.globals.update(node.names) # the enclosing function's, not local
|
|
371
|
+
|
|
372
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
373
|
+
for alias in node.names:
|
|
374
|
+
self.names.add(alias.asname or alias.name.split(".")[0])
|
|
375
|
+
|
|
376
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
377
|
+
for alias in node.names:
|
|
378
|
+
if alias.name != "*":
|
|
379
|
+
self.names.add(alias.asname or alias.name)
|
|
380
|
+
|
|
381
|
+
def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
382
|
+
self.names.add(node.name)
|
|
383
|
+
|
|
384
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
385
|
+
return self.visit_FunctionDef(node)
|
|
386
|
+
|
|
387
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
388
|
+
self.names.add(node.name)
|