adl-toolkit 0.0.0-stage → 0.1.3
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.
- package/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/adl-drift.md +25 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- package/scripts/install_toolkit.py +3584 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""Symbol table extractor — walks the Python CST and produces Symbol records.
|
|
2
|
+
|
|
3
|
+
Moved out of `parser/extractor.py` verbatim (P4.2, PROBLEMS.md): every
|
|
4
|
+
node-type string, helper method, and behavior is unchanged. `extract()`,
|
|
5
|
+
`extract_string()`, the depth-first walk, the variable/attribute dedupe key,
|
|
6
|
+
and `AnalysisResult` assembly now come from `lang.base.SymbolExtractor`
|
|
7
|
+
(shared with the Java extractor) instead of being defined here directly;
|
|
8
|
+
`_build()` — the Python-specific node-type dispatch — is this class's own
|
|
9
|
+
override, same as before.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
from tree_sitter import Node
|
|
18
|
+
|
|
19
|
+
from adl_drift.lang.base import LanguageParser, ParseResult
|
|
20
|
+
from adl_drift.lang.base import SymbolExtractor as _BaseSymbolExtractor
|
|
21
|
+
from adl_drift.parser.literal_eval import find_exec_expansions
|
|
22
|
+
from adl_drift.utils.types import (
|
|
23
|
+
LOCALS_MARKER,
|
|
24
|
+
AnalysisResult,
|
|
25
|
+
ImportRecord,
|
|
26
|
+
Symbol,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SymbolExtractor(_BaseSymbolExtractor):
|
|
31
|
+
"""Extracts function/class symbols + import strings from a parsed file."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, parser: LanguageParser) -> None:
|
|
34
|
+
super().__init__(parser)
|
|
35
|
+
|
|
36
|
+
def _build(self, parse: ParseResult, file_path: Path) -> AnalysisResult:
|
|
37
|
+
symbols: List[Symbol] = []
|
|
38
|
+
imports: List[str] = []
|
|
39
|
+
import_records: List[ImportRecord] = []
|
|
40
|
+
seen_var_keys: set = set() # (scope_chain, parent or "", name) — dedupe variables/attributes
|
|
41
|
+
|
|
42
|
+
for node in self._walk(parse.tree.root_node):
|
|
43
|
+
if node.type == "function_definition":
|
|
44
|
+
sym = self._extract_function(node, parse.source_bytes, file_path)
|
|
45
|
+
if sym:
|
|
46
|
+
symbols.append(sym)
|
|
47
|
+
elif node.type == "class_definition":
|
|
48
|
+
sym = self._extract_class(node, parse.source_bytes, file_path)
|
|
49
|
+
if sym:
|
|
50
|
+
symbols.append(sym)
|
|
51
|
+
elif node.type in ("import_statement", "import_from_statement"):
|
|
52
|
+
imports.append(self.parser.node_text(node, parse.source_bytes).strip())
|
|
53
|
+
import_records.extend(
|
|
54
|
+
self._extract_import_records(node, parse.source_bytes)
|
|
55
|
+
)
|
|
56
|
+
elif node.type == "assignment":
|
|
57
|
+
for sym in self._extract_assignment(node, parse.source_bytes, file_path):
|
|
58
|
+
key = self._dedupe_key(sym)
|
|
59
|
+
if key in seen_var_keys:
|
|
60
|
+
continue
|
|
61
|
+
seen_var_keys.add(key)
|
|
62
|
+
symbols.append(sym)
|
|
63
|
+
|
|
64
|
+
# P2.16: expand exec/eval with literal templates
|
|
65
|
+
for sym in self._expand_exec_symbols(parse, file_path):
|
|
66
|
+
key = self._dedupe_key(sym)
|
|
67
|
+
if key not in seen_var_keys:
|
|
68
|
+
seen_var_keys.add(key)
|
|
69
|
+
symbols.append(sym)
|
|
70
|
+
|
|
71
|
+
return self._finalize(parse, file_path, symbols, imports, import_records)
|
|
72
|
+
|
|
73
|
+
def _extract_import_records(self, node: Node, source: bytes) -> List[ImportRecord]:
|
|
74
|
+
"""P2.1: structured import extraction by walking the CST.
|
|
75
|
+
|
|
76
|
+
Handles plain / aliased / from / relative / parenthesized-multiline /
|
|
77
|
+
wildcard imports. The CST walk also reaches conditional imports
|
|
78
|
+
(``if TYPE_CHECKING:`` blocks), which the old regex path missed.
|
|
79
|
+
"""
|
|
80
|
+
records: List[ImportRecord] = []
|
|
81
|
+
|
|
82
|
+
if node.type == "import_statement":
|
|
83
|
+
for child in node.children:
|
|
84
|
+
if child.type == "dotted_name":
|
|
85
|
+
records.append(ImportRecord(module=self.parser.node_text(child, source)))
|
|
86
|
+
elif child.type == "aliased_import":
|
|
87
|
+
name_node = child.child_by_field_name("name")
|
|
88
|
+
alias_node = child.child_by_field_name("alias")
|
|
89
|
+
if name_node is not None:
|
|
90
|
+
records.append(ImportRecord(
|
|
91
|
+
module=self.parser.node_text(name_node, source),
|
|
92
|
+
alias=self.parser.node_text(alias_node, source) if alias_node else None,
|
|
93
|
+
))
|
|
94
|
+
return records
|
|
95
|
+
|
|
96
|
+
# import_from_statement
|
|
97
|
+
mod_node = node.child_by_field_name("module_name")
|
|
98
|
+
module = ""
|
|
99
|
+
level = 0
|
|
100
|
+
if mod_node is not None:
|
|
101
|
+
if mod_node.type == "relative_import":
|
|
102
|
+
for sub in mod_node.children:
|
|
103
|
+
if sub.type == "import_prefix":
|
|
104
|
+
level = len(self.parser.node_text(sub, source))
|
|
105
|
+
elif sub.type == "dotted_name":
|
|
106
|
+
module = self.parser.node_text(sub, source)
|
|
107
|
+
else:
|
|
108
|
+
module = self.parser.node_text(mod_node, source)
|
|
109
|
+
|
|
110
|
+
seen_import_kw = False
|
|
111
|
+
for child in node.children:
|
|
112
|
+
if child.type == "import":
|
|
113
|
+
seen_import_kw = True
|
|
114
|
+
continue
|
|
115
|
+
if not seen_import_kw:
|
|
116
|
+
continue
|
|
117
|
+
if child.type == "dotted_name":
|
|
118
|
+
records.append(ImportRecord(
|
|
119
|
+
module=module,
|
|
120
|
+
name=self.parser.node_text(child, source),
|
|
121
|
+
level=level,
|
|
122
|
+
))
|
|
123
|
+
elif child.type == "aliased_import":
|
|
124
|
+
name_node = child.child_by_field_name("name")
|
|
125
|
+
alias_node = child.child_by_field_name("alias")
|
|
126
|
+
if name_node is not None:
|
|
127
|
+
records.append(ImportRecord(
|
|
128
|
+
module=module,
|
|
129
|
+
name=self.parser.node_text(name_node, source),
|
|
130
|
+
alias=self.parser.node_text(alias_node, source) if alias_node else None,
|
|
131
|
+
level=level,
|
|
132
|
+
))
|
|
133
|
+
elif child.type == "wildcard_import":
|
|
134
|
+
records.append(ImportRecord(
|
|
135
|
+
module=module, name="*", level=level, is_wildcard=True,
|
|
136
|
+
))
|
|
137
|
+
return records
|
|
138
|
+
|
|
139
|
+
def _extract_function(self, node: Node, source: bytes, file_path: Path) -> Optional[Symbol]:
|
|
140
|
+
name_node = node.child_by_field_name("name")
|
|
141
|
+
if not name_node:
|
|
142
|
+
return None
|
|
143
|
+
name = self.parser.node_text(name_node, source)
|
|
144
|
+
params_node = node.child_by_field_name("parameters")
|
|
145
|
+
signature = (
|
|
146
|
+
f"def {name}{self.parser.node_text(params_node, source)}" if params_node else f"def {name}()"
|
|
147
|
+
)
|
|
148
|
+
return_type_node = node.child_by_field_name("return_type")
|
|
149
|
+
return_type = (
|
|
150
|
+
self.parser.node_text(return_type_node, source).strip() if return_type_node else None
|
|
151
|
+
)
|
|
152
|
+
if return_type:
|
|
153
|
+
signature = f"{signature} -> {return_type}"
|
|
154
|
+
param_types = self._extract_param_types(params_node, source) if params_node else {}
|
|
155
|
+
is_async = self._is_async(node, source)
|
|
156
|
+
decorators = self._extract_decorators(node, source)
|
|
157
|
+
docstring = self._extract_docstring(node, source)
|
|
158
|
+
parent = self._find_parent_class(node, source)
|
|
159
|
+
scope_chain = self._scope_chain(node, source)
|
|
160
|
+
kind = "function"
|
|
161
|
+
is_test = self._looks_like_test(name, decorators, file_path)
|
|
162
|
+
return Symbol(
|
|
163
|
+
name=name,
|
|
164
|
+
kind=kind,
|
|
165
|
+
file_path=file_path,
|
|
166
|
+
position=self.parser.node_position(node),
|
|
167
|
+
decorators=decorators,
|
|
168
|
+
docstring=docstring,
|
|
169
|
+
signature=signature,
|
|
170
|
+
is_async=is_async,
|
|
171
|
+
parent=parent,
|
|
172
|
+
param_types=param_types,
|
|
173
|
+
return_type=return_type,
|
|
174
|
+
is_test=is_test,
|
|
175
|
+
scope_chain=scope_chain,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _looks_like_test(name: str, decorators: List[str], file_path: Path) -> bool:
|
|
180
|
+
if any("pytest.fixture" in d or "fixture" == d.lstrip("@") for d in decorators):
|
|
181
|
+
return True
|
|
182
|
+
if name.startswith("test_") or name == "setUp" or name == "tearDown":
|
|
183
|
+
fname = file_path.name
|
|
184
|
+
if fname.startswith("test_") or fname.endswith("_test.py") or "tests" in file_path.parts:
|
|
185
|
+
return True
|
|
186
|
+
return name.startswith("test_")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def _extract_param_types(self, params_node: Node, source: bytes) -> dict:
|
|
190
|
+
"""Walk `parameters` CST children, return {param_name: annotation_text}.
|
|
191
|
+
|
|
192
|
+
Handles: typed_parameter, typed_default_parameter, and splat variants.
|
|
193
|
+
Untyped params produce no entry.
|
|
194
|
+
"""
|
|
195
|
+
out: dict = {}
|
|
196
|
+
for child in params_node.children:
|
|
197
|
+
ct = child.type
|
|
198
|
+
if ct == "typed_parameter":
|
|
199
|
+
type_node = child.child_by_field_name("type")
|
|
200
|
+
if type_node is None:
|
|
201
|
+
continue
|
|
202
|
+
name = None
|
|
203
|
+
for sub in child.children:
|
|
204
|
+
if sub.type == "identifier":
|
|
205
|
+
name = self.parser.node_text(sub, source)
|
|
206
|
+
break
|
|
207
|
+
if sub.type in ("list_splat_pattern", "dictionary_splat_pattern"):
|
|
208
|
+
for sub2 in sub.children:
|
|
209
|
+
if sub2.type == "identifier":
|
|
210
|
+
name = self.parser.node_text(sub2, source)
|
|
211
|
+
break
|
|
212
|
+
break
|
|
213
|
+
if name:
|
|
214
|
+
out[name] = self.parser.node_text(type_node, source).strip()
|
|
215
|
+
elif ct == "typed_default_parameter":
|
|
216
|
+
name_node = child.child_by_field_name("name")
|
|
217
|
+
type_node = child.child_by_field_name("type")
|
|
218
|
+
if name_node is not None and type_node is not None:
|
|
219
|
+
out[self.parser.node_text(name_node, source)] = (
|
|
220
|
+
self.parser.node_text(type_node, source).strip()
|
|
221
|
+
)
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
def _extract_class(self, node: Node, source: bytes, file_path: Path) -> Optional[Symbol]:
|
|
225
|
+
name_node = node.child_by_field_name("name")
|
|
226
|
+
if not name_node:
|
|
227
|
+
return None
|
|
228
|
+
name = self.parser.node_text(name_node, source)
|
|
229
|
+
decorators = self._extract_decorators(node, source)
|
|
230
|
+
docstring = self._extract_docstring(node, source)
|
|
231
|
+
parent = self._find_parent_class(node, source)
|
|
232
|
+
scope_chain = self._scope_chain(node, source)
|
|
233
|
+
superclasses = self._extract_superclasses(node, source)
|
|
234
|
+
return Symbol(
|
|
235
|
+
name=name,
|
|
236
|
+
kind="class",
|
|
237
|
+
file_path=file_path,
|
|
238
|
+
position=self.parser.node_position(node),
|
|
239
|
+
decorators=decorators,
|
|
240
|
+
docstring=docstring,
|
|
241
|
+
signature=f"class {name}",
|
|
242
|
+
parent=parent,
|
|
243
|
+
superclasses=superclasses,
|
|
244
|
+
scope_chain=scope_chain,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _extract_superclasses(self, node: Node, source: bytes) -> List[str]:
|
|
248
|
+
sc_node = node.child_by_field_name("superclasses")
|
|
249
|
+
if not sc_node:
|
|
250
|
+
return []
|
|
251
|
+
out: List[str] = []
|
|
252
|
+
for child in sc_node.children:
|
|
253
|
+
if child.type in ("identifier", "attribute"):
|
|
254
|
+
out.append(self.parser.node_text(child, source).strip())
|
|
255
|
+
elif child.type == "keyword_argument":
|
|
256
|
+
continue # metaclass=..., skip
|
|
257
|
+
elif child.type == "subscript":
|
|
258
|
+
# Generic[T] etc. — take the base identifier/attribute
|
|
259
|
+
base = child.child_by_field_name("value")
|
|
260
|
+
if base is not None:
|
|
261
|
+
out.append(self.parser.node_text(base, source).strip())
|
|
262
|
+
return out
|
|
263
|
+
|
|
264
|
+
def _extract_decorators(self, node: Node, source: bytes) -> List[str]:
|
|
265
|
+
out: List[str] = []
|
|
266
|
+
sibling = node.prev_sibling
|
|
267
|
+
while sibling and sibling.type == "decorator":
|
|
268
|
+
out.insert(0, self.parser.node_text(sibling, source).strip())
|
|
269
|
+
sibling = sibling.prev_sibling
|
|
270
|
+
for child in node.children:
|
|
271
|
+
if child.type == "decorator":
|
|
272
|
+
txt = self.parser.node_text(child, source).strip()
|
|
273
|
+
if txt not in out:
|
|
274
|
+
out.append(txt)
|
|
275
|
+
return out
|
|
276
|
+
|
|
277
|
+
def _extract_docstring(self, node: Node, source: bytes) -> Optional[str]:
|
|
278
|
+
body = node.child_by_field_name("body")
|
|
279
|
+
if not body:
|
|
280
|
+
return None
|
|
281
|
+
for child in body.children:
|
|
282
|
+
if child.type == "expression_statement":
|
|
283
|
+
for sub in child.children:
|
|
284
|
+
if sub.type == "string":
|
|
285
|
+
raw = self.parser.node_text(sub, source).strip()
|
|
286
|
+
return raw.strip("'\"")
|
|
287
|
+
break
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def _is_async(self, node: Node, source: bytes) -> bool:
|
|
291
|
+
text = source[node.start_byte:min(node.end_byte, node.start_byte + 16)].decode(
|
|
292
|
+
"utf-8", errors="replace"
|
|
293
|
+
)
|
|
294
|
+
return text.lstrip().startswith("async ")
|
|
295
|
+
|
|
296
|
+
def _extract_assignment(self, node: Node, source: bytes, file_path: Path) -> List[Symbol]:
|
|
297
|
+
"""Emit Symbol for module-level vars and `self.X = ...` attributes in __init__.
|
|
298
|
+
|
|
299
|
+
Skips: local variables inside functions, augmented assignments, walrus.
|
|
300
|
+
"""
|
|
301
|
+
left = node.child_by_field_name("left")
|
|
302
|
+
if left is None:
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
# Walk ancestors to determine scope
|
|
306
|
+
parent_func: Optional[str] = None
|
|
307
|
+
parent_class: Optional[str] = None
|
|
308
|
+
class_node: Optional[Node] = None
|
|
309
|
+
p = node.parent
|
|
310
|
+
while p is not None:
|
|
311
|
+
if p.type == "function_definition" and parent_func is None:
|
|
312
|
+
nn = p.child_by_field_name("name")
|
|
313
|
+
if nn is not None:
|
|
314
|
+
parent_func = self.parser.node_text(nn, source)
|
|
315
|
+
elif p.type == "class_definition" and parent_class is None:
|
|
316
|
+
nn = p.child_by_field_name("name")
|
|
317
|
+
if nn is not None:
|
|
318
|
+
parent_class = self.parser.node_text(nn, source)
|
|
319
|
+
class_node = p
|
|
320
|
+
p = p.parent
|
|
321
|
+
|
|
322
|
+
ann_node = node.child_by_field_name("type")
|
|
323
|
+
annotation = self.parser.node_text(ann_node, source).strip() if ann_node is not None else None
|
|
324
|
+
pos = self.parser.node_position(node)
|
|
325
|
+
|
|
326
|
+
# Case 1: module-level assignment (no enclosing function/class)
|
|
327
|
+
if parent_func is None and parent_class is None:
|
|
328
|
+
out: List[Symbol] = []
|
|
329
|
+
for name in self._target_names(left, source):
|
|
330
|
+
out.append(Symbol(
|
|
331
|
+
name=name,
|
|
332
|
+
kind="variable",
|
|
333
|
+
file_path=file_path,
|
|
334
|
+
position=pos,
|
|
335
|
+
signature=annotation,
|
|
336
|
+
))
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
# Case 2: self.X = ... inside __init__ → instance attribute on enclosing class
|
|
340
|
+
if parent_func == "__init__" and parent_class and left.type == "attribute":
|
|
341
|
+
obj = left.child_by_field_name("object")
|
|
342
|
+
attr = left.child_by_field_name("attribute")
|
|
343
|
+
if obj is not None and attr is not None and \
|
|
344
|
+
self.parser.node_text(obj, source) == "self":
|
|
345
|
+
# P1.10 (P3.13 fold-in): attribute ids must be scope-aware —
|
|
346
|
+
# the chain of the *enclosing class* plus the class name, so a
|
|
347
|
+
# function-nested class's attributes don't compute the same id
|
|
348
|
+
# as a same-named top-level class's attributes.
|
|
349
|
+
attr_chain = (
|
|
350
|
+
self._scope_chain(class_node, source) + [parent_class]
|
|
351
|
+
if class_node is not None else []
|
|
352
|
+
)
|
|
353
|
+
return [Symbol(
|
|
354
|
+
name=self.parser.node_text(attr, source),
|
|
355
|
+
kind="attribute",
|
|
356
|
+
file_path=file_path,
|
|
357
|
+
position=pos,
|
|
358
|
+
parent=parent_class,
|
|
359
|
+
signature=annotation,
|
|
360
|
+
scope_chain=attr_chain,
|
|
361
|
+
)]
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
def _target_names(self, target: Node, source: bytes) -> List[str]:
|
|
365
|
+
"""Collect identifier names from an assignment target.
|
|
366
|
+
|
|
367
|
+
Handles `x = ...`, `x: int = ...`, and tuple/list unpacking.
|
|
368
|
+
Skips attribute and subscript targets at module level.
|
|
369
|
+
"""
|
|
370
|
+
t = target.type
|
|
371
|
+
if t == "identifier":
|
|
372
|
+
return [self.parser.node_text(target, source)]
|
|
373
|
+
if t in ("typed_parameter",):
|
|
374
|
+
return []
|
|
375
|
+
if t in ("pattern_list", "tuple_pattern", "list_pattern"):
|
|
376
|
+
names: List[str] = []
|
|
377
|
+
for child in target.children:
|
|
378
|
+
names.extend(self._target_names(child, source))
|
|
379
|
+
return names
|
|
380
|
+
# Strip annotation wrapper: tree-sitter uses different node names by version;
|
|
381
|
+
# the annotated identifier itself remains type "identifier" via child_by_field_name.
|
|
382
|
+
return []
|
|
383
|
+
|
|
384
|
+
def _expand_exec_symbols(self, parse: ParseResult, file_path: Path) -> List[Symbol]:
|
|
385
|
+
"""P2.16: expand exec/eval with literal templates and extract synthesized symbols.
|
|
386
|
+
|
|
387
|
+
Walks the CST looking for exec("...") / eval("...") calls and for-loops that
|
|
388
|
+
unroll exec over a literal iterable. Re-parses each recovered source string and
|
|
389
|
+
extracts symbols from it, marking them metadata={"synthetic": "exec"}.
|
|
390
|
+
|
|
391
|
+
Non-literal inputs abort silently — no symbols emitted for those sites.
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
expansions = find_exec_expansions(parse.tree.root_node, parse.source_bytes)
|
|
395
|
+
except Exception:
|
|
396
|
+
return []
|
|
397
|
+
|
|
398
|
+
out: List[Symbol] = []
|
|
399
|
+
exec_pos = self.parser.node_position(parse.tree.root_node) # file-level fallback
|
|
400
|
+
|
|
401
|
+
for src_str in expansions:
|
|
402
|
+
try:
|
|
403
|
+
sub_parse = self.parser.parse_string(src_str, file_path=file_path)
|
|
404
|
+
except Exception:
|
|
405
|
+
continue
|
|
406
|
+
for node in self._walk(sub_parse.tree.root_node):
|
|
407
|
+
sym: Optional[Symbol] = None
|
|
408
|
+
if node.type == "function_definition":
|
|
409
|
+
sym = self._extract_function(node, sub_parse.source_bytes, file_path)
|
|
410
|
+
elif node.type == "class_definition":
|
|
411
|
+
sym = self._extract_class(node, sub_parse.source_bytes, file_path)
|
|
412
|
+
if sym is not None:
|
|
413
|
+
sym.metadata["synthetic"] = "exec"
|
|
414
|
+
sym.position = exec_pos # attribute back to the exec call site
|
|
415
|
+
out.append(sym)
|
|
416
|
+
return out
|
|
417
|
+
|
|
418
|
+
def _scope_chain(self, node: Node, source: bytes) -> List[str]:
|
|
419
|
+
"""P2.17/P1.10: full lexical scope chain for a symbol — outermost
|
|
420
|
+
enclosing class/function names first, with ``LOCALS_MARKER`` inserted
|
|
421
|
+
after every *function* segment (CPython's __qualname__ semantics).
|
|
422
|
+
|
|
423
|
+
Class-path ids stay unchanged (`m.Foo.bar`), while closures get
|
|
424
|
+
`m.outer.<locals>.bar` — so a class `Foo`'s method can no longer
|
|
425
|
+
collide with a same-named def nested inside a function `Foo`:
|
|
426
|
+
outer1.<locals>.app → scope_chain=["outer1", "<locals>"]
|
|
427
|
+
Foo.bar (method) → scope_chain=["Foo"]
|
|
428
|
+
|
|
429
|
+
``symbol.parent`` (nearest enclosing class) is kept unchanged for
|
|
430
|
+
self/cls resolution; ``scope_chain`` is the separate uniqueness key.
|
|
431
|
+
"""
|
|
432
|
+
chain: List[str] = []
|
|
433
|
+
p = node.parent
|
|
434
|
+
while p is not None:
|
|
435
|
+
if p.type in ("class_definition", "function_definition"):
|
|
436
|
+
nn = p.child_by_field_name("name")
|
|
437
|
+
if nn is not None:
|
|
438
|
+
if p.type == "function_definition":
|
|
439
|
+
# reversed below: marker must follow the function name
|
|
440
|
+
chain.append(LOCALS_MARKER)
|
|
441
|
+
chain.append(self.parser.node_text(nn, source))
|
|
442
|
+
p = p.parent
|
|
443
|
+
chain.reverse() # innermost last → outermost first
|
|
444
|
+
return chain
|
|
445
|
+
|
|
446
|
+
def _find_parent_class(self, node: Node, source: bytes) -> Optional[str]:
|
|
447
|
+
parent = node.parent
|
|
448
|
+
while parent:
|
|
449
|
+
if parent.type == "class_definition":
|
|
450
|
+
name_node = parent.child_by_field_name("name")
|
|
451
|
+
if name_node:
|
|
452
|
+
return self.parser.node_text(name_node, source)
|
|
453
|
+
parent = parent.parent
|
|
454
|
+
return None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Python tree-sitter grammar + query map (P4.1, PROBLEMS.md).
|
|
2
|
+
|
|
3
|
+
Moved out of `parser/core.py` verbatim: the `Language` instantiation from
|
|
4
|
+
`tree-sitter-python` and the `QUERIES` map are unchanged. `parser/core.py`
|
|
5
|
+
now builds its `PythonParser` by delegating to `create_parser()` here
|
|
6
|
+
instead of owning the grammar global directly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from tree_sitter import Language
|
|
14
|
+
|
|
15
|
+
from adl_drift.lang.base import LanguageParser
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import tree_sitter_python as tspython
|
|
19
|
+
|
|
20
|
+
PYTHON_LANGUAGE: Optional[Language] = Language(tspython.language())
|
|
21
|
+
except Exception: # pragma: no cover
|
|
22
|
+
PYTHON_LANGUAGE = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
QUERIES = {
|
|
26
|
+
"function_def": "(function_definition name: (identifier) @name) @func",
|
|
27
|
+
"class_def": "(class_definition name: (identifier) @name) @class",
|
|
28
|
+
"decorator": "(decorator) @dec",
|
|
29
|
+
"call": "(call function: (_) @target) @call",
|
|
30
|
+
"import": "(import_statement) @imp",
|
|
31
|
+
"import_from": "(import_from_statement) @imp",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_parser() -> LanguageParser:
|
|
36
|
+
"""Build a `LanguageParser` configured for Python."""
|
|
37
|
+
if PYTHON_LANGUAGE is None:
|
|
38
|
+
raise RuntimeError(
|
|
39
|
+
"tree-sitter-python not installed. pip install tree-sitter tree-sitter-python"
|
|
40
|
+
)
|
|
41
|
+
return LanguageParser(PYTHON_LANGUAGE, QUERIES)
|