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.
- agent_codinglanguage_mapper/__init__.py +20 -0
- agent_codinglanguage_mapper/__main__.py +4 -0
- agent_codinglanguage_mapper/_version.py +2 -0
- agent_codinglanguage_mapper/adapters/__init__.py +4 -0
- agent_codinglanguage_mapper/adapters/delphi.py +822 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
- agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
- agent_codinglanguage_mapper/cli.py +138 -0
- agent_codinglanguage_mapper/discovery.py +1328 -0
- agent_codinglanguage_mapper/indexer.py +1102 -0
- agent_codinglanguage_mapper/integration.py +186 -0
- agent_codinglanguage_mapper/lsp_server.py +413 -0
- agent_codinglanguage_mapper/lsp_service.py +447 -0
- agent_codinglanguage_mapper/mapper.py +596 -0
- agent_codinglanguage_mapper/models.py +101 -0
- agent_codinglanguage_mapper/protocol.py +152 -0
- agent_codinglanguage_mapper/rendering.py +153 -0
- agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
- agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
- agent_codinglanguage_mapper/worker.py +29 -0
- agent_codinglanguage_mapper/workspace.py +114 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
- 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))
|