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,1001 @@
|
|
|
1
|
+
"""Pass 2: the in-scope class model (bases, MRO, overrides) and the
|
|
2
|
+
resolution of names and attribute chains into edges."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from diffcone.indexer.definitions import (
|
|
10
|
+
_EXPLICIT_SPECIAL_METHODS,
|
|
11
|
+
_STRUCTURAL_BASES,
|
|
12
|
+
_class_attributes,
|
|
13
|
+
_flatten_chain,
|
|
14
|
+
_future_annotations,
|
|
15
|
+
_has_decorator,
|
|
16
|
+
_is_inert_decorator,
|
|
17
|
+
_is_special_method,
|
|
18
|
+
_is_staticmethod,
|
|
19
|
+
_rebound_names,
|
|
20
|
+
)
|
|
21
|
+
from diffcone.indexer.facts import _FuncParams
|
|
22
|
+
from diffcone.indexer.literals import NESTED_SCOPES, _LocalBindings
|
|
23
|
+
from diffcone.indexer.references import _ReferenceCollector
|
|
24
|
+
from diffcone.indexer.scopes import (
|
|
25
|
+
ClassScope,
|
|
26
|
+
External,
|
|
27
|
+
ImportBinding,
|
|
28
|
+
Local,
|
|
29
|
+
ModuleNode,
|
|
30
|
+
ModuleScope,
|
|
31
|
+
Node,
|
|
32
|
+
Resolved,
|
|
33
|
+
Scope,
|
|
34
|
+
Unresolved,
|
|
35
|
+
_absolute_module,
|
|
36
|
+
relative_import_escapes,
|
|
37
|
+
)
|
|
38
|
+
from diffcone.indexer.symbols import FirstPass
|
|
39
|
+
from diffcone.indexer.syntax import (
|
|
40
|
+
BUILTIN_NAMES,
|
|
41
|
+
DEF_NODES,
|
|
42
|
+
FUNC_NODES,
|
|
43
|
+
_digest,
|
|
44
|
+
iter_scope_statements,
|
|
45
|
+
)
|
|
46
|
+
from diffcone.model import (
|
|
47
|
+
CLASS,
|
|
48
|
+
FUNCTION,
|
|
49
|
+
IMPORTS,
|
|
50
|
+
IMPORTS_NAME,
|
|
51
|
+
METHOD,
|
|
52
|
+
MODULE,
|
|
53
|
+
REFERENCES,
|
|
54
|
+
UNRESOLVED_ATTRIBUTE,
|
|
55
|
+
UNRESOLVED_DYNAMIC,
|
|
56
|
+
UNRESOLVED_NAME,
|
|
57
|
+
VARIABLE,
|
|
58
|
+
Edge,
|
|
59
|
+
ExternalReference,
|
|
60
|
+
Symbol,
|
|
61
|
+
UnresolvedReference,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _with_alternatives(bindings: list[Node]) -> Node:
|
|
66
|
+
"""The node for a name bound several ways at module level: the first
|
|
67
|
+
binding (a definition, else the last import), carrying the in-scope
|
|
68
|
+
symbols and modules of the others as alternatives. When the first one
|
|
69
|
+
names nothing in scope (an external fallback tried first) an in-scope
|
|
70
|
+
alternative takes its place, so the reference is not merely external."""
|
|
71
|
+
in_scope: list[str] = []
|
|
72
|
+
for node in bindings:
|
|
73
|
+
if isinstance(node, Resolved):
|
|
74
|
+
in_scope.append(node.symbol)
|
|
75
|
+
elif isinstance(node, ModuleNode):
|
|
76
|
+
in_scope.append(node.module)
|
|
77
|
+
primary = bindings[0]
|
|
78
|
+
if not isinstance(primary, (Resolved, ModuleNode)):
|
|
79
|
+
primary = next((b for b in bindings if isinstance(b, Resolved)), primary)
|
|
80
|
+
if not isinstance(primary, Resolved):
|
|
81
|
+
return primary
|
|
82
|
+
others = tuple(dict.fromkeys(i for i in in_scope if i != primary.symbol))
|
|
83
|
+
if not others:
|
|
84
|
+
return primary
|
|
85
|
+
return Resolved(
|
|
86
|
+
primary.symbol,
|
|
87
|
+
primary.detail,
|
|
88
|
+
primary.uncertain_attr,
|
|
89
|
+
primary.receiver,
|
|
90
|
+
primary.overrides,
|
|
91
|
+
primary.also,
|
|
92
|
+
others,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Resolver(FirstPass):
|
|
97
|
+
"""Pass 2: the class model, and references into edges."""
|
|
98
|
+
|
|
99
|
+
def lookup_super(self, class_id: str, attr: str) -> Node:
|
|
100
|
+
"""``super().<attr>`` in ``class_id``: the next definition after it in
|
|
101
|
+
its MRO, plus (as overrides) what follows it in the MRO of each
|
|
102
|
+
in-scope subclass, where a mixin may come first."""
|
|
103
|
+
hit = self.lookup_in_class(class_id, attr, skip_self=True)
|
|
104
|
+
base = (hit.symbol, hit.detail) if isinstance(hit, Resolved) else None
|
|
105
|
+
extra: list[tuple[str, str]] = []
|
|
106
|
+
for sub in self._descendants.get(class_id, ()):
|
|
107
|
+
mro = self._mro(sub)
|
|
108
|
+
if class_id not in mro:
|
|
109
|
+
continue
|
|
110
|
+
for cid in mro[mro.index(class_id) + 1 :]:
|
|
111
|
+
cscope = self.class_scopes[cid]
|
|
112
|
+
if attr in cscope.members:
|
|
113
|
+
pair = (cscope.members[attr], "")
|
|
114
|
+
elif attr in cscope.bindings:
|
|
115
|
+
pair = (cid, f"attribute:{attr}")
|
|
116
|
+
else:
|
|
117
|
+
continue
|
|
118
|
+
if pair != base and pair not in extra:
|
|
119
|
+
extra.append(pair)
|
|
120
|
+
break
|
|
121
|
+
if not extra or not isinstance(hit, Resolved):
|
|
122
|
+
return hit
|
|
123
|
+
return Resolved(hit.symbol, hit.detail, hit.uncertain_attr, overrides=tuple(extra))
|
|
124
|
+
|
|
125
|
+
def _super_may_reach(self, symbol: Symbol) -> bool:
|
|
126
|
+
"""An unresolved ``super().<name>`` in class K may call ``symbol``
|
|
127
|
+
when its class follows K in some in-scope MRO."""
|
|
128
|
+
owner = symbol.container
|
|
129
|
+
for k in self._super_misses.get(symbol.name, ()):
|
|
130
|
+
for cid in (k, *self._descendants.get(k, ())):
|
|
131
|
+
mro = self._mro(cid)
|
|
132
|
+
if k in mro and owner in mro[mro.index(k) + 1 :]:
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def _constructor_escapes(self, symbol: Symbol, unresolved_names: set[str]) -> bool:
|
|
137
|
+
"""An ``__init__`` also runs whenever a class that inherits it is
|
|
138
|
+
constructed: that happens unseen when such a class escapes or its name
|
|
139
|
+
occurs as an unresolved reference."""
|
|
140
|
+
if symbol.kind != METHOD or symbol.name != "__init__":
|
|
141
|
+
return False
|
|
142
|
+
owner = symbol.container
|
|
143
|
+
if owner not in self.class_scopes:
|
|
144
|
+
return True
|
|
145
|
+
for cid in (owner, *self._descendants.get(owner, ())):
|
|
146
|
+
init = self.lookup_in_class(cid, "__init__")
|
|
147
|
+
if not (isinstance(init, Resolved) and init.symbol == symbol.id):
|
|
148
|
+
continue
|
|
149
|
+
if cid in self.out.escapes or self.index.symbols[cid].name in unresolved_names:
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def _returned_class(self, node: ast.AST, scope: Scope) -> tuple[str, ...] | None:
|
|
154
|
+
"""The classes ``node`` returns, when every ``return`` in it yields
|
|
155
|
+
one: ``def make(): return Provider()`` yields that class, and a
|
|
156
|
+
factory that picks between two yields both. One return that says
|
|
157
|
+
something else, or none at all, says nothing -- a guess here would
|
|
158
|
+
bind a class that never reaches the caller."""
|
|
159
|
+
found: set[str] = set()
|
|
160
|
+
for inner in ast.walk(node):
|
|
161
|
+
if isinstance(inner, NESTED_SCOPES) and inner is not node:
|
|
162
|
+
continue
|
|
163
|
+
if not isinstance(inner, ast.Return) or inner.value is None:
|
|
164
|
+
continue
|
|
165
|
+
cls = self._expression_class(inner.value, scope)
|
|
166
|
+
if cls is None:
|
|
167
|
+
return None
|
|
168
|
+
found.add(cls)
|
|
169
|
+
return tuple(sorted(found)) or None
|
|
170
|
+
|
|
171
|
+
def _expression_class(self, expr: ast.expr, scope: Scope) -> str | None:
|
|
172
|
+
"""The class an expression is an instance of, when it says so: ``C()``
|
|
173
|
+
constructs one, ``C`` is the class itself."""
|
|
174
|
+
target = expr.func if isinstance(expr, ast.Call) else expr
|
|
175
|
+
parts = _flatten_chain(target)
|
|
176
|
+
if parts is None:
|
|
177
|
+
return None
|
|
178
|
+
node = self.resolve_chain(parts, scope)
|
|
179
|
+
if not isinstance(node, Resolved) or node.detail:
|
|
180
|
+
return None
|
|
181
|
+
symbol = self.index.symbols.get(node.symbol)
|
|
182
|
+
return node.symbol if symbol is not None and symbol.kind == CLASS else None
|
|
183
|
+
|
|
184
|
+
def _ensure_bases(self, class_id: str) -> None:
|
|
185
|
+
"""Resolve a class's bases on demand (a dotted base such as
|
|
186
|
+
``Zed.Inner`` may need another class's MRO first)."""
|
|
187
|
+
cscope = self.class_scopes[class_id]
|
|
188
|
+
if cscope.bases_state:
|
|
189
|
+
return
|
|
190
|
+
cscope.bases_state = 1
|
|
191
|
+
for parts in cscope.base_chains:
|
|
192
|
+
node = self._resolve_base_expr(parts, cscope)
|
|
193
|
+
self._record(cscope.id, node, chain=".".join(parts) if parts else "<expr>")
|
|
194
|
+
if (
|
|
195
|
+
isinstance(node, Resolved)
|
|
196
|
+
and not node.detail
|
|
197
|
+
and not node.uncertain_attr
|
|
198
|
+
and node.symbol in self.class_scopes
|
|
199
|
+
and node.symbol != cscope.id
|
|
200
|
+
):
|
|
201
|
+
cscope.bases.append(node.symbol)
|
|
202
|
+
else:
|
|
203
|
+
cscope.complete = False # external, dynamic (``Generic[T]``) or unknown
|
|
204
|
+
if not (parts == ["object"] and node is None):
|
|
205
|
+
cscope.opaque = True
|
|
206
|
+
for name in cscope.base_names:
|
|
207
|
+
if self._calls_back(name, cscope):
|
|
208
|
+
cscope.external_base = True
|
|
209
|
+
cscope.bases_state = 2
|
|
210
|
+
|
|
211
|
+
def _calls_back(self, name: list[str] | None, cscope: ClassScope) -> bool:
|
|
212
|
+
"""Whether a base (as written) is code outside the source roots that
|
|
213
|
+
may call the subclass's methods: not an in-scope class (its own
|
|
214
|
+
status is inherited through the MRO), not a builtin, not a purely
|
|
215
|
+
structural typing/abc base. An unknown expression counts."""
|
|
216
|
+
if name is None:
|
|
217
|
+
return True
|
|
218
|
+
node = self._resolve_base_expr(name, cscope)
|
|
219
|
+
if isinstance(node, Resolved) and not node.detail and node.symbol in self.class_scopes:
|
|
220
|
+
return False
|
|
221
|
+
head = name[0]
|
|
222
|
+
binding = cscope.module.imports.get(head)
|
|
223
|
+
if binding is None:
|
|
224
|
+
if node is None and head in BUILTIN_NAMES and len(name) == 1:
|
|
225
|
+
return False
|
|
226
|
+
canonical = ".".join(name)
|
|
227
|
+
else:
|
|
228
|
+
base = binding.module if binding.attr is None else f"{binding.module}.{binding.attr}"
|
|
229
|
+
canonical = ".".join([base, *name[1:]])
|
|
230
|
+
return canonical not in _STRUCTURAL_BASES
|
|
231
|
+
|
|
232
|
+
def _resolve_base_expr(self, parts: list[str] | None, cscope: ClassScope) -> Node:
|
|
233
|
+
"""A base name is looked up in the enclosing class body (for nested
|
|
234
|
+
classes) and then in the module, as Python does when the class
|
|
235
|
+
statement executes."""
|
|
236
|
+
if parts is None:
|
|
237
|
+
return None # ``Generic[T]``, ``namedtuple(...)``: the collector visits it
|
|
238
|
+
enclosing = cscope.enclosing
|
|
239
|
+
if enclosing is not None and parts[0] in enclosing.members:
|
|
240
|
+
node: Node = Resolved(enclosing.members[parts[0]])
|
|
241
|
+
for attr in parts[1:]:
|
|
242
|
+
node = self._step(node, attr)
|
|
243
|
+
return node
|
|
244
|
+
if enclosing is not None and parts[0] in enclosing.bindings:
|
|
245
|
+
return Resolved(enclosing.id, detail=f"attribute:{parts[0]}")
|
|
246
|
+
return self.resolve_chain(parts, Scope(module=cscope.module))
|
|
247
|
+
|
|
248
|
+
def _mro(self, class_id: str) -> list[str]:
|
|
249
|
+
"""Linearisation over in-scope classes: the class, then its bases
|
|
250
|
+
depth-first left to right keeping the last occurrence of a repeated
|
|
251
|
+
base (C3 for ordinary hierarchies; documented as an approximation).
|
|
252
|
+
Memoised only once every class's bases are resolved."""
|
|
253
|
+
cscope = self.class_scopes[class_id]
|
|
254
|
+
if cscope.mro is not None:
|
|
255
|
+
return cscope.mro
|
|
256
|
+
self._ensure_bases(class_id)
|
|
257
|
+
if cscope.in_mro:
|
|
258
|
+
return [class_id] # inheritance cycle: stop here
|
|
259
|
+
cscope.in_mro = True
|
|
260
|
+
try:
|
|
261
|
+
order: list[str] = []
|
|
262
|
+
for base in cscope.bases:
|
|
263
|
+
order.extend(self._mro(base))
|
|
264
|
+
finally:
|
|
265
|
+
cscope.in_mro = False
|
|
266
|
+
seen: set[str] = {class_id}
|
|
267
|
+
tail: list[str] = []
|
|
268
|
+
for cid in reversed(order):
|
|
269
|
+
if cid not in seen:
|
|
270
|
+
seen.add(cid)
|
|
271
|
+
tail.append(cid)
|
|
272
|
+
result = [class_id, *reversed(tail)]
|
|
273
|
+
if self._bases_final:
|
|
274
|
+
cscope.mro = result
|
|
275
|
+
return result
|
|
276
|
+
|
|
277
|
+
def lookup_in_class(
|
|
278
|
+
self, class_id: str, attr: str, *, skip_self: bool = False, dispatch: bool = False
|
|
279
|
+
) -> Node:
|
|
280
|
+
"""Resolve ``attr`` on a class through its in-scope MRO.
|
|
281
|
+
|
|
282
|
+
A hit found after a class whose bases are not all known is marked
|
|
283
|
+
uncertain: an override in the unknown part of the hierarchy could
|
|
284
|
+
win, so the edge is recorded together with a name-bounded unresolved
|
|
285
|
+
reference. Not found anywhere yields the unresolved reference alone.
|
|
286
|
+
|
|
287
|
+
With ``dispatch`` (a lookup on ``self``/``cls``) the receiver may be
|
|
288
|
+
an instance of any in-scope subclass, so every subclass that defines
|
|
289
|
+
``attr`` itself is returned as an override to record as well.
|
|
290
|
+
"""
|
|
291
|
+
uncertain = False
|
|
292
|
+
for cid in self._mro(class_id)[1 if skip_self else 0 :]:
|
|
293
|
+
cscope = self.class_scopes[cid]
|
|
294
|
+
hit: Resolved | None = None
|
|
295
|
+
if attr in cscope.members:
|
|
296
|
+
hit = Resolved(cscope.members[attr], uncertain_attr=attr if uncertain else "")
|
|
297
|
+
elif attr in cscope.bindings:
|
|
298
|
+
hit = Resolved(
|
|
299
|
+
cid, detail=f"attribute:{attr}", uncertain_attr=attr if uncertain else ""
|
|
300
|
+
)
|
|
301
|
+
if hit is not None:
|
|
302
|
+
if dispatch:
|
|
303
|
+
hit = Resolved(
|
|
304
|
+
hit.symbol,
|
|
305
|
+
hit.detail,
|
|
306
|
+
hit.uncertain_attr,
|
|
307
|
+
overrides=self._overrides_of(class_id, attr, (hit.symbol, hit.detail)),
|
|
308
|
+
)
|
|
309
|
+
return hit
|
|
310
|
+
if not cscope.complete:
|
|
311
|
+
uncertain = True
|
|
312
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, attr)
|
|
313
|
+
|
|
314
|
+
def _external_callers(self) -> None:
|
|
315
|
+
"""A class whose MRO has a base outside the source roots (a
|
|
316
|
+
transport, a handler, a visitor) may have any method called by that
|
|
317
|
+
code, which nothing in the source roots shows: the class depends on
|
|
318
|
+
every method it defines, like its special methods. The generic
|
|
319
|
+
bases of ``Base[T]`` are not in the MRO; their status counts too."""
|
|
320
|
+
for class_id in sorted(self.class_scopes):
|
|
321
|
+
cscope = self.class_scopes[class_id]
|
|
322
|
+
related = set(self._mro(class_id))
|
|
323
|
+
for chain, name in zip(cscope.base_chains, cscope.base_names, strict=True):
|
|
324
|
+
if chain is None and name is not None: # ``Base[T]``
|
|
325
|
+
node = self._resolve_base_expr(name, cscope)
|
|
326
|
+
if isinstance(node, Resolved) and node.symbol in self.class_scopes:
|
|
327
|
+
related |= set(self._mro(node.symbol))
|
|
328
|
+
if not any(self.class_scopes[c].external_base for c in related):
|
|
329
|
+
continue
|
|
330
|
+
for member, member_id in sorted(cscope.members.items()):
|
|
331
|
+
symbol = self.index.symbols.get(member_id)
|
|
332
|
+
if symbol is None or symbol.kind != METHOD or _is_special_method(member):
|
|
333
|
+
continue
|
|
334
|
+
if member in _EXPLICIT_SPECIAL_METHODS:
|
|
335
|
+
continue
|
|
336
|
+
self.out.edges.add(Edge(class_id, member_id, REFERENCES, "external_base"))
|
|
337
|
+
|
|
338
|
+
def _build_descendants(self) -> None:
|
|
339
|
+
subclasses: dict[str, set[str]] = defaultdict(set)
|
|
340
|
+
for cscope in self.class_scopes.values():
|
|
341
|
+
for base in cscope.bases:
|
|
342
|
+
subclasses[base].add(cscope.id)
|
|
343
|
+
for class_id in self.class_scopes:
|
|
344
|
+
seen = {class_id}
|
|
345
|
+
order: list[str] = []
|
|
346
|
+
stack = [class_id]
|
|
347
|
+
while stack:
|
|
348
|
+
for sub in sorted(subclasses.get(stack.pop(), ())):
|
|
349
|
+
if sub not in seen:
|
|
350
|
+
seen.add(sub)
|
|
351
|
+
order.append(sub)
|
|
352
|
+
stack.append(sub)
|
|
353
|
+
self._descendants[class_id] = tuple(order)
|
|
354
|
+
|
|
355
|
+
def _overrides_of(
|
|
356
|
+
self, class_id: str, attr: str, base_hit: tuple[str, str]
|
|
357
|
+
) -> tuple[tuple[str, str], ...]:
|
|
358
|
+
"""What ``attr`` resolves to on each in-scope descendant of
|
|
359
|
+
``class_id`` when that differs from the base hit: a method defined by
|
|
360
|
+
the descendant, one it inherits from a mixin outside the base's
|
|
361
|
+
hierarchy, or a class-attribute rebinding."""
|
|
362
|
+
assert self._bases_final, "overrides need every class's bases resolved"
|
|
363
|
+
found: list[tuple[str, str]] = []
|
|
364
|
+
for sub in self._descendants.get(class_id, ()):
|
|
365
|
+
hit = self.lookup_in_class(sub, attr)
|
|
366
|
+
if isinstance(hit, Resolved):
|
|
367
|
+
pair = (hit.symbol, hit.detail)
|
|
368
|
+
if pair != base_hit and pair not in found:
|
|
369
|
+
found.append(pair)
|
|
370
|
+
return tuple(found)
|
|
371
|
+
|
|
372
|
+
def _module_in_scope(self, name: str) -> bool:
|
|
373
|
+
return name in self._module_prefixes
|
|
374
|
+
|
|
375
|
+
def _resolve_module(self, scope: ModuleScope) -> None:
|
|
376
|
+
assert scope.tree is not None # parsed before a module is resolved
|
|
377
|
+
module_scope = Scope(module=scope)
|
|
378
|
+
# Module-level imports -> init-time edges.
|
|
379
|
+
for node in scope.import_nodes:
|
|
380
|
+
self._import_edges(scope.name, scope, node)
|
|
381
|
+
top_level = [
|
|
382
|
+
s
|
|
383
|
+
for s in scope.tree.body
|
|
384
|
+
if not isinstance(s, DEF_NODES + (ast.Import, ast.ImportFrom))
|
|
385
|
+
]
|
|
386
|
+
collector = _ReferenceCollector(self, scope.name, module_scope, skip_defs=True)
|
|
387
|
+
variable_ids = {
|
|
388
|
+
id(stmt): symbol
|
|
389
|
+
for name, stmt in scope.variable_stmts.items()
|
|
390
|
+
for symbol in [scope.variables[name]]
|
|
391
|
+
}
|
|
392
|
+
for stmt in top_level:
|
|
393
|
+
owner = variable_ids.get(id(stmt))
|
|
394
|
+
if owner is not None:
|
|
395
|
+
# The right-hand side's references belong to the variable symbol.
|
|
396
|
+
_ReferenceCollector(self, owner, module_scope, skip_defs=True).visit(stmt)
|
|
397
|
+
else:
|
|
398
|
+
collector.visit(stmt)
|
|
399
|
+
self._resolve_definitions(scope, scope.tree.body, scope.members, None)
|
|
400
|
+
|
|
401
|
+
def _resolve_definitions(
|
|
402
|
+
self,
|
|
403
|
+
scope: ModuleScope,
|
|
404
|
+
body: list[ast.stmt],
|
|
405
|
+
members: dict[str, str],
|
|
406
|
+
class_scope: ClassScope | None,
|
|
407
|
+
) -> None:
|
|
408
|
+
for stmt in iter_scope_statements(body):
|
|
409
|
+
if isinstance(stmt, ast.ClassDef):
|
|
410
|
+
symbol_id = members.get(stmt.name)
|
|
411
|
+
if symbol_id is None or symbol_id not in self.class_scopes:
|
|
412
|
+
continue
|
|
413
|
+
cscope = self.class_scopes[symbol_id]
|
|
414
|
+
class_level = Scope(
|
|
415
|
+
module=scope,
|
|
416
|
+
locals=set(cscope.bindings),
|
|
417
|
+
class_members=dict(cscope.members),
|
|
418
|
+
class_level=True,
|
|
419
|
+
)
|
|
420
|
+
# The class statement (bases, decorators, body) runs when the
|
|
421
|
+
# module is imported, nested classes included: its references
|
|
422
|
+
# are the class's and, as import-time code, the module's.
|
|
423
|
+
collectors = [
|
|
424
|
+
_ReferenceCollector(self, source, class_level, skip_defs=True)
|
|
425
|
+
for source in (symbol_id, scope.name)
|
|
426
|
+
]
|
|
427
|
+
for creator in (symbol_id, scope.name):
|
|
428
|
+
self.class_creation(creator, cscope.bases, stmt.keywords, Scope(module=scope))
|
|
429
|
+
# Name-chain bases were resolved (and recorded) by _ensure_bases;
|
|
430
|
+
# only dynamic base expressions still need their references collected.
|
|
431
|
+
dynamic_bases = [b for b in stmt.bases if _flatten_chain(b) is None]
|
|
432
|
+
for collector in collectors:
|
|
433
|
+
for expr in dynamic_bases + list(stmt.keywords) + list(stmt.decorator_list):
|
|
434
|
+
collector.visit(expr)
|
|
435
|
+
for inner in stmt.body:
|
|
436
|
+
if not isinstance(inner, DEF_NODES):
|
|
437
|
+
collector.visit(inner)
|
|
438
|
+
# An import in the class body runs when the class is created,
|
|
439
|
+
# at import: the class and the module depend on the imported
|
|
440
|
+
# module's import-time code, as for a module-level import.
|
|
441
|
+
for inner in iter_scope_statements(stmt.body):
|
|
442
|
+
if isinstance(inner, (ast.Import, ast.ImportFrom)):
|
|
443
|
+
for source in (symbol_id, scope.name):
|
|
444
|
+
self._import_edges(source, scope, inner, local=True)
|
|
445
|
+
attributes = _class_attributes(stmt)
|
|
446
|
+
previous = self.out.class_attributes.get(symbol_id)
|
|
447
|
+
if previous is not None: # a conditional second definition
|
|
448
|
+
for name in previous.keys() | attributes.keys():
|
|
449
|
+
attributes[name] = _digest(
|
|
450
|
+
previous.get(name, "") + "|" + attributes.get(name, "")
|
|
451
|
+
)
|
|
452
|
+
self.out.class_attributes[symbol_id] = attributes
|
|
453
|
+
creators = list(stmt.decorator_list) + [k.value for k in stmt.keywords]
|
|
454
|
+
if creators and self._decorators_may_read_docs(
|
|
455
|
+
creators, scope, Scope(module=scope)
|
|
456
|
+
):
|
|
457
|
+
self.out.doc_decorated.add(symbol_id)
|
|
458
|
+
self.out.class_bases[symbol_id] = tuple(sorted(set(cscope.bases)))
|
|
459
|
+
# Open: decorators or keywords, or a base outside the index
|
|
460
|
+
# that may consume the body (``Enum``, a framework model); not
|
|
461
|
+
# ``object``, ``Exception`` or a typing/abc base.
|
|
462
|
+
if not cscope.plain or cscope.external_base:
|
|
463
|
+
self.out.open_classes.add(symbol_id)
|
|
464
|
+
self._resolve_definitions(scope, stmt.body, cscope.members, cscope)
|
|
465
|
+
elif isinstance(stmt, FUNC_NODES):
|
|
466
|
+
symbol_id = members.get(stmt.name)
|
|
467
|
+
if symbol_id is None:
|
|
468
|
+
continue
|
|
469
|
+
self._resolve_function(scope, stmt, symbol_id, class_scope)
|
|
470
|
+
|
|
471
|
+
def _record_decorations(
|
|
472
|
+
self, symbol_id: str, decorators: list[ast.expr], module: ModuleScope, where: Scope
|
|
473
|
+
) -> None:
|
|
474
|
+
"""For each decorator not known inert: the in-scope function it
|
|
475
|
+
resolves to and the in-scope object it is an attribute of
|
|
476
|
+
(``show`` in ``@show.register(int)``, ``app`` in ``@app.command``).
|
|
477
|
+
A decorator may keep the function it decorates there."""
|
|
478
|
+
for dec in decorators:
|
|
479
|
+
if _is_inert_decorator(dec, module):
|
|
480
|
+
continue
|
|
481
|
+
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
482
|
+
parts = _flatten_chain(target)
|
|
483
|
+
if parts is None:
|
|
484
|
+
continue
|
|
485
|
+
decorator = self.resolve_chain(parts, where)
|
|
486
|
+
decorator_id = decorator.symbol if isinstance(decorator, Resolved) else ""
|
|
487
|
+
receiver_id = ""
|
|
488
|
+
if len(parts) > 1:
|
|
489
|
+
receiver = self.resolve_chain(parts[:-1], where)
|
|
490
|
+
if isinstance(receiver, Resolved) and receiver.symbol != symbol_id:
|
|
491
|
+
receiver_id = receiver.symbol
|
|
492
|
+
if decorator_id or receiver_id:
|
|
493
|
+
self.out.decorations.add((symbol_id, decorator_id, receiver_id))
|
|
494
|
+
|
|
495
|
+
def _decorators_may_read_docs(
|
|
496
|
+
self, decorators: list[ast.expr], module: ModuleScope, where: Scope
|
|
497
|
+
) -> bool:
|
|
498
|
+
"""Whether any decorator (or metaclass) may read the docstring of what
|
|
499
|
+
it decorates: one that resolves to nothing visible (external,
|
|
500
|
+
unresolved, a value), or in-scope code that reads ``__doc__`` (the
|
|
501
|
+
decorator itself, a factory whose wrapper does, a class's
|
|
502
|
+
``__init__``/``__new__``/``__call__``). Known inert ones never do."""
|
|
503
|
+
for dec in decorators:
|
|
504
|
+
if _is_inert_decorator(dec, module):
|
|
505
|
+
continue
|
|
506
|
+
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
507
|
+
parts = _flatten_chain(target)
|
|
508
|
+
if parts is None:
|
|
509
|
+
return True
|
|
510
|
+
node = self.resolve_chain(parts, where)
|
|
511
|
+
if not isinstance(node, Resolved) or node.detail:
|
|
512
|
+
return True
|
|
513
|
+
symbol = self.index.symbols.get(node.symbol)
|
|
514
|
+
if symbol is None or symbol.kind not in (FUNCTION, METHOD, CLASS):
|
|
515
|
+
return True
|
|
516
|
+
if symbol.kind == CLASS:
|
|
517
|
+
hooks = [
|
|
518
|
+
self.index.symbols.get(f"{symbol.id}.{m}")
|
|
519
|
+
for m in ("__init__", "__new__", "__call__")
|
|
520
|
+
]
|
|
521
|
+
if any(h is not None and h.reads_docstrings for h in hooks):
|
|
522
|
+
return True
|
|
523
|
+
elif symbol.reads_docstrings:
|
|
524
|
+
return True
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
def _resolve_function(
|
|
528
|
+
self,
|
|
529
|
+
scope: ModuleScope,
|
|
530
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
531
|
+
symbol_id: str,
|
|
532
|
+
class_scope: ClassScope | None,
|
|
533
|
+
) -> None:
|
|
534
|
+
fscope = Scope(module=scope, locals=_LocalBindings().collect(node), literal_node=node)
|
|
535
|
+
bound_method = class_scope is not None and not _is_staticmethod(node)
|
|
536
|
+
if bound_method:
|
|
537
|
+
params = node.args.posonlyargs + node.args.args
|
|
538
|
+
if params:
|
|
539
|
+
fscope.self_name = params[0].arg
|
|
540
|
+
fscope.self_class = class_scope.id
|
|
541
|
+
fscope.method = symbol_id
|
|
542
|
+
fscope.self_is_class = _has_decorator(node, "classmethod")
|
|
543
|
+
fscope.rebound = frozenset(_rebound_names(node))
|
|
544
|
+
positional = [a.arg for a in node.args.posonlyargs + node.args.args]
|
|
545
|
+
fscope.params = {name: i for i, name in enumerate(positional)}
|
|
546
|
+
fscope.params.update({a.arg: None for a in node.args.kwonlyargs})
|
|
547
|
+
defaults: dict[str, tuple[str, ...] | None] = {}
|
|
548
|
+
n_defaults = len(node.args.defaults)
|
|
549
|
+
for name, default in zip(
|
|
550
|
+
positional[len(positional) - n_defaults :], node.args.defaults, strict=True
|
|
551
|
+
):
|
|
552
|
+
defaults[name] = fscope.string_candidates(default)
|
|
553
|
+
for a, default in zip(node.args.kwonlyargs, node.args.kw_defaults, strict=True):
|
|
554
|
+
if default is not None:
|
|
555
|
+
defaults[a.arg] = fscope.string_candidates(default)
|
|
556
|
+
self.out.func_params[symbol_id] = _FuncParams(
|
|
557
|
+
positional=positional,
|
|
558
|
+
bound=bound_method,
|
|
559
|
+
defaults=defaults,
|
|
560
|
+
has_varargs=node.args.vararg is not None or node.args.kwarg is not None,
|
|
561
|
+
)
|
|
562
|
+
returned = self._returned_class(node, fscope)
|
|
563
|
+
if returned is not None:
|
|
564
|
+
self.out.returns[symbol_id] = returned
|
|
565
|
+
# Function-local imports are visible to the whole body.
|
|
566
|
+
for inner in ast.walk(node):
|
|
567
|
+
if isinstance(inner, (ast.Import, ast.ImportFrom)):
|
|
568
|
+
stars: list[str] = []
|
|
569
|
+
self._register_imports(scope, inner, fscope.local_imports, stars)
|
|
570
|
+
self._import_edges(symbol_id, scope, inner, local=True)
|
|
571
|
+
# Decorators, defaults and annotations evaluate where the ``def``
|
|
572
|
+
# statement runs, so the function's own parameters must not shadow
|
|
573
|
+
# them (``def f(info=info)`` refers to the module-level ``info``).
|
|
574
|
+
outer_scope = Scope(
|
|
575
|
+
module=scope,
|
|
576
|
+
locals=set(class_scope.bindings) if class_scope is not None else set(),
|
|
577
|
+
class_members=dict(class_scope.members) if class_scope is not None else {},
|
|
578
|
+
class_level=class_scope is not None,
|
|
579
|
+
)
|
|
580
|
+
outer = _ReferenceCollector(self, symbol_id, outer_scope, skip_defs=True)
|
|
581
|
+
# Decorators, defaults and eagerly evaluated annotations run when the
|
|
582
|
+
# ``def`` does, i.e. when the module is imported (a decorator such as
|
|
583
|
+
# ``@app.get("/")`` calls into a framework then): the module depends on
|
|
584
|
+
# what they reference too.
|
|
585
|
+
at_import = _ReferenceCollector(self, scope.name, outer_scope, skip_defs=True)
|
|
586
|
+
for expr in (
|
|
587
|
+
node.decorator_list
|
|
588
|
+
+ [d for d in node.args.defaults]
|
|
589
|
+
+ [d for d in node.args.kw_defaults if d is not None]
|
|
590
|
+
):
|
|
591
|
+
at_import.visit(expr)
|
|
592
|
+
if not _future_annotations(scope):
|
|
593
|
+
for arg in ast.walk(node.args):
|
|
594
|
+
if isinstance(arg, ast.arg) and arg.annotation is not None:
|
|
595
|
+
at_import.visit(arg.annotation)
|
|
596
|
+
if node.returns is not None:
|
|
597
|
+
at_import.visit(node.returns)
|
|
598
|
+
for dec in node.decorator_list:
|
|
599
|
+
outer.visit(dec)
|
|
600
|
+
if self._decorators_may_read_docs(node.decorator_list, scope, outer_scope):
|
|
601
|
+
self.out.doc_decorated.add(symbol_id)
|
|
602
|
+
self._record_decorations(symbol_id, node.decorator_list, scope, outer_scope)
|
|
603
|
+
all_args = node.args.posonlyargs + node.args.args + node.args.kwonlyargs
|
|
604
|
+
for arg in all_args + [a for a in (node.args.vararg, node.args.kwarg) if a]:
|
|
605
|
+
if arg.annotation is not None:
|
|
606
|
+
outer.visit(arg.annotation)
|
|
607
|
+
if node.returns is not None:
|
|
608
|
+
outer.visit(node.returns)
|
|
609
|
+
for name, default in zip(
|
|
610
|
+
positional[len(positional) - n_defaults :] + [a.arg for a in node.args.kwonlyargs],
|
|
611
|
+
list(node.args.defaults) + list(node.args.kw_defaults),
|
|
612
|
+
strict=True,
|
|
613
|
+
):
|
|
614
|
+
if default is None:
|
|
615
|
+
continue
|
|
616
|
+
outer.visit(default)
|
|
617
|
+
target = (
|
|
618
|
+
self.resolve_chain(parts, outer_scope)
|
|
619
|
+
if (parts := _flatten_chain(default))
|
|
620
|
+
else None
|
|
621
|
+
)
|
|
622
|
+
if isinstance(target, Resolved) and not target.detail:
|
|
623
|
+
aliased = self.index.symbols.get(target.symbol)
|
|
624
|
+
if aliased is not None and aliased.kind == VARIABLE:
|
|
625
|
+
fscope.param_aliases[name] = aliased.id
|
|
626
|
+
collector = _ReferenceCollector(self, symbol_id, fscope)
|
|
627
|
+
for stmt in node.body:
|
|
628
|
+
collector.visit(stmt)
|
|
629
|
+
|
|
630
|
+
def _import_edges(
|
|
631
|
+
self, source: str, scope: ModuleScope, node: ast.stmt, local: bool = False
|
|
632
|
+
) -> None:
|
|
633
|
+
if isinstance(node, ast.Import):
|
|
634
|
+
for alias in node.names:
|
|
635
|
+
self._module_import_edge(source, alias.name)
|
|
636
|
+
elif isinstance(node, ast.ImportFrom):
|
|
637
|
+
if relative_import_escapes(scope.name, scope.is_package, node.level):
|
|
638
|
+
written = "." * node.level + (node.module or "")
|
|
639
|
+
self.out.unresolved.add(
|
|
640
|
+
UnresolvedReference(
|
|
641
|
+
source,
|
|
642
|
+
UNRESOLVED_DYNAMIC,
|
|
643
|
+
written,
|
|
644
|
+
f"import from {written}: above the top-level package of {scope.name} "
|
|
645
|
+
"(check the source roots)",
|
|
646
|
+
)
|
|
647
|
+
)
|
|
648
|
+
return
|
|
649
|
+
base = _absolute_module(scope, node.module, node.level)
|
|
650
|
+
self._module_import_edge(source, base)
|
|
651
|
+
if not self._module_in_scope(base):
|
|
652
|
+
return
|
|
653
|
+
for alias in node.names:
|
|
654
|
+
if alias.name == "*":
|
|
655
|
+
continue
|
|
656
|
+
target = self._step(ModuleNode(base), alias.name)
|
|
657
|
+
if isinstance(target, ModuleNode):
|
|
658
|
+
self._module_import_edge(source, target.module)
|
|
659
|
+
continue
|
|
660
|
+
self._record(
|
|
661
|
+
source,
|
|
662
|
+
target,
|
|
663
|
+
kind=IMPORTS_NAME if not local else REFERENCES,
|
|
664
|
+
chain=f"from {base} import {alias.name}",
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
def _module_import_edge(self, source: str, module: str) -> None:
|
|
668
|
+
if not self._module_in_scope(module):
|
|
669
|
+
if self._module_in_scope(module.split(".")[0]):
|
|
670
|
+
self.out.unresolved.add(
|
|
671
|
+
UnresolvedReference(
|
|
672
|
+
source, UNRESOLVED_ATTRIBUTE, module.rsplit(".", 1)[-1], f"import {module}"
|
|
673
|
+
)
|
|
674
|
+
)
|
|
675
|
+
else:
|
|
676
|
+
hidden = self._misrooted(module)
|
|
677
|
+
if hidden is not None:
|
|
678
|
+
# ``from calc.ops import add`` while the roots name the
|
|
679
|
+
# package ``src.calc``: not a third-party import, an
|
|
680
|
+
# in-scope one discovery cannot follow. Unbounded.
|
|
681
|
+
self.out.unresolved.add(
|
|
682
|
+
UnresolvedReference(
|
|
683
|
+
source,
|
|
684
|
+
UNRESOLVED_DYNAMIC,
|
|
685
|
+
module,
|
|
686
|
+
f"import {module}: the analysed package is named {hidden}; the "
|
|
687
|
+
"source roots name it differently from the import (add a source "
|
|
688
|
+
"root for the directory holding it, e.g. --source-root src)",
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
else:
|
|
692
|
+
self.out.external.add(ExternalReference(source, module))
|
|
693
|
+
return
|
|
694
|
+
parts = module.split(".")
|
|
695
|
+
for i in range(1, len(parts) + 1):
|
|
696
|
+
prefix = ".".join(parts[:i])
|
|
697
|
+
if prefix in self.scopes and prefix != source:
|
|
698
|
+
self.out.edges.add(Edge(source, prefix, IMPORTS))
|
|
699
|
+
|
|
700
|
+
def resolve_dotted(self, chain: list[str]) -> Node:
|
|
701
|
+
"""An absolute dotted name as a string names it (``"pkg.mod.NAME"``):
|
|
702
|
+
the longest in-scope module prefix, then attribute steps. None when
|
|
703
|
+
no prefix is in scope (a third-party target)."""
|
|
704
|
+
for i in range(len(chain), 0, -1):
|
|
705
|
+
module = ".".join(chain[:i])
|
|
706
|
+
if self._module_in_scope(module):
|
|
707
|
+
node: Node = ModuleNode(module)
|
|
708
|
+
for attr in chain[i:]:
|
|
709
|
+
if node is None:
|
|
710
|
+
return None
|
|
711
|
+
node = self._step(node, attr)
|
|
712
|
+
return node
|
|
713
|
+
return None
|
|
714
|
+
|
|
715
|
+
def _misrooted(self, module: str) -> str | None:
|
|
716
|
+
"""The analysed package an absolute import most likely means when the
|
|
717
|
+
source roots name it with a prefix: ``calc`` for ``src.calc`` when
|
|
718
|
+
``src`` is a plain directory, not a package (a src layout analysed
|
|
719
|
+
with the root ``.``)."""
|
|
720
|
+
if self._misrooted_names is None:
|
|
721
|
+
names: dict[str, str] = {}
|
|
722
|
+
for m in sorted(self.scopes):
|
|
723
|
+
parts = m.split(".")
|
|
724
|
+
for i in range(1, len(parts)):
|
|
725
|
+
if ".".join(parts[:i]) in self.scopes:
|
|
726
|
+
break
|
|
727
|
+
candidate = ".".join(parts[: i + 1])
|
|
728
|
+
if candidate in self.scopes:
|
|
729
|
+
names.setdefault(parts[i], candidate)
|
|
730
|
+
break
|
|
731
|
+
self._misrooted_names = names
|
|
732
|
+
return self._misrooted_names.get(module.split(".")[0])
|
|
733
|
+
|
|
734
|
+
def _lookup_base(self, name: str, scope: Scope) -> Node:
|
|
735
|
+
if scope.self_name is not None and name == scope.self_name and scope.self_class:
|
|
736
|
+
return Resolved(scope.self_class, receiver=True)
|
|
737
|
+
if name in scope.local_imports:
|
|
738
|
+
return self._import_binding_node(scope.local_imports[name])
|
|
739
|
+
if name in scope.class_members:
|
|
740
|
+
# The member when it is defined above the use; the module's name
|
|
741
|
+
# when it is defined below (order is not tracked): both.
|
|
742
|
+
module_binding = self._lookup_in_module(scope.module, name, set())
|
|
743
|
+
return _with_alternatives(
|
|
744
|
+
[Resolved(scope.class_members[name])]
|
|
745
|
+
+ ([module_binding] if module_binding is not None else [])
|
|
746
|
+
)
|
|
747
|
+
if name in scope.locals:
|
|
748
|
+
if name in scope.param_aliases:
|
|
749
|
+
return Resolved(scope.param_aliases[name])
|
|
750
|
+
return Local()
|
|
751
|
+
found = self._lookup_in_module(scope.module, name, set())
|
|
752
|
+
if found is not None:
|
|
753
|
+
return found
|
|
754
|
+
if name in BUILTIN_NAMES or (name.startswith("__") and name.endswith("__")):
|
|
755
|
+
return None
|
|
756
|
+
return Unresolved(UNRESOLVED_NAME, name)
|
|
757
|
+
|
|
758
|
+
def _lookup_in_module(self, target: ModuleScope, name: str, seen: set[str]) -> Node:
|
|
759
|
+
"""Resolve ``name`` as seen from inside ``target``'s global namespace.
|
|
760
|
+
|
|
761
|
+
Order: own definitions, import aliases, module-level variables,
|
|
762
|
+
submodules, then star imports. Every analysed star-imported module is
|
|
763
|
+
consulted before an external star import is blamed, so an in-scope
|
|
764
|
+
symbol is never misattributed to a third-party package.
|
|
765
|
+
"""
|
|
766
|
+
# Star imports bind too, and the last binding wins at runtime, which
|
|
767
|
+
# statement order alone does not settle (``from a import f`` then
|
|
768
|
+
# ``from b import *``; Django-style settings star-importing a base
|
|
769
|
+
# and then a local override): every in-scope candidate counts.
|
|
770
|
+
stars, external = self._star_hits(target, name, seen) if target.star_imports else ([], None)
|
|
771
|
+
if name in target.members or name in target.imports:
|
|
772
|
+
# Every binding of the name: a definition beside an import of the
|
|
773
|
+
# same name (a fallback), and imports one of which overwrites
|
|
774
|
+
# another in a try/except or if/else.
|
|
775
|
+
bindings: list[Node] = []
|
|
776
|
+
if name in target.members:
|
|
777
|
+
bindings.append(Resolved(target.members[name]))
|
|
778
|
+
if name in target.imports:
|
|
779
|
+
bindings.append(self._import_binding_node(target.imports[name]))
|
|
780
|
+
for binding in target.alt_imports.get(name, ()):
|
|
781
|
+
bindings.append(self._import_binding_node(binding))
|
|
782
|
+
return _with_alternatives(bindings + stars)
|
|
783
|
+
if name in target.variables:
|
|
784
|
+
return _with_alternatives([Resolved(target.variables[name]), *stars])
|
|
785
|
+
if name in target.bindings:
|
|
786
|
+
return _with_alternatives([Resolved(target.name, detail=f"attribute:{name}"), *stars])
|
|
787
|
+
if self._module_in_scope(f"{target.name}.{name}"):
|
|
788
|
+
return ModuleNode(f"{target.name}.{name}")
|
|
789
|
+
if stars:
|
|
790
|
+
return _with_alternatives(stars)
|
|
791
|
+
return External(external) if external is not None else None
|
|
792
|
+
|
|
793
|
+
def _star_hits(
|
|
794
|
+
self, target: ModuleScope, name: str, seen: set[str]
|
|
795
|
+
) -> tuple[list[Node], str | None]:
|
|
796
|
+
"""What each of ``target``'s star imports binds ``name`` to (in-scope
|
|
797
|
+
hits, in order), and the first out-of-scope star import that might."""
|
|
798
|
+
hits: list[Node] = []
|
|
799
|
+
external: str | None = None
|
|
800
|
+
for star in target.star_imports:
|
|
801
|
+
if star in seen:
|
|
802
|
+
continue
|
|
803
|
+
seen.add(star)
|
|
804
|
+
star_scope = self.scopes.get(star)
|
|
805
|
+
if star_scope is None:
|
|
806
|
+
if external is None and not self._module_in_scope(star):
|
|
807
|
+
external = star
|
|
808
|
+
continue
|
|
809
|
+
found = self._lookup_in_module(star_scope, name, seen)
|
|
810
|
+
if isinstance(found, External):
|
|
811
|
+
external = external or found.module
|
|
812
|
+
elif found is not None:
|
|
813
|
+
hits.append(found)
|
|
814
|
+
return hits, external
|
|
815
|
+
|
|
816
|
+
def _import_binding_node(self, binding: ImportBinding) -> Node:
|
|
817
|
+
if not binding.module:
|
|
818
|
+
# Bound by a relative import above the top-level package (see
|
|
819
|
+
# _import_edges, which records the import as unbounded).
|
|
820
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, binding.attr or "")
|
|
821
|
+
if not self._module_in_scope(binding.module):
|
|
822
|
+
if self._module_in_scope(binding.module.split(".")[0]):
|
|
823
|
+
# ``pkg.missing`` inside an analysed package: not an external
|
|
824
|
+
# dependency, but nothing we can see either (deleted module,
|
|
825
|
+
# compiled extension, generated code).
|
|
826
|
+
name = binding.attr or binding.module.rsplit(".", 1)[-1]
|
|
827
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, name)
|
|
828
|
+
return External(binding.module)
|
|
829
|
+
node: Node = ModuleNode(binding.module)
|
|
830
|
+
if binding.attr is not None:
|
|
831
|
+
# ``pkg/__init__.py: from pkg import ext`` with no ``ext`` module
|
|
832
|
+
# (a compiled extension) resolves through itself: stop there.
|
|
833
|
+
key = (binding.module, binding.attr)
|
|
834
|
+
if key in self._resolving_bindings:
|
|
835
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, binding.attr)
|
|
836
|
+
self._resolving_bindings.add(key)
|
|
837
|
+
try:
|
|
838
|
+
node = self._step(node, binding.attr)
|
|
839
|
+
finally:
|
|
840
|
+
self._resolving_bindings.discard(key)
|
|
841
|
+
return node
|
|
842
|
+
|
|
843
|
+
def _step(self, node: Node, attr: str) -> Node:
|
|
844
|
+
"""Resolve one attribute access on a resolved node."""
|
|
845
|
+
if isinstance(node, ModuleNode):
|
|
846
|
+
sub = f"{node.module}.{attr}"
|
|
847
|
+
target = self.scopes.get(node.module)
|
|
848
|
+
if self._module_in_scope(sub):
|
|
849
|
+
# A package binding of the same name wins at runtime once the
|
|
850
|
+
# package has run (it binds after importing the submodule);
|
|
851
|
+
# either may be meant, so the attribute denotes both.
|
|
852
|
+
shadow: Resolved | None = None
|
|
853
|
+
if target is not None:
|
|
854
|
+
symbol_id = target.members.get(attr) or target.variables.get(attr)
|
|
855
|
+
if symbol_id is not None:
|
|
856
|
+
shadow = Resolved(symbol_id)
|
|
857
|
+
elif attr in target.imports:
|
|
858
|
+
# ``from .main import main``: the re-exported name.
|
|
859
|
+
bound = self._import_binding_node(target.imports[attr])
|
|
860
|
+
if isinstance(bound, Resolved) and bound.symbol != sub:
|
|
861
|
+
shadow = Resolved(bound.symbol, bound.detail)
|
|
862
|
+
elif attr in target.bindings:
|
|
863
|
+
shadow = Resolved(target.name, detail=f"attribute:{attr}")
|
|
864
|
+
if shadow is not None and sub in self.scopes:
|
|
865
|
+
return Resolved(shadow.symbol, shadow.detail, also=(sub,))
|
|
866
|
+
return ModuleNode(sub)
|
|
867
|
+
if target is None:
|
|
868
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, attr)
|
|
869
|
+
found = self._lookup_in_module(target, attr, set())
|
|
870
|
+
if found is None or isinstance(found, Unresolved):
|
|
871
|
+
lazy = target.members.get("__getattr__")
|
|
872
|
+
if lazy is not None:
|
|
873
|
+
# PEP 562: a module ``__getattr__`` serves names the module
|
|
874
|
+
# does not bind (lazy loading): the reference depends on it,
|
|
875
|
+
# and stays bounded by the name for what it hands back.
|
|
876
|
+
return Resolved(lazy, uncertain_attr=attr)
|
|
877
|
+
return found if found is not None else Unresolved(UNRESOLVED_ATTRIBUTE, attr)
|
|
878
|
+
if isinstance(node, External):
|
|
879
|
+
return node
|
|
880
|
+
if isinstance(node, Resolved):
|
|
881
|
+
if node.detail:
|
|
882
|
+
return node # attribute of an opaque module/class attribute: stop here
|
|
883
|
+
symbol = self.index.symbols.get(node.symbol)
|
|
884
|
+
if symbol is None:
|
|
885
|
+
return node
|
|
886
|
+
if symbol.kind == CLASS:
|
|
887
|
+
return self.lookup_in_class(symbol.id, attr, dispatch=node.receiver)
|
|
888
|
+
if symbol.kind == MODULE:
|
|
889
|
+
return self._step(ModuleNode(symbol.id), attr)
|
|
890
|
+
return node # attribute on a function object
|
|
891
|
+
if isinstance(node, Unresolved):
|
|
892
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, attr)
|
|
893
|
+
return None
|
|
894
|
+
|
|
895
|
+
def resolve_chain(self, parts: list[str], scope: Scope) -> Node:
|
|
896
|
+
node = self._lookup_base(parts[0], scope)
|
|
897
|
+
if isinstance(node, Local):
|
|
898
|
+
# ``obj.method`` on a local: the method name bounds what it may be.
|
|
899
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, parts[1]) if len(parts) > 1 else None
|
|
900
|
+
for attr in parts[1:]:
|
|
901
|
+
if node is None:
|
|
902
|
+
return None
|
|
903
|
+
if isinstance(node, Resolved) and node.detail:
|
|
904
|
+
return node
|
|
905
|
+
node = self._step(node, attr)
|
|
906
|
+
return node
|
|
907
|
+
|
|
908
|
+
def resolve_chain_names(self, parts: list[str], scope: Scope) -> tuple[Node, tuple[str, ...]]:
|
|
909
|
+
"""Like resolve_chain, plus the attribute names after the point where
|
|
910
|
+
resolution stopped (an unknown or opaque value: a local, a failed
|
|
911
|
+
step, a variable, a function, a class-level binding). Each is looked
|
|
912
|
+
up on a value of unknown type, so each is a name-bounded reference."""
|
|
913
|
+
node = self._lookup_base(parts[0], scope)
|
|
914
|
+
if isinstance(node, Local):
|
|
915
|
+
if len(parts) < 2:
|
|
916
|
+
return None, ()
|
|
917
|
+
return Unresolved(UNRESOLVED_ATTRIBUTE, parts[1]), tuple(parts[2:])
|
|
918
|
+
for i, attr in enumerate(parts[1:], 1):
|
|
919
|
+
if node is None or isinstance(node, External):
|
|
920
|
+
return node, ()
|
|
921
|
+
if isinstance(node, Resolved):
|
|
922
|
+
symbol = self.index.symbols.get(node.symbol)
|
|
923
|
+
opaque = node.detail or (symbol is not None and symbol.kind not in (CLASS, MODULE))
|
|
924
|
+
if opaque:
|
|
925
|
+
return node, tuple(parts[i:])
|
|
926
|
+
node = self._step(node, attr)
|
|
927
|
+
if isinstance(node, Unresolved):
|
|
928
|
+
return node, tuple(parts[i + 1 :])
|
|
929
|
+
return node, ()
|
|
930
|
+
|
|
931
|
+
def class_creation(
|
|
932
|
+
self, source: str, bases: list[str], keywords: list[ast.keyword], scope: Scope
|
|
933
|
+
) -> None:
|
|
934
|
+
"""Creating a class runs code of its bases and metaclass: the
|
|
935
|
+
``__init_subclass__`` that the new class's MRO finds after itself
|
|
936
|
+
(the union of what each in-scope base finds covers it), and an
|
|
937
|
+
in-scope metaclass's ``__new__`` and ``__init__``."""
|
|
938
|
+
hooks: list[tuple[str, str]] = []
|
|
939
|
+
for base in bases:
|
|
940
|
+
hooks.append((base, "__init_subclass__"))
|
|
941
|
+
for kw in keywords:
|
|
942
|
+
parts = _flatten_chain(kw.value) if kw.arg == "metaclass" else None
|
|
943
|
+
if parts is None:
|
|
944
|
+
continue
|
|
945
|
+
meta = self.resolve_chain(parts, scope)
|
|
946
|
+
if isinstance(meta, Resolved) and not meta.detail and meta.symbol in self.class_scopes:
|
|
947
|
+
hooks += [(meta.symbol, "__new__"), (meta.symbol, "__init__")]
|
|
948
|
+
for class_id, name in hooks:
|
|
949
|
+
hit = self.lookup_in_class(class_id, name)
|
|
950
|
+
if isinstance(hit, Resolved) and not hit.detail and hit.symbol != source:
|
|
951
|
+
self.out.edges.add(Edge(source, hit.symbol, REFERENCES, name))
|
|
952
|
+
|
|
953
|
+
def escape(self, target: Resolved, *, classes: bool = True) -> None:
|
|
954
|
+
"""Record that ``target`` is used as a value (see _mark_escape)."""
|
|
955
|
+
symbol = self.index.symbols.get(target.symbol)
|
|
956
|
+
if symbol is None:
|
|
957
|
+
return
|
|
958
|
+
if symbol.kind in (FUNCTION, METHOD) or (classes and symbol.kind == CLASS):
|
|
959
|
+
self.out.escapes.add(symbol.id)
|
|
960
|
+
for override_id, detail in target.overrides:
|
|
961
|
+
if not detail:
|
|
962
|
+
self.out.escapes.add(override_id)
|
|
963
|
+
|
|
964
|
+
def escape_class_family(self, class_id: str) -> None:
|
|
965
|
+
"""The class of an instance of ``class_id``: any in-scope subclass."""
|
|
966
|
+
self.out.escapes.add(class_id)
|
|
967
|
+
self.out.escapes.update(self._descendants.get(class_id, ()))
|
|
968
|
+
|
|
969
|
+
def _record(self, source: str, node: Node, kind: str = REFERENCES, chain: str = "") -> None:
|
|
970
|
+
if node is None:
|
|
971
|
+
return
|
|
972
|
+
if isinstance(node, Resolved):
|
|
973
|
+
if node.symbol != source:
|
|
974
|
+
self.out.edges.add(Edge(source, node.symbol, kind, node.detail))
|
|
975
|
+
if node.uncertain_attr:
|
|
976
|
+
self.out.unresolved.add(
|
|
977
|
+
UnresolvedReference(source, UNRESOLVED_ATTRIBUTE, node.uncertain_attr, chain)
|
|
978
|
+
)
|
|
979
|
+
for symbol_id, detail in node.overrides:
|
|
980
|
+
if symbol_id != source:
|
|
981
|
+
label = f"override:{detail}" if detail else "override"
|
|
982
|
+
self.out.edges.add(Edge(source, symbol_id, kind, label))
|
|
983
|
+
for module in node.also:
|
|
984
|
+
if module != source:
|
|
985
|
+
self.out.edges.add(Edge(source, module, kind, "module"))
|
|
986
|
+
for other in node.alternatives:
|
|
987
|
+
if other != source:
|
|
988
|
+
self.out.edges.add(Edge(source, other, kind, "alternative binding"))
|
|
989
|
+
if kind == REFERENCES and not node.detail and node.symbol in self.class_scopes:
|
|
990
|
+
# Using a class (``Foo(...)``, subclassing) runs its constructor.
|
|
991
|
+
for hook in ("__init__", "__new__"):
|
|
992
|
+
found = self.lookup_in_class(node.symbol, hook)
|
|
993
|
+
if isinstance(found, Resolved) and found.symbol != source:
|
|
994
|
+
self.out.edges.add(Edge(source, found.symbol, REFERENCES, "constructor"))
|
|
995
|
+
elif isinstance(node, ModuleNode):
|
|
996
|
+
if node.module in self.scopes and node.module != source:
|
|
997
|
+
self.out.edges.add(Edge(source, node.module, kind, "module"))
|
|
998
|
+
elif isinstance(node, External):
|
|
999
|
+
self.out.external.add(ExternalReference(source, node.module))
|
|
1000
|
+
elif isinstance(node, Unresolved):
|
|
1001
|
+
self.out.unresolved.add(UnresolvedReference(source, node.kind, node.name, chain))
|