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,441 @@
|
|
|
1
|
+
"""Pass 1: a module's statements and definitions become symbols, with
|
|
2
|
+
their hashes, line spans and import tables."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from diffcone.indexer.definitions import (
|
|
10
|
+
_annotated_args,
|
|
11
|
+
_canonical_imports,
|
|
12
|
+
_end_line,
|
|
13
|
+
_flatten_chain,
|
|
14
|
+
_future_annotations,
|
|
15
|
+
_has_annotations,
|
|
16
|
+
_import_layout,
|
|
17
|
+
_inert_def,
|
|
18
|
+
_is_inert_decorator,
|
|
19
|
+
_is_literal,
|
|
20
|
+
_is_special_method,
|
|
21
|
+
_start_line,
|
|
22
|
+
_variable_statements,
|
|
23
|
+
)
|
|
24
|
+
from diffcone.indexer.literals import (
|
|
25
|
+
_collect_literal_bindings,
|
|
26
|
+
_collect_store_names,
|
|
27
|
+
_module_mutations,
|
|
28
|
+
)
|
|
29
|
+
from diffcone.indexer.scopes import (
|
|
30
|
+
ClassScope,
|
|
31
|
+
ImportBinding,
|
|
32
|
+
ModuleScope,
|
|
33
|
+
VariableStatement,
|
|
34
|
+
_absolute_module,
|
|
35
|
+
)
|
|
36
|
+
from diffcone.indexer.state import IndexerState
|
|
37
|
+
from diffcone.indexer.syntax import (
|
|
38
|
+
DEF_NODES,
|
|
39
|
+
FUNC_NODES,
|
|
40
|
+
_digest,
|
|
41
|
+
_docstring_hash,
|
|
42
|
+
_split_docstring,
|
|
43
|
+
hash_nodes,
|
|
44
|
+
hash_scope_body,
|
|
45
|
+
iter_scope_statements,
|
|
46
|
+
)
|
|
47
|
+
from diffcone.model import (
|
|
48
|
+
CLASS,
|
|
49
|
+
DEFINED_IN,
|
|
50
|
+
FUNCTION,
|
|
51
|
+
METHOD,
|
|
52
|
+
MODULE,
|
|
53
|
+
REFERENCES,
|
|
54
|
+
VARIABLE,
|
|
55
|
+
Edge,
|
|
56
|
+
Symbol,
|
|
57
|
+
)
|
|
58
|
+
from diffcone.snapshot import member_symbol_id
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _doc_reads(body: list[ast.stmt]):
|
|
62
|
+
"""``__doc__`` names and attributes, and ``getdoc(...)`` calls, in a
|
|
63
|
+
scope's own statements (not in nested definitions, which are symbols
|
|
64
|
+
of their own)."""
|
|
65
|
+
stack: list[ast.AST] = list(body)
|
|
66
|
+
while stack:
|
|
67
|
+
node = stack.pop()
|
|
68
|
+
if isinstance(node, DEF_NODES):
|
|
69
|
+
# Decorators, defaults and bases run in this scope; the body not.
|
|
70
|
+
stack.extend(node.decorator_list)
|
|
71
|
+
if isinstance(node, ast.ClassDef):
|
|
72
|
+
stack.extend([*node.bases, *node.keywords])
|
|
73
|
+
else:
|
|
74
|
+
stack.extend([*node.args.defaults, *(d for d in node.args.kw_defaults if d)])
|
|
75
|
+
continue
|
|
76
|
+
# Reads only: ``__doc__ = """..."""`` assigns a docstring, it reads none.
|
|
77
|
+
if isinstance(node, ast.Name) and node.id == "__doc__":
|
|
78
|
+
if isinstance(node.ctx, ast.Load):
|
|
79
|
+
yield node
|
|
80
|
+
elif isinstance(node, ast.Attribute) and node.attr == "__doc__":
|
|
81
|
+
if isinstance(node.ctx, ast.Load):
|
|
82
|
+
yield node
|
|
83
|
+
elif isinstance(node, ast.Call):
|
|
84
|
+
func = node.func
|
|
85
|
+
name = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "")
|
|
86
|
+
if name == "getdoc":
|
|
87
|
+
yield node
|
|
88
|
+
stack.extend(ast.iter_child_nodes(node))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _reads_docstrings_deep(node: ast.AST) -> bool:
|
|
92
|
+
"""Like _reads_docstrings, nested definitions included."""
|
|
93
|
+
for inner in ast.walk(node):
|
|
94
|
+
if isinstance(inner, (ast.Name, ast.Attribute)) and isinstance(inner.ctx, ast.Load):
|
|
95
|
+
if getattr(inner, "id", None) == "__doc__" or getattr(inner, "attr", None) == "__doc__":
|
|
96
|
+
return True
|
|
97
|
+
if isinstance(inner, ast.Call):
|
|
98
|
+
func = inner.func
|
|
99
|
+
name = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "")
|
|
100
|
+
if name == "getdoc":
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _reads_docstrings(body: list[ast.stmt]) -> bool:
|
|
106
|
+
return next(_doc_reads(body), None) is not None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _names_module_doc(body: list[ast.stmt]) -> bool:
|
|
110
|
+
return any(isinstance(n, ast.Name) for n in _doc_reads(body))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class FirstPass(IndexerState):
|
|
114
|
+
"""Pass 1: modules and their definitions into symbols."""
|
|
115
|
+
|
|
116
|
+
def _module_statements(
|
|
117
|
+
self, scope: ModuleScope, *, register_imports: bool
|
|
118
|
+
) -> tuple[list[ast.stmt], list[ast.stmt], dict[str, VariableStatement]]:
|
|
119
|
+
"""(all scope statements, body without docstring, variable statements)
|
|
120
|
+
of a parsed module; records the import statements and, unless the
|
|
121
|
+
import table was served by the cache, fills it."""
|
|
122
|
+
assert scope.tree is not None
|
|
123
|
+
stmts = list(iter_scope_statements(scope.tree.body))
|
|
124
|
+
scope.import_nodes = [s for s in stmts if isinstance(s, (ast.Import, ast.ImportFrom))]
|
|
125
|
+
if register_imports:
|
|
126
|
+
for node in scope.import_nodes:
|
|
127
|
+
self._register_imports(
|
|
128
|
+
scope, node, scope.imports, scope.star_imports, scope.alt_imports
|
|
129
|
+
)
|
|
130
|
+
body = _split_docstring(scope.tree.body)[1]
|
|
131
|
+
return stmts, body, _variable_statements(scope, body)
|
|
132
|
+
|
|
133
|
+
def _index_module(self, scope: ModuleScope) -> None:
|
|
134
|
+
assert scope.tree is not None
|
|
135
|
+
stmts, body, variable_stmts = self._module_statements(scope, register_imports=True)
|
|
136
|
+
for stmt in stmts:
|
|
137
|
+
if not isinstance(stmt, DEF_NODES + (ast.Import, ast.ImportFrom)):
|
|
138
|
+
scope.bindings |= _collect_store_names(stmt)
|
|
139
|
+
scope.literal_names = _collect_literal_bindings(scope.tree, {})
|
|
140
|
+
scope.mutations = frozenset(_module_mutations(scope.tree))
|
|
141
|
+
imports = tuple(sorted(_canonical_imports(scope)))
|
|
142
|
+
layout = _import_layout(scope)
|
|
143
|
+
# A variable statement is its own symbol, so it leaves the module's
|
|
144
|
+
# body hash; one rebinding a def or class name (``helper = 3`` after
|
|
145
|
+
# ``def helper``) is not a variable symbol and stays in it.
|
|
146
|
+
defined = {s.name for s in stmts if isinstance(s, DEF_NODES)}
|
|
147
|
+
own_symbol = {id(stmt) for name, stmt in variable_stmts.items() if name not in defined}
|
|
148
|
+
module_body_hash = hash_scope_body(
|
|
149
|
+
[s for s in body if id(s) not in own_symbol], strip_imports=True
|
|
150
|
+
)
|
|
151
|
+
module_doc_hash = _docstring_hash([scope.tree.body])
|
|
152
|
+
module_reads_doc = _reads_docstrings(scope.tree.body)
|
|
153
|
+
if module_reads_doc and _names_module_doc(scope.tree.body):
|
|
154
|
+
# ``ArgumentParser(description=__doc__)``: import-time code reads
|
|
155
|
+
# the module's own docstring.
|
|
156
|
+
module_body_hash = _digest(module_body_hash + "|doc:" + module_doc_hash)
|
|
157
|
+
self._add_symbol(
|
|
158
|
+
Symbol(
|
|
159
|
+
id=scope.name,
|
|
160
|
+
kind=MODULE,
|
|
161
|
+
module=scope.name,
|
|
162
|
+
name=scope.name.rsplit(".", 1)[-1],
|
|
163
|
+
path=scope.path,
|
|
164
|
+
lineno=1,
|
|
165
|
+
body_hash=module_body_hash,
|
|
166
|
+
docstring_hash=module_doc_hash,
|
|
167
|
+
definition_hash=_digest("\n".join(imports) + "|layout|" + "\n".join(layout)),
|
|
168
|
+
container=None,
|
|
169
|
+
line_ranges=((1, _end_line(scope.tree)),),
|
|
170
|
+
imports=imports,
|
|
171
|
+
import_layout=layout,
|
|
172
|
+
reads_docstrings=module_reads_doc,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
self._index_definitions(scope, scope.tree.body, scope.name, scope.members, None)
|
|
176
|
+
# Module-level statements that mention a variable may mutate it in place
|
|
177
|
+
# (``REGISTRY[k] = v``, ``NAMES.append(x)``, ``CONFIG.update(...)``), so
|
|
178
|
+
# they are part of that variable's body, not only of the module's.
|
|
179
|
+
mutators: dict[str, list[ast.stmt]] = defaultdict(list)
|
|
180
|
+
variable_ids = {id(s) for s in variable_stmts.values()}
|
|
181
|
+
for stmt in body:
|
|
182
|
+
if isinstance(stmt, DEF_NODES + (ast.Import, ast.ImportFrom)):
|
|
183
|
+
continue
|
|
184
|
+
if id(stmt) in variable_ids:
|
|
185
|
+
continue
|
|
186
|
+
mentioned = {
|
|
187
|
+
n.id for n in ast.walk(stmt) if isinstance(n, ast.Name) and n.id in variable_stmts
|
|
188
|
+
}
|
|
189
|
+
for name in mentioned:
|
|
190
|
+
mutators[name].append(stmt)
|
|
191
|
+
for name, stmt in variable_stmts.items():
|
|
192
|
+
if name in scope.members:
|
|
193
|
+
continue # also a def/class: kept in the module body hash above
|
|
194
|
+
symbol_id = self._member_id(scope.name, name)
|
|
195
|
+
value = stmt.value
|
|
196
|
+
assert value is not None # _variable_statements keeps assignments with a value
|
|
197
|
+
symbol = Symbol(
|
|
198
|
+
id=symbol_id,
|
|
199
|
+
kind=VARIABLE,
|
|
200
|
+
module=scope.name,
|
|
201
|
+
name=name,
|
|
202
|
+
path=scope.path,
|
|
203
|
+
lineno=stmt.lineno,
|
|
204
|
+
body_hash=hash_nodes([value, *mutators.get(name, [])]),
|
|
205
|
+
# An annotation is evaluated at import unless deferred: a
|
|
206
|
+
# change to it is a definition change (see classify).
|
|
207
|
+
annotation_hash=(
|
|
208
|
+
_digest(hash_nodes([stmt.annotation]))
|
|
209
|
+
if isinstance(stmt, ast.AnnAssign)
|
|
210
|
+
else ""
|
|
211
|
+
),
|
|
212
|
+
deferred_annotations=_future_annotations(scope),
|
|
213
|
+
definition_hash="",
|
|
214
|
+
container=scope.name,
|
|
215
|
+
line_ranges=tuple(
|
|
216
|
+
(s.lineno, _end_line(s)) for s in (stmt, *mutators.get(name, []))
|
|
217
|
+
),
|
|
218
|
+
# Binding a literal runs no code when the module is imported;
|
|
219
|
+
# only readers of the value can observe the change. ``__all__``
|
|
220
|
+
# is not inert: it decides what ``from m import *`` binds.
|
|
221
|
+
inert_definition=(
|
|
222
|
+
name != "__all__" and not mutators.get(name) and _is_literal(value)
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
if self._add_symbol(symbol):
|
|
226
|
+
scope.variables[name] = symbol_id
|
|
227
|
+
scope.variable_stmts[name] = stmt
|
|
228
|
+
self.out.edges.add(Edge(symbol_id, scope.name, DEFINED_IN))
|
|
229
|
+
|
|
230
|
+
def _register_imports(
|
|
231
|
+
self,
|
|
232
|
+
scope: ModuleScope,
|
|
233
|
+
node: ast.stmt,
|
|
234
|
+
table: dict[str, ImportBinding],
|
|
235
|
+
stars: list[str],
|
|
236
|
+
alternatives: dict[str, list[ImportBinding]] | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
def bind(name: str, binding: ImportBinding) -> None:
|
|
239
|
+
previous = table.get(name)
|
|
240
|
+
if alternatives is not None and previous is not None and previous != binding:
|
|
241
|
+
if previous not in alternatives.setdefault(name, []):
|
|
242
|
+
alternatives[name].append(previous)
|
|
243
|
+
table[name] = binding
|
|
244
|
+
|
|
245
|
+
if isinstance(node, ast.Import):
|
|
246
|
+
for alias in node.names:
|
|
247
|
+
if alias.asname:
|
|
248
|
+
bind(alias.asname, ImportBinding(alias.name, None))
|
|
249
|
+
else:
|
|
250
|
+
bind(alias.name.split(".")[0], ImportBinding(alias.name.split(".")[0], None))
|
|
251
|
+
elif isinstance(node, ast.ImportFrom):
|
|
252
|
+
base = _absolute_module(scope, node.module, node.level)
|
|
253
|
+
for alias in node.names:
|
|
254
|
+
if alias.name == "*":
|
|
255
|
+
stars.append(base)
|
|
256
|
+
else:
|
|
257
|
+
bind(alias.asname or alias.name, ImportBinding(base, alias.name))
|
|
258
|
+
|
|
259
|
+
def _index_definitions(
|
|
260
|
+
self,
|
|
261
|
+
scope: ModuleScope,
|
|
262
|
+
body: list[ast.stmt],
|
|
263
|
+
container_id: str,
|
|
264
|
+
members: dict[str, str],
|
|
265
|
+
class_scope: ClassScope | None,
|
|
266
|
+
) -> None:
|
|
267
|
+
funcs: dict[str, list[ast.FunctionDef | ast.AsyncFunctionDef]] = {}
|
|
268
|
+
classes: dict[str, list[ast.ClassDef]] = {}
|
|
269
|
+
order: list[str] = []
|
|
270
|
+
for stmt in iter_scope_statements(body):
|
|
271
|
+
if isinstance(stmt, FUNC_NODES):
|
|
272
|
+
funcs.setdefault(stmt.name, []).append(stmt)
|
|
273
|
+
elif isinstance(stmt, ast.ClassDef):
|
|
274
|
+
classes.setdefault(stmt.name, []).append(stmt)
|
|
275
|
+
else:
|
|
276
|
+
continue
|
|
277
|
+
if stmt.name not in order:
|
|
278
|
+
order.append(stmt.name)
|
|
279
|
+
for name in order:
|
|
280
|
+
if class_scope is None and container_id == scope.name:
|
|
281
|
+
symbol_id = self._member_id(scope.name, name)
|
|
282
|
+
else:
|
|
283
|
+
symbol_id = f"{container_id}.{name}"
|
|
284
|
+
if name in classes:
|
|
285
|
+
nodes = classes[name]
|
|
286
|
+
first = nodes[0]
|
|
287
|
+
member_names = sorted(
|
|
288
|
+
{
|
|
289
|
+
s.name
|
|
290
|
+
for n in nodes
|
|
291
|
+
for s in iter_scope_statements(n.body)
|
|
292
|
+
if isinstance(s, DEF_NODES)
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def class_hashes(nodes=nodes, member_names=member_names):
|
|
297
|
+
definition_parts: list[ast.AST] = []
|
|
298
|
+
for n in nodes:
|
|
299
|
+
definition_parts += (
|
|
300
|
+
list(n.bases) + list(n.keywords) + list(n.decorator_list)
|
|
301
|
+
)
|
|
302
|
+
return (
|
|
303
|
+
_digest(
|
|
304
|
+
"\n".join(
|
|
305
|
+
hash_scope_body(_split_docstring(n.body)[1], strip_imports=False)
|
|
306
|
+
for n in nodes
|
|
307
|
+
)
|
|
308
|
+
),
|
|
309
|
+
_digest(hash_nodes(definition_parts) + "|" + ",".join(member_names)),
|
|
310
|
+
_docstring_hash([n.body for n in nodes]),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
body_hash, definition_hash, doc_hash = class_hashes()
|
|
314
|
+
symbol = Symbol(
|
|
315
|
+
id=symbol_id,
|
|
316
|
+
kind=CLASS,
|
|
317
|
+
module=scope.name,
|
|
318
|
+
name=name,
|
|
319
|
+
path=scope.path,
|
|
320
|
+
lineno=first.lineno,
|
|
321
|
+
body_hash=body_hash,
|
|
322
|
+
definition_hash=definition_hash,
|
|
323
|
+
container=container_id,
|
|
324
|
+
line_ranges=tuple((_start_line(n), _end_line(n)) for n in nodes),
|
|
325
|
+
docstring_hash=doc_hash,
|
|
326
|
+
reads_docstrings=any(_reads_docstrings(n.body) for n in nodes),
|
|
327
|
+
)
|
|
328
|
+
if not self._add_symbol(symbol):
|
|
329
|
+
continue
|
|
330
|
+
members[name] = symbol_id
|
|
331
|
+
self.out.edges.add(Edge(symbol_id, container_id, DEFINED_IN))
|
|
332
|
+
cscope = ClassScope(id=symbol_id, module=scope, enclosing=class_scope)
|
|
333
|
+
cscope.plain = len(nodes) == 1 and not (first.decorator_list or first.keywords)
|
|
334
|
+
for n in nodes:
|
|
335
|
+
cscope.base_chains.extend(_flatten_chain(b) for b in n.bases)
|
|
336
|
+
cscope.base_names.extend(
|
|
337
|
+
_flatten_chain(b.value if isinstance(b, ast.Subscript) else b)
|
|
338
|
+
for b in n.bases
|
|
339
|
+
)
|
|
340
|
+
self.class_scopes[symbol_id] = cscope
|
|
341
|
+
self._added_classes.append(cscope)
|
|
342
|
+
for n in nodes:
|
|
343
|
+
for stmt in iter_scope_statements(n.body):
|
|
344
|
+
if not isinstance(stmt, DEF_NODES):
|
|
345
|
+
cscope.bindings |= _collect_store_names(stmt)
|
|
346
|
+
# Every definition of the class (``if``/``else`` variants) is
|
|
347
|
+
# one symbol, so its members are indexed together: a method
|
|
348
|
+
# defined in several of them is one symbol too.
|
|
349
|
+
bodies = [stmt for n in nodes for stmt in n.body]
|
|
350
|
+
self._index_definitions(scope, bodies, symbol_id, cscope.members, cscope)
|
|
351
|
+
# Special methods run implicitly on instances (``==``, ``len()``,
|
|
352
|
+
# calling one, ``with``): whatever references the class may
|
|
353
|
+
# trigger them, so the class depends on them.
|
|
354
|
+
for member, member_id in sorted(cscope.members.items()):
|
|
355
|
+
member_symbol = self.index.symbols.get(member_id)
|
|
356
|
+
if (
|
|
357
|
+
_is_special_method(member)
|
|
358
|
+
and member_symbol is not None
|
|
359
|
+
and member_symbol.kind == METHOD
|
|
360
|
+
):
|
|
361
|
+
self.out.edges.add(Edge(symbol_id, member_id, REFERENCES, "special_method"))
|
|
362
|
+
else:
|
|
363
|
+
nodes = funcs[name]
|
|
364
|
+
first = nodes[0]
|
|
365
|
+
|
|
366
|
+
def function_hashes(nodes=nodes):
|
|
367
|
+
definition_parts: list[ast.AST] = []
|
|
368
|
+
annotation_parts: list[ast.AST] = []
|
|
369
|
+
for n in nodes:
|
|
370
|
+
definition_parts.append(n.args)
|
|
371
|
+
definition_parts += list(n.decorator_list)
|
|
372
|
+
if n.returns is not None:
|
|
373
|
+
annotation_parts.append(n.returns)
|
|
374
|
+
# The definition hash excludes annotations: detach them
|
|
375
|
+
# while hashing (no copy of the tree) and restore them.
|
|
376
|
+
detached = [a for n in nodes for a in _annotated_args(n.args)]
|
|
377
|
+
annotations = [a.annotation for a in detached]
|
|
378
|
+
for a in detached:
|
|
379
|
+
a.annotation = None
|
|
380
|
+
try:
|
|
381
|
+
definition = hash_nodes(definition_parts)
|
|
382
|
+
finally:
|
|
383
|
+
for a, annotation in zip(detached, annotations, strict=True):
|
|
384
|
+
a.annotation = annotation
|
|
385
|
+
annotation_parts = [
|
|
386
|
+
*(a for a in annotations if a is not None),
|
|
387
|
+
*annotation_parts,
|
|
388
|
+
]
|
|
389
|
+
return (
|
|
390
|
+
_digest(hash_nodes(annotation_parts)),
|
|
391
|
+
_digest(
|
|
392
|
+
"\n".join(hash_nodes(list(_split_docstring(n.body)[1])) for n in nodes)
|
|
393
|
+
),
|
|
394
|
+
_digest(definition + "|" + ",".join(type(n).__name__ for n in nodes)),
|
|
395
|
+
_docstring_hash([n.body for n in nodes]),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
annotation_hash, body_hash, definition_hash, doc_hash = function_hashes()
|
|
399
|
+
deferred = (
|
|
400
|
+
_future_annotations(scope)
|
|
401
|
+
and all(_is_inert_decorator(d, scope) for n in nodes for d in n.decorator_list)
|
|
402
|
+
and (class_scope is None or class_scope.plain)
|
|
403
|
+
)
|
|
404
|
+
header = all(_inert_def(n, scope) for n in nodes) and (
|
|
405
|
+
class_scope is None or class_scope.plain
|
|
406
|
+
)
|
|
407
|
+
inert = header and (deferred or not any(_has_annotations(n) for n in nodes))
|
|
408
|
+
symbol = Symbol(
|
|
409
|
+
id=symbol_id,
|
|
410
|
+
kind=METHOD if class_scope is not None else FUNCTION,
|
|
411
|
+
module=scope.name,
|
|
412
|
+
name=name,
|
|
413
|
+
path=scope.path,
|
|
414
|
+
lineno=first.lineno,
|
|
415
|
+
body_hash=body_hash,
|
|
416
|
+
docstring_hash=doc_hash,
|
|
417
|
+
definition_hash=definition_hash,
|
|
418
|
+
container=container_id,
|
|
419
|
+
line_ranges=tuple((_start_line(n), _end_line(n)) for n in nodes),
|
|
420
|
+
annotation_hash=annotation_hash,
|
|
421
|
+
deferred_annotations=deferred,
|
|
422
|
+
inert_definition=inert,
|
|
423
|
+
inert_header=header,
|
|
424
|
+
# Nested functions are not symbols: a decorator factory's
|
|
425
|
+
# wrapper reading ``f.__doc__`` counts for the factory.
|
|
426
|
+
reads_docstrings=any(_reads_docstrings_deep(n) for n in nodes),
|
|
427
|
+
)
|
|
428
|
+
if not self._add_symbol(symbol):
|
|
429
|
+
continue
|
|
430
|
+
members[name] = symbol_id
|
|
431
|
+
self.out.edges.add(Edge(symbol_id, container_id, DEFINED_IN))
|
|
432
|
+
|
|
433
|
+
def _member_id(self, module: str, name: str) -> str:
|
|
434
|
+
return member_symbol_id(module, name, self._children.get(module, frozenset()))
|
|
435
|
+
|
|
436
|
+
def modules_with_prefix(self, prefix: str) -> tuple[str, ...]:
|
|
437
|
+
return tuple(sorted(m for m in self.scopes if m.startswith(prefix)))
|
|
438
|
+
|
|
439
|
+
def symbol_names_with_prefix(self, prefix: str) -> tuple[str, ...]:
|
|
440
|
+
names = {s.name for s in self.index.symbols.values() if s.name.startswith(prefix)}
|
|
441
|
+
return tuple(sorted(names))
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""AST helpers the indexer and discovery share: name tables, source
|
|
2
|
+
decoding, scope statements and the hashes of bodies and definitions."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import builtins
|
|
8
|
+
import copy
|
|
9
|
+
import hashlib
|
|
10
|
+
import io
|
|
11
|
+
import tokenize
|
|
12
|
+
|
|
13
|
+
BUILTIN_NAMES = frozenset(dir(builtins))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Builtins whose argument nothing can bound: what they run is a string of
|
|
17
|
+
# code or the module's own namespace. ``__import__`` is not among them -- it
|
|
18
|
+
# names a module, and a name is exactly what the literal machinery bounds
|
|
19
|
+
# (pandas imports its hard dependencies with ``__import__`` in a loop over a
|
|
20
|
+
# literal tuple, and treating that as unbounded selected its whole suite).
|
|
21
|
+
DYNAMIC_CALLS = frozenset({"eval", "exec", "globals", "vars"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Reflection that observes names or signatures without naming them. Recorded
|
|
25
|
+
# for evidence mode only (SourceIndex.reflection); static planning ignores it.
|
|
26
|
+
REFLECTIVE_BUILTINS = frozenset({"dir", "hasattr", "vars"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
REFLECTIVE_CALLS = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"inspect.getmembers",
|
|
32
|
+
"inspect.getmembers_static",
|
|
33
|
+
"inspect.signature",
|
|
34
|
+
"inspect.getfullargspec",
|
|
35
|
+
"inspect.getcallargs",
|
|
36
|
+
"inspect.get_annotations",
|
|
37
|
+
"typing.get_type_hints",
|
|
38
|
+
"annotationlib.get_annotations",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
REFLECTIVE_ATTRIBUTES = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"__dict__",
|
|
46
|
+
"__annotations__",
|
|
47
|
+
"__signature__",
|
|
48
|
+
"__code__",
|
|
49
|
+
"__defaults__",
|
|
50
|
+
"__kwdefaults__",
|
|
51
|
+
"modules", # sys.modules: any module, found by name
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# How far to follow a module name passed from caller to caller before giving
|
|
57
|
+
# up and leaving the dynamic reference where it is.
|
|
58
|
+
IMPORT_ATTRIBUTION_DEPTH = 4
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
DEF_NODES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
FUNC_NODES = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def decode_source(data: bytes) -> str:
|
|
68
|
+
"""A source file as text, in the encoding it declares. Python reads a
|
|
69
|
+
coding cookie (PEP 263) or a BOM before it reads the source, and a file
|
|
70
|
+
that declares one is not UTF-8 (pip's latin-1 test package)."""
|
|
71
|
+
encoding, _ = tokenize.detect_encoding(io.BytesIO(data).readline)
|
|
72
|
+
return data.decode(encoding)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _digest(text: str) -> str:
|
|
76
|
+
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:16]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _StripDefs(ast.NodeTransformer):
|
|
80
|
+
"""Remove nested definitions (and optionally imports) from a scope body.
|
|
81
|
+
|
|
82
|
+
Definitions are hashed as their own symbols and imports as the module's
|
|
83
|
+
definition hash, so leaving them here would double-count changes.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, strip_imports: bool) -> None:
|
|
87
|
+
self.strip_imports = strip_imports
|
|
88
|
+
|
|
89
|
+
def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> ast.stmt | None:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.stmt | None:
|
|
93
|
+
return self.visit_FunctionDef(node)
|
|
94
|
+
|
|
95
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> ast.stmt | None:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def visit_Import(self, node: ast.Import) -> ast.stmt | None:
|
|
99
|
+
return None if self.strip_imports else node
|
|
100
|
+
|
|
101
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.stmt | None:
|
|
102
|
+
return None if self.strip_imports else node
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _dump(node: ast.AST) -> str:
|
|
106
|
+
return ast.dump(node, include_attributes=False)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def hash_nodes(nodes: list[ast.AST]) -> str:
|
|
110
|
+
return _digest("\n".join(_dump(n) for n in nodes))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _contains_definition_or_import(stmt: ast.stmt, strip_imports: bool) -> bool:
|
|
114
|
+
for node in ast.walk(stmt):
|
|
115
|
+
if isinstance(node, DEF_NODES):
|
|
116
|
+
return True
|
|
117
|
+
if strip_imports and isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
118
|
+
return True
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _split_docstring(stmts: list[ast.stmt]) -> tuple[str, list[ast.stmt]]:
|
|
123
|
+
"""(docstring text, statements without it) for a module/class/function body."""
|
|
124
|
+
if (
|
|
125
|
+
stmts
|
|
126
|
+
and isinstance(stmts[0], ast.Expr)
|
|
127
|
+
and isinstance(stmts[0].value, ast.Constant)
|
|
128
|
+
and isinstance(stmts[0].value.value, str)
|
|
129
|
+
):
|
|
130
|
+
return stmts[0].value.value, stmts[1:]
|
|
131
|
+
return "", stmts
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _docstring_hash(bodies: list[list[ast.stmt]]) -> str:
|
|
135
|
+
docs = [_split_docstring(b)[0] for b in bodies]
|
|
136
|
+
return _digest("\n".join(docs)) if any(docs) else ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def hash_scope_body(stmts: list[ast.stmt], strip_imports: bool) -> str:
|
|
140
|
+
"""Hash a scope's statements with nested definitions (and optionally
|
|
141
|
+
imports) removed. Only statements that actually contain one are copied
|
|
142
|
+
and stripped; the rest are dumped as they are, which avoids a deep copy
|
|
143
|
+
of every statement (the dominant cost of indexing)."""
|
|
144
|
+
kept: list[ast.AST] = []
|
|
145
|
+
for stmt in stmts:
|
|
146
|
+
if isinstance(stmt, DEF_NODES) or (
|
|
147
|
+
strip_imports and isinstance(stmt, (ast.Import, ast.ImportFrom))
|
|
148
|
+
):
|
|
149
|
+
continue
|
|
150
|
+
if _contains_definition_or_import(stmt, strip_imports):
|
|
151
|
+
stripped = _StripDefs(strip_imports).visit(copy.deepcopy(stmt))
|
|
152
|
+
if stripped is not None:
|
|
153
|
+
kept.append(stripped)
|
|
154
|
+
else:
|
|
155
|
+
kept.append(stmt)
|
|
156
|
+
return hash_nodes(kept)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def iter_scope_statements(body: list[ast.stmt]):
|
|
160
|
+
"""Yield statements of a scope, descending into compound statements but
|
|
161
|
+
never into function or class definitions."""
|
|
162
|
+
for stmt in body:
|
|
163
|
+
yield stmt
|
|
164
|
+
if isinstance(stmt, DEF_NODES):
|
|
165
|
+
continue
|
|
166
|
+
for attr in ("body", "orelse", "finalbody"):
|
|
167
|
+
child = getattr(stmt, attr, None)
|
|
168
|
+
if isinstance(child, list):
|
|
169
|
+
yield from iter_scope_statements(child)
|
|
170
|
+
for handler in getattr(stmt, "handlers", []) or []:
|
|
171
|
+
yield from iter_scope_statements(handler.body)
|
|
172
|
+
for case in getattr(stmt, "cases", []) or []:
|
|
173
|
+
yield from iter_scope_statements(case.body)
|