adl-toolkit 0.0.0-stage → 0.1.1

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.
Files changed (104) hide show
  1. package/INSTALL_TOOLKIT.md +163 -0
  2. package/README.md +269 -2
  3. package/adl_drift/__init__.py +6 -0
  4. package/adl_drift/cli/__init__.py +1 -0
  5. package/adl_drift/cli/commands.py +993 -0
  6. package/adl_drift/cli/main.py +744 -0
  7. package/adl_drift/cortex/__init__.py +1 -0
  8. package/adl_drift/cortex/embeddings.py +96 -0
  9. package/adl_drift/cortex/memory.py +1749 -0
  10. package/adl_drift/cortex/patterns.py +89 -0
  11. package/adl_drift/cortex/telemetry.py +192 -0
  12. package/adl_drift/detectors/__init__.py +1 -0
  13. package/adl_drift/detectors/async_patterns.py +115 -0
  14. package/adl_drift/detectors/base.py +121 -0
  15. package/adl_drift/detectors/config.py +122 -0
  16. package/adl_drift/detectors/data_access.py +128 -0
  17. package/adl_drift/detectors/decorators.py +122 -0
  18. package/adl_drift/detectors/dynamic_patterns.py +260 -0
  19. package/adl_drift/detectors/errors.py +134 -0
  20. package/adl_drift/detectors/imports.py +132 -0
  21. package/adl_drift/detectors/logging_patterns.py +108 -0
  22. package/adl_drift/detectors/routes.py +257 -0
  23. package/adl_drift/detectors/security.py +236 -0
  24. package/adl_drift/detectors/taint.py +406 -0
  25. package/adl_drift/detectors/tests.py +95 -0
  26. package/adl_drift/detectors/type_conventions.py +81 -0
  27. package/adl_drift/graph/__init__.py +1 -0
  28. package/adl_drift/graph/builder.py +2670 -0
  29. package/adl_drift/graph/cfg.py +461 -0
  30. package/adl_drift/graph/cluster.py +40 -0
  31. package/adl_drift/graph/edges.py +86 -0
  32. package/adl_drift/graph/node.py +90 -0
  33. package/adl_drift/harness/__init__.py +10 -0
  34. package/adl_drift/harness/setup.py +832 -0
  35. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  36. package/adl_drift/harness/skills/drift-generic.md +19 -0
  37. package/adl_drift/harness/skills/drift.md +25 -0
  38. package/adl_drift/harness/watch.py +84 -0
  39. package/adl_drift/lang/__init__.py +8 -0
  40. package/adl_drift/lang/base.py +464 -0
  41. package/adl_drift/lang/java/__init__.py +6 -0
  42. package/adl_drift/lang/java/backend.py +97 -0
  43. package/adl_drift/lang/java/cfg.py +1424 -0
  44. package/adl_drift/lang/java/config.py +707 -0
  45. package/adl_drift/lang/java/detectors/__init__.py +29 -0
  46. package/adl_drift/lang/java/detectors/additional.py +445 -0
  47. package/adl_drift/lang/java/detectors/base.py +154 -0
  48. package/adl_drift/lang/java/detectors/data_access.py +148 -0
  49. package/adl_drift/lang/java/detectors/imports.py +53 -0
  50. package/adl_drift/lang/java/detectors/routes.py +159 -0
  51. package/adl_drift/lang/java/detectors/security.py +386 -0
  52. package/adl_drift/lang/java/detectors/tests.py +74 -0
  53. package/adl_drift/lang/java/extractor.py +582 -0
  54. package/adl_drift/lang/java/ids.py +99 -0
  55. package/adl_drift/lang/java/parser.py +49 -0
  56. package/adl_drift/lang/java/resolution/__init__.py +1 -0
  57. package/adl_drift/lang/java/resolution/constants.py +1619 -0
  58. package/adl_drift/lang/java/resolution/constructors.py +278 -0
  59. package/adl_drift/lang/java/resolution/context.py +305 -0
  60. package/adl_drift/lang/java/resolution/dispatch.py +540 -0
  61. package/adl_drift/lang/java/resolution/functional.py +1318 -0
  62. package/adl_drift/lang/java/resolution/generated.py +95 -0
  63. package/adl_drift/lang/java/resolution/model.py +153 -0
  64. package/adl_drift/lang/java/resolution/orchestration.py +866 -0
  65. package/adl_drift/lang/java/resolution/overloads.py +1275 -0
  66. package/adl_drift/lang/java/resolution/scopes.py +1111 -0
  67. package/adl_drift/lang/java/resolution/types.py +2947 -0
  68. package/adl_drift/lang/java/resolver.py +1401 -0
  69. package/adl_drift/lang/java/scopes.py +69 -0
  70. package/adl_drift/lang/java/taint.py +500 -0
  71. package/adl_drift/lang/java/type_provenance.py +342 -0
  72. package/adl_drift/lang/python/__init__.py +8 -0
  73. package/adl_drift/lang/python/backend.py +94 -0
  74. package/adl_drift/lang/python/cfg.py +537 -0
  75. package/adl_drift/lang/python/extractor.py +454 -0
  76. package/adl_drift/lang/python/parser.py +41 -0
  77. package/adl_drift/lang/python/resolver.py +1789 -0
  78. package/adl_drift/lang/python/taint.py +365 -0
  79. package/adl_drift/lang/registry.py +117 -0
  80. package/adl_drift/parser/__init__.py +1 -0
  81. package/adl_drift/parser/cache.py +38 -0
  82. package/adl_drift/parser/core.py +33 -0
  83. package/adl_drift/parser/extractor.py +17 -0
  84. package/adl_drift/parser/literal_eval.py +326 -0
  85. package/adl_drift/scanner/__init__.py +1 -0
  86. package/adl_drift/scanner/discovery.py +338 -0
  87. package/adl_drift/scanner/incremental.py +261 -0
  88. package/adl_drift/scanner/parallel.py +224 -0
  89. package/adl_drift/server/__init__.py +1 -0
  90. package/adl_drift/server/handlers.py +2849 -0
  91. package/adl_drift/server/mcp_server.py +127 -0
  92. package/adl_drift/server/mcp_stdio.py +335 -0
  93. package/adl_drift/server/protocol.py +71 -0
  94. package/adl_drift/server/static/index.html +696 -0
  95. package/adl_drift/server/web.py +350 -0
  96. package/adl_drift/utils/__init__.py +1 -0
  97. package/adl_drift/utils/config.py +77 -0
  98. package/adl_drift/utils/fswalk.py +52 -0
  99. package/adl_drift/utils/types.py +345 -0
  100. package/bin/adl-drift.mjs +35 -0
  101. package/bin/adl-toolkit.mjs +488 -0
  102. package/package.json +29 -4
  103. package/pyproject.toml +49 -0
  104. 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)