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,596 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from .indexer import WorkspaceIndexCache, build_workspace_snapshot
|
|
11
|
+
from .models import Language, Problem, Project, Reference, Relation, Resolution, SourceRange, Symbol
|
|
12
|
+
from .protocol import AgentRequest, AgentResponse, Context, Focus, Page, ProtocolError
|
|
13
|
+
from .workspace import WorkspaceSnapshot, stable_id
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_SOURCE_CHUNK_CHARS = 6000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _symbol_mapping(symbol: Symbol) -> dict[str, object]:
|
|
20
|
+
return {
|
|
21
|
+
"target_id": symbol.target_id,
|
|
22
|
+
"name": symbol.name,
|
|
23
|
+
"qualified_name": symbol.qualified_name,
|
|
24
|
+
"kind": symbol.kind,
|
|
25
|
+
"language": symbol.language.value,
|
|
26
|
+
"project_id": symbol.project_id,
|
|
27
|
+
"file_id": symbol.file_id,
|
|
28
|
+
"citation": _citation(symbol.name_range or symbol.range),
|
|
29
|
+
"signature": symbol.signature,
|
|
30
|
+
"native_kind": symbol.native_kind,
|
|
31
|
+
"role": symbol.role,
|
|
32
|
+
"parent_id": symbol.parent_id,
|
|
33
|
+
"name_range": (
|
|
34
|
+
{
|
|
35
|
+
"path": symbol.name_range.path,
|
|
36
|
+
"start_line": symbol.name_range.start_line,
|
|
37
|
+
"start_character": symbol.name_range.start_character,
|
|
38
|
+
"end_line": symbol.name_range.end_line,
|
|
39
|
+
"end_character": symbol.name_range.end_character,
|
|
40
|
+
}
|
|
41
|
+
if symbol.name_range is not None
|
|
42
|
+
else None
|
|
43
|
+
),
|
|
44
|
+
"range": {
|
|
45
|
+
"path": symbol.range.path,
|
|
46
|
+
"start_line": symbol.range.start_line,
|
|
47
|
+
"start_character": symbol.range.start_character,
|
|
48
|
+
"end_line": symbol.range.end_line,
|
|
49
|
+
"end_character": symbol.range.end_character,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _range_mapping(source_range: SourceRange) -> dict[str, object]:
|
|
55
|
+
return {
|
|
56
|
+
"path": source_range.path,
|
|
57
|
+
"start_line": source_range.start_line,
|
|
58
|
+
"start_character": source_range.start_character,
|
|
59
|
+
"end_line": source_range.end_line,
|
|
60
|
+
"end_character": source_range.end_character,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _citation(source_range: SourceRange) -> str:
|
|
65
|
+
return f"{source_range.path}:{source_range.start_line + 1}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _reference_mapping(reference: Reference) -> dict[str, object]:
|
|
69
|
+
return {
|
|
70
|
+
"source_id": reference.source_id,
|
|
71
|
+
"target_id": reference.target_id,
|
|
72
|
+
"name": reference.name,
|
|
73
|
+
"language": reference.language.value,
|
|
74
|
+
"resolution": reference.resolution.value,
|
|
75
|
+
"reference_kind": reference.reference_kind,
|
|
76
|
+
"qualifier": reference.qualifier,
|
|
77
|
+
"import_path": reference.import_path,
|
|
78
|
+
"imported_symbols": [list(item) for item in reference.imported_symbols],
|
|
79
|
+
"module_alias": reference.module_alias,
|
|
80
|
+
"wildcard_import": reference.wildcard_import,
|
|
81
|
+
"project_wide": reference.project_wide,
|
|
82
|
+
"range": _range_mapping(reference.range),
|
|
83
|
+
"citation": _citation(reference.range),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _relation_mapping(
|
|
88
|
+
relation: Relation,
|
|
89
|
+
source_symbol: Symbol | None,
|
|
90
|
+
target_symbol: Symbol | None,
|
|
91
|
+
) -> dict[str, object]:
|
|
92
|
+
return {
|
|
93
|
+
"source_id": relation.source_id,
|
|
94
|
+
"target_id": relation.target_id,
|
|
95
|
+
"target_name": relation.target_name,
|
|
96
|
+
"target_library": relation.target_library,
|
|
97
|
+
"kind": relation.kind,
|
|
98
|
+
"language": source_symbol.language.value if source_symbol is not None else "",
|
|
99
|
+
"resolution": relation.resolution.value,
|
|
100
|
+
"evidence": [_range_mapping(item) for item in relation.evidence],
|
|
101
|
+
"citations": [_citation(item) for item in relation.evidence],
|
|
102
|
+
"source_symbol": _symbol_mapping(source_symbol) if source_symbol is not None else None,
|
|
103
|
+
"target_symbol": _symbol_mapping(target_symbol) if target_symbol is not None else None,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _problem_mapping(problem: Problem) -> dict[str, object]:
|
|
108
|
+
return {
|
|
109
|
+
"code": problem.code,
|
|
110
|
+
"message": problem.message,
|
|
111
|
+
"severity": problem.severity,
|
|
112
|
+
"language": problem.language.value if problem.language is not None else "",
|
|
113
|
+
"range": _range_mapping(problem.range) if problem.range is not None else None,
|
|
114
|
+
"citation": _citation(problem.range) if problem.range is not None else "",
|
|
115
|
+
"metadata": problem.metadata,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CodebaseMapper:
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
root: str | Path,
|
|
123
|
+
snapshot: WorkspaceSnapshot | None = None,
|
|
124
|
+
*,
|
|
125
|
+
languages: tuple[Language, ...] | None = None,
|
|
126
|
+
lazy_details: bool = False,
|
|
127
|
+
index_cache: WorkspaceIndexCache | None = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
self.root = Path(root).expanduser().resolve()
|
|
130
|
+
self.snapshot = snapshot or self._synthetic_snapshot()
|
|
131
|
+
self._languages = languages
|
|
132
|
+
self._lazy_details = lazy_details
|
|
133
|
+
self._index_cache = index_cache
|
|
134
|
+
self._detailed_files: set[str] = set()
|
|
135
|
+
self._focus = Focus()
|
|
136
|
+
self._closed = False
|
|
137
|
+
|
|
138
|
+
def _synthetic_snapshot(self) -> WorkspaceSnapshot:
|
|
139
|
+
project_id = stable_id("project", str(self.root))
|
|
140
|
+
return WorkspaceSnapshot(projects=(Project(project_id, self.root.name or str(self.root), str(self.root)),))
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def open(cls, root: str | Path, languages: str | tuple[Language, ...] = "auto") -> "CodebaseMapper":
|
|
144
|
+
selected = None if languages == "auto" else tuple(Language(item) if isinstance(item, str) else item for item in languages)
|
|
145
|
+
index_cache = WorkspaceIndexCache()
|
|
146
|
+
return cls(
|
|
147
|
+
root,
|
|
148
|
+
build_workspace_snapshot(root, selected, detail_files=(), index_cache=index_cache),
|
|
149
|
+
languages=selected,
|
|
150
|
+
lazy_details=True,
|
|
151
|
+
index_cache=index_cache,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def __enter__(self) -> "CodebaseMapper":
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def __exit__(self, exc_type: object, exc: object, traceback: object) -> None:
|
|
158
|
+
self.close()
|
|
159
|
+
|
|
160
|
+
def close(self) -> None:
|
|
161
|
+
self._closed = True
|
|
162
|
+
|
|
163
|
+
def dispatch(self, request: AgentRequest) -> AgentResponse:
|
|
164
|
+
if self._closed:
|
|
165
|
+
raise RuntimeError("mapper is closed")
|
|
166
|
+
self._refresh_changed_sources()
|
|
167
|
+
request = self._request_with_focus(request)
|
|
168
|
+
if request.action == "open":
|
|
169
|
+
items: list[dict[str, object]] = [
|
|
170
|
+
{
|
|
171
|
+
"project_id": project.project_id,
|
|
172
|
+
"name": project.name,
|
|
173
|
+
"root": project.root,
|
|
174
|
+
"languages": [language.value for language in project.languages],
|
|
175
|
+
"kind": project.kind,
|
|
176
|
+
}
|
|
177
|
+
for project in self.snapshot.projects
|
|
178
|
+
]
|
|
179
|
+
return self._page_response(request, items)
|
|
180
|
+
if request.action == "find":
|
|
181
|
+
query = request.query.casefold()
|
|
182
|
+
requested_source = self.snapshot.files_by_id.get(request.file_id)
|
|
183
|
+
symbols = [symbol for symbol in self.snapshot.symbols
|
|
184
|
+
if (query in symbol.name.casefold() or query in symbol.qualified_name.casefold())
|
|
185
|
+
and (not request.project_id or symbol.project_id == request.project_id)
|
|
186
|
+
and (
|
|
187
|
+
not request.file_id
|
|
188
|
+
or symbol.file_id == request.file_id
|
|
189
|
+
or requested_source is not None
|
|
190
|
+
and Path(symbol.range.path).as_posix() == requested_source.path
|
|
191
|
+
)
|
|
192
|
+
and (not request.languages or symbol.language in request.languages)]
|
|
193
|
+
return self._page_response(request, [_symbol_mapping(symbol) for symbol in symbols])
|
|
194
|
+
if request.action in {"inspect", "focus"}:
|
|
195
|
+
self._ensure_details(request)
|
|
196
|
+
symbol = self.snapshot.symbols_by_id.get(request.target_id)
|
|
197
|
+
if symbol is None:
|
|
198
|
+
return AgentResponse(self.snapshot.revision, {"items": []}, focus=self._focus)
|
|
199
|
+
if request.action == "focus":
|
|
200
|
+
self._focus = Focus(
|
|
201
|
+
project_id=request.project_id or symbol.project_id,
|
|
202
|
+
file_id=request.file_id or symbol.file_id,
|
|
203
|
+
target_id=symbol.target_id,
|
|
204
|
+
languages=request.languages or (symbol.language,),
|
|
205
|
+
)
|
|
206
|
+
response_focus = self._focus
|
|
207
|
+
if not response_focus.target_id:
|
|
208
|
+
response_focus = Focus(
|
|
209
|
+
project_id=symbol.project_id,
|
|
210
|
+
file_id=symbol.file_id,
|
|
211
|
+
target_id=symbol.target_id,
|
|
212
|
+
languages=(symbol.language,),
|
|
213
|
+
)
|
|
214
|
+
return self._inspect_response(request, symbol, response_focus)
|
|
215
|
+
if request.action == "problems":
|
|
216
|
+
items = [_problem_mapping(problem) for problem in self.snapshot.problems]
|
|
217
|
+
return self._page_response(request, items)
|
|
218
|
+
self._ensure_details(request)
|
|
219
|
+
return self._trace_response(request)
|
|
220
|
+
|
|
221
|
+
request = dispatch
|
|
222
|
+
handle = dispatch
|
|
223
|
+
|
|
224
|
+
def _request_with_focus(self, request: AgentRequest) -> AgentRequest:
|
|
225
|
+
if not self._focus.target_id:
|
|
226
|
+
return request
|
|
227
|
+
target_id = request.target_id
|
|
228
|
+
project_id = request.project_id
|
|
229
|
+
file_id = request.file_id
|
|
230
|
+
languages = request.languages
|
|
231
|
+
if request.action in {"inspect", "trace", "focus"} and not target_id:
|
|
232
|
+
target_id = self._focus.target_id
|
|
233
|
+
if request.action == "find":
|
|
234
|
+
if request.project_id or request.file_id or request.languages:
|
|
235
|
+
project_id = request.project_id
|
|
236
|
+
file_id = request.file_id
|
|
237
|
+
languages = request.languages
|
|
238
|
+
else:
|
|
239
|
+
project_id = self._focus.project_id
|
|
240
|
+
file_id = file_id or self._focus.file_id
|
|
241
|
+
languages = self._focus.languages
|
|
242
|
+
if (
|
|
243
|
+
target_id == request.target_id
|
|
244
|
+
and project_id == request.project_id
|
|
245
|
+
and file_id == request.file_id
|
|
246
|
+
and languages == request.languages
|
|
247
|
+
):
|
|
248
|
+
return request
|
|
249
|
+
return replace(
|
|
250
|
+
request,
|
|
251
|
+
target_id=target_id,
|
|
252
|
+
project_id=project_id,
|
|
253
|
+
file_id=file_id,
|
|
254
|
+
languages=languages,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def _refresh_changed_sources(self) -> None:
|
|
258
|
+
if (
|
|
259
|
+
not self._lazy_details
|
|
260
|
+
or self._index_cache is None
|
|
261
|
+
or not self._index_cache.sources_changed(self.root)
|
|
262
|
+
):
|
|
263
|
+
return
|
|
264
|
+
self.snapshot = build_workspace_snapshot(
|
|
265
|
+
self.root,
|
|
266
|
+
self._languages,
|
|
267
|
+
detail_files=(),
|
|
268
|
+
index_cache=self._index_cache,
|
|
269
|
+
)
|
|
270
|
+
self._detailed_files.clear()
|
|
271
|
+
self._focus = Focus()
|
|
272
|
+
|
|
273
|
+
def _ensure_details(self, request: AgentRequest) -> None:
|
|
274
|
+
if not self._lazy_details or not request.target_id:
|
|
275
|
+
return
|
|
276
|
+
target = self.snapshot.symbols_by_id.get(request.target_id)
|
|
277
|
+
if target is None:
|
|
278
|
+
return
|
|
279
|
+
if request.action in {"inspect", "focus"} or request.relation in {"callees", "uses"}:
|
|
280
|
+
source_file = self.snapshot.files_by_id.get(target.file_id)
|
|
281
|
+
required = {source_file.path} if source_file is not None else set()
|
|
282
|
+
else:
|
|
283
|
+
required = {item.path for item in self.snapshot.files}
|
|
284
|
+
missing = required - self._detailed_files
|
|
285
|
+
if not missing:
|
|
286
|
+
return
|
|
287
|
+
self.snapshot = build_workspace_snapshot(
|
|
288
|
+
self.root,
|
|
289
|
+
self._languages,
|
|
290
|
+
detail_files=missing,
|
|
291
|
+
base_snapshot=self.snapshot,
|
|
292
|
+
index_cache=self._index_cache,
|
|
293
|
+
)
|
|
294
|
+
self._detailed_files.update(missing)
|
|
295
|
+
|
|
296
|
+
def _inspect_response(
|
|
297
|
+
self,
|
|
298
|
+
request: AgentRequest,
|
|
299
|
+
symbol: Symbol,
|
|
300
|
+
focus: Focus,
|
|
301
|
+
) -> AgentResponse:
|
|
302
|
+
if request.detail == "members":
|
|
303
|
+
members = [item for item in self.snapshot.symbols if item.parent_id == symbol.target_id]
|
|
304
|
+
return self._page_response(request, [_symbol_mapping(item) for item in members], focus)
|
|
305
|
+
if request.detail == "implementations":
|
|
306
|
+
implementations = self._implementation_symbols(symbol)
|
|
307
|
+
return self._page_response(request, [_symbol_mapping(item) for item in implementations], focus)
|
|
308
|
+
|
|
309
|
+
item = _symbol_mapping(symbol)
|
|
310
|
+
text_key = ""
|
|
311
|
+
full_text = ""
|
|
312
|
+
text_offset = 0
|
|
313
|
+
if request.detail == "declaration":
|
|
314
|
+
text_key = "declaration"
|
|
315
|
+
full_text = symbol.signature
|
|
316
|
+
item["citation"] = _citation(symbol.name_range or symbol.range)
|
|
317
|
+
elif request.detail in {"body", "context"}:
|
|
318
|
+
source_range = symbol.range
|
|
319
|
+
context_lines = 8 if request.detail == "context" else 0
|
|
320
|
+
full_text, source_path, first_line = self._source_excerpt(source_range, context_lines=context_lines)
|
|
321
|
+
text_key = "source"
|
|
322
|
+
item["citation"] = f"{source_path}:{first_line}"
|
|
323
|
+
if text_key:
|
|
324
|
+
text_offset = self._decode_cursor(request)
|
|
325
|
+
if text_offset > len(full_text) or (request.cursor and text_offset == len(full_text)):
|
|
326
|
+
raise ProtocolError("cursor offset is invalid")
|
|
327
|
+
item[text_key] = full_text[text_offset:text_offset + _SOURCE_CHUNK_CHARS]
|
|
328
|
+
if text_key == "source":
|
|
329
|
+
line_offset = full_text[:text_offset].count("\n")
|
|
330
|
+
item["citation"] = f"{symbol.range.path}:{int(str(item['citation']).rsplit(':', 1)[1]) + line_offset}"
|
|
331
|
+
item["truncated"] = text_offset + len(cast(str, item[text_key])) < len(full_text)
|
|
332
|
+
item = self._bounded_detail_item(item, request.detail, request.max_chars)
|
|
333
|
+
item_chars = len(json.dumps(item, sort_keys=True, separators=(",", ":")))
|
|
334
|
+
page = Page()
|
|
335
|
+
if text_key:
|
|
336
|
+
returned_text = cast(str, item.get(text_key, ""))
|
|
337
|
+
if not returned_text and text_offset < len(full_text):
|
|
338
|
+
raise ProtocolError("max_chars is too small to return source evidence")
|
|
339
|
+
end = text_offset + len(returned_text)
|
|
340
|
+
source_truncated = end < len(full_text)
|
|
341
|
+
item["truncated"] = bool(item.get("truncated")) or source_truncated
|
|
342
|
+
page = Page(
|
|
343
|
+
returned=1,
|
|
344
|
+
total=1,
|
|
345
|
+
truncated=source_truncated,
|
|
346
|
+
next_cursor=self._encode_cursor(request, end) if source_truncated else "",
|
|
347
|
+
)
|
|
348
|
+
return AgentResponse(
|
|
349
|
+
self.snapshot.revision,
|
|
350
|
+
{"item": item},
|
|
351
|
+
focus,
|
|
352
|
+
page=page,
|
|
353
|
+
context=Context(chars=item_chars),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _implementation_symbols(self, symbol: Symbol) -> list[Symbol]:
|
|
357
|
+
target_ids = {symbol.target_id}
|
|
358
|
+
if symbol.language is Language.DELPHI:
|
|
359
|
+
key = symbol.qualified_name.casefold()
|
|
360
|
+
signature_key = self._normalized_delphi_signature(symbol)
|
|
361
|
+
for candidate in self.snapshot.symbols:
|
|
362
|
+
roles = {candidate.role, symbol.role}
|
|
363
|
+
if (
|
|
364
|
+
candidate.project_id == symbol.project_id
|
|
365
|
+
and candidate.language is symbol.language
|
|
366
|
+
and candidate.kind == symbol.kind
|
|
367
|
+
and candidate.qualified_name.casefold() == key
|
|
368
|
+
and self._normalized_delphi_signature(candidate) == signature_key
|
|
369
|
+
and "implementation" in roles
|
|
370
|
+
and bool(roles.intersection({"declaration", "definition"}))
|
|
371
|
+
):
|
|
372
|
+
target_ids.add(candidate.target_id)
|
|
373
|
+
for relation in self.snapshot.relations_by_target.get(symbol.target_id, ()):
|
|
374
|
+
if relation.kind == "implements" and relation.resolution is Resolution.COMPLETE and relation.source_id:
|
|
375
|
+
target_ids.add(relation.source_id)
|
|
376
|
+
return [item for item in self.snapshot.symbols if item.target_id in target_ids]
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _normalized_delphi_signature(symbol: Symbol) -> str:
|
|
380
|
+
signature = " ".join(symbol.signature.casefold().split())
|
|
381
|
+
indexed_name = symbol.name.casefold()
|
|
382
|
+
simple_name = symbol.qualified_name.rsplit(".", 1)[-1].casefold()
|
|
383
|
+
signature = signature.replace(indexed_name, simple_name, 1)
|
|
384
|
+
|
|
385
|
+
depth = 0
|
|
386
|
+
for index, character in enumerate(signature):
|
|
387
|
+
if character == "(":
|
|
388
|
+
depth += 1
|
|
389
|
+
elif character == ")" and depth:
|
|
390
|
+
depth -= 1
|
|
391
|
+
elif character == ";" and depth == 0:
|
|
392
|
+
signature = signature[:index]
|
|
393
|
+
break
|
|
394
|
+
return "".join(signature.split())
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def _bounded_detail_item(
|
|
398
|
+
item: dict[str, object],
|
|
399
|
+
detail: str,
|
|
400
|
+
max_chars: int,
|
|
401
|
+
) -> dict[str, object]:
|
|
402
|
+
def encoded_size(value: dict[str, object]) -> int:
|
|
403
|
+
return len(json.dumps(value, sort_keys=True, separators=(",", ":")))
|
|
404
|
+
|
|
405
|
+
if encoded_size(item) <= max_chars:
|
|
406
|
+
return item
|
|
407
|
+
|
|
408
|
+
bounded: dict[str, object] = {"target_id": item["target_id"], "truncated": True}
|
|
409
|
+
for key in ("citation", "name", "language"):
|
|
410
|
+
candidate = {**bounded, key: item[key]}
|
|
411
|
+
if encoded_size(candidate) <= max_chars:
|
|
412
|
+
bounded = candidate
|
|
413
|
+
|
|
414
|
+
detail_key = "source" if detail in {"body", "context"} else "declaration" if detail == "declaration" else ""
|
|
415
|
+
if detail_key:
|
|
416
|
+
text = cast(str, item[detail_key])
|
|
417
|
+
low = 0
|
|
418
|
+
high = len(text)
|
|
419
|
+
while low <= high:
|
|
420
|
+
middle = (low + high) // 2
|
|
421
|
+
candidate = {**bounded, detail_key: text[:middle]}
|
|
422
|
+
if encoded_size(candidate) <= max_chars:
|
|
423
|
+
low = middle + 1
|
|
424
|
+
else:
|
|
425
|
+
high = middle - 1
|
|
426
|
+
bounded[detail_key] = text[:max(0, high)]
|
|
427
|
+
return bounded
|
|
428
|
+
|
|
429
|
+
for key in ("kind", "role", "project_id", "file_id", "qualified_name", "signature"):
|
|
430
|
+
candidate = {**bounded, key: item[key]}
|
|
431
|
+
if encoded_size(candidate) <= max_chars:
|
|
432
|
+
bounded = candidate
|
|
433
|
+
return bounded
|
|
434
|
+
|
|
435
|
+
def _source_excerpt(self, source_range: SourceRange, *, context_lines: int) -> tuple[str, str, int]:
|
|
436
|
+
cached = self._index_cache.entries.get(source_range.path) if self._index_cache is not None else None
|
|
437
|
+
if cached is not None:
|
|
438
|
+
lines = cached.source.splitlines()
|
|
439
|
+
else:
|
|
440
|
+
path = Path(source_range.path)
|
|
441
|
+
source_path = path if path.is_absolute() else self.root / path
|
|
442
|
+
try:
|
|
443
|
+
lines = source_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
444
|
+
except OSError:
|
|
445
|
+
return "", source_range.path, source_range.start_line + 1
|
|
446
|
+
start = max(0, source_range.start_line - context_lines)
|
|
447
|
+
end = min(len(lines), source_range.end_line + context_lines + 1)
|
|
448
|
+
text = "\n".join(lines[start:end])
|
|
449
|
+
return text, source_range.path, start + 1
|
|
450
|
+
|
|
451
|
+
def _trace_response(self, request: AgentRequest) -> AgentResponse:
|
|
452
|
+
relation = request.relation
|
|
453
|
+
references = self.snapshot.references
|
|
454
|
+
aliases = self.snapshot.symbol_aliases_by_id.get(request.target_id, ())
|
|
455
|
+
request_target_ids = tuple(dict.fromkeys(item.target_id for item in aliases))
|
|
456
|
+
if not request_target_ids:
|
|
457
|
+
request_target_ids = (request.target_id,)
|
|
458
|
+
request_target_id_set = set(request_target_ids)
|
|
459
|
+
if relation == "references":
|
|
460
|
+
selected = [item for item in references if item.target_id in request_target_id_set]
|
|
461
|
+
return self._page_response(request, [_reference_mapping(item) for item in selected])
|
|
462
|
+
if relation in {"callers", "used_by"}:
|
|
463
|
+
target_ids = {
|
|
464
|
+
item.source_id for item in references if item.target_id in request_target_id_set
|
|
465
|
+
}
|
|
466
|
+
target_ids.update(
|
|
467
|
+
item.source_id
|
|
468
|
+
for item in self.snapshot.relations
|
|
469
|
+
if relation == "used_by" and item.target_id in request_target_id_set
|
|
470
|
+
)
|
|
471
|
+
caller_symbols = [
|
|
472
|
+
self.snapshot.symbols_by_id[item] for item in target_ids if item in self.snapshot.symbols_by_id
|
|
473
|
+
]
|
|
474
|
+
return self._page_response(request, [_symbol_mapping(item) for item in caller_symbols])
|
|
475
|
+
if relation in {"callees", "uses"}:
|
|
476
|
+
target_ids = {
|
|
477
|
+
item.target_id
|
|
478
|
+
for item in references
|
|
479
|
+
if item.source_id in request_target_id_set and item.target_id
|
|
480
|
+
}
|
|
481
|
+
target_ids.update(
|
|
482
|
+
item.target_id
|
|
483
|
+
for item in self.snapshot.relations
|
|
484
|
+
if relation == "uses"
|
|
485
|
+
and item.source_id in request_target_id_set
|
|
486
|
+
and item.target_id
|
|
487
|
+
)
|
|
488
|
+
callee_symbols = [
|
|
489
|
+
self.snapshot.symbols_by_id[item] for item in target_ids if item in self.snapshot.symbols_by_id
|
|
490
|
+
]
|
|
491
|
+
return self._page_response(request, [_symbol_mapping(item) for item in callee_symbols])
|
|
492
|
+
|
|
493
|
+
relations = tuple(
|
|
494
|
+
dict.fromkeys(
|
|
495
|
+
item
|
|
496
|
+
for target_id in request_target_ids
|
|
497
|
+
for item in (
|
|
498
|
+
self.snapshot.relations_by_source.get(target_id, ())
|
|
499
|
+
+ self.snapshot.relations_by_target.get(target_id, ())
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
if relation:
|
|
504
|
+
relations = tuple(item for item in relations if item.kind == relation)
|
|
505
|
+
relation_items: list[dict[str, object]] = []
|
|
506
|
+
for item in relations:
|
|
507
|
+
source_symbol = self.snapshot.symbols_by_id.get(item.source_id)
|
|
508
|
+
target_symbol = self.snapshot.symbols_by_id.get(item.target_id)
|
|
509
|
+
relation_items.append(_relation_mapping(item, source_symbol, target_symbol))
|
|
510
|
+
return self._page_response(request, relation_items)
|
|
511
|
+
|
|
512
|
+
def _page_response(
|
|
513
|
+
self,
|
|
514
|
+
request: AgentRequest,
|
|
515
|
+
items: list[dict[str, object]],
|
|
516
|
+
focus: Focus | None = None,
|
|
517
|
+
) -> AgentResponse:
|
|
518
|
+
start = self._decode_cursor(request)
|
|
519
|
+
selected: list[dict[str, object]] = []
|
|
520
|
+
used = 0
|
|
521
|
+
for item in items[start:start + request.max_items]:
|
|
522
|
+
size = len(json.dumps(item, sort_keys=True, separators=(",", ":")))
|
|
523
|
+
if used + size > request.max_chars:
|
|
524
|
+
if not selected:
|
|
525
|
+
bounded = self._bounded_page_item(item, request.max_chars)
|
|
526
|
+
selected.append(bounded)
|
|
527
|
+
used = len(json.dumps(bounded, sort_keys=True, separators=(",", ":")))
|
|
528
|
+
break
|
|
529
|
+
selected.append(item)
|
|
530
|
+
used += size
|
|
531
|
+
end = start + len(selected)
|
|
532
|
+
cursor = self._encode_cursor(request, end) if end < len(items) and selected else ""
|
|
533
|
+
return AgentResponse(
|
|
534
|
+
self.snapshot.revision,
|
|
535
|
+
{"items": selected},
|
|
536
|
+
focus=self._focus if focus is None else focus,
|
|
537
|
+
page=Page(
|
|
538
|
+
returned=len(selected),
|
|
539
|
+
total=len(items),
|
|
540
|
+
truncated=end < len(items),
|
|
541
|
+
next_cursor=cursor,
|
|
542
|
+
),
|
|
543
|
+
context=Context(chars=used),
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
@staticmethod
|
|
547
|
+
def _bounded_page_item(item: dict[str, object], max_chars: int) -> dict[str, object]:
|
|
548
|
+
bounded: dict[str, object] = {"truncated": True}
|
|
549
|
+
identifying_fields = (
|
|
550
|
+
"target_id",
|
|
551
|
+
"name",
|
|
552
|
+
"language",
|
|
553
|
+
"citation",
|
|
554
|
+
"kind",
|
|
555
|
+
"role",
|
|
556
|
+
"project_id",
|
|
557
|
+
"file_id",
|
|
558
|
+
"source_id",
|
|
559
|
+
"qualified_name",
|
|
560
|
+
"code",
|
|
561
|
+
"resolution",
|
|
562
|
+
"target_name",
|
|
563
|
+
)
|
|
564
|
+
for key in identifying_fields:
|
|
565
|
+
if key not in item:
|
|
566
|
+
continue
|
|
567
|
+
candidate = {**bounded, key: item[key]}
|
|
568
|
+
if len(json.dumps(candidate, sort_keys=True, separators=(",", ":"))) <= max_chars:
|
|
569
|
+
bounded = candidate
|
|
570
|
+
return bounded
|
|
571
|
+
|
|
572
|
+
def _fingerprint(self, request: AgentRequest) -> str:
|
|
573
|
+
data = request.to_mapping()
|
|
574
|
+
data.pop("cursor")
|
|
575
|
+
return json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
576
|
+
|
|
577
|
+
def _encode_cursor(self, request: AgentRequest, offset: int) -> str:
|
|
578
|
+
payload = {"revision": self.snapshot.revision, "request": self._fingerprint(request), "offset": offset}
|
|
579
|
+
raw = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
580
|
+
return base64.urlsafe_b64encode(raw).decode("ascii")
|
|
581
|
+
|
|
582
|
+
def _decode_cursor(self, request: AgentRequest) -> int:
|
|
583
|
+
if not request.cursor:
|
|
584
|
+
return 0
|
|
585
|
+
try:
|
|
586
|
+
data = json.loads(base64.urlsafe_b64decode(request.cursor.encode("ascii")).decode("utf-8"))
|
|
587
|
+
if data["revision"] != self.snapshot.revision or data["request"] != self._fingerprint(request):
|
|
588
|
+
raise ProtocolError("cursor does not match this workspace or request")
|
|
589
|
+
offset = data["offset"]
|
|
590
|
+
if isinstance(offset, bool) or not isinstance(offset, int) or offset < 0:
|
|
591
|
+
raise ProtocolError("cursor offset is invalid")
|
|
592
|
+
return cast(int, offset)
|
|
593
|
+
except ProtocolError:
|
|
594
|
+
raise
|
|
595
|
+
except (binascii.Error, KeyError, TypeError, UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc:
|
|
596
|
+
raise ProtocolError("cursor is invalid") from exc
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Language(str, Enum):
|
|
8
|
+
PYTHON = "python"
|
|
9
|
+
RUST = "rust"
|
|
10
|
+
CSHARP = "csharp"
|
|
11
|
+
DELPHI = "delphi"
|
|
12
|
+
CPP = "cpp"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Resolution(str, Enum):
|
|
16
|
+
COMPLETE = "complete"
|
|
17
|
+
SOUND_PARTIAL = "sound_partial"
|
|
18
|
+
AMBIGUOUS = "ambiguous"
|
|
19
|
+
UNRESOLVED = "unresolved"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class SourceRange:
|
|
24
|
+
path: str
|
|
25
|
+
start_line: int
|
|
26
|
+
start_character: int
|
|
27
|
+
end_line: int
|
|
28
|
+
end_character: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Project:
|
|
33
|
+
project_id: str
|
|
34
|
+
name: str
|
|
35
|
+
root: str
|
|
36
|
+
languages: tuple[Language, ...] = ()
|
|
37
|
+
kind: str = "synthetic"
|
|
38
|
+
include_paths: tuple[str, ...] = ()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class SourceFile:
|
|
43
|
+
file_id: str
|
|
44
|
+
path: str
|
|
45
|
+
language: Language
|
|
46
|
+
project_id: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Symbol:
|
|
51
|
+
target_id: str
|
|
52
|
+
name: str
|
|
53
|
+
qualified_name: str
|
|
54
|
+
kind: str
|
|
55
|
+
language: Language
|
|
56
|
+
range: SourceRange
|
|
57
|
+
project_id: str
|
|
58
|
+
file_id: str
|
|
59
|
+
signature: str = ""
|
|
60
|
+
native_kind: str = ""
|
|
61
|
+
parent_id: str = ""
|
|
62
|
+
name_range: SourceRange | None = None
|
|
63
|
+
role: str = "definition"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Reference:
|
|
68
|
+
source_id: str
|
|
69
|
+
target_id: str
|
|
70
|
+
name: str
|
|
71
|
+
language: Language
|
|
72
|
+
range: SourceRange
|
|
73
|
+
resolution: Resolution = Resolution.COMPLETE
|
|
74
|
+
reference_kind: str = "reference"
|
|
75
|
+
qualifier: str = ""
|
|
76
|
+
import_path: str = ""
|
|
77
|
+
imported_symbols: tuple[tuple[str, str], ...] = ()
|
|
78
|
+
module_alias: str = ""
|
|
79
|
+
wildcard_import: bool = False
|
|
80
|
+
project_wide: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class Relation:
|
|
85
|
+
source_id: str
|
|
86
|
+
target_id: str
|
|
87
|
+
kind: str
|
|
88
|
+
resolution: Resolution = Resolution.COMPLETE
|
|
89
|
+
evidence: tuple[SourceRange, ...] = ()
|
|
90
|
+
target_name: str = ""
|
|
91
|
+
target_library: str = ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class Problem:
|
|
96
|
+
code: str
|
|
97
|
+
message: str
|
|
98
|
+
severity: str = "warning"
|
|
99
|
+
language: Language | None = None
|
|
100
|
+
range: SourceRange | None = None
|
|
101
|
+
metadata: dict[str, object] = field(default_factory=dict)
|