python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from .agent_context import AgentContext
|
|
11
|
+
from .agent_metrics import build_path_metrics
|
|
12
|
+
from .agent_protocol import AgentRequest
|
|
13
|
+
from .lsp_server import outline_unit_name
|
|
14
|
+
from .parallel_outline import (
|
|
15
|
+
OutlineResult,
|
|
16
|
+
OutlineTask,
|
|
17
|
+
ParallelBuildStats,
|
|
18
|
+
run_outline_tasks,
|
|
19
|
+
)
|
|
20
|
+
from .parse_artifacts import OUTLINE_PRODUCER_VERSION
|
|
21
|
+
from .progress import ProgressCallback, ProgressEvent
|
|
22
|
+
from .project_config import ProjectPathConfig
|
|
23
|
+
from .project_discovery import (
|
|
24
|
+
DelphiProjectDiscovery,
|
|
25
|
+
discover_delphi_project,
|
|
26
|
+
populate_workspace_sources,
|
|
27
|
+
)
|
|
28
|
+
from .project_indexer import ProjectIndexer, ProjectIndexResult
|
|
29
|
+
from .semantic import Scope, SourceRange, Symbol, SymbolIndex, SymbolKind
|
|
30
|
+
from .semantic_builder import SemanticModel
|
|
31
|
+
from .source_reader import read_source_text
|
|
32
|
+
|
|
33
|
+
_MAX_IMPLEMENTATION_ITEMS = 50
|
|
34
|
+
_MAX_IMPLEMENTATION_SOURCE_FILES = 4
|
|
35
|
+
_MAX_REFERENCE_ITEMS = 50
|
|
36
|
+
_MAX_REFERENCE_PAGES = 8
|
|
37
|
+
# A references view performs an internal find followed by one or more trace
|
|
38
|
+
# requests. Keep that bounded request on the workspace revision opened for it;
|
|
39
|
+
# the 60-second window is deliberately well above the 20-second query budget.
|
|
40
|
+
_REFERENCE_INITIAL_REVISION_REUSE_SECONDS = 60.0
|
|
41
|
+
_REFERENCE_LIMITATIONS = [
|
|
42
|
+
"Only resolved semantic references are reported; text matches are excluded."
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class CodebaseIndex:
|
|
48
|
+
root: str
|
|
49
|
+
discovery: DelphiProjectDiscovery
|
|
50
|
+
models: Mapping[str, SemanticModel]
|
|
51
|
+
symbol_index: SymbolIndex
|
|
52
|
+
project_results: dict[str, ProjectIndexResult]
|
|
53
|
+
parallel_stats: ParallelBuildStats = ParallelBuildStats(0, 0, 0, 0.0, 0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class _ProjectIndexTask:
|
|
58
|
+
ordinal: int
|
|
59
|
+
source_path: str
|
|
60
|
+
search_paths: tuple[str, ...]
|
|
61
|
+
include_paths: tuple[str, ...]
|
|
62
|
+
defines: tuple[str, ...]
|
|
63
|
+
retain_project_syntax: bool
|
|
64
|
+
source_roots: tuple[str, ...]
|
|
65
|
+
project_config: ProjectPathConfig | None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class _ProjectIndexTaskResult:
|
|
70
|
+
ordinal: int
|
|
71
|
+
source_path: str
|
|
72
|
+
result: ProjectIndexResult
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _index_project_task(task: _ProjectIndexTask) -> _ProjectIndexTaskResult:
|
|
76
|
+
indexer = ProjectIndexer(
|
|
77
|
+
search_paths=task.search_paths,
|
|
78
|
+
include_paths=task.include_paths,
|
|
79
|
+
defines=task.defines,
|
|
80
|
+
source_roots=task.source_roots,
|
|
81
|
+
project_config=task.project_config,
|
|
82
|
+
retain_syntax=task.retain_project_syntax,
|
|
83
|
+
)
|
|
84
|
+
raw_result = indexer.index(task.source_path)
|
|
85
|
+
result = raw_result
|
|
86
|
+
if not task.retain_project_syntax:
|
|
87
|
+
result = replace(
|
|
88
|
+
raw_result,
|
|
89
|
+
parsed_units=[
|
|
90
|
+
replace(unit, syntax_tree=None, preprocessed=None)
|
|
91
|
+
for unit in raw_result.parsed_units
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
return _ProjectIndexTaskResult(task.ordinal, task.source_path, result)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_codebase_index(
|
|
98
|
+
root: str | Path,
|
|
99
|
+
*,
|
|
100
|
+
project_file: str | Path | None = None,
|
|
101
|
+
index_projects: bool = False,
|
|
102
|
+
main_projects_only: bool = False,
|
|
103
|
+
scan_workspace_sources: bool | None = None,
|
|
104
|
+
retain_project_syntax: bool = True,
|
|
105
|
+
retain_project_results: bool = True,
|
|
106
|
+
retain_outline_models: bool = True,
|
|
107
|
+
project_source_roots: Iterable[str | Path] = (),
|
|
108
|
+
on_progress: ProgressCallback | None = None,
|
|
109
|
+
on_project_result: Callable[[str, ProjectIndexResult], None] | None = None,
|
|
110
|
+
on_outline_result: Callable[[OutlineResult], None] | None = None,
|
|
111
|
+
workers: int = 0,
|
|
112
|
+
project_workers: int | None = None,
|
|
113
|
+
outline_memory_budget_bytes: int | None = None,
|
|
114
|
+
outline_heartbeat: Callable[[], object] | None = None,
|
|
115
|
+
outline_force_heartbeat: Callable[[], object] | None = None,
|
|
116
|
+
outline_active_worker_limit: Callable[[], int] | None = None,
|
|
117
|
+
use_artifact_cache: bool = True,
|
|
118
|
+
discovery: DelphiProjectDiscovery | None = None,
|
|
119
|
+
outline_source_files: Iterable[str] | None = None,
|
|
120
|
+
) -> CodebaseIndex:
|
|
121
|
+
progress = _MonotonicProgress(on_progress)
|
|
122
|
+
root_path = Path(root).expanduser().resolve()
|
|
123
|
+
if scan_workspace_sources is None:
|
|
124
|
+
scan_workspace_sources = not (main_projects_only and index_projects)
|
|
125
|
+
if discovery is None:
|
|
126
|
+
discovery = discover_delphi_project(
|
|
127
|
+
root_path,
|
|
128
|
+
project_file=project_file,
|
|
129
|
+
main_projects_only=main_projects_only,
|
|
130
|
+
scan_workspace_sources=scan_workspace_sources,
|
|
131
|
+
on_progress=progress,
|
|
132
|
+
)
|
|
133
|
+
project_results: dict[str, ProjectIndexResult] = {}
|
|
134
|
+
if index_projects and main_projects_only:
|
|
135
|
+
streamed_sources = (
|
|
136
|
+
{source.casefold(): source for source in discovery.source_files}
|
|
137
|
+
if scan_workspace_sources
|
|
138
|
+
else {}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def accept_project_result(
|
|
142
|
+
path: str,
|
|
143
|
+
result: ProjectIndexResult,
|
|
144
|
+
) -> None:
|
|
145
|
+
_add_project_result_sources(streamed_sources, result)
|
|
146
|
+
if on_project_result is not None:
|
|
147
|
+
on_project_result(path, result)
|
|
148
|
+
|
|
149
|
+
project_results = _build_project_results(
|
|
150
|
+
discovery,
|
|
151
|
+
retain_project_syntax=retain_project_syntax,
|
|
152
|
+
retain_results=retain_project_results,
|
|
153
|
+
source_roots=tuple(project_source_roots),
|
|
154
|
+
progress=progress,
|
|
155
|
+
on_progress=on_progress,
|
|
156
|
+
on_result=accept_project_result,
|
|
157
|
+
workers=workers if project_workers is None else project_workers,
|
|
158
|
+
)
|
|
159
|
+
if retain_project_results:
|
|
160
|
+
_set_project_source_inventory(
|
|
161
|
+
discovery,
|
|
162
|
+
project_results,
|
|
163
|
+
merge_existing=scan_workspace_sources,
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
_set_source_inventory(discovery, streamed_sources)
|
|
167
|
+
if not discovery.project_files:
|
|
168
|
+
populate_workspace_sources(discovery, on_progress=progress)
|
|
169
|
+
|
|
170
|
+
models: dict[str, SemanticModel] = {}
|
|
171
|
+
lines_processed = 0
|
|
172
|
+
symbols_discovered = 0
|
|
173
|
+
completed_outlines = 0
|
|
174
|
+
sources_to_outline = (
|
|
175
|
+
tuple(discovery.source_files)
|
|
176
|
+
if outline_source_files is None
|
|
177
|
+
else tuple(outline_source_files)
|
|
178
|
+
)
|
|
179
|
+
outline_tasks = tuple(
|
|
180
|
+
OutlineTask(
|
|
181
|
+
ordinal,
|
|
182
|
+
source,
|
|
183
|
+
tuple(discovery.defines),
|
|
184
|
+
False,
|
|
185
|
+
workspace_root=str(root_path),
|
|
186
|
+
include_paths=tuple(discovery.include_paths),
|
|
187
|
+
project_config=discovery.project_config,
|
|
188
|
+
producer_version=OUTLINE_PRODUCER_VERSION,
|
|
189
|
+
)
|
|
190
|
+
for ordinal, source in enumerate(sources_to_outline)
|
|
191
|
+
if Path(source).suffix.casefold() in {".pas", ".dpr", ".dpk", ".inc"}
|
|
192
|
+
)
|
|
193
|
+
outline_task_count = len(outline_tasks)
|
|
194
|
+
|
|
195
|
+
def on_outline_complete(result: OutlineResult) -> None:
|
|
196
|
+
nonlocal completed_outlines, lines_processed, symbols_discovered
|
|
197
|
+
completed_outlines += 1
|
|
198
|
+
if not result.read_error:
|
|
199
|
+
lines_processed += result.lines_processed
|
|
200
|
+
symbols_discovered += result.symbols_discovered
|
|
201
|
+
_emit_progress(
|
|
202
|
+
progress,
|
|
203
|
+
"outline",
|
|
204
|
+
result.source_path,
|
|
205
|
+
len(discovery.source_files),
|
|
206
|
+
completed_outlines,
|
|
207
|
+
outline_task_count,
|
|
208
|
+
"source unreadable" if result.read_error else "source outlined",
|
|
209
|
+
lines_processed=lines_processed,
|
|
210
|
+
symbols_discovered=symbols_discovered,
|
|
211
|
+
)
|
|
212
|
+
if on_outline_result is not None:
|
|
213
|
+
on_outline_result(result)
|
|
214
|
+
|
|
215
|
+
outline_batch = run_outline_tasks(
|
|
216
|
+
outline_tasks,
|
|
217
|
+
configured_workers=workers,
|
|
218
|
+
use_artifact_cache=use_artifact_cache,
|
|
219
|
+
memory_budget_bytes=outline_memory_budget_bytes,
|
|
220
|
+
include_files=discovery.source_files,
|
|
221
|
+
on_complete=on_outline_complete,
|
|
222
|
+
retain_results=retain_outline_models,
|
|
223
|
+
heartbeat=outline_heartbeat,
|
|
224
|
+
force_heartbeat=outline_force_heartbeat,
|
|
225
|
+
active_worker_limit=outline_active_worker_limit,
|
|
226
|
+
)
|
|
227
|
+
for result in outline_batch.results:
|
|
228
|
+
if result.read_error or result.model is None:
|
|
229
|
+
continue
|
|
230
|
+
models[result.source_path] = result.model
|
|
231
|
+
|
|
232
|
+
symbol_index = SymbolIndex()
|
|
233
|
+
for model in models.values():
|
|
234
|
+
symbol_index.register_unit(model.unit_scope.name, model.unit_scope)
|
|
235
|
+
for model in models.values():
|
|
236
|
+
model.index = symbol_index
|
|
237
|
+
|
|
238
|
+
_emit_progress(
|
|
239
|
+
progress,
|
|
240
|
+
"relations",
|
|
241
|
+
str(root_path),
|
|
242
|
+
len(discovery.source_files),
|
|
243
|
+
len(discovery.source_files),
|
|
244
|
+
len(discovery.source_files),
|
|
245
|
+
"semantic relations indexed",
|
|
246
|
+
lines_processed=lines_processed,
|
|
247
|
+
symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if index_projects and not main_projects_only:
|
|
251
|
+
project_results = _build_project_results(
|
|
252
|
+
discovery,
|
|
253
|
+
retain_project_syntax=retain_project_syntax,
|
|
254
|
+
retain_results=retain_project_results,
|
|
255
|
+
source_roots=tuple(project_source_roots),
|
|
256
|
+
progress=progress,
|
|
257
|
+
on_progress=on_progress,
|
|
258
|
+
on_result=on_project_result,
|
|
259
|
+
workers=workers if project_workers is None else project_workers,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
index = CodebaseIndex(
|
|
263
|
+
root=str(root_path),
|
|
264
|
+
discovery=discovery,
|
|
265
|
+
models=models,
|
|
266
|
+
symbol_index=symbol_index,
|
|
267
|
+
project_results=project_results,
|
|
268
|
+
parallel_stats=outline_batch.stats,
|
|
269
|
+
)
|
|
270
|
+
_emit_progress(
|
|
271
|
+
progress,
|
|
272
|
+
"complete",
|
|
273
|
+
str(root_path),
|
|
274
|
+
len(discovery.source_files),
|
|
275
|
+
len(discovery.source_files),
|
|
276
|
+
len(discovery.source_files),
|
|
277
|
+
"codebase index complete",
|
|
278
|
+
lines_processed=lines_processed,
|
|
279
|
+
symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
|
|
280
|
+
)
|
|
281
|
+
return index
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def build_units_payload(
|
|
285
|
+
root: str | Path,
|
|
286
|
+
*,
|
|
287
|
+
query: str,
|
|
288
|
+
project_file: str | Path | None = None,
|
|
289
|
+
index_projects: bool = False,
|
|
290
|
+
workers: int = 0,
|
|
291
|
+
use_artifact_cache: bool = True,
|
|
292
|
+
discovery: DelphiProjectDiscovery | None = None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
root_path = Path(root).expanduser().resolve()
|
|
295
|
+
if discovery is None:
|
|
296
|
+
discovery = discover_delphi_project(
|
|
297
|
+
root_path,
|
|
298
|
+
project_file=project_file,
|
|
299
|
+
scan_workspace_sources=True,
|
|
300
|
+
)
|
|
301
|
+
needle = query.casefold().strip()
|
|
302
|
+
candidates = tuple(
|
|
303
|
+
source
|
|
304
|
+
for source in discovery.source_files
|
|
305
|
+
if _source_matches_units_query(source, needle)
|
|
306
|
+
)
|
|
307
|
+
index = build_codebase_index(
|
|
308
|
+
root_path,
|
|
309
|
+
project_file=project_file,
|
|
310
|
+
index_projects=index_projects,
|
|
311
|
+
workers=workers,
|
|
312
|
+
use_artifact_cache=use_artifact_cache,
|
|
313
|
+
discovery=discovery,
|
|
314
|
+
outline_source_files=candidates,
|
|
315
|
+
)
|
|
316
|
+
return _units_payload(index, query=query)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _source_matches_units_query(source: str, needle: str) -> bool:
|
|
320
|
+
if not needle or needle in source.casefold():
|
|
321
|
+
return True
|
|
322
|
+
try:
|
|
323
|
+
text = read_source_text(Path(source))
|
|
324
|
+
except OSError:
|
|
325
|
+
return False
|
|
326
|
+
return needle in outline_unit_name(text, source).casefold()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _build_project_results(
|
|
330
|
+
discovery: DelphiProjectDiscovery,
|
|
331
|
+
*,
|
|
332
|
+
retain_project_syntax: bool,
|
|
333
|
+
retain_results: bool,
|
|
334
|
+
source_roots: tuple[str | Path, ...],
|
|
335
|
+
progress: ProgressCallback,
|
|
336
|
+
on_progress: ProgressCallback | None,
|
|
337
|
+
on_result: Callable[[str, ProjectIndexResult], None] | None,
|
|
338
|
+
workers: int,
|
|
339
|
+
) -> dict[str, ProjectIndexResult]:
|
|
340
|
+
project_results: dict[str, ProjectIndexResult] = {}
|
|
341
|
+
project_total = len(discovery.project_files)
|
|
342
|
+
if project_total > 1 and workers != 1:
|
|
343
|
+
completed = 0
|
|
344
|
+
|
|
345
|
+
def on_complete(result: _ProjectIndexTaskResult) -> None:
|
|
346
|
+
nonlocal completed
|
|
347
|
+
completed += 1
|
|
348
|
+
_emit_progress(
|
|
349
|
+
on_progress,
|
|
350
|
+
"projects",
|
|
351
|
+
result.source_path,
|
|
352
|
+
project_total,
|
|
353
|
+
completed,
|
|
354
|
+
project_total,
|
|
355
|
+
"main project indexed",
|
|
356
|
+
)
|
|
357
|
+
if on_result is not None:
|
|
358
|
+
on_result(result.source_path, result.result)
|
|
359
|
+
|
|
360
|
+
tasks = tuple(
|
|
361
|
+
_ProjectIndexTask(
|
|
362
|
+
ordinal=ordinal,
|
|
363
|
+
source_path=project,
|
|
364
|
+
search_paths=tuple(discovery.search_paths),
|
|
365
|
+
include_paths=tuple(discovery.include_paths),
|
|
366
|
+
defines=tuple(discovery.defines),
|
|
367
|
+
retain_project_syntax=retain_project_syntax,
|
|
368
|
+
source_roots=tuple(str(root) for root in source_roots),
|
|
369
|
+
project_config=discovery.project_config,
|
|
370
|
+
)
|
|
371
|
+
for ordinal, project in enumerate(discovery.project_files)
|
|
372
|
+
)
|
|
373
|
+
batch = run_outline_tasks(
|
|
374
|
+
cast(Iterable[OutlineTask], tasks),
|
|
375
|
+
configured_workers=workers,
|
|
376
|
+
use_artifact_cache=False,
|
|
377
|
+
on_complete=cast(Callable[[OutlineResult], None], on_complete),
|
|
378
|
+
retain_results=retain_results,
|
|
379
|
+
task_runner=cast(
|
|
380
|
+
Callable[[OutlineTask], OutlineResult],
|
|
381
|
+
_index_project_task,
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
results = cast(tuple[_ProjectIndexTaskResult, ...], batch.results)
|
|
385
|
+
return {
|
|
386
|
+
result.source_path: result.result
|
|
387
|
+
for result in results
|
|
388
|
+
} if retain_results else {}
|
|
389
|
+
|
|
390
|
+
for ordinal, project in enumerate(discovery.project_files, start=1):
|
|
391
|
+
indexer = ProjectIndexer(
|
|
392
|
+
search_paths=discovery.search_paths,
|
|
393
|
+
include_paths=discovery.include_paths,
|
|
394
|
+
defines=discovery.defines,
|
|
395
|
+
on_progress=progress,
|
|
396
|
+
source_roots=source_roots,
|
|
397
|
+
project_config=discovery.project_config,
|
|
398
|
+
retain_syntax=retain_project_syntax,
|
|
399
|
+
)
|
|
400
|
+
raw_result = indexer.index(project)
|
|
401
|
+
result = raw_result
|
|
402
|
+
if not retain_project_syntax:
|
|
403
|
+
result = replace(
|
|
404
|
+
raw_result,
|
|
405
|
+
parsed_units=[
|
|
406
|
+
replace(unit, syntax_tree=None, preprocessed=None)
|
|
407
|
+
for unit in raw_result.parsed_units
|
|
408
|
+
],
|
|
409
|
+
)
|
|
410
|
+
# Do not invoke streaming consumers while the parser/indexer and its
|
|
411
|
+
# raw syntax/preprocessor graph are still reachable. The compact
|
|
412
|
+
# result is self-contained, so releasing those owners first keeps a
|
|
413
|
+
# sequential project export genuinely bounded.
|
|
414
|
+
del raw_result
|
|
415
|
+
del indexer
|
|
416
|
+
if on_result is not None:
|
|
417
|
+
on_result(project, result)
|
|
418
|
+
if retain_results:
|
|
419
|
+
project_results[project] = result
|
|
420
|
+
_emit_progress(
|
|
421
|
+
on_progress,
|
|
422
|
+
"projects",
|
|
423
|
+
project,
|
|
424
|
+
project_total,
|
|
425
|
+
ordinal,
|
|
426
|
+
project_total,
|
|
427
|
+
"main project indexed",
|
|
428
|
+
)
|
|
429
|
+
return project_results
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _add_project_result_sources(
|
|
433
|
+
sources: dict[str, str],
|
|
434
|
+
result: ProjectIndexResult,
|
|
435
|
+
) -> None:
|
|
436
|
+
for unit in result.parsed_units:
|
|
437
|
+
resolved = str(Path(unit.path).expanduser().resolve())
|
|
438
|
+
sources.setdefault(resolved.casefold(), resolved)
|
|
439
|
+
for include in result.include_files:
|
|
440
|
+
resolved = str(Path(include.path).expanduser().resolve())
|
|
441
|
+
sources.setdefault(resolved.casefold(), resolved)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _set_source_inventory(
|
|
445
|
+
discovery: DelphiProjectDiscovery,
|
|
446
|
+
sources: Mapping[str, str],
|
|
447
|
+
) -> None:
|
|
448
|
+
discovery.source_files = sorted(sources.values(), key=str.casefold)
|
|
449
|
+
discovery.unit_paths = {}
|
|
450
|
+
for source in discovery.source_files:
|
|
451
|
+
key = Path(source).stem.casefold()
|
|
452
|
+
discovery.unit_paths.setdefault(key, []).append(source)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _set_project_source_inventory(
|
|
456
|
+
discovery: DelphiProjectDiscovery,
|
|
457
|
+
project_results: dict[str, ProjectIndexResult],
|
|
458
|
+
*,
|
|
459
|
+
merge_existing: bool = False,
|
|
460
|
+
) -> None:
|
|
461
|
+
sources = (
|
|
462
|
+
{source.casefold(): source for source in discovery.source_files}
|
|
463
|
+
if merge_existing
|
|
464
|
+
else {}
|
|
465
|
+
)
|
|
466
|
+
for result in project_results.values():
|
|
467
|
+
for unit in result.parsed_units:
|
|
468
|
+
resolved = str(Path(unit.path).expanduser().resolve())
|
|
469
|
+
sources.setdefault(resolved.casefold(), resolved)
|
|
470
|
+
for include in result.include_files:
|
|
471
|
+
resolved = str(Path(include.path).expanduser().resolve())
|
|
472
|
+
sources.setdefault(resolved.casefold(), resolved)
|
|
473
|
+
discovery.source_files = sorted(sources.values(), key=str.casefold)
|
|
474
|
+
discovery.unit_paths = {}
|
|
475
|
+
for source in discovery.source_files:
|
|
476
|
+
key = Path(source).stem.casefold()
|
|
477
|
+
discovery.unit_paths.setdefault(key, []).append(source)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _emit_progress(
|
|
481
|
+
callback: ProgressCallback | None,
|
|
482
|
+
phase: str,
|
|
483
|
+
path: str,
|
|
484
|
+
files_discovered: int,
|
|
485
|
+
files_completed: int,
|
|
486
|
+
files_total: int | None,
|
|
487
|
+
detail: str,
|
|
488
|
+
*,
|
|
489
|
+
lines_processed: int = 0,
|
|
490
|
+
symbols_discovered: int = 0,
|
|
491
|
+
) -> None:
|
|
492
|
+
if callback is not None:
|
|
493
|
+
callback(
|
|
494
|
+
ProgressEvent(
|
|
495
|
+
phase,
|
|
496
|
+
"delphi",
|
|
497
|
+
path,
|
|
498
|
+
files_discovered,
|
|
499
|
+
files_completed,
|
|
500
|
+
files_total,
|
|
501
|
+
lines_processed,
|
|
502
|
+
symbols_discovered,
|
|
503
|
+
0,
|
|
504
|
+
detail,
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class _MonotonicProgress:
|
|
510
|
+
def __init__(self, callback: ProgressCallback | None) -> None:
|
|
511
|
+
self._callback = callback
|
|
512
|
+
self._files_discovered = 0
|
|
513
|
+
self._files_completed = 0
|
|
514
|
+
self._files_total: int | None = None
|
|
515
|
+
self._lines_processed = 0
|
|
516
|
+
self._symbols_discovered = 0
|
|
517
|
+
self._cached_files = 0
|
|
518
|
+
|
|
519
|
+
def __call__(self, event: ProgressEvent) -> None:
|
|
520
|
+
self._files_discovered = max(self._files_discovered, event.files_discovered)
|
|
521
|
+
self._files_completed = max(self._files_completed, event.files_completed)
|
|
522
|
+
if event.files_total is not None:
|
|
523
|
+
self._files_total = max(self._files_total or 0, event.files_total)
|
|
524
|
+
self._lines_processed = max(self._lines_processed, event.lines_processed)
|
|
525
|
+
self._symbols_discovered = max(self._symbols_discovered, event.symbols_discovered)
|
|
526
|
+
self._cached_files = max(self._cached_files, event.cached_files)
|
|
527
|
+
if self._callback is not None:
|
|
528
|
+
self._callback(
|
|
529
|
+
replace(
|
|
530
|
+
event,
|
|
531
|
+
files_discovered=self._files_discovered,
|
|
532
|
+
files_completed=self._files_completed,
|
|
533
|
+
files_total=self._files_total,
|
|
534
|
+
lines_processed=self._lines_processed,
|
|
535
|
+
symbols_discovered=self._symbols_discovered,
|
|
536
|
+
cached_files=self._cached_files,
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def render_layer(
|
|
542
|
+
index: CodebaseIndex,
|
|
543
|
+
layer: str,
|
|
544
|
+
*,
|
|
545
|
+
query: str = "",
|
|
546
|
+
output_format: str = "markdown",
|
|
547
|
+
) -> str:
|
|
548
|
+
payload = layer_payload(index, layer, query=query)
|
|
549
|
+
return render_payload(payload, output_format=output_format)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def render_payload(
|
|
553
|
+
payload: dict[str, Any],
|
|
554
|
+
*,
|
|
555
|
+
output_format: str = "markdown",
|
|
556
|
+
) -> str:
|
|
557
|
+
if output_format == "json":
|
|
558
|
+
return json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
559
|
+
return _render_markdown(payload)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def layer_payload(index: CodebaseIndex, layer: str, *, query: str = "") -> dict[str, Any]:
|
|
563
|
+
normalized_layer = layer.casefold()
|
|
564
|
+
if normalized_layer == "overview":
|
|
565
|
+
return _overview_payload(index)
|
|
566
|
+
if normalized_layer == "projects":
|
|
567
|
+
return _projects_payload(index)
|
|
568
|
+
if normalized_layer == "units":
|
|
569
|
+
return _units_payload(index, query=query)
|
|
570
|
+
if normalized_layer == "unit":
|
|
571
|
+
return _unit_payload(index, query=query)
|
|
572
|
+
if normalized_layer == "symbols":
|
|
573
|
+
return _symbols_payload(index, query=query)
|
|
574
|
+
if normalized_layer == "symbol":
|
|
575
|
+
return _symbol_payload(index, query=query)
|
|
576
|
+
if normalized_layer == "implementation":
|
|
577
|
+
return _implementation_payload(index, query=query)
|
|
578
|
+
if normalized_layer == "references":
|
|
579
|
+
return _references_payload(index, query=query)
|
|
580
|
+
if normalized_layer == "problems":
|
|
581
|
+
return _problems_payload(index)
|
|
582
|
+
if normalized_layer == "metrics":
|
|
583
|
+
return _metrics_payload(index, query=query)
|
|
584
|
+
raise ValueError(f"Unknown layer: {layer}")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _overview_payload(index: CodebaseIndex) -> dict[str, Any]:
|
|
588
|
+
return {
|
|
589
|
+
"layer": "overview",
|
|
590
|
+
"root": index.root,
|
|
591
|
+
"project_count": len(index.discovery.project_files),
|
|
592
|
+
"source_count": len(index.discovery.source_files),
|
|
593
|
+
"unit_count": len(index.models),
|
|
594
|
+
"search_paths": index.discovery.search_paths,
|
|
595
|
+
"include_paths": index.discovery.include_paths,
|
|
596
|
+
"defines": index.discovery.defines,
|
|
597
|
+
"problems": [_problem_item(problem) for problem in index.discovery.problems],
|
|
598
|
+
"projects": index.discovery.project_files,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _projects_payload(index: CodebaseIndex) -> dict[str, Any]:
|
|
603
|
+
items: list[dict[str, Any]] = []
|
|
604
|
+
if index.project_results:
|
|
605
|
+
for project, result in index.project_results.items():
|
|
606
|
+
items.append(
|
|
607
|
+
{
|
|
608
|
+
"path": project,
|
|
609
|
+
"parsed_units": [
|
|
610
|
+
{"name": unit.name, "path": unit.path, "has_error": unit.has_error}
|
|
611
|
+
for unit in result.parsed_units
|
|
612
|
+
],
|
|
613
|
+
"include_files": [{"name": item.name, "path": item.path} for item in result.include_files],
|
|
614
|
+
"not_found_units": result.not_found_units,
|
|
615
|
+
"problems": [
|
|
616
|
+
{"kind": problem.problem_type.value, "file": problem.file_name, "message": problem.description}
|
|
617
|
+
for problem in result.problems
|
|
618
|
+
],
|
|
619
|
+
}
|
|
620
|
+
)
|
|
621
|
+
else:
|
|
622
|
+
for project in index.discovery.project_files:
|
|
623
|
+
items.append(
|
|
624
|
+
{
|
|
625
|
+
"path": project,
|
|
626
|
+
"parsed_units": [],
|
|
627
|
+
"include_files": [],
|
|
628
|
+
"not_found_units": [],
|
|
629
|
+
"problems": [],
|
|
630
|
+
"deep_indexed": False,
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
return {"layer": "projects", "root": index.root, "items": items}
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _units_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
637
|
+
needle = query.casefold().strip()
|
|
638
|
+
items = []
|
|
639
|
+
for file_name, model in sorted(index.models.items(), key=lambda item: item[0].casefold()):
|
|
640
|
+
if needle and needle not in model.unit_scope.name.casefold() and needle not in file_name.casefold():
|
|
641
|
+
continue
|
|
642
|
+
items.append(
|
|
643
|
+
{
|
|
644
|
+
"name": model.unit_scope.name,
|
|
645
|
+
"path": file_name,
|
|
646
|
+
"symbol_count": sum(1 for _ in _iter_symbols(model.unit_scope, include_unit=False)),
|
|
647
|
+
}
|
|
648
|
+
)
|
|
649
|
+
return {"layer": "units", "root": index.root, "items": items}
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _unit_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
653
|
+
needle = query.casefold().strip()
|
|
654
|
+
items = []
|
|
655
|
+
for file_name, model in sorted(index.models.items(), key=lambda item: item[0].casefold()):
|
|
656
|
+
if needle and needle not in model.unit_scope.name.casefold() and needle not in file_name.casefold():
|
|
657
|
+
continue
|
|
658
|
+
items.append(
|
|
659
|
+
{
|
|
660
|
+
"name": model.unit_scope.name,
|
|
661
|
+
"path": file_name,
|
|
662
|
+
"symbols": [_symbol_item(symbol) for symbol in _iter_symbols(model.unit_scope, include_unit=False)],
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
return {"layer": "unit", "root": index.root, "items": items}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _symbols_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
669
|
+
needle = query.casefold().strip()
|
|
670
|
+
symbols = []
|
|
671
|
+
for symbol in _all_symbols(index):
|
|
672
|
+
if symbol.kind.value == "unit":
|
|
673
|
+
continue
|
|
674
|
+
if needle and needle not in symbol.name.casefold():
|
|
675
|
+
continue
|
|
676
|
+
symbols.append(_symbol_item(symbol))
|
|
677
|
+
symbols.sort(key=lambda item: (item["name"].casefold(), item["path"].casefold(), item["line"]))
|
|
678
|
+
return {"layer": "symbols", "root": index.root, "query": query, "items": symbols}
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _symbol_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
682
|
+
needle = query.casefold().strip()
|
|
683
|
+
matches = []
|
|
684
|
+
for symbol in _all_symbols(index):
|
|
685
|
+
if needle and needle not in symbol.name.casefold():
|
|
686
|
+
continue
|
|
687
|
+
item = _symbol_item(symbol)
|
|
688
|
+
if symbol.member_scope is not None:
|
|
689
|
+
item["children"] = [_symbol_item(child) for child in _iter_symbols(symbol.member_scope, include_unit=False)]
|
|
690
|
+
matches.append(item)
|
|
691
|
+
return {"layer": "symbol", "root": index.root, "query": query, "items": matches[:50]}
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _implementation_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
695
|
+
needle = query.casefold().strip()
|
|
696
|
+
if not needle:
|
|
697
|
+
return {
|
|
698
|
+
"layer": "implementation",
|
|
699
|
+
"root": index.root,
|
|
700
|
+
"query": query,
|
|
701
|
+
"items": [],
|
|
702
|
+
"message": "Pass a class, routine, or member name in query to read focused source.",
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
all_symbols = tuple(_all_symbols(index))
|
|
706
|
+
matches = [symbol for symbol in all_symbols if symbol.name.casefold() == needle]
|
|
707
|
+
if not matches:
|
|
708
|
+
matches = [
|
|
709
|
+
symbol
|
|
710
|
+
for symbol in all_symbols
|
|
711
|
+
if symbol.kind.value != "unit" and needle in symbol.name.casefold()
|
|
712
|
+
]
|
|
713
|
+
unique_matches: dict[tuple[str, str, int, int, str], Symbol] = {}
|
|
714
|
+
for symbol in matches:
|
|
715
|
+
unique_matches.setdefault(
|
|
716
|
+
(
|
|
717
|
+
symbol.decl_range.file_name,
|
|
718
|
+
symbol.name.casefold(),
|
|
719
|
+
symbol.decl_range.start_line,
|
|
720
|
+
symbol.decl_range.start_col,
|
|
721
|
+
symbol.kind.value,
|
|
722
|
+
),
|
|
723
|
+
symbol,
|
|
724
|
+
)
|
|
725
|
+
matches = sorted(
|
|
726
|
+
unique_matches.values(),
|
|
727
|
+
key=lambda symbol: (
|
|
728
|
+
symbol.decl_range.file_name.casefold(),
|
|
729
|
+
symbol.decl_range.start_line,
|
|
730
|
+
symbol.decl_range.start_col,
|
|
731
|
+
symbol.name.casefold(),
|
|
732
|
+
symbol.kind.value,
|
|
733
|
+
),
|
|
734
|
+
)[:_MAX_IMPLEMENTATION_ITEMS]
|
|
735
|
+
|
|
736
|
+
items: list[dict[str, Any]] = []
|
|
737
|
+
source_cache = _BoundedSourceCache(_MAX_IMPLEMENTATION_SOURCE_FILES)
|
|
738
|
+
routine_lookup = _build_routine_lookup(all_symbols)
|
|
739
|
+
seen_keys: set[tuple[str, str, int, str]] = set()
|
|
740
|
+
for symbol in matches:
|
|
741
|
+
item = _implementation_item(symbol, source_cache, routine_lookup)
|
|
742
|
+
if item is None:
|
|
743
|
+
continue
|
|
744
|
+
key = (item["name"].casefold(), item["path"], item["line"], item["kind"])
|
|
745
|
+
if key in seen_keys:
|
|
746
|
+
continue
|
|
747
|
+
seen_keys.add(key)
|
|
748
|
+
items.append(item)
|
|
749
|
+
items.sort(key=lambda item: (item["path"].casefold(), item["line"], item["name"].casefold()))
|
|
750
|
+
return {"layer": "implementation", "root": index.root, "query": query, "items": items}
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class _BoundedSourceCache(OrderedDict[str, list[str]]):
|
|
754
|
+
def __init__(self, max_entries: int) -> None:
|
|
755
|
+
super().__init__()
|
|
756
|
+
self.max_entries = max_entries
|
|
757
|
+
|
|
758
|
+
def get(self, key: str, default: list[str] | None = None) -> list[str] | None:
|
|
759
|
+
value = super().get(key, default)
|
|
760
|
+
if key in self:
|
|
761
|
+
self.move_to_end(key)
|
|
762
|
+
return value
|
|
763
|
+
|
|
764
|
+
def __setitem__(self, key: str, value: list[str]) -> None:
|
|
765
|
+
super().__setitem__(key, value)
|
|
766
|
+
self.move_to_end(key)
|
|
767
|
+
while len(self) > self.max_entries:
|
|
768
|
+
self.popitem(last=False)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
@dataclass(frozen=True)
|
|
772
|
+
class _RoutineLookup:
|
|
773
|
+
by_name: dict[tuple[str, str], tuple[Symbol, ...]]
|
|
774
|
+
by_owner: dict[tuple[str, str], tuple[Symbol, ...]]
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _build_routine_lookup(symbols: Iterable[Symbol]) -> _RoutineLookup:
|
|
778
|
+
by_name_lists: dict[tuple[str, str], list[Symbol]] = {}
|
|
779
|
+
by_owner_lists: dict[tuple[str, str], list[Symbol]] = {}
|
|
780
|
+
for symbol in symbols:
|
|
781
|
+
if symbol.kind not in _ROUTINE_KINDS:
|
|
782
|
+
continue
|
|
783
|
+
normalized_name = symbol.name.casefold()
|
|
784
|
+
path = symbol.decl_range.file_name
|
|
785
|
+
by_name_lists.setdefault((path, normalized_name), []).append(symbol)
|
|
786
|
+
owner, separator, _member = normalized_name.rpartition(".")
|
|
787
|
+
if separator:
|
|
788
|
+
by_owner_lists.setdefault((path, owner), []).append(symbol)
|
|
789
|
+
return _RoutineLookup(
|
|
790
|
+
by_name={key: tuple(value) for key, value in by_name_lists.items()},
|
|
791
|
+
by_owner={key: tuple(value) for key, value in by_owner_lists.items()},
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _implementation_item(
|
|
796
|
+
symbol: Symbol,
|
|
797
|
+
source_cache: MutableMapping[str, list[str]],
|
|
798
|
+
routine_lookup: _RoutineLookup,
|
|
799
|
+
) -> dict[str, Any] | None:
|
|
800
|
+
fragments: list[dict[str, Any]] = []
|
|
801
|
+
if symbol.kind in {SymbolKind.CLASS, SymbolKind.RECORD, SymbolKind.INTERFACE}:
|
|
802
|
+
declaration = _source_fragment(symbol.decl_range, "declaration", source_cache)
|
|
803
|
+
if declaration is not None:
|
|
804
|
+
fragments.append(declaration)
|
|
805
|
+
fragments.extend(
|
|
806
|
+
_implementation_fragments_for_type(
|
|
807
|
+
symbol, source_cache, routine_lookup
|
|
808
|
+
)
|
|
809
|
+
)
|
|
810
|
+
elif symbol.kind in _ROUTINE_KINDS:
|
|
811
|
+
fragment = _source_fragment(symbol.decl_range, "implementation", source_cache)
|
|
812
|
+
if fragment is not None:
|
|
813
|
+
fragments.append(fragment)
|
|
814
|
+
else:
|
|
815
|
+
fragment = _source_fragment(symbol.decl_range, "declaration", source_cache)
|
|
816
|
+
if fragment is not None:
|
|
817
|
+
fragments.append(fragment)
|
|
818
|
+
fragments.extend(
|
|
819
|
+
_implementation_fragments_for_member(
|
|
820
|
+
symbol, source_cache, routine_lookup
|
|
821
|
+
)
|
|
822
|
+
)
|
|
823
|
+
if not fragments:
|
|
824
|
+
return None
|
|
825
|
+
item = _symbol_item(symbol)
|
|
826
|
+
item["fragments"] = fragments
|
|
827
|
+
return item
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _implementation_fragments_for_type(
|
|
831
|
+
symbol: Symbol,
|
|
832
|
+
source_cache: MutableMapping[str, list[str]],
|
|
833
|
+
routine_lookup: _RoutineLookup,
|
|
834
|
+
) -> list[dict[str, Any]]:
|
|
835
|
+
fragments: list[dict[str, Any]] = []
|
|
836
|
+
seen_ranges: set[SourceRange] = set()
|
|
837
|
+
candidates = routine_lookup.by_owner.get(
|
|
838
|
+
(symbol.decl_range.file_name, symbol.name.casefold()), ()
|
|
839
|
+
)
|
|
840
|
+
for candidate in candidates:
|
|
841
|
+
fragment = _source_fragment(candidate.decl_range, "implementation", source_cache)
|
|
842
|
+
if fragment is None or candidate.decl_range in seen_ranges:
|
|
843
|
+
continue
|
|
844
|
+
seen_ranges.add(candidate.decl_range)
|
|
845
|
+
fragment["symbol"] = candidate.name
|
|
846
|
+
fragments.append(fragment)
|
|
847
|
+
fragments.sort(key=lambda item: (item["range"]["start_line"], item["range"]["start_col"]))
|
|
848
|
+
return fragments
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _implementation_fragments_for_member(
|
|
852
|
+
symbol: Symbol,
|
|
853
|
+
source_cache: MutableMapping[str, list[str]],
|
|
854
|
+
routine_lookup: _RoutineLookup,
|
|
855
|
+
) -> list[dict[str, Any]]:
|
|
856
|
+
owner = symbol.scope.owner
|
|
857
|
+
if owner is None or owner.kind not in {SymbolKind.CLASS, SymbolKind.RECORD, SymbolKind.INTERFACE}:
|
|
858
|
+
return []
|
|
859
|
+
qualified_name = f"{owner.name.casefold()}.{symbol.name.casefold()}"
|
|
860
|
+
fragments: list[dict[str, Any]] = []
|
|
861
|
+
candidates = routine_lookup.by_name.get(
|
|
862
|
+
(symbol.decl_range.file_name, qualified_name), ()
|
|
863
|
+
)
|
|
864
|
+
for candidate in candidates:
|
|
865
|
+
fragment = _source_fragment(candidate.decl_range, "implementation", source_cache)
|
|
866
|
+
if fragment is not None:
|
|
867
|
+
fragment["symbol"] = candidate.name
|
|
868
|
+
fragments.append(fragment)
|
|
869
|
+
return fragments
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _source_fragment(
|
|
873
|
+
source_range: SourceRange,
|
|
874
|
+
fragment_kind: str,
|
|
875
|
+
source_cache: MutableMapping[str, list[str]],
|
|
876
|
+
) -> dict[str, Any] | None:
|
|
877
|
+
lines = source_cache.get(source_range.file_name)
|
|
878
|
+
if lines is None:
|
|
879
|
+
try:
|
|
880
|
+
text = read_source_text(Path(source_range.file_name))
|
|
881
|
+
except (OSError, UnicodeError):
|
|
882
|
+
return None
|
|
883
|
+
lines = text.splitlines(keepends=True)
|
|
884
|
+
source_cache[source_range.file_name] = lines
|
|
885
|
+
if source_range.start_line < 1 or source_range.start_line > len(lines):
|
|
886
|
+
return None
|
|
887
|
+
end_line = min(max(source_range.end_line, source_range.start_line), len(lines))
|
|
888
|
+
snippet = "".join(lines[source_range.start_line - 1 : end_line]).rstrip("\r\n")
|
|
889
|
+
return {
|
|
890
|
+
"fragment_kind": fragment_kind,
|
|
891
|
+
"range": _range_item(source_range),
|
|
892
|
+
"line_count": end_line - source_range.start_line + 1,
|
|
893
|
+
"text": snippet,
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _references_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
898
|
+
return build_references_payload(index.root, query=query)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def build_references_payload(
|
|
902
|
+
root: str | Path,
|
|
903
|
+
*,
|
|
904
|
+
query: str,
|
|
905
|
+
project_file: str | Path | None = None,
|
|
906
|
+
workers: int = 0,
|
|
907
|
+
) -> dict[str, Any]:
|
|
908
|
+
root_path = Path(root).expanduser().resolve()
|
|
909
|
+
payload: dict[str, Any] = {
|
|
910
|
+
"layer": "references",
|
|
911
|
+
"root": str(root_path),
|
|
912
|
+
"query": query,
|
|
913
|
+
"target": None,
|
|
914
|
+
"items": [],
|
|
915
|
+
"completeness": "sound_partial",
|
|
916
|
+
"scan_complete": False,
|
|
917
|
+
"items_truncated": False,
|
|
918
|
+
"limitations": list(_REFERENCE_LIMITATIONS),
|
|
919
|
+
}
|
|
920
|
+
symbol_query = query.strip()
|
|
921
|
+
selector_path = ""
|
|
922
|
+
if "::" in symbol_query:
|
|
923
|
+
path_part, symbol_part = symbol_query.rsplit("::", 1)
|
|
924
|
+
if path_part.strip() and symbol_part.strip():
|
|
925
|
+
selector_path = path_part.strip().replace("\\", "/").casefold()
|
|
926
|
+
symbol_query = symbol_part.strip()
|
|
927
|
+
needle = symbol_query.casefold()
|
|
928
|
+
if not needle:
|
|
929
|
+
payload["message"] = (
|
|
930
|
+
"Pass one exact symbol name or qualified name in query to find "
|
|
931
|
+
"resolved semantic references."
|
|
932
|
+
)
|
|
933
|
+
return payload
|
|
934
|
+
|
|
935
|
+
context = AgentContext.open(
|
|
936
|
+
root_path,
|
|
937
|
+
project_file=project_file,
|
|
938
|
+
workers=workers,
|
|
939
|
+
revision_check_interval_seconds=_REFERENCE_INITIAL_REVISION_REUSE_SECONDS,
|
|
940
|
+
reference_query=symbol_query,
|
|
941
|
+
reference_path=selector_path,
|
|
942
|
+
)
|
|
943
|
+
found = context.handle(
|
|
944
|
+
AgentRequest(
|
|
945
|
+
action="find",
|
|
946
|
+
query=symbol_query,
|
|
947
|
+
max_items=50,
|
|
948
|
+
max_chars=40000,
|
|
949
|
+
)
|
|
950
|
+
)
|
|
951
|
+
candidates = _mapping_items(found.result)
|
|
952
|
+
exact_targets: dict[str, dict[str, object]] = {}
|
|
953
|
+
for candidate in candidates:
|
|
954
|
+
name = candidate.get("name")
|
|
955
|
+
qualified_name = candidate.get("qualified_name")
|
|
956
|
+
candidate_path = candidate.get("path")
|
|
957
|
+
target_id = candidate.get("target_id")
|
|
958
|
+
if not (
|
|
959
|
+
isinstance(name, str)
|
|
960
|
+
and isinstance(qualified_name, str)
|
|
961
|
+
and (name.casefold() == needle or qualified_name.casefold() == needle)
|
|
962
|
+
):
|
|
963
|
+
continue
|
|
964
|
+
if selector_path and not (
|
|
965
|
+
isinstance(candidate_path, str)
|
|
966
|
+
and candidate_path.replace("\\", "/").casefold() == selector_path
|
|
967
|
+
):
|
|
968
|
+
continue
|
|
969
|
+
if isinstance(target_id, str):
|
|
970
|
+
exact_targets.setdefault(target_id, dict(candidate))
|
|
971
|
+
|
|
972
|
+
logical_targets = _logical_reference_targets(exact_targets.values())
|
|
973
|
+
if not logical_targets:
|
|
974
|
+
payload["scan_complete"] = not found.page.truncated
|
|
975
|
+
payload["message"] = f"No exact symbol target found for query: {query}"
|
|
976
|
+
return payload
|
|
977
|
+
if len(logical_targets) > 1:
|
|
978
|
+
payload["candidates"] = logical_targets
|
|
979
|
+
payload["items_truncated"] = found.page.truncated
|
|
980
|
+
payload["message"] = (
|
|
981
|
+
"The query resolves to multiple symbols; pass one candidate as "
|
|
982
|
+
"'<path>::<qualified_name>'."
|
|
983
|
+
)
|
|
984
|
+
return payload
|
|
985
|
+
|
|
986
|
+
target = logical_targets[0]
|
|
987
|
+
payload["target"] = target
|
|
988
|
+
target_id = target["target_id"]
|
|
989
|
+
assert isinstance(target_id, str)
|
|
990
|
+
trace_request = AgentRequest(
|
|
991
|
+
action="trace",
|
|
992
|
+
target_id=target_id,
|
|
993
|
+
relation="references",
|
|
994
|
+
max_items=_MAX_REFERENCE_ITEMS,
|
|
995
|
+
max_chars=40000,
|
|
996
|
+
)
|
|
997
|
+
trace_items: list[dict[str, object]] = []
|
|
998
|
+
remaining_cursor = ""
|
|
999
|
+
for _page_number in range(_MAX_REFERENCE_PAGES):
|
|
1000
|
+
traced = context.handle(trace_request)
|
|
1001
|
+
trace_items.extend(_mapping_items(traced.result))
|
|
1002
|
+
remaining_cursor = traced.page.next_cursor
|
|
1003
|
+
if not remaining_cursor:
|
|
1004
|
+
break
|
|
1005
|
+
trace_request = replace(trace_request, cursor=remaining_cursor)
|
|
1006
|
+
|
|
1007
|
+
metadata = next(
|
|
1008
|
+
(
|
|
1009
|
+
item
|
|
1010
|
+
for item in trace_items
|
|
1011
|
+
if item.get("item_type") == "relation_metadata"
|
|
1012
|
+
),
|
|
1013
|
+
None,
|
|
1014
|
+
)
|
|
1015
|
+
reference_items = [
|
|
1016
|
+
_reference_view_item(target, item)
|
|
1017
|
+
for item in trace_items
|
|
1018
|
+
if item.get("item_type") == "relation"
|
|
1019
|
+
]
|
|
1020
|
+
payload["items"] = _dedupe_reference_view_items(reference_items)
|
|
1021
|
+
if metadata is None:
|
|
1022
|
+
payload["items_truncated"] = True
|
|
1023
|
+
payload["message"] = "Reference metadata was truncated by the bounded trace."
|
|
1024
|
+
return payload
|
|
1025
|
+
|
|
1026
|
+
payload["completeness"] = metadata.get("completeness", "sound_partial")
|
|
1027
|
+
payload["scan_complete"] = metadata.get("scan_complete") is True
|
|
1028
|
+
payload["items_truncated"] = (
|
|
1029
|
+
bool(remaining_cursor) or metadata.get("scan_complete") is not True
|
|
1030
|
+
)
|
|
1031
|
+
for key in (
|
|
1032
|
+
"unresolved_references",
|
|
1033
|
+
"ambiguous_references",
|
|
1034
|
+
"deep_problem_count",
|
|
1035
|
+
"problems",
|
|
1036
|
+
"problems_truncated",
|
|
1037
|
+
):
|
|
1038
|
+
if key in metadata:
|
|
1039
|
+
payload[key] = metadata[key]
|
|
1040
|
+
limitations = metadata.get("limitations")
|
|
1041
|
+
if isinstance(limitations, list):
|
|
1042
|
+
payload["limitations"] = limitations
|
|
1043
|
+
return payload
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def _mapping_items(value: object) -> list[dict[str, object]]:
|
|
1047
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
1048
|
+
return []
|
|
1049
|
+
return [dict(item) for item in value if isinstance(item, Mapping)]
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _logical_reference_targets(
|
|
1053
|
+
targets: Iterable[dict[str, object]],
|
|
1054
|
+
) -> list[dict[str, object]]:
|
|
1055
|
+
logical: dict[tuple[object, object, object], dict[str, object]] = {}
|
|
1056
|
+
for target in targets:
|
|
1057
|
+
key = (
|
|
1058
|
+
target.get("path"),
|
|
1059
|
+
target.get("qualified_name"),
|
|
1060
|
+
target.get("kind"),
|
|
1061
|
+
)
|
|
1062
|
+
logical.setdefault(key, target)
|
|
1063
|
+
return sorted(
|
|
1064
|
+
logical.values(),
|
|
1065
|
+
key=lambda target: (
|
|
1066
|
+
str(target.get("qualified_name", "")).casefold(),
|
|
1067
|
+
str(target.get("path", "")).casefold(),
|
|
1068
|
+
_integer(target.get("line")),
|
|
1069
|
+
),
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _reference_view_item(
|
|
1074
|
+
target: Mapping[str, object],
|
|
1075
|
+
relation: Mapping[str, object],
|
|
1076
|
+
) -> dict[str, object]:
|
|
1077
|
+
evidence = relation.get("evidence")
|
|
1078
|
+
referencing_symbol = relation.get("referencing_symbol")
|
|
1079
|
+
evidence_item = dict(evidence) if isinstance(evidence, Mapping) else {}
|
|
1080
|
+
symbol_item = (
|
|
1081
|
+
dict(referencing_symbol)
|
|
1082
|
+
if isinstance(referencing_symbol, Mapping)
|
|
1083
|
+
else {}
|
|
1084
|
+
)
|
|
1085
|
+
return {
|
|
1086
|
+
"name": target.get("name", ""),
|
|
1087
|
+
"target_id": target.get("target_id", ""),
|
|
1088
|
+
"path": evidence_item.get("path", ""),
|
|
1089
|
+
"line": evidence_item.get("line", 0),
|
|
1090
|
+
"column": evidence_item.get("column", 0),
|
|
1091
|
+
"kind": evidence_item.get("kind", ""),
|
|
1092
|
+
"referencing_symbol": symbol_item,
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _dedupe_reference_view_items(
|
|
1097
|
+
items: Iterable[dict[str, object]],
|
|
1098
|
+
) -> list[dict[str, object]]:
|
|
1099
|
+
groups: dict[tuple[object, ...], list[dict[str, object]]] = {}
|
|
1100
|
+
for item in items:
|
|
1101
|
+
key = (
|
|
1102
|
+
item["target_id"],
|
|
1103
|
+
item["path"],
|
|
1104
|
+
item["line"],
|
|
1105
|
+
item["column"],
|
|
1106
|
+
item["kind"],
|
|
1107
|
+
)
|
|
1108
|
+
groups.setdefault(key, []).append(item)
|
|
1109
|
+
unique: list[dict[str, object]] = []
|
|
1110
|
+
seen_exact: set[str] = set()
|
|
1111
|
+
for group in groups.values():
|
|
1112
|
+
rich = [
|
|
1113
|
+
item
|
|
1114
|
+
for item in group
|
|
1115
|
+
if isinstance(item.get("referencing_symbol"), dict)
|
|
1116
|
+
and bool(item["referencing_symbol"])
|
|
1117
|
+
]
|
|
1118
|
+
for item in rich or group[:1]:
|
|
1119
|
+
exact_key = json.dumps(
|
|
1120
|
+
item,
|
|
1121
|
+
ensure_ascii=False,
|
|
1122
|
+
sort_keys=True,
|
|
1123
|
+
separators=(",", ":"),
|
|
1124
|
+
)
|
|
1125
|
+
if exact_key in seen_exact:
|
|
1126
|
+
continue
|
|
1127
|
+
seen_exact.add(exact_key)
|
|
1128
|
+
unique.append(item)
|
|
1129
|
+
return sorted(
|
|
1130
|
+
unique,
|
|
1131
|
+
key=lambda item: (
|
|
1132
|
+
str(item["path"]).casefold(),
|
|
1133
|
+
_integer(item["line"]),
|
|
1134
|
+
_integer(item["column"]),
|
|
1135
|
+
str(item["kind"]),
|
|
1136
|
+
str(item["target_id"]),
|
|
1137
|
+
json.dumps(
|
|
1138
|
+
item,
|
|
1139
|
+
ensure_ascii=False,
|
|
1140
|
+
sort_keys=True,
|
|
1141
|
+
separators=(",", ":"),
|
|
1142
|
+
),
|
|
1143
|
+
),
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _integer(value: object) -> int:
|
|
1148
|
+
return value if isinstance(value, int) else 0
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _problems_payload(index: CodebaseIndex) -> dict[str, Any]:
|
|
1152
|
+
items = [_problem_item(problem) for problem in index.discovery.problems]
|
|
1153
|
+
for project, result in index.project_results.items():
|
|
1154
|
+
for problem in result.problems:
|
|
1155
|
+
items.append(
|
|
1156
|
+
{
|
|
1157
|
+
"kind": problem.problem_type.value,
|
|
1158
|
+
"origin": project,
|
|
1159
|
+
"message": problem.description,
|
|
1160
|
+
"file": problem.file_name,
|
|
1161
|
+
}
|
|
1162
|
+
)
|
|
1163
|
+
return {
|
|
1164
|
+
"layer": "problems",
|
|
1165
|
+
"root": index.root,
|
|
1166
|
+
"items": items,
|
|
1167
|
+
"deep_indexed": bool(index.project_results),
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def _metrics_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
1172
|
+
return _metrics_payload_from_discovery(
|
|
1173
|
+
index.root,
|
|
1174
|
+
index.discovery,
|
|
1175
|
+
query=query,
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def build_metrics_payload(
|
|
1180
|
+
root: str | Path,
|
|
1181
|
+
*,
|
|
1182
|
+
query: str = "",
|
|
1183
|
+
project_file: str | Path | None = None,
|
|
1184
|
+
workers: int = 0,
|
|
1185
|
+
discovery: DelphiProjectDiscovery | None = None,
|
|
1186
|
+
) -> dict[str, Any]:
|
|
1187
|
+
root_path = Path(root).expanduser().resolve()
|
|
1188
|
+
if discovery is None:
|
|
1189
|
+
discovery = discover_delphi_project(
|
|
1190
|
+
root_path,
|
|
1191
|
+
project_file=project_file,
|
|
1192
|
+
scan_workspace_sources=True,
|
|
1193
|
+
)
|
|
1194
|
+
return _metrics_payload_from_discovery(
|
|
1195
|
+
str(root_path),
|
|
1196
|
+
discovery,
|
|
1197
|
+
query=query,
|
|
1198
|
+
workers=workers,
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def _metrics_payload_from_discovery(
|
|
1203
|
+
root: str,
|
|
1204
|
+
discovery: DelphiProjectDiscovery,
|
|
1205
|
+
*,
|
|
1206
|
+
query: str,
|
|
1207
|
+
workers: int = 0,
|
|
1208
|
+
) -> dict[str, Any]:
|
|
1209
|
+
project_name = "Workspace"
|
|
1210
|
+
if len(discovery.project_files) == 1:
|
|
1211
|
+
project_name = Path(discovery.project_files[0]).stem
|
|
1212
|
+
metrics = build_path_metrics(
|
|
1213
|
+
root,
|
|
1214
|
+
discovery.source_files,
|
|
1215
|
+
defines=discovery.defines,
|
|
1216
|
+
include_paths=discovery.include_paths,
|
|
1217
|
+
project_config=discovery.project_config,
|
|
1218
|
+
project_name=project_name,
|
|
1219
|
+
workers=workers,
|
|
1220
|
+
)
|
|
1221
|
+
needle = query.casefold().strip()
|
|
1222
|
+
units = [
|
|
1223
|
+
unit.to_mapping(detail=True)
|
|
1224
|
+
for unit in metrics.file_metrics
|
|
1225
|
+
if not needle or needle in unit.name.casefold() or needle in unit.path.casefold()
|
|
1226
|
+
]
|
|
1227
|
+
return {
|
|
1228
|
+
"layer": "metrics",
|
|
1229
|
+
"root": root,
|
|
1230
|
+
"project": metrics.to_mapping(),
|
|
1231
|
+
"items": units,
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def _all_symbols(index: CodebaseIndex) -> Iterable[Symbol]:
|
|
1236
|
+
for model in index.models.values():
|
|
1237
|
+
yield from _iter_symbols(model.unit_scope)
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def _iter_symbols(scope: Scope, *, include_unit: bool = True, seen: set[int] | None = None) -> Iterable[Symbol]:
|
|
1241
|
+
if seen is None:
|
|
1242
|
+
seen = set()
|
|
1243
|
+
scope_id = id(scope)
|
|
1244
|
+
if scope_id in seen:
|
|
1245
|
+
return
|
|
1246
|
+
seen.add(scope_id)
|
|
1247
|
+
for symbols in scope.symbols.values():
|
|
1248
|
+
for symbol in symbols:
|
|
1249
|
+
if include_unit or symbol.kind.value != "unit":
|
|
1250
|
+
yield symbol
|
|
1251
|
+
if symbol.member_scope is not None:
|
|
1252
|
+
yield from _iter_symbols(symbol.member_scope, include_unit=include_unit, seen=seen)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _symbol_item(symbol: Symbol) -> dict[str, Any]:
|
|
1256
|
+
return {
|
|
1257
|
+
"name": symbol.name,
|
|
1258
|
+
"kind": symbol.kind.value,
|
|
1259
|
+
"path": symbol.decl_range.file_name,
|
|
1260
|
+
"line": symbol.decl_range.start_line,
|
|
1261
|
+
"column": symbol.decl_range.start_col,
|
|
1262
|
+
"range": _range_item(symbol.decl_range),
|
|
1263
|
+
"visibility": symbol.visibility.value,
|
|
1264
|
+
"type": symbol.type_ref.display_name(),
|
|
1265
|
+
"owner": symbol.scope.owner.name if symbol.scope.owner is not None else symbol.scope.name,
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
_ROUTINE_KINDS = {
|
|
1270
|
+
SymbolKind.PROCEDURE,
|
|
1271
|
+
SymbolKind.FUNCTION,
|
|
1272
|
+
SymbolKind.CONSTRUCTOR,
|
|
1273
|
+
SymbolKind.DESTRUCTOR,
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def _range_item(source_range: SourceRange) -> dict[str, int | str]:
|
|
1278
|
+
return {
|
|
1279
|
+
"path": source_range.file_name,
|
|
1280
|
+
"start_line": source_range.start_line,
|
|
1281
|
+
"start_col": source_range.start_col,
|
|
1282
|
+
"end_line": source_range.end_line,
|
|
1283
|
+
"end_col": source_range.end_col,
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def _problem_item(problem: Any) -> dict[str, str]:
|
|
1288
|
+
return {
|
|
1289
|
+
"kind": problem.kind,
|
|
1290
|
+
"origin": problem.origin,
|
|
1291
|
+
"message": problem.message,
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _render_markdown(payload: dict[str, Any]) -> str:
|
|
1296
|
+
lines = [f"# Delphi Codebase Layer: {payload['layer']}", ""]
|
|
1297
|
+
if payload["layer"] == "overview":
|
|
1298
|
+
lines.extend(
|
|
1299
|
+
[
|
|
1300
|
+
f"- Root: `{payload['root']}`",
|
|
1301
|
+
f"- Projects: {payload['project_count']}",
|
|
1302
|
+
f"- Sources: {payload['source_count']}",
|
|
1303
|
+
f"- Units: {payload['unit_count']}",
|
|
1304
|
+
f"- Defines: {', '.join(payload['defines']) or '(none)'}",
|
|
1305
|
+
"",
|
|
1306
|
+
"## Search Paths",
|
|
1307
|
+
]
|
|
1308
|
+
)
|
|
1309
|
+
lines.extend(f"- `{path}`" for path in payload["search_paths"])
|
|
1310
|
+
lines.append("")
|
|
1311
|
+
lines.append("## Include Paths")
|
|
1312
|
+
lines.extend(f"- `{path}`" for path in payload["include_paths"])
|
|
1313
|
+
elif payload["layer"] in {"units", "symbols"}:
|
|
1314
|
+
for item in payload["items"]:
|
|
1315
|
+
lines.append(f"- `{item['name']}` {item.get('kind', 'unit')} at `{item['path']}:{item.get('line', 1)}`")
|
|
1316
|
+
elif payload["layer"] == "references":
|
|
1317
|
+
if payload.get("message"):
|
|
1318
|
+
lines.append(str(payload["message"]))
|
|
1319
|
+
for item in payload["items"]:
|
|
1320
|
+
referencing_symbol = item.get("referencing_symbol", {})
|
|
1321
|
+
owner = (
|
|
1322
|
+
referencing_symbol.get("qualified_name", "")
|
|
1323
|
+
if isinstance(referencing_symbol, dict)
|
|
1324
|
+
else ""
|
|
1325
|
+
)
|
|
1326
|
+
lines.append(
|
|
1327
|
+
f"- `{item['name']}` {item['kind']} at "
|
|
1328
|
+
f"`{item['path']}:{item['line']}:{item['column']}`"
|
|
1329
|
+
+ (f" from `{owner}`" if owner else "")
|
|
1330
|
+
)
|
|
1331
|
+
lines.extend(
|
|
1332
|
+
[
|
|
1333
|
+
"",
|
|
1334
|
+
f"- Completeness: `{payload['completeness']}`",
|
|
1335
|
+
f"- Scan complete: `{str(payload['scan_complete']).lower()}`",
|
|
1336
|
+
f"- Items truncated: `{str(payload['items_truncated']).lower()}`",
|
|
1337
|
+
]
|
|
1338
|
+
)
|
|
1339
|
+
elif payload["layer"] in {"unit", "symbol"}:
|
|
1340
|
+
for item in payload["items"]:
|
|
1341
|
+
lines.append(f"## {item['name']}")
|
|
1342
|
+
lines.append(f"- Path: `{item['path']}`")
|
|
1343
|
+
for symbol in item.get("symbols", item.get("children", [])):
|
|
1344
|
+
lines.append(
|
|
1345
|
+
f"- `{symbol['name']}` {symbol['kind']} at `{symbol['path']}:{symbol['line']}`"
|
|
1346
|
+
)
|
|
1347
|
+
elif payload["layer"] == "implementation":
|
|
1348
|
+
if payload.get("message"):
|
|
1349
|
+
lines.append(payload["message"])
|
|
1350
|
+
for item in payload["items"]:
|
|
1351
|
+
lines.append(f"## {item['name']}")
|
|
1352
|
+
lines.append(f"- Kind: `{item['kind']}`")
|
|
1353
|
+
lines.append(f"- Path: `{item['path']}`")
|
|
1354
|
+
for fragment in item["fragments"]:
|
|
1355
|
+
source_range = fragment["range"]
|
|
1356
|
+
lines.append("")
|
|
1357
|
+
lines.append(
|
|
1358
|
+
f"### {fragment['fragment_kind']} `{source_range['path']}:{source_range['start_line']}`"
|
|
1359
|
+
)
|
|
1360
|
+
if fragment.get("symbol"):
|
|
1361
|
+
lines.append(f"- Symbol: `{fragment['symbol']}`")
|
|
1362
|
+
lines.append(f"- Lines: {fragment['line_count']}")
|
|
1363
|
+
lines.append("")
|
|
1364
|
+
lines.append("```pascal")
|
|
1365
|
+
lines.append(fragment["text"])
|
|
1366
|
+
lines.append("```")
|
|
1367
|
+
elif payload["layer"] == "projects":
|
|
1368
|
+
for item in payload["items"]:
|
|
1369
|
+
lines.append(f"## `{item['path']}`")
|
|
1370
|
+
for unit in item["parsed_units"]:
|
|
1371
|
+
lines.append(f"- Unit `{unit['name']}` at `{unit['path']}`")
|
|
1372
|
+
elif payload["layer"] == "problems":
|
|
1373
|
+
for item in payload["items"]:
|
|
1374
|
+
lines.append(f"- {item['kind']}: {item['message']} (`{item.get('origin', '')}`)")
|
|
1375
|
+
elif payload["layer"] == "metrics":
|
|
1376
|
+
project = payload["project"]
|
|
1377
|
+
lines.extend(
|
|
1378
|
+
[
|
|
1379
|
+
f"- Project LOC: {project['total_loc']}",
|
|
1380
|
+
f"- Project LOC with includes: {project['total_loc_with_includes']}",
|
|
1381
|
+
f"- Units: {project['unit_count']}",
|
|
1382
|
+
f"- Maintainability index: {project['maintainability_index']:.2f}",
|
|
1383
|
+
]
|
|
1384
|
+
)
|
|
1385
|
+
for item in payload["items"]:
|
|
1386
|
+
lines.extend(
|
|
1387
|
+
[
|
|
1388
|
+
"",
|
|
1389
|
+
f"## {item['name']}",
|
|
1390
|
+
f"- Path: `{item['path']}`",
|
|
1391
|
+
f"- LOC: {item['lines']['total_lines']}",
|
|
1392
|
+
f"- Cyclomatic maximum: {item['cyclomatic']['maximum']}",
|
|
1393
|
+
f"- Maintainability index: {item['maintainability_index']:.2f}",
|
|
1394
|
+
f"- Instability: {item['instability']:.3f}",
|
|
1395
|
+
f"- Abstractness: {item['abstractness']:.3f}",
|
|
1396
|
+
f"- Distance: {item['distance']:.3f}",
|
|
1397
|
+
]
|
|
1398
|
+
)
|
|
1399
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
__all__ = [
|
|
1403
|
+
"CodebaseIndex",
|
|
1404
|
+
"build_codebase_index",
|
|
1405
|
+
"build_metrics_payload",
|
|
1406
|
+
"build_references_payload",
|
|
1407
|
+
"layer_payload",
|
|
1408
|
+
"render_layer",
|
|
1409
|
+
"render_payload",
|
|
1410
|
+
]
|