agent-codinglanguage-mapper 1.0.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.
Files changed (42) hide show
  1. agent_codinglanguage_mapper/__init__.py +20 -0
  2. agent_codinglanguage_mapper/__main__.py +4 -0
  3. agent_codinglanguage_mapper/_version.py +2 -0
  4. agent_codinglanguage_mapper/adapters/__init__.py +4 -0
  5. agent_codinglanguage_mapper/adapters/delphi.py +822 -0
  6. agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
  7. agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
  8. agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
  9. agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
  10. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
  11. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
  12. agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
  13. agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
  14. agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
  15. agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
  16. agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
  17. agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
  18. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
  19. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
  20. agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
  21. agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
  22. agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
  23. agent_codinglanguage_mapper/cli.py +138 -0
  24. agent_codinglanguage_mapper/discovery.py +1328 -0
  25. agent_codinglanguage_mapper/indexer.py +1102 -0
  26. agent_codinglanguage_mapper/integration.py +186 -0
  27. agent_codinglanguage_mapper/lsp_server.py +413 -0
  28. agent_codinglanguage_mapper/lsp_service.py +447 -0
  29. agent_codinglanguage_mapper/mapper.py +596 -0
  30. agent_codinglanguage_mapper/models.py +101 -0
  31. agent_codinglanguage_mapper/protocol.py +152 -0
  32. agent_codinglanguage_mapper/rendering.py +153 -0
  33. agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
  34. agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
  35. agent_codinglanguage_mapper/worker.py +29 -0
  36. agent_codinglanguage_mapper/workspace.py +114 -0
  37. agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
  38. agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
  39. agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
  40. agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
  41. agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
  42. agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1722 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from bisect import bisect_right
5
+ from collections import defaultdict
6
+ from dataclasses import dataclass, replace
7
+ from pathlib import Path
8
+ import re
9
+ from typing import Iterator, Protocol, cast
10
+
11
+ from tree_sitter import Language as TreeSitterLanguage, Node, Parser, Query, QueryCursor, Tree
12
+ import tree_sitter_c_sharp
13
+ import tree_sitter_cpp
14
+ import tree_sitter_python
15
+ import tree_sitter_rust
16
+
17
+ from ..models import Language, Problem, Reference, Relation, Resolution, SourceRange, Symbol
18
+ from ..workspace import stable_id
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ParsedDocument:
23
+ path: str
24
+ source: str
25
+ encoded: bytes
26
+ line_starts: tuple[int, ...]
27
+ tree: Tree
28
+ parse_count: int = 1
29
+ compatibility_edits: tuple[CompatibilityEdit, ...] = ()
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CompatibilityEdit:
34
+ handler: str
35
+ start_byte: int
36
+ end_byte: int
37
+ is_error: bool = False
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class AdapterResult:
42
+ symbols: tuple[Symbol, ...] = ()
43
+ references: tuple[Reference, ...] = ()
44
+ relations: tuple[Relation, ...] = ()
45
+ problems: tuple[Problem, ...] = ()
46
+ document: object | None = None
47
+
48
+
49
+ class LanguageAdapter(Protocol):
50
+ language: Language
51
+ extensions: tuple[str, ...]
52
+
53
+ def detect(self, path: str | Path) -> bool: ...
54
+
55
+ def parse_outline(self, path: str | Path, source: str) -> AdapterResult: ...
56
+
57
+ def parse_detail(self, path: str | Path, source: str, outline: AdapterResult) -> AdapterResult: ...
58
+
59
+ def resolve(self, result: AdapterResult) -> AdapterResult: ...
60
+
61
+
62
+ _DEFINITIONS: dict[Language, dict[str, str]] = {
63
+ Language.PYTHON: {
64
+ "class_definition": "class",
65
+ "function_definition": "function",
66
+ },
67
+ Language.RUST: {
68
+ "const_item": "constant",
69
+ "enum_item": "enum",
70
+ "function_item": "function",
71
+ "function_signature_item": "function",
72
+ "impl_item": "implementation",
73
+ "macro_definition": "macro",
74
+ "mod_item": "module",
75
+ "static_item": "static",
76
+ "struct_item": "struct",
77
+ "trait_item": "trait",
78
+ "type_item": "type_alias",
79
+ "union_item": "union",
80
+ },
81
+ Language.CSHARP: {
82
+ "class_declaration": "class",
83
+ "constructor_declaration": "constructor",
84
+ "conversion_operator_declaration": "operator",
85
+ "delegate_declaration": "delegate",
86
+ "destructor_declaration": "destructor",
87
+ "enum_declaration": "enum",
88
+ "enum_member_declaration": "enum_member",
89
+ "event_declaration": "event",
90
+ "indexer_declaration": "indexer",
91
+ "interface_declaration": "interface",
92
+ "method_declaration": "method",
93
+ "namespace_declaration": "namespace",
94
+ "file_scoped_namespace_declaration": "namespace",
95
+ "operator_declaration": "operator",
96
+ "property_declaration": "property",
97
+ "record_declaration": "record",
98
+ "record_struct_declaration": "record_struct",
99
+ "struct_declaration": "struct",
100
+ },
101
+ Language.CPP: {
102
+ "alias_declaration": "type_alias",
103
+ "class_specifier": "class",
104
+ "concept_definition": "concept",
105
+ "enum_specifier": "enum",
106
+ "function_definition": "function",
107
+ "namespace_definition": "namespace",
108
+ "type_definition": "type_alias",
109
+ "union_specifier": "union",
110
+ },
111
+ }
112
+
113
+ _CALLS = {"call", "call_expression", "invocation_expression"}
114
+ _IMPORTS = {
115
+ "import_statement",
116
+ "import_from_statement",
117
+ "use_declaration",
118
+ "using_directive",
119
+ "preproc_include",
120
+ }
121
+ _IDENTIFIERS = {
122
+ "field_identifier",
123
+ "identifier",
124
+ "namespace_identifier",
125
+ "type_identifier",
126
+ }
127
+ _PYTHON_COMPAT_ERROR = re.compile(
128
+ r"^\s*(?:print(?:\s|>>)|exec\s|except\s+[^:\n]+,\s*\w+\s*:|raise\s+\w+\s*,)",
129
+ re.MULTILINE,
130
+ )
131
+
132
+ _DETAIL_NODE_TYPES: dict[Language, tuple[str, ...]] = {
133
+ Language.PYTHON: ("call", "class_definition", "import_from_statement", "import_statement"),
134
+ Language.RUST: ("call_expression", "impl_item", "use_declaration"),
135
+ Language.CSHARP: (
136
+ "class_declaration",
137
+ "interface_declaration",
138
+ "invocation_expression",
139
+ "record_declaration",
140
+ "struct_declaration",
141
+ "using_directive",
142
+ ),
143
+ Language.CPP: ("call_expression", "class_specifier", "preproc_include"),
144
+ }
145
+
146
+ _FFI_NODE_TYPES: dict[Language, tuple[str, ...]] = {
147
+ Language.PYTHON: ("assignment", "call"),
148
+ Language.RUST: ("function_item", "function_signature_item"),
149
+ Language.CSHARP: ("method_declaration",),
150
+ Language.CPP: ("declaration", "function_definition"),
151
+ }
152
+
153
+ _FFI_MARKERS: dict[Language, tuple[bytes, ...]] = {
154
+ Language.PYTHON: (b"ctypes", b".dlopen", b"ffi.cdef"),
155
+ Language.RUST: (b"extern", b"export_name"),
156
+ Language.CSHARP: (b"DllImport", b"LibraryImport"),
157
+ Language.CPP: (b'extern "C"', b"__declspec(dllexport)", b"visibility"),
158
+ }
159
+
160
+
161
+ class TreeSitterAdapter:
162
+ def __init__(
163
+ self,
164
+ language: Language,
165
+ extensions: tuple[str, ...],
166
+ grammar: object,
167
+ *,
168
+ workspace_root: str | Path | None = None,
169
+ include_paths: tuple[str | Path, ...] = (),
170
+ ) -> None:
171
+ self.language = language
172
+ self.extensions = extensions
173
+ self._tree_language = TreeSitterLanguage(getattr(grammar, "language")())
174
+ self._parser = Parser(self._tree_language)
175
+ self._detail_query = self._node_query(_DETAIL_NODE_TYPES[language])
176
+ self._ffi_query = self._node_query(_FFI_NODE_TYPES[language])
177
+ self._workspace_root = (
178
+ Path(workspace_root).expanduser().resolve() if workspace_root is not None else None
179
+ )
180
+ self._include_paths = tuple(
181
+ (
182
+ (self._workspace_root / include_path).resolve()
183
+ if self._workspace_root is not None and not Path(include_path).is_absolute()
184
+ else Path(include_path).expanduser().resolve()
185
+ )
186
+ for include_path in include_paths
187
+ )
188
+
189
+ def detect(self, path: str | Path) -> bool:
190
+ return Path(path).suffix.casefold() in self.extensions
191
+
192
+ def parse_outline(self, path: str | Path, source: str) -> AdapterResult:
193
+ document = self._parse(path, source)
194
+ module_name = Path(path).stem or "workspace"
195
+ module_id = stable_id("symbol", self.language.value, str(path), module_name, "0")
196
+ symbols: list[Symbol] = [
197
+ Symbol(
198
+ module_id,
199
+ module_name,
200
+ module_name,
201
+ "module",
202
+ self.language,
203
+ self._range(path, document.tree.root_node, document.encoded, document.line_starts),
204
+ "",
205
+ str(path),
206
+ "",
207
+ document.tree.root_node.type,
208
+ "",
209
+ None,
210
+ )
211
+ ]
212
+ problems = [self._compatibility_problem(document, edit) for edit in document.compatibility_edits]
213
+ self._collect(document.tree.root_node, document, (module_name,), module_id, symbols, problems)
214
+ return AdapterResult(tuple(symbols), problems=tuple(problems), document=document)
215
+
216
+ def parse_detail(self, path: str | Path, source: str, outline: AdapterResult) -> AdapterResult:
217
+ document = outline.document
218
+ if not isinstance(document, ParsedDocument) or document.path != str(path) or document.source != source:
219
+ document = self._parse(path, source)
220
+ references: list[Reference] = []
221
+ relations: list[Relation] = []
222
+ for node in self._query_nodes(self._detail_query, document.tree.root_node):
223
+ owner = self._owner(outline.symbols, node, document.encoded, document.line_starts)
224
+ if node.type in _CALLS:
225
+ target = node.child_by_field_name("function")
226
+ if owner is not None and target is not None:
227
+ name = self._target_name(target, document.encoded)
228
+ if name:
229
+ references.append(
230
+ Reference(
231
+ owner.target_id,
232
+ "",
233
+ name,
234
+ self.language,
235
+ self._range(path, target, document.encoded, document.line_starts),
236
+ Resolution.UNRESOLVED,
237
+ "call",
238
+ self._target_qualifier(target, document.encoded),
239
+ )
240
+ )
241
+ elif node.type in _IMPORTS:
242
+ references.extend(
243
+ self._import_references(
244
+ node,
245
+ owner,
246
+ path,
247
+ document.encoded,
248
+ document.line_starts,
249
+ )
250
+ )
251
+ relations.extend(
252
+ self._base_relations(node, owner, path, document.encoded, document.line_starts)
253
+ )
254
+ relations.extend(
255
+ self._ffi_relations(
256
+ document.tree.root_node,
257
+ outline.symbols,
258
+ path,
259
+ document.encoded,
260
+ document.line_starts,
261
+ )
262
+ )
263
+ return AdapterResult(
264
+ outline.symbols,
265
+ tuple(references),
266
+ tuple(relations),
267
+ outline.problems,
268
+ document,
269
+ )
270
+
271
+ def resolve(self, result: AdapterResult) -> AdapterResult:
272
+ by_name: dict[str, list[Symbol]] = defaultdict(list)
273
+ for symbol in result.symbols:
274
+ if symbol.kind == "module":
275
+ continue
276
+ by_name[symbol.name].append(symbol)
277
+ if symbol.qualified_name != symbol.name:
278
+ by_name[symbol.qualified_name].append(symbol)
279
+
280
+ references = tuple(self._resolve_reference(item, by_name) for item in result.references)
281
+ relations = tuple(self._resolve_relation(item, by_name) for item in result.relations)
282
+ return replace(result, references=references, relations=relations)
283
+
284
+ def _node_query(self, node_types: tuple[str, ...]) -> Query:
285
+ patterns = " ".join(f"({node_type})" for node_type in node_types)
286
+ return Query(self._tree_language, f"[{patterns}] @node")
287
+
288
+ @staticmethod
289
+ def _query_nodes(query: Query, root: Node) -> tuple[Node, ...] | list[Node]:
290
+ return QueryCursor(query).captures(root).get("node", ())
291
+
292
+ def _parse(self, path: str | Path, source: str) -> ParsedDocument:
293
+ encoded = source.encode("utf-8")
294
+ line_starts = (0, *(match.end() for match in re.finditer(b"\n", encoded)))
295
+ parser_source, compatibility_edits = self._normalize_parser_source(path, encoded)
296
+ return ParsedDocument(
297
+ str(path),
298
+ source,
299
+ encoded,
300
+ line_starts,
301
+ self._parser.parse(parser_source),
302
+ compatibility_edits=compatibility_edits,
303
+ )
304
+
305
+ def _normalize_parser_source(
306
+ self,
307
+ path: str | Path,
308
+ source: bytes,
309
+ ) -> tuple[bytes, tuple[CompatibilityEdit, ...]]:
310
+ if self.language is Language.RUST:
311
+ return self._normalize_rust(source)
312
+ if self.language is Language.CSHARP:
313
+ return self._normalize_csharp(source)
314
+ if self.language is Language.CPP:
315
+ return self._normalize_cpp(path, source)
316
+ return source, ()
317
+
318
+ @staticmethod
319
+ def _masked(source: bytes, start: int, end: int, *, first: int | None = None) -> bytes:
320
+ replacement = bytearray(source[start:end])
321
+ for index, value in enumerate(replacement):
322
+ if value not in {10, 13}:
323
+ replacement[index] = 32
324
+ if first is not None and replacement:
325
+ replacement[0] = first
326
+ return bytes(replacement)
327
+
328
+ def _normalize_rust(self, source: bytes) -> tuple[bytes, tuple[CompatibilityEdit, ...]]:
329
+ normalized = bytearray(source)
330
+ edits: list[CompatibilityEdit] = []
331
+ for block in re.finditer(rb'\bunsafe\s+extern(?:\s+"[^"\r\n]*")?\s*\{', source):
332
+ end = self._balanced_end(source, block.end() - 1, 123, 125)
333
+ if end is None:
334
+ continue
335
+ for match in re.finditer(rb"\bsafe(?=[ \t]+fn\b)", source[block.end():end]):
336
+ start = block.end() + match.start()
337
+ stop = block.end() + match.end()
338
+ normalized[start:stop] = b" " * (stop - start)
339
+ edits.append(CompatibilityEdit("rust-2024-safe-extern-item", start, stop))
340
+ function_pointer = re.compile(
341
+ rb'\bas[ \t]+(?:unsafe[ \t]+)?(?:extern[ \t]+"[^"\r\n]+"[ \t]+)?fn[ \t]*\('
342
+ )
343
+ for declaration in function_pointer.finditer(source):
344
+ end = self._balanced_end(source, declaration.end() - 1, 40, 41)
345
+ if end is None:
346
+ continue
347
+ for match in re.finditer(rb"(?<=&)_\b", source[declaration.end():end]):
348
+ start = declaration.end() + match.start()
349
+ stop = declaration.end() + match.end()
350
+ normalized[start:stop] = b"T"
351
+ edits.append(CompatibilityEdit("rust-inferred-reference-type", start, stop))
352
+ for match in re.finditer(rb"(?<=::<)-(?=\d)", source):
353
+ normalized[match.start():match.end()] = b" "
354
+ edits.append(
355
+ CompatibilityEdit("rust-negative-const-generic-literal", match.start(), match.end())
356
+ )
357
+ return bytes(normalized), tuple(edits)
358
+
359
+ def _normalize_csharp(self, source: bytes) -> tuple[bytes, tuple[CompatibilityEdit, ...]]:
360
+ normalized = bytearray(source)
361
+ edits: list[CompatibilityEdit] = []
362
+ symbols: set[bytes] = set()
363
+ stack: list[tuple[bool, bool, bool]] = []
364
+ active = True
365
+ lexical_state = ("normal", 0)
366
+ first_conditional: tuple[int, int] | None = None
367
+ unbalanced: tuple[int, int] | None = None
368
+ lines = tuple(re.finditer(rb"[^\n]*(?:\n|$)", source))
369
+ for line in lines:
370
+ if line.start() == line.end():
371
+ continue
372
+ content = source[line.start():line.end()]
373
+ directive = (
374
+ re.match(rb"^[ \t]*#(if|elif|else|endif|define|undef)\b([^\r\n]*)", content)
375
+ if lexical_state[0] == "normal"
376
+ else None
377
+ )
378
+ if directive is not None:
379
+ kind, expression = directive.group(1), directive.group(2).strip()
380
+ if kind == b"if":
381
+ condition = self._csharp_condition(expression, symbols)
382
+ stack.append((active, condition, active and condition))
383
+ active = active and condition
384
+ if first_conditional is None:
385
+ first_conditional = (line.start(), line.end())
386
+ elif kind == b"elif":
387
+ if stack:
388
+ parent_active, branch_taken, _ = stack[-1]
389
+ condition = not branch_taken and self._csharp_condition(expression, symbols)
390
+ active = parent_active and condition
391
+ stack[-1] = (parent_active, branch_taken or condition, active)
392
+ else:
393
+ unbalanced = unbalanced or (line.start(), line.end())
394
+ elif kind == b"else":
395
+ if stack:
396
+ parent_active, branch_taken, _ = stack[-1]
397
+ active = parent_active and not branch_taken
398
+ stack[-1] = (parent_active, True, active)
399
+ else:
400
+ unbalanced = unbalanced or (line.start(), line.end())
401
+ elif kind == b"endif":
402
+ if stack:
403
+ parent_active, _, _ = stack.pop()
404
+ active = parent_active
405
+ else:
406
+ unbalanced = unbalanced or (line.start(), line.end())
407
+ elif kind == b"define" and active and expression:
408
+ symbols.add(expression.split(maxsplit=1)[0])
409
+ elif kind == b"undef" and active and expression:
410
+ symbols.discard(expression.split(maxsplit=1)[0])
411
+ normalized[line.start():line.end()] = self._masked(source, line.start(), line.end())
412
+ continue
413
+ if not active:
414
+ normalized[line.start():line.end()] = self._masked(source, line.start(), line.end())
415
+ continue
416
+ lexical_state = self._csharp_lex_state(content, lexical_state)
417
+ if first_conditional is not None:
418
+ edits.append(CompatibilityEdit("csharp-conditional-compilation", *first_conditional))
419
+ if stack and first_conditional is not None:
420
+ unbalanced = unbalanced or first_conditional
421
+ if unbalanced is not None:
422
+ edits.append(CompatibilityEdit("csharp-unbalanced-conditional", *unbalanced, is_error=True))
423
+
424
+ parser_source = bytes(normalized)
425
+ cursor = 0
426
+ while True:
427
+ match = re.search(rb"\bis[ \t\r\n]*\[", parser_source[cursor:])
428
+ if match is None:
429
+ break
430
+ bracket = cursor + match.end() - 1
431
+ end = self._balanced_end(parser_source, bracket, 91, 93)
432
+ if end is None:
433
+ break
434
+ normalized[bracket:end] = self._masked(parser_source, bracket, end, first=95)
435
+ edits.append(CompatibilityEdit("csharp-list-pattern", bracket, end))
436
+ parser_source = bytes(normalized)
437
+ cursor = end
438
+ return bytes(normalized), tuple(edits)
439
+
440
+ @staticmethod
441
+ def _csharp_lex_state(line: bytes, initial: tuple[str, int]) -> tuple[str, int]:
442
+ state, delimiter = initial
443
+ index = 0
444
+ while index < len(line):
445
+ if state == "block_comment":
446
+ end = line.find(b"*/", index)
447
+ if end < 0:
448
+ return state, delimiter
449
+ state = "normal"
450
+ index = end + 2
451
+ continue
452
+ if state == "verbatim_string":
453
+ quote = line.find(b'"', index)
454
+ if quote < 0:
455
+ return state, delimiter
456
+ if quote + 1 < len(line) and line[quote + 1] == 34:
457
+ index = quote + 2
458
+ continue
459
+ state = "normal"
460
+ index = quote + 1
461
+ continue
462
+ if state == "raw_string":
463
+ end = line.find(b'"' * delimiter, index)
464
+ if end < 0:
465
+ return state, delimiter
466
+ state = "normal"
467
+ index = end + delimiter
468
+ continue
469
+
470
+ if line[index:index + 2] == b"//":
471
+ return "normal", 0
472
+ if line[index:index + 2] == b"/*":
473
+ state = "block_comment"
474
+ index += 2
475
+ continue
476
+ if line[index:index + 2] == b'@"':
477
+ state = "verbatim_string"
478
+ index += 2
479
+ continue
480
+ if line[index] == 34:
481
+ quotes = 1
482
+ while index + quotes < len(line) and line[index + quotes] == 34:
483
+ quotes += 1
484
+ if quotes >= 3:
485
+ state = "raw_string"
486
+ delimiter = quotes
487
+ index += quotes
488
+ continue
489
+ index += 1
490
+ escaped = False
491
+ while index < len(line) and line[index] not in {10, 13}:
492
+ value = line[index]
493
+ index += 1
494
+ if escaped:
495
+ escaped = False
496
+ elif value == 92:
497
+ escaped = True
498
+ elif value == 34:
499
+ break
500
+ continue
501
+ if line[index] == 39:
502
+ index += 1
503
+ escaped = False
504
+ while index < len(line) and line[index] not in {10, 13}:
505
+ value = line[index]
506
+ index += 1
507
+ if escaped:
508
+ escaped = False
509
+ elif value == 92:
510
+ escaped = True
511
+ elif value == 39:
512
+ break
513
+ continue
514
+ index += 1
515
+ return state, delimiter
516
+
517
+ @staticmethod
518
+ def _csharp_condition(expression: bytes, symbols: set[bytes]) -> bool:
519
+ tokens = re.findall(rb"true|false|[A-Za-z_]\w*|&&|\|\||==|!=|[!()]", expression)
520
+ position = 0
521
+
522
+ def primary() -> bool:
523
+ nonlocal position
524
+ if position >= len(tokens):
525
+ return False
526
+ token = tokens[position]
527
+ position += 1
528
+ if token == b"!":
529
+ return not primary()
530
+ if token == b"(":
531
+ value = disjunction()
532
+ if position < len(tokens) and tokens[position] == b")":
533
+ position += 1
534
+ return value
535
+ if token == b"true":
536
+ return True
537
+ if token == b"false":
538
+ return False
539
+ return token in symbols
540
+
541
+ def equality() -> bool:
542
+ nonlocal position
543
+ value = primary()
544
+ while position < len(tokens) and tokens[position] in {b"==", b"!="}:
545
+ operator = tokens[position]
546
+ position += 1
547
+ right = primary()
548
+ value = value == right if operator == b"==" else value != right
549
+ return value
550
+
551
+ def conjunction() -> bool:
552
+ nonlocal position
553
+ value = equality()
554
+ while position < len(tokens) and tokens[position] == b"&&":
555
+ position += 1
556
+ right = equality()
557
+ value = value and right
558
+ return value
559
+
560
+ def disjunction() -> bool:
561
+ nonlocal position
562
+ value = conjunction()
563
+ while position < len(tokens) and tokens[position] == b"||":
564
+ position += 1
565
+ right = conjunction()
566
+ value = value or right
567
+ return value
568
+
569
+ return disjunction()
570
+
571
+ def _normalize_cpp(
572
+ self,
573
+ path: str | Path,
574
+ source: bytes,
575
+ ) -> tuple[bytes, tuple[CompatibilityEdit, ...]]:
576
+ normalized = bytearray(source)
577
+ edits: list[CompatibilityEdit] = []
578
+ defined_macros = set(
579
+ re.findall(rb"(?m)^[ \t]*#define[ \t]+([A-Z][A-Z0-9_]*)\b", source)
580
+ )
581
+ defined_macros.update(self._cpp_direct_include_macros(path, source))
582
+ lines = tuple(re.finditer(rb"[^\n]*(?:\n|$)", source))
583
+ depth = 0
584
+ index = 0
585
+ while index < len(lines):
586
+ line = lines[index]
587
+ content = source[line.start():line.end()]
588
+ stripped = content.strip(b" \t\r\n")
589
+ if stripped.startswith(b"#define") and content.rstrip(b" \t\r\n").endswith(b"\\"):
590
+ end_index = index
591
+ while end_index + 1 < len(lines) and source[lines[end_index].start():lines[end_index].end()].rstrip(
592
+ b" \t\r\n"
593
+ ).endswith(b"\\"):
594
+ end_index += 1
595
+ end = lines[end_index].end()
596
+ normalized[line.start():end] = self._masked(source, line.start(), end)
597
+ edits.append(CompatibilityEdit("cpp-multiline-macro-definition", line.start(), end))
598
+ index = end_index + 1
599
+ continue
600
+ if stripped.startswith((b"#define", b"#undef", b"#include")) and depth > 0:
601
+ normalized[line.start():line.end()] = self._masked(source, line.start(), line.end())
602
+ handler = self._cpp_preprocessor_handler(source, line.start())
603
+ edits.append(CompatibilityEdit(handler, line.start(), line.end()))
604
+ index += 1
605
+ continue
606
+ if depth > 0 and stripped in defined_macros:
607
+ normalized[line.start():line.end()] = self._masked(source, line.start(), line.end())
608
+ handler = self._cpp_preprocessor_handler(source, line.start())
609
+ edits.append(CompatibilityEdit(handler, line.start(), line.end()))
610
+ index += 1
611
+ continue
612
+ declaration_macro = re.fullmatch(
613
+ rb"([A-Z][A-Z0-9_]*)\s*\([^\r\n]*\)[ \t\r]*(?:\n)?",
614
+ content,
615
+ )
616
+ if declaration_macro is not None and declaration_macro.group(1) in defined_macros:
617
+ replacement = self._masked(source, line.start(), line.end(), first=59)
618
+ normalized[line.start():line.end()] = replacement
619
+ edits.append(CompatibilityEdit("cpp-declaration-macro", line.start(), line.end()))
620
+ index += 1
621
+ continue
622
+ depth += self._cpp_line_depth(bytes(normalized[line.start():line.end()]))
623
+ depth = max(0, depth)
624
+ index += 1
625
+ return bytes(normalized), tuple(edits)
626
+
627
+ def _cpp_direct_include_macros(self, path: str | Path, source: bytes) -> set[bytes]:
628
+ source_path = Path(path)
629
+ if not source_path.is_absolute() and self._workspace_root is not None:
630
+ source_path = self._workspace_root / source_path
631
+ macros: set[bytes] = set()
632
+ visited: set[Path] = set()
633
+
634
+ def collect(header: Path) -> None:
635
+ try:
636
+ resolved = header.resolve()
637
+ if self._workspace_root is not None:
638
+ resolved.relative_to(self._workspace_root)
639
+ if resolved in visited:
640
+ return
641
+ header_source = resolved.read_bytes()
642
+ except (OSError, ValueError):
643
+ return
644
+ visited.add(resolved)
645
+ macros.update(
646
+ re.findall(
647
+ rb"(?m)^[ \t]*#define[ \t]+([A-Z][A-Z0-9_]*)\b",
648
+ header_source,
649
+ )
650
+ )
651
+ for nested, quoted in self._cpp_includes(header_source):
652
+ candidates = ([resolved.parent / nested] if quoted else []) + [
653
+ include_root / nested for include_root in self._include_paths
654
+ ]
655
+ for candidate in candidates:
656
+ if candidate.is_file():
657
+ collect(candidate)
658
+ break
659
+
660
+ for relative, quoted in self._cpp_includes(source):
661
+ candidates = ([source_path.parent / relative] if quoted else []) + [
662
+ include_root / relative for include_root in self._include_paths
663
+ ]
664
+ for candidate in candidates:
665
+ if candidate.is_file():
666
+ collect(candidate)
667
+ break
668
+ return macros
669
+
670
+ @staticmethod
671
+ def _cpp_includes(source: bytes) -> tuple[tuple[str, bool], ...]:
672
+ includes: list[tuple[str, bool]] = []
673
+ pattern = re.compile(rb'(?m)^[ \t]*#include[ \t]+(?P<open>["<])(?P<path>[^">\r\n]+)[">]')
674
+ for match in pattern.finditer(source):
675
+ try:
676
+ includes.append((match.group("path").decode("utf-8"), match.group("open") == b'"'))
677
+ except UnicodeDecodeError:
678
+ continue
679
+ return tuple(includes)
680
+
681
+ @staticmethod
682
+ def _cpp_preprocessor_handler(source: bytes, start: int) -> str:
683
+ statement_start = max(source.rfind(b";", 0, start), source.rfind(b"}", 0, start)) + 1
684
+ context = source[statement_start:start]
685
+ if re.search(rb"=\s*\{[^}]*$", context, re.DOTALL) is not None:
686
+ return "cpp-preprocessor-in-initializer"
687
+ return "cpp-preprocessor-in-expression"
688
+
689
+ @staticmethod
690
+ def _cpp_line_depth(line: bytes) -> int:
691
+ line = re.sub(rb'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|//.*', b"", line)
692
+ return line.count(b"(") + line.count(b"{") + line.count(b"[") - line.count(b")") - line.count(b"}") - line.count(b"]")
693
+
694
+ @staticmethod
695
+ def _balanced_end(source: bytes, start: int, opening: int, closing: int) -> int | None:
696
+ depth = 0
697
+ quote: int | None = None
698
+ escaped = False
699
+ for index in range(start, len(source)):
700
+ value = source[index]
701
+ if quote is not None:
702
+ if escaped:
703
+ escaped = False
704
+ elif value == 92:
705
+ escaped = True
706
+ elif value == quote:
707
+ quote = None
708
+ continue
709
+ if value in {34, 39}:
710
+ quote = value
711
+ elif value == opening:
712
+ depth += 1
713
+ elif value == closing:
714
+ depth -= 1
715
+ if depth == 0:
716
+ return index + 1
717
+ return None
718
+
719
+ def _compatibility_problem(self, document: ParsedDocument, edit: CompatibilityEdit) -> Problem:
720
+ start_line, start_character = TreeSitterAdapter._point_at(
721
+ document.encoded, edit.start_byte, document.line_starts
722
+ )
723
+ end_line, end_character = TreeSitterAdapter._point_at(
724
+ document.encoded, edit.end_byte, document.line_starts
725
+ )
726
+ return Problem(
727
+ "syntax-error" if edit.is_error else "syntax-compatibility",
728
+ (
729
+ f"Rejected malformed compatibility construct: {edit.handler}"
730
+ if edit.is_error
731
+ else f"Applied position-preserving compatibility handler: {edit.handler}"
732
+ ),
733
+ "error" if edit.is_error else "warning",
734
+ self.language,
735
+ SourceRange(document.path, start_line, start_character, end_line, end_character),
736
+ {"native_kind": "source-normalization", "handler": edit.handler},
737
+ )
738
+
739
+ def _collect(
740
+ self,
741
+ node: Node,
742
+ document: ParsedDocument,
743
+ parents: tuple[str, ...],
744
+ parent_id: str,
745
+ symbols: list[Symbol],
746
+ problems: list[Problem],
747
+ ) -> None:
748
+ compatibility_handler = self._compatibility_handler(node, document.encoded)
749
+ if compatibility_handler is not None:
750
+ problems.append(
751
+ Problem(
752
+ "syntax-compatibility",
753
+ f"Applied position-preserving compatibility handler: {compatibility_handler}",
754
+ "warning",
755
+ self.language,
756
+ self._range(document.path, node, document.encoded, document.line_starts),
757
+ {"native_kind": node.type, "handler": compatibility_handler},
758
+ )
759
+ )
760
+ elif node.type == "ERROR" or node.is_missing:
761
+ problems.append(
762
+ Problem(
763
+ "syntax-error",
764
+ f"Tree-sitter reported {node.type}",
765
+ "error",
766
+ self.language,
767
+ self._range(document.path, node, document.encoded, document.line_starts),
768
+ {"native_kind": node.type},
769
+ )
770
+ )
771
+
772
+ definitions = self._definitions(node, document.encoded)
773
+ next_parents = parents
774
+ next_parent_id = parent_id
775
+ for name, kind in definitions:
776
+ if not name:
777
+ continue
778
+ qualified = ".".join((*parents, name))
779
+ target_id = stable_id(
780
+ "symbol",
781
+ self.language.value,
782
+ document.path,
783
+ qualified,
784
+ str(node.start_byte),
785
+ )
786
+ symbol = Symbol(
787
+ target_id,
788
+ name,
789
+ qualified,
790
+ kind,
791
+ self.language,
792
+ self._range(document.path, node, document.encoded, document.line_starts),
793
+ "",
794
+ document.path,
795
+ self._signature(node, document.encoded),
796
+ node.type,
797
+ parent_id,
798
+ self._symbol_name_range(
799
+ node,
800
+ name,
801
+ document.path,
802
+ document.encoded,
803
+ document.line_starts,
804
+ ),
805
+ )
806
+ symbols.append(symbol)
807
+ if kind in {
808
+ "class",
809
+ "extension_block",
810
+ "function",
811
+ "implementation",
812
+ "interface",
813
+ "method",
814
+ "module",
815
+ "namespace",
816
+ "record",
817
+ "record_struct",
818
+ "struct",
819
+ "trait",
820
+ "union",
821
+ }:
822
+ next_parents = (*parents, name)
823
+ next_parent_id = target_id
824
+ break
825
+
826
+ for child in node.children:
827
+ self._collect(child, document, next_parents, next_parent_id, symbols, problems)
828
+
829
+ def _definitions(self, node: Node, source: bytes) -> tuple[tuple[str, str], ...]:
830
+ if self.language is Language.CSHARP and self._csharp_extension_block(node, source):
831
+ return (("extension", "extension_block"),)
832
+ if self.language is Language.CSHARP and node.type == "local_function_statement" and any(
833
+ self._csharp_extension_block(ancestor, source) for ancestor in self._ancestors(node)
834
+ ):
835
+ name = node.child_by_field_name("name")
836
+ return ((self._text(name, source), "extension_member"),) if name is not None else ()
837
+ kind = _DEFINITIONS[self.language].get(node.type)
838
+ if kind is not None:
839
+ return ((self._definition_name(node, source), kind),)
840
+ if self.language is Language.CSHARP and node.type == "event_field_declaration":
841
+ names = [self._text(name, source) for name in self._descendants(node, "variable_declarator")]
842
+ return tuple((name, "event") for name in names)
843
+ if self.language is Language.CPP and node.type == "declaration":
844
+ declarator = self._first_descendant(node, "function_declarator")
845
+ if declarator is not None:
846
+ return ((self._declarator_name(declarator, source), "function"),)
847
+ return ()
848
+
849
+ def _definition_name(self, node: Node, source: bytes) -> str:
850
+ if self.language is Language.RUST and node.type == "impl_item":
851
+ implemented_type = node.child_by_field_name("type")
852
+ trait = node.child_by_field_name("trait")
853
+ type_name = self._target_name(implemented_type, source) if implemented_type is not None else ""
854
+ trait_name = self._target_name(trait, source) if trait is not None else ""
855
+ return f"impl<{trait_name}>:{type_name}" if trait_name else f"impl:{type_name}"
856
+ if self.language is Language.CSHARP and node.type == "indexer_declaration":
857
+ return "this[]"
858
+ field = node.child_by_field_name("name")
859
+ if field is not None:
860
+ return self._text(field, source)
861
+ if node.type in {"function_definition", "declaration"}:
862
+ declarator = node.child_by_field_name("declarator") or self._first_descendant(node, "function_declarator")
863
+ if declarator is not None:
864
+ return self._declarator_name(declarator, source)
865
+ for child in node.named_children:
866
+ if child.type in _IDENTIFIERS:
867
+ return self._text(child, source)
868
+ return ""
869
+
870
+ def _declarator_name(self, node: Node, source: bytes) -> str:
871
+ declarator = node.child_by_field_name("declarator")
872
+ if declarator is not None:
873
+ if declarator.type in _IDENTIFIERS:
874
+ return self._text(declarator, source)
875
+ return self._declarator_name(declarator, source)
876
+ identifiers = [item for item in self._walk(node) if item.type in _IDENTIFIERS]
877
+ return self._text(identifiers[-1], source) if identifiers else ""
878
+
879
+ def _signature(self, node: Node, source: bytes) -> str:
880
+ body = node.child_by_field_name("body")
881
+ end = body.start_byte if body is not None else node.end_byte
882
+ signature = source[node.start_byte:end].decode("utf-8", "replace").strip()
883
+ return " ".join(signature.split())[:2000]
884
+
885
+ def _symbol_name_range(
886
+ self,
887
+ node: Node,
888
+ name: str,
889
+ path: str,
890
+ source: bytes,
891
+ line_starts: tuple[int, ...],
892
+ ) -> SourceRange | None:
893
+ simple = name.rsplit(":", 1)[-1].rsplit(".", 1)[-1]
894
+ for candidate in self._walk(node):
895
+ if candidate.type in _IDENTIFIERS and self._text(candidate, source) == simple:
896
+ return self._range(path, candidate, source, line_starts)
897
+ return None
898
+
899
+ def _compatibility_handler(self, node: Node, source: bytes) -> str | None:
900
+ if self.language is Language.PYTHON:
901
+ if node.type in {"exec_statement", "print_statement"}:
902
+ return "python-2-compatibility"
903
+ if node.type != "ERROR":
904
+ return None
905
+ snippet = self._text(node, source)
906
+ if _PYTHON_COMPAT_ERROR.search(snippet) is not None:
907
+ return "python-2-compatibility"
908
+ parent = node.parent
909
+ if (
910
+ parent is not None
911
+ and parent.type == "type_parameter"
912
+ and re.fullmatch(r"[A-Za-z_]\w*\s*=", snippet.strip()) is not None
913
+ ):
914
+ return "python-type-parameter-default"
915
+
916
+ if self.language is Language.CSHARP:
917
+ return "csharp-14-extension-members" if self._csharp_extension_block(node, source) else None
918
+ if self.language is not Language.CPP:
919
+ return None
920
+ if node.type != "ERROR" and not node.is_missing:
921
+ return None
922
+ snippet = self._text(node, source)
923
+ parent = node.parent
924
+ if node.is_missing and node.type == "type_identifier" and parent is not None:
925
+ optional_parameter = parent.parent
926
+ if (
927
+ parent.type == "compound_literal_expression"
928
+ and optional_parameter is not None
929
+ and optional_parameter.type == "optional_parameter_declaration"
930
+ and self._text(parent, source).strip() == "{}"
931
+ ):
932
+ return "cpp-braced-default-parameter"
933
+ if node.type == "ERROR" and parent is not None and parent.type == "initializer_list":
934
+ parent_text = self._text(parent, source)
935
+ stripped = snippet.strip()
936
+ if re.search(r"(?m)^\s*#define\b", parent_text) is not None and (
937
+ stripped.startswith("#define")
938
+ or stripped.startswith("#undef")
939
+ or re.fullmatch(r"#[A-Za-z_]\w*", stripped) is not None
940
+ ):
941
+ return "cpp-preprocessor-in-initializer"
942
+ return None
943
+
944
+ def _csharp_extension_block(self, node: Node, source: bytes) -> bool:
945
+ if self.language is not Language.CSHARP or node.type != "constructor_declaration":
946
+ return False
947
+ name = node.child_by_field_name("name")
948
+ if name is None or self._text(name, source) != "extension":
949
+ return False
950
+ owner = next(
951
+ (ancestor for ancestor in self._ancestors(node) if ancestor.type == "class_declaration"),
952
+ None,
953
+ )
954
+ owner_name = owner.child_by_field_name("name") if owner is not None else None
955
+ return owner_name is not None and self._text(owner_name, source) != "extension"
956
+
957
+ def _base_relations(
958
+ self,
959
+ node: Node,
960
+ owner: Symbol | None,
961
+ path: str | Path,
962
+ source: bytes,
963
+ line_starts: tuple[int, ...],
964
+ ) -> tuple[Relation, ...]:
965
+ base_node: Node | None = None
966
+ kind = "inherits"
967
+ relation_owner = owner
968
+ if self.language is Language.PYTHON and node.type == "class_definition":
969
+ base_node = node.child_by_field_name("superclasses")
970
+ relation_owner = next((item for item in (owner,) if item and item.kind == "class"), owner)
971
+ elif self.language is Language.CSHARP and node.type in {
972
+ "class_declaration",
973
+ "interface_declaration",
974
+ "record_declaration",
975
+ "record_struct_declaration",
976
+ "struct_declaration",
977
+ }:
978
+ base_node = next((child for child in node.named_children if child.type == "base_list"), None)
979
+ elif self.language is Language.CPP and node.type in {"class_specifier", "struct_specifier"}:
980
+ base_node = next((child for child in node.named_children if child.type == "base_class_clause"), None)
981
+ elif self.language is Language.RUST and node.type == "impl_item":
982
+ base_node = node.child_by_field_name("trait")
983
+ kind = "implements"
984
+ if base_node is None or relation_owner is None:
985
+ return ()
986
+
987
+ names = self._type_names(base_node, source)
988
+ return tuple(
989
+ Relation(
990
+ relation_owner.target_id,
991
+ "",
992
+ kind,
993
+ Resolution.UNRESOLVED,
994
+ (self._range(path, base_node, source, line_starts),),
995
+ name,
996
+ )
997
+ for name in names
998
+ if name not in {"object", "System.Object"}
999
+ )
1000
+
1001
+ def _type_names(self, node: Node, source: bytes) -> tuple[str, ...]:
1002
+ if node.type in _IDENTIFIERS or node.type in {"generic_name", "qualified_name", "scoped_type_identifier"}:
1003
+ return (self._target_name(node, source),)
1004
+ names: list[str] = []
1005
+ for child in node.named_children:
1006
+ if child.type in {"access_specifier", "type_argument_list"}:
1007
+ continue
1008
+ if child.type in _IDENTIFIERS or child.type in {
1009
+ "attribute",
1010
+ "generic_name",
1011
+ "qualified_identifier",
1012
+ "qualified_name",
1013
+ "scoped_type_identifier",
1014
+ }:
1015
+ name = self._target_name(child, source)
1016
+ if name:
1017
+ names.append(name)
1018
+ return tuple(dict.fromkeys(names))
1019
+
1020
+ def _import_name(self, node: Node, source: bytes) -> str:
1021
+ path = node.child_by_field_name("path") or node.child_by_field_name("module_name")
1022
+ if path is not None:
1023
+ return self._text(path, source).strip("<>\"")
1024
+ text = self._text(node, source).strip()
1025
+ for prefix in ("#include", "import", "from", "use", "using"):
1026
+ if text.startswith(prefix):
1027
+ return text[len(prefix):].strip().strip(";<>\"")
1028
+ return text
1029
+
1030
+ def _import_references(
1031
+ self,
1032
+ node: Node,
1033
+ owner: Symbol | None,
1034
+ path: str | Path,
1035
+ source: bytes,
1036
+ line_starts: tuple[int, ...],
1037
+ ) -> tuple[Reference, ...]:
1038
+ text = self._text(node, source).strip()
1039
+ source_id = owner.target_id if owner else ""
1040
+ source_range = self._range(path, node, source, line_starts)
1041
+
1042
+ def reference(
1043
+ name: str,
1044
+ import_path: str,
1045
+ *,
1046
+ imported_symbols: tuple[tuple[str, str], ...] = (),
1047
+ module_alias: str = "",
1048
+ wildcard: bool = False,
1049
+ project_wide: bool = False,
1050
+ ) -> Reference:
1051
+ return Reference(
1052
+ source_id,
1053
+ "",
1054
+ name,
1055
+ self.language,
1056
+ source_range,
1057
+ Resolution.UNRESOLVED,
1058
+ "import",
1059
+ "",
1060
+ import_path,
1061
+ imported_symbols,
1062
+ module_alias,
1063
+ wildcard,
1064
+ project_wide,
1065
+ )
1066
+
1067
+ if self.language is Language.PYTHON:
1068
+ from_match = re.match(r"from\s+([.A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s+import\s+(.+)$", text, re.DOTALL)
1069
+ if from_match is not None:
1070
+ import_path = from_match.group(1)
1071
+ body = from_match.group(2).strip().strip("()")
1072
+ if body == "*":
1073
+ return (reference("*", import_path, wildcard=True),)
1074
+ result: list[Reference] = []
1075
+ for item in body.split(","):
1076
+ parts = re.split(r"\s+as\s+", item.strip(), maxsplit=1)
1077
+ original = parts[0].strip()
1078
+ alias = parts[1].strip() if len(parts) == 2 else original
1079
+ if original:
1080
+ result.append(
1081
+ reference(
1082
+ alias,
1083
+ import_path,
1084
+ imported_symbols=((original, alias),),
1085
+ )
1086
+ )
1087
+ return tuple(result)
1088
+ import_match = re.match(r"import\s+(.+)$", text, re.DOTALL)
1089
+ if import_match is not None:
1090
+ result = []
1091
+ for item in import_match.group(1).split(","):
1092
+ parts = re.split(r"\s+as\s+", item.strip(), maxsplit=1)
1093
+ import_path = parts[0].strip()
1094
+ alias = parts[1].strip() if len(parts) == 2 else import_path.split(".", 1)[0]
1095
+ if import_path:
1096
+ result.append(reference(alias, import_path, module_alias=alias))
1097
+ return tuple(result)
1098
+
1099
+ if self.language is Language.RUST:
1100
+ body = re.sub(r"^use\s+|;$", "", text).strip()
1101
+ if "::{" in body and body.endswith("}"):
1102
+ prefix, members = body.split("::{", 1)
1103
+ result = []
1104
+ for item in members[:-1].split(","):
1105
+ member = item.strip()
1106
+ if member == "*":
1107
+ result.append(reference("*", prefix, wildcard=True))
1108
+ continue
1109
+ parts = re.split(r"\s+as\s+", member, maxsplit=1)
1110
+ original = parts[0].strip()
1111
+ alias = parts[1].strip() if len(parts) == 2 else original
1112
+ if original:
1113
+ result.append(
1114
+ reference(alias, prefix, imported_symbols=((original, alias),))
1115
+ )
1116
+ return tuple(result)
1117
+ if body.endswith("::*"):
1118
+ return (reference("*", body[:-3], wildcard=True),)
1119
+ parts = re.split(r"\s+as\s+", body, maxsplit=1)
1120
+ qualified = parts[0].strip()
1121
+ segments = qualified.split("::")
1122
+ if len(segments) > 1:
1123
+ original = segments[-1]
1124
+ alias = parts[1].strip() if len(parts) == 2 else original
1125
+ return (
1126
+ reference(
1127
+ alias,
1128
+ "::".join(segments[:-1]),
1129
+ imported_symbols=((original, alias),),
1130
+ ),
1131
+ )
1132
+ if qualified:
1133
+ alias = parts[1].strip() if len(parts) == 2 else qualified
1134
+ return (reference(alias, qualified, module_alias=alias),)
1135
+
1136
+ if self.language is Language.CSHARP:
1137
+ body = text.rstrip(";").strip()
1138
+ project_wide = body.startswith("global using ")
1139
+ body = re.sub(r"^global\s+", "", body)
1140
+ body = re.sub(r"^using\s+", "", body)
1141
+ static = body.startswith("static ")
1142
+ body = re.sub(r"^static\s+", "", body)
1143
+ alias_match = re.match(r"([A-Za-z_]\w*)\s*=\s*(.+)$", body)
1144
+ if alias_match is not None:
1145
+ qualified = alias_match.group(2).strip()
1146
+ segments = qualified.split(".")
1147
+ return (
1148
+ reference(
1149
+ alias_match.group(1),
1150
+ ".".join(segments[:-1]),
1151
+ imported_symbols=((segments[-1], alias_match.group(1)),),
1152
+ project_wide=project_wide,
1153
+ ),
1154
+ )
1155
+ if body:
1156
+ return (
1157
+ reference(
1158
+ body,
1159
+ body,
1160
+ wildcard=True,
1161
+ project_wide=project_wide,
1162
+ module_alias=body if static else "",
1163
+ ),
1164
+ )
1165
+
1166
+ if self.language is Language.CPP:
1167
+ import_path = self._import_name(node, source)
1168
+ return (reference(import_path, import_path, wildcard=True),) if import_path else ()
1169
+ return ()
1170
+
1171
+ def _ffi_relations(
1172
+ self,
1173
+ root: Node,
1174
+ symbols: tuple[Symbol, ...],
1175
+ path: str | Path,
1176
+ source: bytes,
1177
+ line_starts: tuple[int, ...],
1178
+ ) -> tuple[Relation, ...]:
1179
+ if not any(marker in source for marker in _FFI_MARKERS[self.language]):
1180
+ return ()
1181
+ relations: list[Relation] = []
1182
+ seen: set[tuple[str, str, str]] = set()
1183
+
1184
+ def add(node: Node, target_name: str, target_library: str = "") -> None:
1185
+ owner = self._owner(symbols, node, source, line_starts)
1186
+ key = (owner.target_id if owner else "", target_name, target_library)
1187
+ if owner is None or not target_name or key in seen:
1188
+ return
1189
+ seen.add(key)
1190
+ relations.append(
1191
+ Relation(
1192
+ owner.target_id,
1193
+ "",
1194
+ "ffi",
1195
+ Resolution.SOUND_PARTIAL,
1196
+ (self._range(path, node, source, line_starts),),
1197
+ target_name,
1198
+ target_library,
1199
+ )
1200
+ )
1201
+
1202
+ aliases: dict[str, str] = {}
1203
+ python_loaders, python_prototypes = self._python_ffi_call_names(root, source)
1204
+ ffi_nodes = sorted(
1205
+ self._query_nodes(self._ffi_query, root),
1206
+ key=lambda item: (item.start_byte, item.end_byte, item.type),
1207
+ )
1208
+ if self.language is Language.PYTHON:
1209
+ for node in ffi_nodes:
1210
+ if node.type != "assignment":
1211
+ continue
1212
+ left = node.child_by_field_name("left")
1213
+ right = node.child_by_field_name("right")
1214
+ if left is None or right is None or right.type != "call":
1215
+ continue
1216
+ function_name = self._python_call_name(right, source)
1217
+ if function_name in python_loaders:
1218
+ aliases[self._target_name(left, source)] = self._python_literal_argument(
1219
+ right,
1220
+ source,
1221
+ )
1222
+ if function_name in python_prototypes:
1223
+ add(node, self._target_name(left, source))
1224
+
1225
+ for node in ffi_nodes:
1226
+ if self.language is Language.PYTHON and node.type == "assignment":
1227
+ continue
1228
+ elif self.language is Language.PYTHON and node.type == "call":
1229
+ function = node.child_by_field_name("function")
1230
+ if function is not None and function.type == "attribute":
1231
+ owner = function.child_by_field_name("object")
1232
+ attribute = function.child_by_field_name("attribute")
1233
+ target_name = self._target_name(attribute, source)
1234
+ if owner is not None and owner.type == "call":
1235
+ loader = self._python_call_name(owner, source)
1236
+ if loader in python_loaders:
1237
+ add(node, target_name, self._python_literal_argument(owner, source))
1238
+ elif owner is not None and owner.type == "identifier":
1239
+ owner_name = self._target_name(owner, source)
1240
+ if owner_name in aliases:
1241
+ add(node, target_name, aliases[owner_name])
1242
+ elif owner_name == "ffi" and target_name == "cdef":
1243
+ declaration = self._python_literal_argument(node, source)
1244
+ for declared in re.findall(r"\b([A-Za-z_]\w*)\s*\(", declaration):
1245
+ if declared not in {"cdef", "if", "while"}:
1246
+ add(node, declared)
1247
+ elif self.language is Language.CSHARP and node.type == "method_declaration":
1248
+ attribute = self._csharp_ffi_attribute(node, source)
1249
+ if attribute is not None:
1250
+ library, entry = self._csharp_ffi_attribute_arguments(attribute, source)
1251
+ name_node = node.child_by_field_name("name")
1252
+ add(
1253
+ node,
1254
+ entry or self._target_name(name_node, source),
1255
+ library,
1256
+ )
1257
+ elif self.language is Language.RUST and node.type in {"function_item", "function_signature_item"}:
1258
+ ancestors = tuple(self._ancestors(node))
1259
+ parent_types = {item.type for item in ancestors}
1260
+ local_attributes = self._preceding_attributes(node)
1261
+ foreign = next(
1262
+ (item for item in ancestors if item.type == "foreign_mod_item"),
1263
+ None,
1264
+ )
1265
+ foreign_attributes = (
1266
+ self._preceding_attributes(foreign)
1267
+ if foreign is not None
1268
+ else ()
1269
+ )
1270
+ export = self._rust_attribute_value(
1271
+ local_attributes,
1272
+ "export_name",
1273
+ source,
1274
+ )
1275
+ if "foreign_mod_item" in parent_types:
1276
+ link_name = self._rust_attribute_value(
1277
+ local_attributes,
1278
+ "link_name",
1279
+ source,
1280
+ )
1281
+ library = self._rust_attribute_value(
1282
+ foreign_attributes,
1283
+ "name",
1284
+ source,
1285
+ container="link",
1286
+ )
1287
+ target = link_name or export or self._definition_name(node, source)
1288
+ add(
1289
+ node,
1290
+ target,
1291
+ library,
1292
+ )
1293
+ elif (
1294
+ node.type == "function_item"
1295
+ and self._rust_function_abi(node, source) in {"C", "system"}
1296
+ and (export or self._rust_has_no_mangle(local_attributes, source))
1297
+ ):
1298
+ add(
1299
+ node,
1300
+ export or self._definition_name(node, source),
1301
+ )
1302
+ elif self.language is Language.CPP and node.type in {"declaration", "function_definition"}:
1303
+ linkage = next(
1304
+ (item for item in self._ancestors(node) if item.type == "linkage_specification"),
1305
+ None,
1306
+ )
1307
+ linkage_value = linkage.child_by_field_name("value") if linkage is not None else None
1308
+ declaration_prefix = self._cpp_declaration_prefix(node, source)
1309
+ if (
1310
+ linkage_value is not None
1311
+ and self._text(linkage_value, source) == '"C"'
1312
+ or "__declspec(dllexport)" in declaration_prefix
1313
+ or re.search(
1314
+ r"__attribute__\s*\(\([^)]*visibility\s*\(\s*\"default\"",
1315
+ declaration_prefix,
1316
+ )
1317
+ ):
1318
+ add(node, self._definition_name(node, source))
1319
+ return tuple(relations)
1320
+
1321
+ def _python_ffi_call_names(self, root: Node, source: bytes) -> tuple[set[str], set[str]]:
1322
+ loaders = {
1323
+ "ctypes.CDLL",
1324
+ "ctypes.OleDLL",
1325
+ "ctypes.PyDLL",
1326
+ "ctypes.WinDLL",
1327
+ "ctypes.cdll.LoadLibrary",
1328
+ "ctypes.oledll.LoadLibrary",
1329
+ "ctypes.pydll.LoadLibrary",
1330
+ "ctypes.windll.LoadLibrary",
1331
+ "ffi.dlopen",
1332
+ }
1333
+ prototypes = {"ctypes.CFUNCTYPE", "ctypes.WINFUNCTYPE"}
1334
+ if self.language is not Language.PYTHON:
1335
+ return loaders, prototypes
1336
+
1337
+ imported_loaders = {"CDLL", "OleDLL", "PyDLL", "WinDLL"}
1338
+ imported_prototypes = {"CFUNCTYPE", "WINFUNCTYPE"}
1339
+ imported_libraries = {"cdll", "oledll", "pydll", "windll"}
1340
+ for node in self._walk(root):
1341
+ if node.type == "import_statement":
1342
+ for item in node.named_children:
1343
+ original = item.child_by_field_name("name") if item.type == "aliased_import" else item
1344
+ alias = item.child_by_field_name("alias") if item.type == "aliased_import" else item
1345
+ if original is not None and alias is not None and self._text(original, source) == "ctypes":
1346
+ prefix = self._text(alias, source)
1347
+ loaders.update(
1348
+ {
1349
+ f"{prefix}.{name}"
1350
+ for name in imported_loaders
1351
+ }
1352
+ )
1353
+ loaders.update(
1354
+ f"{prefix}.{name}.LoadLibrary" for name in imported_libraries
1355
+ )
1356
+ prototypes.update(f"{prefix}.{name}" for name in imported_prototypes)
1357
+ elif node.type == "import_from_statement":
1358
+ module = node.child_by_field_name("module_name")
1359
+ if module is None or self._text(module, source) != "ctypes":
1360
+ continue
1361
+ for item in node.named_children:
1362
+ if item == module:
1363
+ continue
1364
+ original_node = item.child_by_field_name("name") if item.type == "aliased_import" else item
1365
+ alias_node = item.child_by_field_name("alias") if item.type == "aliased_import" else item
1366
+ if original_node is None or alias_node is None:
1367
+ continue
1368
+ original_name = self._text(original_node, source)
1369
+ alias_name = self._text(alias_node, source)
1370
+ if original_name in imported_loaders:
1371
+ loaders.add(alias_name)
1372
+ elif original_name in imported_libraries:
1373
+ loaders.add(f"{alias_name}.LoadLibrary")
1374
+ elif original_name in imported_prototypes:
1375
+ prototypes.add(alias_name)
1376
+ return loaders, prototypes
1377
+
1378
+ def _csharp_ffi_attribute(self, node: Node, source: bytes) -> Node | None:
1379
+ for child in node.named_children:
1380
+ if child.type != "attribute_list":
1381
+ continue
1382
+ for item in self._walk(child):
1383
+ if item.type != "attribute":
1384
+ continue
1385
+ name = item.child_by_field_name("name")
1386
+ if self._target_name(name, source) in {
1387
+ "DllImport",
1388
+ "DllImportAttribute",
1389
+ "LibraryImport",
1390
+ "LibraryImportAttribute",
1391
+ }:
1392
+ return item
1393
+ return None
1394
+
1395
+ def _csharp_ffi_attribute_arguments(self, attribute: Node, source: bytes) -> tuple[str, str]:
1396
+ library = ""
1397
+ entry = ""
1398
+ arguments = next(
1399
+ (item for item in attribute.named_children if item.type == "attribute_argument_list"),
1400
+ None,
1401
+ )
1402
+ if arguments is None:
1403
+ return library, entry
1404
+ for argument in arguments.named_children:
1405
+ if argument.type != "attribute_argument":
1406
+ continue
1407
+ literal = next(
1408
+ (item for item in argument.named_children if item.type == "string_literal"),
1409
+ None,
1410
+ )
1411
+ if literal is None:
1412
+ continue
1413
+ value = self._csharp_string_literal(literal, source)
1414
+ name = argument.child_by_field_name("name")
1415
+ if name is not None and self._text(name, source) == "EntryPoint":
1416
+ entry = value
1417
+ elif name is None and not library:
1418
+ library = value
1419
+ return library, entry
1420
+
1421
+ def _csharp_string_literal(self, node: Node, source: bytes) -> str:
1422
+ text = self._text(node, source)
1423
+ if text.startswith('@"') and text.endswith('"'):
1424
+ return text[2:-1].replace('""', '"')
1425
+ try:
1426
+ value = ast.literal_eval(text)
1427
+ except (SyntaxError, ValueError):
1428
+ return ""
1429
+ return value if isinstance(value, str) else ""
1430
+
1431
+ def _preceding_attributes(self, node: Node) -> tuple[Node, ...]:
1432
+ attributes: list[Node] = []
1433
+ sibling = node.prev_named_sibling
1434
+ while sibling is not None and sibling.type == "attribute_item":
1435
+ attribute = next(
1436
+ (item for item in sibling.named_children if item.type == "attribute"),
1437
+ None,
1438
+ )
1439
+ if attribute is not None:
1440
+ attributes.append(attribute)
1441
+ sibling = sibling.prev_named_sibling
1442
+ return tuple(reversed(attributes))
1443
+
1444
+ def _rust_attribute_value(
1445
+ self,
1446
+ attributes: tuple[Node, ...],
1447
+ key: str,
1448
+ source: bytes,
1449
+ *,
1450
+ container: str = "",
1451
+ ) -> str:
1452
+ for attribute in attributes:
1453
+ identifiers = [
1454
+ item
1455
+ for item in self._walk(attribute)
1456
+ if item.type == "identifier"
1457
+ ]
1458
+ if not identifiers:
1459
+ continue
1460
+ attribute_name = self._text(identifiers[0], source)
1461
+ if container and attribute_name != container:
1462
+ continue
1463
+ if not container and attribute_name != key:
1464
+ continue
1465
+ for index, identifier in enumerate(identifiers):
1466
+ if self._text(identifier, source) != key:
1467
+ continue
1468
+ after_identifier = identifier.end_byte
1469
+ literal = next(
1470
+ (
1471
+ item
1472
+ for item in self._walk(attribute)
1473
+ if item.type == "string_literal" and item.start_byte >= after_identifier
1474
+ ),
1475
+ None,
1476
+ )
1477
+ if literal is not None:
1478
+ return self._text(literal, source).strip('"')
1479
+ if index == 0 and not container:
1480
+ break
1481
+ return ""
1482
+
1483
+ def _rust_has_no_mangle(self, attributes: tuple[Node, ...], source: bytes) -> bool:
1484
+ for attribute in attributes:
1485
+ identifiers = [
1486
+ self._text(item, source)
1487
+ for item in self._walk(attribute)
1488
+ if item.type == "identifier"
1489
+ ]
1490
+ if identifiers[:1] == ["no_mangle"]:
1491
+ return True
1492
+ if identifiers[:2] == ["unsafe", "no_mangle"]:
1493
+ return True
1494
+ return False
1495
+
1496
+ def _rust_function_abi(self, node: Node, source: bytes) -> str:
1497
+ for child in node.named_children:
1498
+ if child.type != "function_modifiers":
1499
+ continue
1500
+ for item in self._walk(child):
1501
+ if item.type != "extern_modifier":
1502
+ continue
1503
+ literal = next(
1504
+ (candidate for candidate in item.named_children if candidate.type == "string_literal"),
1505
+ None,
1506
+ )
1507
+ if literal is not None:
1508
+ return self._text(literal, source).strip('"')
1509
+ return ""
1510
+
1511
+ def _python_call_name(self, node: Node, source: bytes) -> str:
1512
+ function = node.child_by_field_name("function")
1513
+ return self._text(function, source).strip() if function is not None else ""
1514
+
1515
+ def _python_literal_argument(self, node: Node, source: bytes) -> str:
1516
+ arguments = node.child_by_field_name("arguments")
1517
+ if arguments is None or not arguments.named_children:
1518
+ return ""
1519
+ literal = arguments.named_children[0]
1520
+ if literal.type != "string":
1521
+ return ""
1522
+ try:
1523
+ value = ast.literal_eval(self._text(literal, source))
1524
+ except (SyntaxError, ValueError):
1525
+ return ""
1526
+ return value if isinstance(value, str) else ""
1527
+
1528
+ def _cpp_declaration_prefix(self, node: Node, source: bytes) -> str:
1529
+ body = node.child_by_field_name("body")
1530
+ end_byte = body.start_byte if body is not None else node.end_byte
1531
+ start_byte = node.start_byte
1532
+ prefix = bytearray(source[start_byte:end_byte])
1533
+ for item in self._walk(node):
1534
+ if item.type != "comment" or item.start_byte >= end_byte:
1535
+ continue
1536
+ comment_start = max(item.start_byte, start_byte) - start_byte
1537
+ comment_end = min(item.end_byte, end_byte) - start_byte
1538
+ prefix[comment_start:comment_end] = b" " * (comment_end - comment_start)
1539
+ return prefix.decode("utf-8", "replace")
1540
+
1541
+ def _target_name(self, node: Node | None, source: bytes) -> str:
1542
+ if node is None:
1543
+ return ""
1544
+ text = self._text(node, source).strip()
1545
+ text = re.sub(r"(?:::<[^>]+>|<[^>]+>)$", "", text)
1546
+ parts = re.findall(r"[A-Za-z_]\w*", text)
1547
+ return parts[-1] if parts else text
1548
+
1549
+ def _target_qualifier(self, node: Node, source: bytes) -> str:
1550
+ parts = re.findall(r"[A-Za-z_]\w*", self._text(node, source))
1551
+ return ".".join(parts[:-1]) if len(parts) > 1 else ""
1552
+
1553
+ @staticmethod
1554
+ def _resolve_reference(item: Reference, by_name: dict[str, list[Symbol]]) -> Reference:
1555
+ candidates = by_name.get(item.name, [])
1556
+ if len(candidates) == 1:
1557
+ return replace(item, target_id=candidates[0].target_id, resolution=Resolution.COMPLETE)
1558
+ if len(candidates) > 1:
1559
+ return replace(item, resolution=Resolution.AMBIGUOUS)
1560
+ return replace(item, target_id="", resolution=Resolution.UNRESOLVED)
1561
+
1562
+ @staticmethod
1563
+ def _resolve_relation(item: Relation, by_name: dict[str, list[Symbol]]) -> Relation:
1564
+ candidates = by_name.get(item.target_name, [])
1565
+ if len(candidates) == 1:
1566
+ kind = "implements" if candidates[0].kind == "interface" else item.kind
1567
+ return replace(item, target_id=candidates[0].target_id, kind=kind, resolution=Resolution.COMPLETE)
1568
+ if len(candidates) > 1:
1569
+ return replace(item, target_id="", resolution=Resolution.AMBIGUOUS)
1570
+ return replace(item, target_id="", resolution=Resolution.UNRESOLVED)
1571
+
1572
+ @staticmethod
1573
+ def _text(node: Node, source: bytes) -> str:
1574
+ return source[node.start_byte:node.end_byte].decode("utf-8", "replace")
1575
+
1576
+ @staticmethod
1577
+ def _range(
1578
+ path: str | Path,
1579
+ node: Node,
1580
+ source: bytes,
1581
+ line_starts: tuple[int, ...],
1582
+ ) -> SourceRange:
1583
+ start_line, start_character = TreeSitterAdapter._point_at(source, node.start_byte, line_starts)
1584
+ end_line, end_character = TreeSitterAdapter._point_at(source, node.end_byte, line_starts)
1585
+ return SourceRange(
1586
+ str(path),
1587
+ start_line,
1588
+ start_character,
1589
+ end_line,
1590
+ end_character,
1591
+ )
1592
+
1593
+ @staticmethod
1594
+ def _point_at(source: bytes, byte_offset: int, line_starts: tuple[int, ...]) -> tuple[int, int]:
1595
+ row = max(0, bisect_right(line_starts, byte_offset) - 1)
1596
+ line_start = line_starts[row]
1597
+ text = source[line_start:byte_offset].decode("utf-8", "replace")
1598
+ return row, len(text.encode("utf-16-le")) // 2
1599
+
1600
+ @staticmethod
1601
+ def _walk(node: Node) -> Iterator[Node]:
1602
+ yield node
1603
+ for child in node.named_children:
1604
+ yield from TreeSitterAdapter._walk(child)
1605
+
1606
+ @staticmethod
1607
+ def _ancestors(node: Node) -> Iterator[Node]:
1608
+ current = node.parent
1609
+ while current is not None:
1610
+ yield current
1611
+ current = current.parent
1612
+
1613
+ @staticmethod
1614
+ def _descendants(node: Node, kind: str) -> Iterator[Node]:
1615
+ for item in TreeSitterAdapter._walk(node):
1616
+ if item.type == kind:
1617
+ name = item.child_by_field_name("name")
1618
+ yield name or item
1619
+
1620
+ @staticmethod
1621
+ def _first_descendant(node: Node, kind: str) -> Node | None:
1622
+ return next((item for item in TreeSitterAdapter._walk(node) if item.type == kind), None)
1623
+
1624
+ @staticmethod
1625
+ def _owner(
1626
+ symbols: tuple[Symbol, ...],
1627
+ node: Node,
1628
+ source: bytes,
1629
+ line_starts: tuple[int, ...],
1630
+ ) -> Symbol | None:
1631
+ start = TreeSitterAdapter._point_at(source, node.start_byte, line_starts)
1632
+ end = TreeSitterAdapter._point_at(source, node.end_byte, line_starts)
1633
+ matching = [
1634
+ item
1635
+ for item in symbols
1636
+ if (item.range.start_line, item.range.start_character) <= start
1637
+ and end <= (item.range.end_line, item.range.end_character)
1638
+ ]
1639
+ if not matching:
1640
+ return None
1641
+ return min(
1642
+ matching,
1643
+ key=lambda item: (
1644
+ item.range.end_line - item.range.start_line,
1645
+ item.range.end_character - item.range.start_character,
1646
+ ),
1647
+ )
1648
+
1649
+
1650
+ class PythonAdapter(TreeSitterAdapter):
1651
+ def __init__(self) -> None:
1652
+ super().__init__(Language.PYTHON, (".py", ".pyi", ".pyw"), tree_sitter_python)
1653
+
1654
+
1655
+ class RustAdapter(TreeSitterAdapter):
1656
+ def __init__(self) -> None:
1657
+ super().__init__(Language.RUST, (".rs",), tree_sitter_rust)
1658
+
1659
+
1660
+ class CSharpAdapter(TreeSitterAdapter):
1661
+ def __init__(self) -> None:
1662
+ super().__init__(Language.CSHARP, (".cs", ".csx"), tree_sitter_c_sharp)
1663
+
1664
+
1665
+ class CppAdapter(TreeSitterAdapter):
1666
+ def __init__(
1667
+ self,
1668
+ workspace_root: str | Path | None = None,
1669
+ include_paths: tuple[str | Path, ...] = (),
1670
+ ) -> None:
1671
+ super().__init__(
1672
+ Language.CPP,
1673
+ (".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".ixx", ".cppm"),
1674
+ tree_sitter_cpp,
1675
+ workspace_root=workspace_root,
1676
+ include_paths=include_paths,
1677
+ )
1678
+
1679
+
1680
+ _ADAPTERS = (PythonAdapter(), RustAdapter(), CSharpAdapter(), CppAdapter())
1681
+
1682
+
1683
+ def registered_adapters() -> tuple[LanguageAdapter, ...]:
1684
+ # Import lazily so the heavier Delphi grammar is loaded only when registry access is needed.
1685
+ from .delphi import DelphiAdapter
1686
+
1687
+ return cast(tuple[LanguageAdapter, ...], (*_ADAPTERS, DelphiAdapter()))
1688
+
1689
+
1690
+ def adapters_for(path: str | Path) -> tuple[Language, ...]:
1691
+ return tuple(adapter.language for adapter in registered_adapters() if adapter.detect(path))
1692
+
1693
+
1694
+ def adapter_for(
1695
+ language: Language,
1696
+ *,
1697
+ workspace_root: str | Path | None = None,
1698
+ include_paths: tuple[str | Path, ...] = (),
1699
+ ) -> LanguageAdapter:
1700
+ if language is Language.CPP and (workspace_root is not None or include_paths):
1701
+ return CppAdapter(workspace_root, include_paths)
1702
+ return next(item for item in registered_adapters() if item.language is language)
1703
+
1704
+
1705
+ def parse_source(
1706
+ language: Language,
1707
+ path: str | Path,
1708
+ source: str,
1709
+ *,
1710
+ include_detail: bool = True,
1711
+ workspace_root: str | Path | None = None,
1712
+ include_paths: tuple[str | Path, ...] = (),
1713
+ ) -> AdapterResult:
1714
+ adapter = adapter_for(
1715
+ language,
1716
+ workspace_root=workspace_root,
1717
+ include_paths=include_paths,
1718
+ )
1719
+ outline = adapter.parse_outline(path, source)
1720
+ if not include_detail:
1721
+ return outline
1722
+ return adapter.resolve(adapter.parse_detail(path, source, outline))