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,1881 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import unicodedata
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from .agent_protocol import AgentProtocolError
|
|
11
|
+
from .agent_workspace import AgentWorkspace
|
|
12
|
+
from .consts import AttributeName, SyntaxNodeType
|
|
13
|
+
from .nodes import SyntaxNode
|
|
14
|
+
from .parser import DelphiParser
|
|
15
|
+
from .parser_backend import ParserMode
|
|
16
|
+
from .preprocessor import PreprocessedSource
|
|
17
|
+
from .project_config import workspace_include_loader
|
|
18
|
+
from .project_indexer import ProjectIndexer, ProjectProblem
|
|
19
|
+
from .semantic import (
|
|
20
|
+
GenericInstanceTypeRef,
|
|
21
|
+
NamedTypeRef,
|
|
22
|
+
ProcTypeRef,
|
|
23
|
+
ReferenceKind,
|
|
24
|
+
Scope,
|
|
25
|
+
ScopeKind,
|
|
26
|
+
SourceRange,
|
|
27
|
+
Symbol,
|
|
28
|
+
SymbolIndex,
|
|
29
|
+
SymbolKind,
|
|
30
|
+
TypeRef,
|
|
31
|
+
)
|
|
32
|
+
from .source_reader import read_source_text
|
|
33
|
+
from .workspace import WorkspaceSemanticResult, build_workspace_semantics_from_roots
|
|
34
|
+
|
|
35
|
+
_ROUTINE_KINDS = frozenset(
|
|
36
|
+
{
|
|
37
|
+
SymbolKind.METHOD.value,
|
|
38
|
+
SymbolKind.FUNCTION.value,
|
|
39
|
+
SymbolKind.PROCEDURE.value,
|
|
40
|
+
SymbolKind.CONSTRUCTOR.value,
|
|
41
|
+
SymbolKind.DESTRUCTOR.value,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
_TYPE_KINDS = frozenset(
|
|
45
|
+
{
|
|
46
|
+
SymbolKind.CLASS.value,
|
|
47
|
+
SymbolKind.RECORD.value,
|
|
48
|
+
SymbolKind.INTERFACE.value,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
_GroupKey = tuple[str, str, str, str]
|
|
52
|
+
_AdjacencyValue = TypeVar("_AdjacencyValue")
|
|
53
|
+
_MAX_RELATION_PROBLEM_SUMMARIES = 50
|
|
54
|
+
_MAX_REFERENCE_CANDIDATE_FILES = 64
|
|
55
|
+
_REFERENCE_SEMANTIC_BATCH_SIZE = 8
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class RelationTarget:
|
|
60
|
+
target_id: str
|
|
61
|
+
source_path: str
|
|
62
|
+
path: str
|
|
63
|
+
unit_id: str
|
|
64
|
+
unit_name: str
|
|
65
|
+
name: str
|
|
66
|
+
qualified_name: str
|
|
67
|
+
kind: str
|
|
68
|
+
signature: str
|
|
69
|
+
line: int
|
|
70
|
+
column: int
|
|
71
|
+
card: Mapping[str, object]
|
|
72
|
+
visibility: str = ""
|
|
73
|
+
type_name: str = ""
|
|
74
|
+
owner: str = ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class _ReferenceRecord:
|
|
79
|
+
target_group: _GroupKey
|
|
80
|
+
owner_group: _GroupKey | None
|
|
81
|
+
kind: ReferenceKind
|
|
82
|
+
evidence: Mapping[str, object]
|
|
83
|
+
referencing_symbol: Mapping[str, object] | None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class _TypeEdge:
|
|
88
|
+
source_group: _GroupKey
|
|
89
|
+
target_group: _GroupKey
|
|
90
|
+
evidence: Mapping[str, object]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class _UnitEdge:
|
|
95
|
+
source_group: _GroupKey
|
|
96
|
+
target_group: _GroupKey
|
|
97
|
+
evidence: Mapping[str, object]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ProjectRelationIndex:
|
|
101
|
+
"""Lazy deep semantic graph for one selected project revision."""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
workspace: AgentWorkspace,
|
|
106
|
+
project_id: str,
|
|
107
|
+
revision: str,
|
|
108
|
+
targets: Sequence[RelationTarget],
|
|
109
|
+
*,
|
|
110
|
+
targets_complete: bool = True,
|
|
111
|
+
unit_targets_complete: bool = True,
|
|
112
|
+
) -> None:
|
|
113
|
+
self.project_id = project_id
|
|
114
|
+
self.revision = revision
|
|
115
|
+
self.targets_complete = targets_complete
|
|
116
|
+
self.unit_targets_complete = unit_targets_complete
|
|
117
|
+
self._workspace = workspace
|
|
118
|
+
self._root = workspace.root
|
|
119
|
+
self._unit_name_counts: dict[str, int] = {}
|
|
120
|
+
for unit in workspace.units:
|
|
121
|
+
normalized_name = _normalized(unit.name)
|
|
122
|
+
self._unit_name_counts[normalized_name] = (
|
|
123
|
+
self._unit_name_counts.get(normalized_name, 0) + 1
|
|
124
|
+
)
|
|
125
|
+
self._targets = {target.target_id: target for target in targets}
|
|
126
|
+
duplicate_target_units = {
|
|
127
|
+
_normalized(target.unit_name)
|
|
128
|
+
for target in targets
|
|
129
|
+
if self._unit_name_counts.get(_normalized(target.unit_name), 0) > 1
|
|
130
|
+
}
|
|
131
|
+
self._project_memberships = (
|
|
132
|
+
_workspace_project_memberships(workspace)
|
|
133
|
+
if duplicate_target_units
|
|
134
|
+
else ()
|
|
135
|
+
)
|
|
136
|
+
self._target_groups = {
|
|
137
|
+
target.target_id: _target_group(target)
|
|
138
|
+
for target in targets
|
|
139
|
+
}
|
|
140
|
+
self._groups: dict[_GroupKey, list[RelationTarget]] = {}
|
|
141
|
+
for target in targets:
|
|
142
|
+
self._groups.setdefault(_target_group(target), []).append(target)
|
|
143
|
+
for group in self._groups.values():
|
|
144
|
+
group.sort(key=_target_sort_key)
|
|
145
|
+
|
|
146
|
+
self._path_display: dict[str, str] = {}
|
|
147
|
+
self._exact_targets: dict[tuple[str, int, int, str, str], list[RelationTarget]] = {}
|
|
148
|
+
self._position_targets: dict[tuple[str, int, int, str], list[RelationTarget]] = {}
|
|
149
|
+
self._line_targets: dict[tuple[str, int, str, str], list[RelationTarget]] = {}
|
|
150
|
+
self._identity_targets: dict[tuple[str, str, str], list[RelationTarget]] = {}
|
|
151
|
+
for target in targets:
|
|
152
|
+
path_key = _path_key(target.source_path)
|
|
153
|
+
self._path_display.setdefault(path_key, target.path)
|
|
154
|
+
exact = (path_key, target.line, target.column, target.kind, _normalized(target.name))
|
|
155
|
+
self._exact_targets.setdefault(exact, []).append(target)
|
|
156
|
+
position = (path_key, target.line, target.column, target.kind)
|
|
157
|
+
self._position_targets.setdefault(position, []).append(target)
|
|
158
|
+
line = (path_key, target.line, target.kind, _normalized(target.name))
|
|
159
|
+
self._line_targets.setdefault(line, []).append(target)
|
|
160
|
+
identity = (
|
|
161
|
+
_normalized(target.qualified_name),
|
|
162
|
+
target.kind,
|
|
163
|
+
_target_signature_identity(target),
|
|
164
|
+
)
|
|
165
|
+
self._identity_targets.setdefault(identity, []).append(target)
|
|
166
|
+
|
|
167
|
+
self._roots: dict[str, SyntaxNode] = {}
|
|
168
|
+
self._problems: list[dict[str, object]] = []
|
|
169
|
+
self._semantics: WorkspaceSemanticResult | None = None
|
|
170
|
+
self._semantic_unit_paths: dict[int, str] = {}
|
|
171
|
+
self._symbol_targets: dict[int, RelationTarget | None] = {}
|
|
172
|
+
self._references: list[_ReferenceRecord] = []
|
|
173
|
+
self._unit_edges: list[_UnitEdge] = []
|
|
174
|
+
self._inherits: list[_TypeEdge] = []
|
|
175
|
+
self._implements: list[_TypeEdge] = []
|
|
176
|
+
self._unresolved_references = 0
|
|
177
|
+
self._ambiguous_references = 0
|
|
178
|
+
self._deep_graph_ready = False
|
|
179
|
+
self._reference_groups_loaded: set[_GroupKey] = set()
|
|
180
|
+
self._reference_groups_complete: dict[_GroupKey, bool] = {}
|
|
181
|
+
self._unit_sources_loaded: set[_GroupKey] = set()
|
|
182
|
+
self._index_graph()
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def estimated_cache_bytes(self) -> int:
|
|
186
|
+
adjacency_maps = (
|
|
187
|
+
self._references_by_target,
|
|
188
|
+
self._calls_by_target,
|
|
189
|
+
self._calls_by_owner,
|
|
190
|
+
self._unit_edges_by_source,
|
|
191
|
+
self._unit_edges_by_target,
|
|
192
|
+
self._inherits_by_source,
|
|
193
|
+
self._implements_by_source,
|
|
194
|
+
)
|
|
195
|
+
adjacency_groups = sum(len(groups) for groups in adjacency_maps)
|
|
196
|
+
adjacency_slots = sum(
|
|
197
|
+
len(values)
|
|
198
|
+
for groups in adjacency_maps
|
|
199
|
+
for values in groups.values()
|
|
200
|
+
)
|
|
201
|
+
edge_bytes = (
|
|
202
|
+
len(self._references) * 384
|
|
203
|
+
+ len(self._unit_edges) * 320
|
|
204
|
+
+ (len(self._inherits) + len(self._implements)) * 288
|
|
205
|
+
)
|
|
206
|
+
return (
|
|
207
|
+
4096
|
|
208
|
+
+ len(self._targets) * 512
|
|
209
|
+
+ edge_bytes
|
|
210
|
+
+ adjacency_groups * 192
|
|
211
|
+
+ adjacency_slots * 8
|
|
212
|
+
+ len(self._problems) * 512
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def has_target(self, target_id: str) -> bool:
|
|
216
|
+
return target_id in self._targets
|
|
217
|
+
|
|
218
|
+
def trace(
|
|
219
|
+
self,
|
|
220
|
+
target_id: str,
|
|
221
|
+
relation: str,
|
|
222
|
+
*,
|
|
223
|
+
cancel_check: Callable[[], None] | None = None,
|
|
224
|
+
max_relations: int = 12,
|
|
225
|
+
) -> list[dict[str, object]]:
|
|
226
|
+
target = self._targets.get(target_id)
|
|
227
|
+
if target is None:
|
|
228
|
+
raise AgentProtocolError("target_not_found", f"Target not found: {target_id}.")
|
|
229
|
+
self._validate_applicability(target, relation)
|
|
230
|
+
group = self._target_groups[target_id]
|
|
231
|
+
active_project = self._workspace.active_project
|
|
232
|
+
lightweight_units = (
|
|
233
|
+
active_project is not None
|
|
234
|
+
and active_project.kind == "workspace"
|
|
235
|
+
)
|
|
236
|
+
if relation == "references" and (
|
|
237
|
+
len(self._workspace.units) >= 256
|
|
238
|
+
or (
|
|
239
|
+
target.kind != SymbolKind.UNIT.value
|
|
240
|
+
and self._unit_name_counts.get(
|
|
241
|
+
_normalized(target.unit_name),
|
|
242
|
+
0,
|
|
243
|
+
) > 1
|
|
244
|
+
)
|
|
245
|
+
):
|
|
246
|
+
self._ensure_reference_graph(
|
|
247
|
+
group,
|
|
248
|
+
target,
|
|
249
|
+
cancel_check=cancel_check,
|
|
250
|
+
max_relations=max_relations,
|
|
251
|
+
)
|
|
252
|
+
elif relation == "uses" and lightweight_units:
|
|
253
|
+
self._ensure_unit_graph((group,))
|
|
254
|
+
elif relation == "used_by" and lightweight_units:
|
|
255
|
+
self._ensure_unit_graph(
|
|
256
|
+
tuple(
|
|
257
|
+
candidate_group
|
|
258
|
+
for candidate_group, candidates in self._groups.items()
|
|
259
|
+
if candidates and candidates[0].kind == SymbolKind.UNIT.value
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
self._ensure_deep_graph(cancel_check=cancel_check)
|
|
264
|
+
|
|
265
|
+
if relation == "references":
|
|
266
|
+
items = [
|
|
267
|
+
self._relation_item(
|
|
268
|
+
relation,
|
|
269
|
+
record.owner_group or record.target_group,
|
|
270
|
+
record.evidence,
|
|
271
|
+
referencing_symbol=record.referencing_symbol,
|
|
272
|
+
)
|
|
273
|
+
for record in self._references_by_target.get(group, ())
|
|
274
|
+
]
|
|
275
|
+
if target.kind == SymbolKind.UNIT.value:
|
|
276
|
+
items.extend(
|
|
277
|
+
self._relation_item(relation, edge.source_group, edge.evidence)
|
|
278
|
+
for edge in self._unit_edges_by_target.get(group, ())
|
|
279
|
+
)
|
|
280
|
+
elif relation == "callers":
|
|
281
|
+
items = [
|
|
282
|
+
self._relation_item(relation, record.owner_group, record.evidence)
|
|
283
|
+
for record in self._calls_by_target.get(group, ())
|
|
284
|
+
if record.owner_group is not None
|
|
285
|
+
]
|
|
286
|
+
elif relation == "callees":
|
|
287
|
+
items = [
|
|
288
|
+
self._relation_item(relation, record.target_group, record.evidence)
|
|
289
|
+
for record in self._calls_by_owner.get(group, ())
|
|
290
|
+
]
|
|
291
|
+
elif relation == "uses":
|
|
292
|
+
items = [
|
|
293
|
+
self._relation_item(relation, edge.target_group, edge.evidence)
|
|
294
|
+
for edge in self._unit_edges_by_source.get(group, ())
|
|
295
|
+
]
|
|
296
|
+
elif relation == "used_by":
|
|
297
|
+
items = [
|
|
298
|
+
self._relation_item(relation, edge.source_group, edge.evidence)
|
|
299
|
+
for edge in self._unit_edges_by_target.get(group, ())
|
|
300
|
+
]
|
|
301
|
+
elif relation == "inherits":
|
|
302
|
+
items = [
|
|
303
|
+
self._relation_item(relation, edge.target_group, edge.evidence)
|
|
304
|
+
for edge in self._inherits_by_source.get(group, ())
|
|
305
|
+
]
|
|
306
|
+
elif relation == "implements":
|
|
307
|
+
items = [
|
|
308
|
+
self._relation_item(relation, edge.target_group, edge.evidence)
|
|
309
|
+
for edge in self._implements_by_source.get(group, ())
|
|
310
|
+
]
|
|
311
|
+
else: # AgentRequest validation normally rejects this first.
|
|
312
|
+
raise AgentProtocolError("invalid_relation", f"Unsupported relation value: {relation!r}.")
|
|
313
|
+
|
|
314
|
+
compact_items = [item for item in items if item is not None]
|
|
315
|
+
relations = _dedupe_mappings(compact_items)
|
|
316
|
+
relations.sort(key=_relation_sort_key)
|
|
317
|
+
problem_summaries, problem_count = _summarize_relation_problems(
|
|
318
|
+
self._problems,
|
|
319
|
+
limit=_MAX_RELATION_PROBLEM_SUMMARIES,
|
|
320
|
+
)
|
|
321
|
+
result = list(relations)
|
|
322
|
+
result.append(
|
|
323
|
+
self._metadata(
|
|
324
|
+
relation,
|
|
325
|
+
problem_summaries=problem_summaries,
|
|
326
|
+
problem_count=problem_count,
|
|
327
|
+
scan_complete=(
|
|
328
|
+
self._deep_graph_ready
|
|
329
|
+
or self._reference_groups_complete.get(group, False)
|
|
330
|
+
if relation == "references"
|
|
331
|
+
else True
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
def _ensure_unit_graph(self, groups: Sequence[_GroupKey]) -> None:
|
|
338
|
+
if self._deep_graph_ready:
|
|
339
|
+
return
|
|
340
|
+
for group in groups:
|
|
341
|
+
if group in self._unit_sources_loaded:
|
|
342
|
+
continue
|
|
343
|
+
self._unit_sources_loaded.add(group)
|
|
344
|
+
candidates = self._groups.get(group)
|
|
345
|
+
if not candidates:
|
|
346
|
+
continue
|
|
347
|
+
source = candidates[0]
|
|
348
|
+
try:
|
|
349
|
+
text = read_source_text(Path(source.source_path))
|
|
350
|
+
parsed = DelphiParser(
|
|
351
|
+
include_paths=self._workspace.include_paths,
|
|
352
|
+
defines=self._workspace.defines,
|
|
353
|
+
include_loader=workspace_include_loader(
|
|
354
|
+
self._workspace.project_config,
|
|
355
|
+
self._workspace.include_paths,
|
|
356
|
+
),
|
|
357
|
+
mode=(
|
|
358
|
+
ParserMode.TOLERANT
|
|
359
|
+
if len(self._workspace.units) >= 256
|
|
360
|
+
else ParserMode.STRICT
|
|
361
|
+
),
|
|
362
|
+
).parse(
|
|
363
|
+
text,
|
|
364
|
+
source.source_path,
|
|
365
|
+
build_semantic=False,
|
|
366
|
+
)
|
|
367
|
+
except Exception as error:
|
|
368
|
+
self._problems.append(
|
|
369
|
+
{
|
|
370
|
+
"item_type": "relation_problem",
|
|
371
|
+
"kind": "cant_parse_file",
|
|
372
|
+
"message": _safe_problem_message(
|
|
373
|
+
f"{type(error).__name__}: {error}",
|
|
374
|
+
self._root,
|
|
375
|
+
(
|
|
376
|
+
source.source_path,
|
|
377
|
+
getattr(error, "filename", None),
|
|
378
|
+
getattr(error, "filename2", None),
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
"path": source.path,
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
continue
|
|
385
|
+
if parsed.problems:
|
|
386
|
+
first = parsed.problems[0]
|
|
387
|
+
self._problems.append(
|
|
388
|
+
{
|
|
389
|
+
"item_type": "relation_problem",
|
|
390
|
+
"kind": "partial_parse",
|
|
391
|
+
"message": (
|
|
392
|
+
f"Partial DelphiAST parse at {first.line}:{first.column}: "
|
|
393
|
+
f"{first.message}"
|
|
394
|
+
),
|
|
395
|
+
"path": source.path,
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
self._add_lightweight_unit_edges(
|
|
399
|
+
source,
|
|
400
|
+
parsed.root,
|
|
401
|
+
parsed.preprocessed,
|
|
402
|
+
)
|
|
403
|
+
self._index_graph()
|
|
404
|
+
self._problems = _dedupe_mappings(self._problems)
|
|
405
|
+
|
|
406
|
+
def _add_lightweight_unit_edges(
|
|
407
|
+
self,
|
|
408
|
+
source: RelationTarget,
|
|
409
|
+
root: SyntaxNode,
|
|
410
|
+
preprocessed: PreprocessedSource,
|
|
411
|
+
) -> None:
|
|
412
|
+
source_group = _target_group(source)
|
|
413
|
+
unit_targets: dict[str, list[RelationTarget]] = {}
|
|
414
|
+
for target in self._targets.values():
|
|
415
|
+
if target.kind != SymbolKind.UNIT.value:
|
|
416
|
+
continue
|
|
417
|
+
unit_targets.setdefault(
|
|
418
|
+
_normalized(target.unit_name or target.name),
|
|
419
|
+
[],
|
|
420
|
+
).append(target)
|
|
421
|
+
pending = [root]
|
|
422
|
+
while pending:
|
|
423
|
+
node = pending.pop()
|
|
424
|
+
pending.extend(reversed(node.child_nodes))
|
|
425
|
+
if node.typ not in {
|
|
426
|
+
SyntaxNodeType.ntUses,
|
|
427
|
+
SyntaxNodeType.ntContains,
|
|
428
|
+
SyntaxNodeType.ntRequires,
|
|
429
|
+
}:
|
|
430
|
+
continue
|
|
431
|
+
for child in node.child_nodes:
|
|
432
|
+
if child.typ != SyntaxNodeType.ntUnit:
|
|
433
|
+
continue
|
|
434
|
+
name = child.get_attribute(AttributeName.anName).strip()
|
|
435
|
+
candidates = unit_targets.get(_normalized(name), ())
|
|
436
|
+
resolved_target = self._lightweight_unit_target(source, candidates)
|
|
437
|
+
if resolved_target is None:
|
|
438
|
+
self._unresolved_references += 1
|
|
439
|
+
self._problems.append(
|
|
440
|
+
{
|
|
441
|
+
"item_type": "relation_problem",
|
|
442
|
+
"kind": "unresolved_unit",
|
|
443
|
+
"message": f"Unit not found: {name}",
|
|
444
|
+
"path": source.path,
|
|
445
|
+
"line": child.line,
|
|
446
|
+
"column": child.col,
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
continue
|
|
450
|
+
mapped_file, mapped_line, mapped_column = (
|
|
451
|
+
preprocessed.map_position(child.line, child.col)
|
|
452
|
+
)
|
|
453
|
+
evidence = {
|
|
454
|
+
"path": (
|
|
455
|
+
self._display_path(mapped_file)
|
|
456
|
+
if mapped_file and mapped_line > 0
|
|
457
|
+
else source.path
|
|
458
|
+
),
|
|
459
|
+
"line": mapped_line if mapped_line > 0 else child.line,
|
|
460
|
+
"column": (
|
|
461
|
+
mapped_column if mapped_column > 0 else child.col
|
|
462
|
+
),
|
|
463
|
+
"kind": (
|
|
464
|
+
"contains"
|
|
465
|
+
if node.typ == SyntaxNodeType.ntContains
|
|
466
|
+
else "uses"
|
|
467
|
+
),
|
|
468
|
+
}
|
|
469
|
+
self._unit_edges.append(
|
|
470
|
+
_UnitEdge(
|
|
471
|
+
source_group=source_group,
|
|
472
|
+
target_group=_target_group(resolved_target),
|
|
473
|
+
evidence=evidence,
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def _lightweight_unit_target(
|
|
478
|
+
self,
|
|
479
|
+
source: RelationTarget,
|
|
480
|
+
candidates: Sequence[RelationTarget],
|
|
481
|
+
) -> RelationTarget | None:
|
|
482
|
+
if len(candidates) == 1:
|
|
483
|
+
return candidates[0]
|
|
484
|
+
source_path = _path_key(source.source_path)
|
|
485
|
+
local = [
|
|
486
|
+
candidate
|
|
487
|
+
for candidate in candidates
|
|
488
|
+
if any(
|
|
489
|
+
source_path in membership
|
|
490
|
+
and _path_key(candidate.source_path) in membership
|
|
491
|
+
for membership in self._project_memberships
|
|
492
|
+
)
|
|
493
|
+
]
|
|
494
|
+
local_groups = {_target_group(candidate) for candidate in local}
|
|
495
|
+
if len(local_groups) == 1:
|
|
496
|
+
return min(local, key=_target_sort_key)
|
|
497
|
+
source_parent = _path_key(Path(source.source_path).parent)
|
|
498
|
+
same_directory = [
|
|
499
|
+
candidate
|
|
500
|
+
for candidate in candidates
|
|
501
|
+
if _path_key(Path(candidate.source_path).parent) == source_parent
|
|
502
|
+
]
|
|
503
|
+
same_directory_groups = {
|
|
504
|
+
_target_group(candidate) for candidate in same_directory
|
|
505
|
+
}
|
|
506
|
+
if len(same_directory_groups) == 1:
|
|
507
|
+
return min(same_directory, key=_target_sort_key)
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
def _ensure_reference_graph(
|
|
511
|
+
self,
|
|
512
|
+
group: _GroupKey,
|
|
513
|
+
target: RelationTarget,
|
|
514
|
+
*,
|
|
515
|
+
cancel_check: Callable[[], None] | None,
|
|
516
|
+
max_relations: int,
|
|
517
|
+
) -> None:
|
|
518
|
+
if self._deep_graph_ready or group in self._reference_groups_loaded:
|
|
519
|
+
return
|
|
520
|
+
scan_complete = self._scan_reference_sources(
|
|
521
|
+
group,
|
|
522
|
+
target,
|
|
523
|
+
cancel_check=cancel_check,
|
|
524
|
+
max_relations=max_relations,
|
|
525
|
+
)
|
|
526
|
+
self._reference_groups_loaded.add(group)
|
|
527
|
+
self._reference_groups_complete[group] = scan_complete
|
|
528
|
+
self._index_graph()
|
|
529
|
+
self._problems = _dedupe_mappings(self._problems)
|
|
530
|
+
|
|
531
|
+
def _scan_reference_sources(
|
|
532
|
+
self,
|
|
533
|
+
group: _GroupKey,
|
|
534
|
+
target: RelationTarget,
|
|
535
|
+
*,
|
|
536
|
+
cancel_check: Callable[[], None] | None,
|
|
537
|
+
max_relations: int,
|
|
538
|
+
) -> bool:
|
|
539
|
+
parser = DelphiParser(
|
|
540
|
+
include_paths=self._workspace.include_paths,
|
|
541
|
+
defines=self._workspace.defines,
|
|
542
|
+
include_loader=workspace_include_loader(
|
|
543
|
+
self._workspace.project_config,
|
|
544
|
+
self._workspace.include_paths,
|
|
545
|
+
),
|
|
546
|
+
mode=(
|
|
547
|
+
ParserMode.TOLERANT
|
|
548
|
+
if len(self._workspace.units) >= 256
|
|
549
|
+
else ParserMode.STRICT
|
|
550
|
+
),
|
|
551
|
+
legacy_fallback_max_chars=0,
|
|
552
|
+
)
|
|
553
|
+
target_path = Path(target.source_path).expanduser().resolve()
|
|
554
|
+
bounded = len(self._workspace.units) >= 256
|
|
555
|
+
|
|
556
|
+
def relation_budget_reached() -> bool:
|
|
557
|
+
return (
|
|
558
|
+
bounded
|
|
559
|
+
and len(self._references_by_target.get(group, ()))
|
|
560
|
+
>= max_relations
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
target_parse_path = target_path
|
|
564
|
+
target_parsed = None
|
|
565
|
+
target_is_unit = False
|
|
566
|
+
owner_candidates: list[Path] = []
|
|
567
|
+
for unit in self._workspace.units:
|
|
568
|
+
source_path = Path(unit.path)
|
|
569
|
+
if not source_path.is_absolute():
|
|
570
|
+
source_path = self._workspace.root / source_path
|
|
571
|
+
source_path = source_path.expanduser().resolve()
|
|
572
|
+
if source_path == target_path:
|
|
573
|
+
target_is_unit = True
|
|
574
|
+
break
|
|
575
|
+
if _normalized(unit.name) == _normalized(target.unit_name):
|
|
576
|
+
owner_candidates.append(source_path)
|
|
577
|
+
if not target_is_unit:
|
|
578
|
+
target_path_key = _path_key(target_path)
|
|
579
|
+
for owner_path in owner_candidates:
|
|
580
|
+
try:
|
|
581
|
+
owner_text = read_source_text(owner_path)
|
|
582
|
+
owner_parsed = parser.parse(
|
|
583
|
+
owner_text,
|
|
584
|
+
str(owner_path),
|
|
585
|
+
build_semantic=False,
|
|
586
|
+
)
|
|
587
|
+
except Exception as error: # noqa: BLE001
|
|
588
|
+
self._add_load_problem(owner_path, error)
|
|
589
|
+
continue
|
|
590
|
+
if any(
|
|
591
|
+
_path_key(entry.file_name) == target_path_key
|
|
592
|
+
for entry in owner_parsed.preprocessed.source_map
|
|
593
|
+
):
|
|
594
|
+
target_parse_path = owner_path
|
|
595
|
+
target_parsed = owner_parsed
|
|
596
|
+
break
|
|
597
|
+
if target_parsed is None:
|
|
598
|
+
try:
|
|
599
|
+
target_text = read_source_text(target_path)
|
|
600
|
+
target_parsed = parser.parse(
|
|
601
|
+
target_text,
|
|
602
|
+
str(target_path),
|
|
603
|
+
build_semantic=False,
|
|
604
|
+
)
|
|
605
|
+
except Exception as error: # noqa: BLE001
|
|
606
|
+
self._add_load_problem(target_path, error)
|
|
607
|
+
return True
|
|
608
|
+
|
|
609
|
+
def add_semantics(
|
|
610
|
+
candidates: Sequence[tuple[Path, SyntaxNode, PreprocessedSource]],
|
|
611
|
+
) -> None:
|
|
612
|
+
roots = {str(target_parse_path): target_parsed.root}
|
|
613
|
+
sources = {
|
|
614
|
+
str(target_parse_path): target_parsed.preprocessed,
|
|
615
|
+
}
|
|
616
|
+
for source_path, root, preprocessed in candidates:
|
|
617
|
+
roots[str(source_path)] = root
|
|
618
|
+
sources[str(source_path)] = preprocessed
|
|
619
|
+
semantics = build_workspace_semantics_from_roots(
|
|
620
|
+
roots,
|
|
621
|
+
preprocessed_sources=sources,
|
|
622
|
+
)
|
|
623
|
+
self._semantics = semantics
|
|
624
|
+
self._roots = roots
|
|
625
|
+
# Semantic symbols belong to this short-lived candidate batch.
|
|
626
|
+
# Object ids may be reused after the previous batch is released.
|
|
627
|
+
self._symbol_targets.clear()
|
|
628
|
+
self._build_graph(
|
|
629
|
+
semantics,
|
|
630
|
+
cancel_check=cancel_check,
|
|
631
|
+
)
|
|
632
|
+
self._add_semantic_problems(semantics)
|
|
633
|
+
self._index_graph()
|
|
634
|
+
|
|
635
|
+
needle = target.name.casefold()
|
|
636
|
+
unit_needle = target.unit_name.casefold()
|
|
637
|
+
legacy_candidates_parsed = 0
|
|
638
|
+
exact_extras_parsed = 0
|
|
639
|
+
scan_complete = True
|
|
640
|
+
candidate_batch: list[tuple[Path, SyntaxNode, PreprocessedSource]] = []
|
|
641
|
+
semantics_built = False
|
|
642
|
+
|
|
643
|
+
def flush_candidates() -> bool:
|
|
644
|
+
nonlocal semantics_built
|
|
645
|
+
add_semantics(candidate_batch)
|
|
646
|
+
candidate_batch.clear()
|
|
647
|
+
semantics_built = True
|
|
648
|
+
return relation_budget_reached()
|
|
649
|
+
|
|
650
|
+
for unit in self._workspace.units:
|
|
651
|
+
if cancel_check is not None:
|
|
652
|
+
cancel_check()
|
|
653
|
+
source_path = Path(unit.path)
|
|
654
|
+
if not source_path.is_absolute():
|
|
655
|
+
source_path = self._workspace.root / source_path
|
|
656
|
+
source_path = source_path.expanduser().resolve()
|
|
657
|
+
if source_path == target_path:
|
|
658
|
+
continue
|
|
659
|
+
if _normalized(unit.name) == _normalized(target.unit_name):
|
|
660
|
+
continue
|
|
661
|
+
try:
|
|
662
|
+
text = read_source_text(source_path)
|
|
663
|
+
except Exception as error: # noqa: BLE001
|
|
664
|
+
self._add_load_problem(source_path, error)
|
|
665
|
+
continue
|
|
666
|
+
folded_text = text.casefold()
|
|
667
|
+
if needle not in folded_text or unit_needle not in folded_text:
|
|
668
|
+
continue
|
|
669
|
+
legacy_limit_reached = (
|
|
670
|
+
legacy_candidates_parsed >= _MAX_REFERENCE_CANDIDATE_FILES
|
|
671
|
+
)
|
|
672
|
+
if legacy_limit_reached:
|
|
673
|
+
scan_complete = False
|
|
674
|
+
exact_candidate = _text_contains_reference_token(
|
|
675
|
+
text,
|
|
676
|
+
needle,
|
|
677
|
+
) and _text_contains_reference_token(text, unit_needle)
|
|
678
|
+
if legacy_limit_reached and (
|
|
679
|
+
not exact_candidate
|
|
680
|
+
or exact_extras_parsed >= _MAX_REFERENCE_CANDIDATE_FILES
|
|
681
|
+
):
|
|
682
|
+
continue
|
|
683
|
+
if cancel_check is not None:
|
|
684
|
+
cancel_check()
|
|
685
|
+
try:
|
|
686
|
+
parsed = parser.parse(
|
|
687
|
+
text,
|
|
688
|
+
str(source_path),
|
|
689
|
+
build_semantic=False,
|
|
690
|
+
)
|
|
691
|
+
except Exception as error: # noqa: BLE001
|
|
692
|
+
self._add_load_problem(source_path, error)
|
|
693
|
+
continue
|
|
694
|
+
if legacy_limit_reached:
|
|
695
|
+
exact_extras_parsed += 1
|
|
696
|
+
else:
|
|
697
|
+
legacy_candidates_parsed += 1
|
|
698
|
+
if cancel_check is not None:
|
|
699
|
+
cancel_check()
|
|
700
|
+
candidate_batch.append(
|
|
701
|
+
(source_path, parsed.root, parsed.preprocessed)
|
|
702
|
+
)
|
|
703
|
+
if (
|
|
704
|
+
len(candidate_batch) >= _REFERENCE_SEMANTIC_BATCH_SIZE
|
|
705
|
+
and flush_candidates()
|
|
706
|
+
):
|
|
707
|
+
return False
|
|
708
|
+
budget_reached = (
|
|
709
|
+
(candidate_batch or not semantics_built)
|
|
710
|
+
and flush_candidates()
|
|
711
|
+
)
|
|
712
|
+
return scan_complete and not budget_reached
|
|
713
|
+
|
|
714
|
+
def _add_load_problem(
|
|
715
|
+
self,
|
|
716
|
+
source_path: Path,
|
|
717
|
+
error: Exception,
|
|
718
|
+
) -> None:
|
|
719
|
+
self._problems.append(
|
|
720
|
+
{
|
|
721
|
+
"item_type": "relation_problem",
|
|
722
|
+
"kind": "cant_parse_file",
|
|
723
|
+
"message": _safe_problem_message(
|
|
724
|
+
f"{type(error).__name__}: {error}",
|
|
725
|
+
self._root,
|
|
726
|
+
(
|
|
727
|
+
source_path,
|
|
728
|
+
getattr(error, "filename", None),
|
|
729
|
+
getattr(error, "filename2", None),
|
|
730
|
+
),
|
|
731
|
+
),
|
|
732
|
+
"path": _display_project_path(
|
|
733
|
+
source_path,
|
|
734
|
+
self._root,
|
|
735
|
+
),
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
def _ensure_deep_graph(
|
|
740
|
+
self,
|
|
741
|
+
*,
|
|
742
|
+
cancel_check: Callable[[], None] | None = None,
|
|
743
|
+
) -> None:
|
|
744
|
+
if self._deep_graph_ready:
|
|
745
|
+
return
|
|
746
|
+
if cancel_check is None:
|
|
747
|
+
roots, preprocessed_sources, load_problems = _load_project_roots(
|
|
748
|
+
self._workspace,
|
|
749
|
+
)
|
|
750
|
+
else:
|
|
751
|
+
roots, preprocessed_sources, load_problems = _load_project_roots(
|
|
752
|
+
self._workspace,
|
|
753
|
+
cancel_check=cancel_check,
|
|
754
|
+
)
|
|
755
|
+
self._roots = roots
|
|
756
|
+
self._problems = list(load_problems)
|
|
757
|
+
self._semantics = (
|
|
758
|
+
build_workspace_semantics_from_roots(
|
|
759
|
+
roots,
|
|
760
|
+
preprocessed_sources=preprocessed_sources,
|
|
761
|
+
)
|
|
762
|
+
if roots
|
|
763
|
+
else None
|
|
764
|
+
)
|
|
765
|
+
self._symbol_targets.clear()
|
|
766
|
+
self._references.clear()
|
|
767
|
+
self._unit_edges.clear()
|
|
768
|
+
self._inherits.clear()
|
|
769
|
+
self._implements.clear()
|
|
770
|
+
self._unresolved_references = 0
|
|
771
|
+
self._ambiguous_references = 0
|
|
772
|
+
if self._semantics is not None:
|
|
773
|
+
self._build_graph(
|
|
774
|
+
self._semantics,
|
|
775
|
+
cancel_check=cancel_check,
|
|
776
|
+
)
|
|
777
|
+
self._add_contains_edges(self._semantics)
|
|
778
|
+
self._add_semantic_problems(self._semantics)
|
|
779
|
+
self._deep_graph_ready = True
|
|
780
|
+
self._index_graph()
|
|
781
|
+
self._problems = _dedupe_mappings(self._problems)
|
|
782
|
+
|
|
783
|
+
def _index_graph(self) -> None:
|
|
784
|
+
references_by_target: dict[_GroupKey, list[_ReferenceRecord]] = {}
|
|
785
|
+
calls_by_target: dict[_GroupKey, list[_ReferenceRecord]] = {}
|
|
786
|
+
calls_by_owner: dict[_GroupKey, list[_ReferenceRecord]] = {}
|
|
787
|
+
for record in self._references:
|
|
788
|
+
references_by_target.setdefault(record.target_group, []).append(record)
|
|
789
|
+
if record.kind != ReferenceKind.CALL:
|
|
790
|
+
continue
|
|
791
|
+
calls_by_target.setdefault(record.target_group, []).append(record)
|
|
792
|
+
if record.owner_group is not None:
|
|
793
|
+
calls_by_owner.setdefault(record.owner_group, []).append(record)
|
|
794
|
+
self._references_by_target = _freeze_adjacency(references_by_target)
|
|
795
|
+
self._calls_by_target = _freeze_adjacency(calls_by_target)
|
|
796
|
+
self._calls_by_owner = _freeze_adjacency(calls_by_owner)
|
|
797
|
+
|
|
798
|
+
unit_edges_by_source: dict[_GroupKey, list[_UnitEdge]] = {}
|
|
799
|
+
unit_edges_by_target: dict[_GroupKey, list[_UnitEdge]] = {}
|
|
800
|
+
for edge in self._unit_edges:
|
|
801
|
+
unit_edges_by_source.setdefault(edge.source_group, []).append(edge)
|
|
802
|
+
unit_edges_by_target.setdefault(edge.target_group, []).append(edge)
|
|
803
|
+
self._unit_edges_by_source = _freeze_adjacency(unit_edges_by_source)
|
|
804
|
+
self._unit_edges_by_target = _freeze_adjacency(unit_edges_by_target)
|
|
805
|
+
|
|
806
|
+
inherits_by_source: dict[_GroupKey, list[_TypeEdge]] = {}
|
|
807
|
+
for edge in self._inherits:
|
|
808
|
+
inherits_by_source.setdefault(edge.source_group, []).append(edge)
|
|
809
|
+
self._inherits_by_source = _freeze_adjacency(inherits_by_source)
|
|
810
|
+
|
|
811
|
+
implements_by_source: dict[_GroupKey, list[_TypeEdge]] = {}
|
|
812
|
+
for edge in self._implements:
|
|
813
|
+
implements_by_source.setdefault(edge.source_group, []).append(edge)
|
|
814
|
+
self._implements_by_source = _freeze_adjacency(implements_by_source)
|
|
815
|
+
|
|
816
|
+
def _build_graph(
|
|
817
|
+
self,
|
|
818
|
+
semantics: WorkspaceSemanticResult,
|
|
819
|
+
*,
|
|
820
|
+
cancel_check: Callable[[], None] | None = None,
|
|
821
|
+
) -> None:
|
|
822
|
+
self._semantic_unit_paths = {
|
|
823
|
+
id(model.unit_scope): _path_key(file_name)
|
|
824
|
+
for file_name, model in semantics.models.items()
|
|
825
|
+
}
|
|
826
|
+
for model in semantics.models.values():
|
|
827
|
+
if cancel_check is not None:
|
|
828
|
+
cancel_check()
|
|
829
|
+
for reference in model.references:
|
|
830
|
+
resolved = reference.resolved
|
|
831
|
+
fallback_resolved = False
|
|
832
|
+
if resolved is None and reference.kind == ReferenceKind.VALUE:
|
|
833
|
+
resolved = _resolve_declaring_type_field(
|
|
834
|
+
reference.name,
|
|
835
|
+
reference.scope,
|
|
836
|
+
reference.ref_range,
|
|
837
|
+
)
|
|
838
|
+
fallback_resolved = resolved is not None
|
|
839
|
+
if resolved is None:
|
|
840
|
+
self._unresolved_references += 1
|
|
841
|
+
continue
|
|
842
|
+
resolved_matches_unit = self._reference_matches_resolved_unit(
|
|
843
|
+
reference.ref_range,
|
|
844
|
+
resolved,
|
|
845
|
+
)
|
|
846
|
+
if resolved_matches_unit:
|
|
847
|
+
candidate_groups = (
|
|
848
|
+
self._exact_candidate_groups(resolved)
|
|
849
|
+
if fallback_resolved
|
|
850
|
+
else self._resolved_candidate_groups(resolved)
|
|
851
|
+
)
|
|
852
|
+
else:
|
|
853
|
+
candidate_groups = self._project_local_candidate_groups(
|
|
854
|
+
reference.ref_range,
|
|
855
|
+
resolved,
|
|
856
|
+
)
|
|
857
|
+
if len(candidate_groups) != 1:
|
|
858
|
+
if len(candidate_groups) > 1:
|
|
859
|
+
self._ambiguous_references += 1
|
|
860
|
+
elif resolved.kind == SymbolKind.TYPE_PARAMETER:
|
|
861
|
+
continue
|
|
862
|
+
else:
|
|
863
|
+
self._unresolved_references += 1
|
|
864
|
+
continue
|
|
865
|
+
target_group = next(iter(candidate_groups))
|
|
866
|
+
owner = self._scope_target(
|
|
867
|
+
reference.scope,
|
|
868
|
+
call_owner=reference.kind == ReferenceKind.CALL,
|
|
869
|
+
)
|
|
870
|
+
owner_group = _target_group(owner) if owner is not None else None
|
|
871
|
+
evidence = self._evidence(reference.ref_range, reference.kind.value)
|
|
872
|
+
record = _ReferenceRecord(
|
|
873
|
+
target_group=target_group,
|
|
874
|
+
owner_group=owner_group,
|
|
875
|
+
kind=reference.kind,
|
|
876
|
+
evidence=evidence,
|
|
877
|
+
referencing_symbol=self._referencing_symbol(reference.scope),
|
|
878
|
+
)
|
|
879
|
+
self._references.append(record)
|
|
880
|
+
if reference.kind == ReferenceKind.UNIT and owner_group is not None:
|
|
881
|
+
self._unit_edges.append(
|
|
882
|
+
_UnitEdge(
|
|
883
|
+
source_group=owner_group,
|
|
884
|
+
target_group=target_group,
|
|
885
|
+
evidence=evidence,
|
|
886
|
+
)
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
seen_symbols: set[int] = set()
|
|
890
|
+
for model in semantics.models.values():
|
|
891
|
+
if cancel_check is not None:
|
|
892
|
+
cancel_check()
|
|
893
|
+
for symbol in _symbols_in_scope(model.unit_scope):
|
|
894
|
+
if (
|
|
895
|
+
id(symbol) in seen_symbols
|
|
896
|
+
or symbol.kind
|
|
897
|
+
not in {SymbolKind.CLASS, SymbolKind.INTERFACE, SymbolKind.RECORD}
|
|
898
|
+
or not symbol.base_types
|
|
899
|
+
):
|
|
900
|
+
continue
|
|
901
|
+
seen_symbols.add(id(symbol))
|
|
902
|
+
source_target = self._target_for_symbol(symbol)
|
|
903
|
+
if source_target is None:
|
|
904
|
+
continue
|
|
905
|
+
source_group = _target_group(source_target)
|
|
906
|
+
for base_ref in symbol.base_types:
|
|
907
|
+
base_symbol = _resolve_type_symbol(base_ref, symbol.scope, semantics.index)
|
|
908
|
+
base_target = self._target_for_symbol(base_symbol) if base_symbol is not None else None
|
|
909
|
+
if base_target is None:
|
|
910
|
+
self._unresolved_references += 1
|
|
911
|
+
continue
|
|
912
|
+
target_group = _target_group(base_target)
|
|
913
|
+
evidence = self._evidence(symbol.decl_range, "base_type")
|
|
914
|
+
relation = _classify_type_relation(symbol.kind, base_symbol.kind)
|
|
915
|
+
if relation == "inherits":
|
|
916
|
+
self._inherits.append(_TypeEdge(source_group, target_group, evidence))
|
|
917
|
+
elif relation == "implements":
|
|
918
|
+
self._implements.append(_TypeEdge(source_group, target_group, evidence))
|
|
919
|
+
|
|
920
|
+
def _reference_matches_resolved_unit(
|
|
921
|
+
self,
|
|
922
|
+
reference_range: SourceRange,
|
|
923
|
+
resolved: Symbol,
|
|
924
|
+
) -> bool:
|
|
925
|
+
unit_name = _symbol_unit_name(resolved)
|
|
926
|
+
if self._unit_name_counts.get(_normalized(unit_name), 0) <= 1:
|
|
927
|
+
return True
|
|
928
|
+
if _path_key(reference_range.file_name) == _path_key(
|
|
929
|
+
resolved.decl_range.file_name
|
|
930
|
+
):
|
|
931
|
+
return True
|
|
932
|
+
reference_path = _path_key(reference_range.file_name)
|
|
933
|
+
declaration_path = _path_key(resolved.decl_range.file_name)
|
|
934
|
+
return any(
|
|
935
|
+
reference_path in membership
|
|
936
|
+
and declaration_path in membership
|
|
937
|
+
for membership in self._project_memberships
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
def _add_contains_edges(self, semantics: WorkspaceSemanticResult) -> None:
|
|
941
|
+
unit_targets = {
|
|
942
|
+
_normalized(target.unit_name or target.name): target
|
|
943
|
+
for target in self._targets.values()
|
|
944
|
+
if target.kind == SymbolKind.UNIT.value
|
|
945
|
+
}
|
|
946
|
+
for file_name, root in self._roots.items():
|
|
947
|
+
contains = root.find_node(SyntaxNodeType.ntContains)
|
|
948
|
+
if contains is None:
|
|
949
|
+
continue
|
|
950
|
+
model = semantics.models.get(file_name)
|
|
951
|
+
source_target = self._target_for_symbol(model.unit_scope.owner) if model is not None else None
|
|
952
|
+
if source_target is None:
|
|
953
|
+
continue
|
|
954
|
+
source_group = _target_group(source_target)
|
|
955
|
+
for child in contains.child_nodes:
|
|
956
|
+
if child.typ != SyntaxNodeType.ntUnit:
|
|
957
|
+
continue
|
|
958
|
+
name = child.get_attribute(AttributeName.anName)
|
|
959
|
+
target = unit_targets.get(_normalized(name))
|
|
960
|
+
if target is None:
|
|
961
|
+
self._unresolved_references += 1
|
|
962
|
+
continue
|
|
963
|
+
evidence = self._evidence(
|
|
964
|
+
SourceRange(
|
|
965
|
+
file_name=child.file_name or file_name,
|
|
966
|
+
start_line=child.line,
|
|
967
|
+
start_col=child.col,
|
|
968
|
+
end_line=child.line,
|
|
969
|
+
end_col=child.col,
|
|
970
|
+
),
|
|
971
|
+
"contains",
|
|
972
|
+
)
|
|
973
|
+
self._unit_edges.append(_UnitEdge(source_group, _target_group(target), evidence))
|
|
974
|
+
|
|
975
|
+
def _add_semantic_problems(self, semantics: WorkspaceSemanticResult) -> None:
|
|
976
|
+
for model in semantics.models.values():
|
|
977
|
+
for problem in model.problems:
|
|
978
|
+
self._problems.append(
|
|
979
|
+
{
|
|
980
|
+
"item_type": "relation_problem",
|
|
981
|
+
"kind": "semantic",
|
|
982
|
+
"message": problem.message,
|
|
983
|
+
"path": self._display_path(problem.range.file_name),
|
|
984
|
+
"line": problem.range.start_line,
|
|
985
|
+
"column": problem.range.start_col,
|
|
986
|
+
}
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
def _resolved_candidate_groups(self, symbol: Symbol) -> set[_GroupKey]:
|
|
990
|
+
groups: set[_GroupKey] = set()
|
|
991
|
+
for candidate in (symbol, *symbol.overloads):
|
|
992
|
+
target = self._target_for_symbol(candidate)
|
|
993
|
+
if target is not None:
|
|
994
|
+
groups.add(_target_group(target))
|
|
995
|
+
return groups
|
|
996
|
+
|
|
997
|
+
def _project_local_candidate_groups(
|
|
998
|
+
self,
|
|
999
|
+
reference_range: SourceRange,
|
|
1000
|
+
symbol: Symbol,
|
|
1001
|
+
) -> set[_GroupKey]:
|
|
1002
|
+
identity = (
|
|
1003
|
+
_normalized(_semantic_qualified_name(symbol)),
|
|
1004
|
+
symbol.kind.value,
|
|
1005
|
+
_symbol_signature_identity(symbol),
|
|
1006
|
+
)
|
|
1007
|
+
reference_path = _path_key(reference_range.file_name)
|
|
1008
|
+
candidates = self._identity_targets.get(identity, ())
|
|
1009
|
+
project_local = {
|
|
1010
|
+
_target_group(candidate)
|
|
1011
|
+
for candidate in candidates
|
|
1012
|
+
if any(
|
|
1013
|
+
reference_path in membership
|
|
1014
|
+
and _path_key(candidate.source_path) in membership
|
|
1015
|
+
for membership in self._project_memberships
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
if len(project_local) == 1:
|
|
1019
|
+
return project_local
|
|
1020
|
+
reference_parent = _path_key(Path(reference_range.file_name).parent)
|
|
1021
|
+
same_directory = {
|
|
1022
|
+
_target_group(candidate)
|
|
1023
|
+
for candidate in candidates
|
|
1024
|
+
if _path_key(Path(candidate.source_path).parent) == reference_parent
|
|
1025
|
+
}
|
|
1026
|
+
if len(same_directory) == 1:
|
|
1027
|
+
return same_directory
|
|
1028
|
+
return project_local
|
|
1029
|
+
|
|
1030
|
+
def _exact_candidate_groups(self, symbol: Symbol) -> set[_GroupKey]:
|
|
1031
|
+
groups: set[_GroupKey] = set()
|
|
1032
|
+
for candidate in (symbol, *symbol.overloads):
|
|
1033
|
+
exact = (
|
|
1034
|
+
_path_key(candidate.decl_range.file_name),
|
|
1035
|
+
candidate.decl_range.start_line,
|
|
1036
|
+
candidate.decl_range.start_col,
|
|
1037
|
+
candidate.kind.value,
|
|
1038
|
+
_normalized(candidate.name),
|
|
1039
|
+
)
|
|
1040
|
+
groups.update(
|
|
1041
|
+
_target_group(target)
|
|
1042
|
+
for target in self._exact_targets.get(exact, ())
|
|
1043
|
+
)
|
|
1044
|
+
return groups
|
|
1045
|
+
|
|
1046
|
+
def _target_for_symbol(self, symbol: Symbol | None) -> RelationTarget | None:
|
|
1047
|
+
if symbol is None:
|
|
1048
|
+
return None
|
|
1049
|
+
cached = self._symbol_targets.get(id(symbol), ...)
|
|
1050
|
+
if cached is not ...:
|
|
1051
|
+
return cached
|
|
1052
|
+
path_key = _path_key(symbol.decl_range.file_name)
|
|
1053
|
+
exact = (
|
|
1054
|
+
path_key,
|
|
1055
|
+
symbol.decl_range.start_line,
|
|
1056
|
+
symbol.decl_range.start_col,
|
|
1057
|
+
symbol.kind.value,
|
|
1058
|
+
_normalized(symbol.name),
|
|
1059
|
+
)
|
|
1060
|
+
candidates = list(self._exact_targets.get(exact, ()))
|
|
1061
|
+
if not candidates:
|
|
1062
|
+
position = (
|
|
1063
|
+
path_key,
|
|
1064
|
+
symbol.decl_range.start_line,
|
|
1065
|
+
symbol.decl_range.start_col,
|
|
1066
|
+
symbol.kind.value,
|
|
1067
|
+
)
|
|
1068
|
+
candidates = list(self._position_targets.get(position, ()))
|
|
1069
|
+
if not candidates:
|
|
1070
|
+
line = (
|
|
1071
|
+
path_key,
|
|
1072
|
+
symbol.decl_range.start_line,
|
|
1073
|
+
symbol.kind.value,
|
|
1074
|
+
_normalized(symbol.name),
|
|
1075
|
+
)
|
|
1076
|
+
candidates = list(self._line_targets.get(line, ()))
|
|
1077
|
+
if not candidates:
|
|
1078
|
+
qualified_name = _semantic_qualified_name(symbol)
|
|
1079
|
+
candidates = list(
|
|
1080
|
+
self._identity_targets.get(
|
|
1081
|
+
(
|
|
1082
|
+
_normalized(qualified_name),
|
|
1083
|
+
symbol.kind.value,
|
|
1084
|
+
_symbol_signature_identity(symbol),
|
|
1085
|
+
),
|
|
1086
|
+
(),
|
|
1087
|
+
)
|
|
1088
|
+
)
|
|
1089
|
+
if self._unit_name_counts.get(
|
|
1090
|
+
_normalized(_symbol_unit_name(symbol)),
|
|
1091
|
+
0,
|
|
1092
|
+
) > 1:
|
|
1093
|
+
semantic_unit_path = self._semantic_unit_paths.get(
|
|
1094
|
+
id(_symbol_unit_scope(symbol)),
|
|
1095
|
+
"",
|
|
1096
|
+
)
|
|
1097
|
+
provenance_path = semantic_unit_path or path_key
|
|
1098
|
+
candidates = [
|
|
1099
|
+
candidate
|
|
1100
|
+
for candidate in candidates
|
|
1101
|
+
if _path_key(candidate.source_path) == provenance_path
|
|
1102
|
+
]
|
|
1103
|
+
target = min(candidates, key=_target_sort_key) if candidates else None
|
|
1104
|
+
self._symbol_targets[id(symbol)] = target
|
|
1105
|
+
return target
|
|
1106
|
+
|
|
1107
|
+
def _scope_target(self, scope: Scope, *, call_owner: bool = False) -> RelationTarget | None:
|
|
1108
|
+
current: Scope | None = scope
|
|
1109
|
+
while current is not None:
|
|
1110
|
+
if current.kind == ScopeKind.ROUTINE:
|
|
1111
|
+
target = self._target_for_symbol(current.owner)
|
|
1112
|
+
if target is not None:
|
|
1113
|
+
return target
|
|
1114
|
+
if call_owner:
|
|
1115
|
+
return None
|
|
1116
|
+
elif current.kind == ScopeKind.UNIT:
|
|
1117
|
+
if call_owner:
|
|
1118
|
+
return None
|
|
1119
|
+
target = self._target_for_symbol(current.owner)
|
|
1120
|
+
if target is not None:
|
|
1121
|
+
return target
|
|
1122
|
+
current = current.parent
|
|
1123
|
+
return None
|
|
1124
|
+
|
|
1125
|
+
def _relation_item(
|
|
1126
|
+
self,
|
|
1127
|
+
relation: str,
|
|
1128
|
+
group: _GroupKey | None,
|
|
1129
|
+
evidence: Mapping[str, object],
|
|
1130
|
+
*,
|
|
1131
|
+
referencing_symbol: Mapping[str, object] | None = None,
|
|
1132
|
+
) -> dict[str, object] | None:
|
|
1133
|
+
if group is None:
|
|
1134
|
+
return None
|
|
1135
|
+
candidates = self._groups.get(group)
|
|
1136
|
+
if not candidates:
|
|
1137
|
+
return None
|
|
1138
|
+
target = candidates[0]
|
|
1139
|
+
card = (
|
|
1140
|
+
dict(target.card)
|
|
1141
|
+
if target.card
|
|
1142
|
+
else {
|
|
1143
|
+
"target_id": target.target_id,
|
|
1144
|
+
"unit_id": target.unit_id,
|
|
1145
|
+
"name": target.name,
|
|
1146
|
+
"qualified_name": target.qualified_name,
|
|
1147
|
+
"kind": target.kind,
|
|
1148
|
+
"path": target.path,
|
|
1149
|
+
"line": target.line,
|
|
1150
|
+
"column": target.column,
|
|
1151
|
+
"visibility": target.visibility,
|
|
1152
|
+
"owner": target.owner,
|
|
1153
|
+
"type": target.type_name,
|
|
1154
|
+
}
|
|
1155
|
+
)
|
|
1156
|
+
item: dict[str, object] = {
|
|
1157
|
+
"item_type": "relation",
|
|
1158
|
+
"relation": relation,
|
|
1159
|
+
**card,
|
|
1160
|
+
"evidence": dict(evidence),
|
|
1161
|
+
}
|
|
1162
|
+
if referencing_symbol is not None:
|
|
1163
|
+
item["referencing_symbol"] = dict(referencing_symbol)
|
|
1164
|
+
return item
|
|
1165
|
+
|
|
1166
|
+
def _referencing_symbol(
|
|
1167
|
+
self,
|
|
1168
|
+
scope: Scope,
|
|
1169
|
+
) -> dict[str, object] | None:
|
|
1170
|
+
current: Scope | None = scope
|
|
1171
|
+
while current is not None:
|
|
1172
|
+
symbol = current.owner
|
|
1173
|
+
if symbol is not None:
|
|
1174
|
+
source_range = symbol.name_range
|
|
1175
|
+
return {
|
|
1176
|
+
"name": symbol.name,
|
|
1177
|
+
"qualified_name": _semantic_qualified_name(symbol),
|
|
1178
|
+
"kind": symbol.kind.value,
|
|
1179
|
+
"path": self._display_path(source_range.file_name),
|
|
1180
|
+
"line": source_range.start_line,
|
|
1181
|
+
"column": source_range.start_col,
|
|
1182
|
+
}
|
|
1183
|
+
current = current.parent
|
|
1184
|
+
return None
|
|
1185
|
+
|
|
1186
|
+
def _evidence(self, source_range: SourceRange, kind: str) -> dict[str, object]:
|
|
1187
|
+
return {
|
|
1188
|
+
"path": self._display_path(source_range.file_name),
|
|
1189
|
+
"line": source_range.start_line,
|
|
1190
|
+
"column": source_range.start_col,
|
|
1191
|
+
"kind": kind,
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
def _display_path(self, file_name: str) -> str:
|
|
1195
|
+
key = _path_key(file_name)
|
|
1196
|
+
known = self._path_display.get(key)
|
|
1197
|
+
if known is not None:
|
|
1198
|
+
return known
|
|
1199
|
+
path = Path(file_name).expanduser()
|
|
1200
|
+
if path.is_absolute():
|
|
1201
|
+
try:
|
|
1202
|
+
return path.resolve().relative_to(self._root).as_posix()
|
|
1203
|
+
except ValueError:
|
|
1204
|
+
return f"@external/relation/{_stable_component(path.name)}"
|
|
1205
|
+
return file_name.replace("\\", "/")
|
|
1206
|
+
|
|
1207
|
+
def _metadata(
|
|
1208
|
+
self,
|
|
1209
|
+
relation: str,
|
|
1210
|
+
*,
|
|
1211
|
+
problem_summaries: Sequence[Mapping[str, object]],
|
|
1212
|
+
problem_count: int,
|
|
1213
|
+
scan_complete: bool,
|
|
1214
|
+
) -> dict[str, object]:
|
|
1215
|
+
limitations = {
|
|
1216
|
+
"references": ["Only resolved semantic references are reported; text matches are excluded."],
|
|
1217
|
+
"callers": [
|
|
1218
|
+
"CALL edges cover explicit parsed calls only.",
|
|
1219
|
+
"Bare parameterless, overload, virtual, and procedural dispatch can be incomplete.",
|
|
1220
|
+
],
|
|
1221
|
+
"callees": [
|
|
1222
|
+
"CALL edges cover explicit parsed calls only.",
|
|
1223
|
+
"Bare parameterless, overload, virtual, and procedural dispatch can be incomplete.",
|
|
1224
|
+
],
|
|
1225
|
+
"uses": ["Includes resolved uses clauses and project/package contains clauses."],
|
|
1226
|
+
"used_by": ["Includes resolved uses clauses and project/package contains clauses."],
|
|
1227
|
+
"inherits": ["Reports resolved class-to-class and interface-to-interface base types."],
|
|
1228
|
+
"implements": [
|
|
1229
|
+
"Reports class/record base types resolved to interfaces; property delegation is not modeled."
|
|
1230
|
+
],
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
"item_type": "relation_metadata",
|
|
1234
|
+
"relation": relation,
|
|
1235
|
+
"completeness": "sound_partial",
|
|
1236
|
+
"unresolved_references": self._unresolved_references,
|
|
1237
|
+
"ambiguous_references": self._ambiguous_references,
|
|
1238
|
+
"deep_problem_count": problem_count,
|
|
1239
|
+
"problems": [dict(problem) for problem in problem_summaries],
|
|
1240
|
+
"problems_truncated": problem_count > len(problem_summaries),
|
|
1241
|
+
"scan_complete": scan_complete,
|
|
1242
|
+
"limitations": limitations[relation],
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
@staticmethod
|
|
1246
|
+
def _validate_applicability(target: RelationTarget, relation: str) -> None:
|
|
1247
|
+
applicable = True
|
|
1248
|
+
if relation in {"callers", "callees"}:
|
|
1249
|
+
applicable = target.kind in _ROUTINE_KINDS
|
|
1250
|
+
elif relation in {"uses", "used_by"}:
|
|
1251
|
+
applicable = target.kind == SymbolKind.UNIT.value
|
|
1252
|
+
elif relation == "inherits":
|
|
1253
|
+
applicable = target.kind in {SymbolKind.CLASS.value, SymbolKind.INTERFACE.value}
|
|
1254
|
+
elif relation == "implements":
|
|
1255
|
+
applicable = target.kind in {SymbolKind.CLASS.value, SymbolKind.RECORD.value}
|
|
1256
|
+
if not applicable:
|
|
1257
|
+
raise AgentProtocolError(
|
|
1258
|
+
"relation_not_applicable",
|
|
1259
|
+
f"Relation {relation!r} does not apply to {target.kind} target {target.target_id}.",
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _load_project_roots(
|
|
1264
|
+
workspace: AgentWorkspace,
|
|
1265
|
+
*,
|
|
1266
|
+
cancel_check: Callable[[], None] | None = None,
|
|
1267
|
+
) -> tuple[
|
|
1268
|
+
dict[str, SyntaxNode],
|
|
1269
|
+
dict[str, PreprocessedSource],
|
|
1270
|
+
list[dict[str, object]],
|
|
1271
|
+
]:
|
|
1272
|
+
active_project = workspace.active_project
|
|
1273
|
+
if active_project is None:
|
|
1274
|
+
raise AgentProtocolError("project_required", "Select a project before tracing relations.")
|
|
1275
|
+
parser_mode = (
|
|
1276
|
+
ParserMode.TOLERANT
|
|
1277
|
+
if len(workspace.units) >= 256
|
|
1278
|
+
else ParserMode.STRICT
|
|
1279
|
+
)
|
|
1280
|
+
if active_project.kind != "workspace":
|
|
1281
|
+
if cancel_check is not None:
|
|
1282
|
+
cancel_check()
|
|
1283
|
+
project_path = Path(active_project.path)
|
|
1284
|
+
if not project_path.is_absolute():
|
|
1285
|
+
project_path = workspace.root / project_path
|
|
1286
|
+
indexer = ProjectIndexer(
|
|
1287
|
+
search_paths=workspace.search_paths,
|
|
1288
|
+
include_paths=workspace.include_paths,
|
|
1289
|
+
defines=workspace.defines,
|
|
1290
|
+
mode=parser_mode,
|
|
1291
|
+
project_config=workspace.project_config,
|
|
1292
|
+
)
|
|
1293
|
+
result = indexer.index(str(project_path.resolve()))
|
|
1294
|
+
if cancel_check is not None:
|
|
1295
|
+
cancel_check()
|
|
1296
|
+
roots = {
|
|
1297
|
+
str(Path(unit.path).expanduser().resolve()): unit.syntax_tree
|
|
1298
|
+
for unit in result.parsed_units
|
|
1299
|
+
if unit.syntax_tree is not None
|
|
1300
|
+
}
|
|
1301
|
+
preprocessed_sources = {
|
|
1302
|
+
str(Path(unit.path).expanduser().resolve()): unit.preprocessed
|
|
1303
|
+
for unit in result.parsed_units
|
|
1304
|
+
if unit.syntax_tree is not None and unit.preprocessed is not None
|
|
1305
|
+
}
|
|
1306
|
+
problems = [_project_problem_item(problem, workspace.root) for problem in result.problems]
|
|
1307
|
+
return roots, preprocessed_sources, problems
|
|
1308
|
+
|
|
1309
|
+
parser = DelphiParser(
|
|
1310
|
+
include_paths=workspace.include_paths,
|
|
1311
|
+
defines=workspace.defines,
|
|
1312
|
+
include_loader=workspace_include_loader(
|
|
1313
|
+
workspace.project_config,
|
|
1314
|
+
workspace.include_paths,
|
|
1315
|
+
),
|
|
1316
|
+
mode=parser_mode,
|
|
1317
|
+
legacy_fallback_max_chars=0,
|
|
1318
|
+
)
|
|
1319
|
+
roots: dict[str, SyntaxNode] = {}
|
|
1320
|
+
preprocessed_sources: dict[str, PreprocessedSource] = {}
|
|
1321
|
+
problems: list[dict[str, object]] = []
|
|
1322
|
+
for unit in workspace.units:
|
|
1323
|
+
if cancel_check is not None:
|
|
1324
|
+
cancel_check()
|
|
1325
|
+
source_path = Path(unit.path)
|
|
1326
|
+
if not source_path.is_absolute():
|
|
1327
|
+
source_path = workspace.root / source_path
|
|
1328
|
+
source_path = source_path.expanduser().resolve()
|
|
1329
|
+
try:
|
|
1330
|
+
text = read_source_text(source_path)
|
|
1331
|
+
parsed = parser.parse(
|
|
1332
|
+
text,
|
|
1333
|
+
str(source_path),
|
|
1334
|
+
build_semantic=False,
|
|
1335
|
+
)
|
|
1336
|
+
roots[str(source_path)] = parsed.root
|
|
1337
|
+
preprocessed_sources[str(source_path)] = parsed.preprocessed
|
|
1338
|
+
if parsed.problems:
|
|
1339
|
+
first = parsed.problems[0]
|
|
1340
|
+
problems.append(
|
|
1341
|
+
{
|
|
1342
|
+
"item_type": "relation_problem",
|
|
1343
|
+
"kind": "partial_parse",
|
|
1344
|
+
"message": (
|
|
1345
|
+
f"Partial DelphiAST parse at {first.line}:{first.column}: "
|
|
1346
|
+
f"{first.message}"
|
|
1347
|
+
),
|
|
1348
|
+
"path": _display_project_path(source_path, workspace.root),
|
|
1349
|
+
}
|
|
1350
|
+
)
|
|
1351
|
+
except Exception as exc:
|
|
1352
|
+
problems.append(
|
|
1353
|
+
{
|
|
1354
|
+
"item_type": "relation_problem",
|
|
1355
|
+
"kind": "cant_parse_file",
|
|
1356
|
+
"message": _safe_problem_message(
|
|
1357
|
+
f"{type(exc).__name__}: {exc}",
|
|
1358
|
+
workspace.root,
|
|
1359
|
+
(
|
|
1360
|
+
source_path,
|
|
1361
|
+
getattr(exc, "filename", None),
|
|
1362
|
+
getattr(exc, "filename2", None),
|
|
1363
|
+
),
|
|
1364
|
+
),
|
|
1365
|
+
"path": _display_project_path(source_path, workspace.root),
|
|
1366
|
+
}
|
|
1367
|
+
)
|
|
1368
|
+
return roots, preprocessed_sources, problems
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def _project_problem_item(problem: ProjectProblem, root: Path) -> dict[str, object]:
|
|
1372
|
+
return {
|
|
1373
|
+
"item_type": "relation_problem",
|
|
1374
|
+
"kind": problem.problem_type.value,
|
|
1375
|
+
"message": _safe_problem_message(problem.description, root, (problem.file_name,)),
|
|
1376
|
+
"path": _display_project_path(problem.file_name, root),
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def _safe_problem_message(
|
|
1381
|
+
message: str,
|
|
1382
|
+
root: Path,
|
|
1383
|
+
known_paths: Iterable[str | PurePath | None],
|
|
1384
|
+
) -> str:
|
|
1385
|
+
replacements: dict[str, str] = {}
|
|
1386
|
+
for value in known_paths:
|
|
1387
|
+
if value is None:
|
|
1388
|
+
continue
|
|
1389
|
+
original = str(value)
|
|
1390
|
+
pattern = _separator_agnostic_absolute_path_pattern(original)
|
|
1391
|
+
if pattern is None:
|
|
1392
|
+
continue
|
|
1393
|
+
safe_path = _display_project_path(original, root)
|
|
1394
|
+
replacements[pattern] = safe_path
|
|
1395
|
+
for pattern in sorted(replacements, key=len, reverse=True):
|
|
1396
|
+
replacement = replacements[pattern]
|
|
1397
|
+
message = re.sub(pattern, lambda _match: replacement, message)
|
|
1398
|
+
return message
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def _display_project_path(path: str | PurePath, root: Path) -> str:
|
|
1402
|
+
path_text = str(path)
|
|
1403
|
+
if not _is_cross_platform_absolute(path_text):
|
|
1404
|
+
return Path(path_text).as_posix()
|
|
1405
|
+
host_path = Path(path_text)
|
|
1406
|
+
if host_path.is_absolute():
|
|
1407
|
+
resolved = host_path.expanduser().resolve()
|
|
1408
|
+
relative = _portable_relative_to_root(resolved, root)
|
|
1409
|
+
if relative is not None:
|
|
1410
|
+
return relative
|
|
1411
|
+
return f"@external/relation/{_stable_component(resolved.name)}"
|
|
1412
|
+
relative = _portable_relative_to_root(path_text, root)
|
|
1413
|
+
if relative is not None:
|
|
1414
|
+
return relative
|
|
1415
|
+
return f"@external/relation/{_stable_component(_portable_name(path_text))}"
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def _is_cross_platform_absolute(path: str | PurePath) -> bool:
|
|
1419
|
+
text = str(path)
|
|
1420
|
+
return PurePosixPath(text).is_absolute() or PureWindowsPath(text).is_absolute()
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def _separator_agnostic_absolute_path_pattern(path: str | PurePath) -> str | None:
|
|
1424
|
+
text = str(path)
|
|
1425
|
+
windows_path = PureWindowsPath(text)
|
|
1426
|
+
if windows_path.is_absolute():
|
|
1427
|
+
canonical = windows_path.as_posix()
|
|
1428
|
+
case_insensitive = True
|
|
1429
|
+
else:
|
|
1430
|
+
posix_path = PurePosixPath(text)
|
|
1431
|
+
if not posix_path.is_absolute():
|
|
1432
|
+
return None
|
|
1433
|
+
canonical = posix_path.as_posix()
|
|
1434
|
+
case_insensitive = False
|
|
1435
|
+
pattern = r"[\\/]+".join(re.escape(component) for component in canonical.split("/"))
|
|
1436
|
+
return f"(?i:{pattern})" if case_insensitive else pattern
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def _portable_relative_to_root(path: str | PurePath, root: Path) -> str | None:
|
|
1440
|
+
text = str(path)
|
|
1441
|
+
posix_path = PurePosixPath(text)
|
|
1442
|
+
posix_root = PurePosixPath(str(root))
|
|
1443
|
+
if posix_path.is_absolute() and posix_root.is_absolute():
|
|
1444
|
+
try:
|
|
1445
|
+
return posix_path.relative_to(posix_root).as_posix()
|
|
1446
|
+
except ValueError:
|
|
1447
|
+
pass
|
|
1448
|
+
|
|
1449
|
+
windows_path = PureWindowsPath(text)
|
|
1450
|
+
windows_root = PureWindowsPath(str(root))
|
|
1451
|
+
if windows_path.is_absolute() and windows_root.is_absolute():
|
|
1452
|
+
try:
|
|
1453
|
+
return windows_path.relative_to(windows_root).as_posix()
|
|
1454
|
+
except ValueError:
|
|
1455
|
+
pass
|
|
1456
|
+
return None
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def _portable_name(path: str | PurePath) -> str:
|
|
1460
|
+
return PurePosixPath(str(path).replace("\\", "/")).name or "unknown"
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
def _resolve_type_symbol(type_ref: TypeRef, scope: Scope, index: SymbolIndex) -> Symbol | None:
|
|
1464
|
+
if isinstance(type_ref, GenericInstanceTypeRef):
|
|
1465
|
+
return _resolve_type_symbol(type_ref.base, scope, index)
|
|
1466
|
+
if not isinstance(type_ref, NamedTypeRef):
|
|
1467
|
+
return None
|
|
1468
|
+
if type_ref.unit_name:
|
|
1469
|
+
unit_scope = index.lookup_unit(type_ref.unit_name)
|
|
1470
|
+
if unit_scope is not None:
|
|
1471
|
+
symbols = unit_scope.lookup_local(type_ref.name)
|
|
1472
|
+
return symbols[0] if symbols else None
|
|
1473
|
+
if "." in type_ref.name:
|
|
1474
|
+
symbols = index.resolve_qualified(type_ref.name)
|
|
1475
|
+
if symbols:
|
|
1476
|
+
return symbols[0]
|
|
1477
|
+
symbols = scope.resolve(type_ref.name)
|
|
1478
|
+
return symbols[0] if symbols else None
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
def _resolve_declaring_type_field(
|
|
1482
|
+
name: str,
|
|
1483
|
+
scope: Scope,
|
|
1484
|
+
reference_range: SourceRange,
|
|
1485
|
+
) -> Symbol | None:
|
|
1486
|
+
"""Resolve an unqualified field used by a qualified method implementation."""
|
|
1487
|
+
if name.startswith("&"):
|
|
1488
|
+
return None
|
|
1489
|
+
parts = name.split(".")
|
|
1490
|
+
explicit_self = len(parts) > 1 and parts[0].casefold() == "self"
|
|
1491
|
+
if not explicit_self or len(parts) != 2:
|
|
1492
|
+
return None
|
|
1493
|
+
if _has_escaped_reference_prefix(reference_range, name):
|
|
1494
|
+
return None
|
|
1495
|
+
field_name = parts[1]
|
|
1496
|
+
current: Scope | None = scope
|
|
1497
|
+
while current is not None:
|
|
1498
|
+
owner = current.owner
|
|
1499
|
+
if (
|
|
1500
|
+
current.kind == ScopeKind.ROUTINE
|
|
1501
|
+
and owner is not None
|
|
1502
|
+
and "." in owner.name
|
|
1503
|
+
):
|
|
1504
|
+
type_name = owner.name.rsplit(".", 1)[0]
|
|
1505
|
+
unit_scope = current
|
|
1506
|
+
while unit_scope.parent is not None:
|
|
1507
|
+
unit_scope = unit_scope.parent
|
|
1508
|
+
type_symbols = [
|
|
1509
|
+
candidate
|
|
1510
|
+
for candidate in unit_scope.lookup_local(type_name)
|
|
1511
|
+
if candidate.kind in {
|
|
1512
|
+
SymbolKind.CLASS,
|
|
1513
|
+
SymbolKind.RECORD,
|
|
1514
|
+
SymbolKind.TYPE,
|
|
1515
|
+
}
|
|
1516
|
+
and candidate.member_scope is not None
|
|
1517
|
+
]
|
|
1518
|
+
fields = {
|
|
1519
|
+
id(candidate): candidate
|
|
1520
|
+
for type_symbol in type_symbols
|
|
1521
|
+
for candidate in type_symbol.member_scope.lookup_local(field_name)
|
|
1522
|
+
if candidate.kind == SymbolKind.FIELD
|
|
1523
|
+
}
|
|
1524
|
+
return next(iter(fields.values())) if len(fields) == 1 else None
|
|
1525
|
+
current = current.parent
|
|
1526
|
+
return None
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
def _has_escaped_reference_prefix(
|
|
1530
|
+
reference_range: SourceRange,
|
|
1531
|
+
name: str,
|
|
1532
|
+
) -> bool:
|
|
1533
|
+
root_name = name.split(".", 1)[0]
|
|
1534
|
+
if reference_range.start_col <= 1 or not root_name:
|
|
1535
|
+
return False
|
|
1536
|
+
try:
|
|
1537
|
+
lines = read_source_text(Path(reference_range.file_name)).splitlines()
|
|
1538
|
+
except (OSError, UnicodeError):
|
|
1539
|
+
return False
|
|
1540
|
+
if not 1 <= reference_range.start_line <= len(lines):
|
|
1541
|
+
return False
|
|
1542
|
+
line = lines[reference_range.start_line - 1]
|
|
1543
|
+
position = reference_range.start_col - 1
|
|
1544
|
+
window_start = max(0, position - len(root_name) - 1)
|
|
1545
|
+
window_end = min(len(line), position + len(root_name) + 1)
|
|
1546
|
+
escaped_root = re.compile(
|
|
1547
|
+
rf"&{re.escape(root_name)}\b",
|
|
1548
|
+
re.IGNORECASE,
|
|
1549
|
+
)
|
|
1550
|
+
return escaped_root.search(line, window_start, window_end) is not None
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def _classify_type_relation(source_kind: SymbolKind, target_kind: SymbolKind) -> str | None:
|
|
1554
|
+
if (
|
|
1555
|
+
source_kind == SymbolKind.CLASS
|
|
1556
|
+
and target_kind == SymbolKind.CLASS
|
|
1557
|
+
) or (
|
|
1558
|
+
source_kind == SymbolKind.INTERFACE
|
|
1559
|
+
and target_kind == SymbolKind.INTERFACE
|
|
1560
|
+
):
|
|
1561
|
+
return "inherits"
|
|
1562
|
+
if source_kind in {SymbolKind.CLASS, SymbolKind.RECORD} and target_kind == SymbolKind.INTERFACE:
|
|
1563
|
+
return "implements"
|
|
1564
|
+
return None
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def _symbols_in_scope(scope: Scope) -> Iterable[Symbol]:
|
|
1568
|
+
seen_scopes: set[int] = set()
|
|
1569
|
+
|
|
1570
|
+
def visit(current: Scope) -> Iterable[Symbol]:
|
|
1571
|
+
if id(current) in seen_scopes:
|
|
1572
|
+
return
|
|
1573
|
+
seen_scopes.add(id(current))
|
|
1574
|
+
for symbols in current.symbols.values():
|
|
1575
|
+
for symbol in symbols:
|
|
1576
|
+
yield symbol
|
|
1577
|
+
if symbol.member_scope is not None:
|
|
1578
|
+
yield from visit(symbol.member_scope)
|
|
1579
|
+
|
|
1580
|
+
yield from visit(scope)
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _workspace_project_memberships(
|
|
1584
|
+
workspace: AgentWorkspace,
|
|
1585
|
+
) -> tuple[frozenset[str], ...]:
|
|
1586
|
+
parser = DelphiParser(
|
|
1587
|
+
include_paths=workspace.include_paths,
|
|
1588
|
+
defines=workspace.defines,
|
|
1589
|
+
include_loader=workspace_include_loader(
|
|
1590
|
+
workspace.project_config,
|
|
1591
|
+
workspace.include_paths,
|
|
1592
|
+
),
|
|
1593
|
+
mode=ParserMode.TOLERANT,
|
|
1594
|
+
legacy_fallback_max_chars=0,
|
|
1595
|
+
)
|
|
1596
|
+
unit_paths_by_name: dict[str, list[Path]] = {}
|
|
1597
|
+
for unit in workspace.units:
|
|
1598
|
+
unit_path = Path(unit.path)
|
|
1599
|
+
if not unit_path.is_absolute():
|
|
1600
|
+
unit_path = workspace.root / unit_path
|
|
1601
|
+
unit_paths_by_name.setdefault(_normalized(unit.name), []).append(
|
|
1602
|
+
unit_path.expanduser().resolve()
|
|
1603
|
+
)
|
|
1604
|
+
memberships: list[frozenset[str]] = []
|
|
1605
|
+
for project in workspace.projects:
|
|
1606
|
+
if project.kind == "workspace":
|
|
1607
|
+
continue
|
|
1608
|
+
project_path = Path(project.path)
|
|
1609
|
+
if not project_path.is_absolute():
|
|
1610
|
+
project_path = workspace.root / project_path
|
|
1611
|
+
project_path = project_path.expanduser().resolve()
|
|
1612
|
+
try:
|
|
1613
|
+
text = read_source_text(project_path)
|
|
1614
|
+
root = parser.parse(
|
|
1615
|
+
text,
|
|
1616
|
+
str(project_path),
|
|
1617
|
+
build_semantic=False,
|
|
1618
|
+
).root
|
|
1619
|
+
except Exception: # noqa: BLE001, S112
|
|
1620
|
+
continue
|
|
1621
|
+
members = {_path_key(project_path)}
|
|
1622
|
+
pending = [root]
|
|
1623
|
+
while pending:
|
|
1624
|
+
node = pending.pop()
|
|
1625
|
+
pending.extend(reversed(node.child_nodes))
|
|
1626
|
+
if node.typ != SyntaxNodeType.ntUnit:
|
|
1627
|
+
continue
|
|
1628
|
+
declared_path = node.get_attribute(AttributeName.anPath).strip()
|
|
1629
|
+
if not declared_path:
|
|
1630
|
+
unit_name = node.get_attribute(AttributeName.anName).strip()
|
|
1631
|
+
local_candidates = [
|
|
1632
|
+
candidate
|
|
1633
|
+
for candidate in unit_paths_by_name.get(
|
|
1634
|
+
_normalized(unit_name),
|
|
1635
|
+
(),
|
|
1636
|
+
)
|
|
1637
|
+
if candidate.parent == project_path.parent
|
|
1638
|
+
]
|
|
1639
|
+
if len(local_candidates) == 1:
|
|
1640
|
+
members.add(_path_key(str(local_candidates[0])))
|
|
1641
|
+
continue
|
|
1642
|
+
member_path = Path(declared_path.replace("\\", "/"))
|
|
1643
|
+
if not member_path.is_absolute():
|
|
1644
|
+
member_path = project_path.parent / member_path
|
|
1645
|
+
members.add(_path_key(str(member_path.expanduser().resolve())))
|
|
1646
|
+
if len(members) > 1:
|
|
1647
|
+
memberships.append(frozenset(members))
|
|
1648
|
+
return tuple(memberships)
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
def _symbol_unit_scope(symbol: Symbol) -> Scope:
|
|
1652
|
+
scope = symbol.scope
|
|
1653
|
+
while scope.parent is not None:
|
|
1654
|
+
scope = scope.parent
|
|
1655
|
+
return scope
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
def _symbol_unit_name(symbol: Symbol) -> str:
|
|
1659
|
+
return _symbol_unit_scope(symbol).name
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
def _semantic_qualified_name(symbol: Symbol) -> str:
|
|
1663
|
+
scope = symbol.scope
|
|
1664
|
+
unit_scope = scope
|
|
1665
|
+
while unit_scope.parent is not None:
|
|
1666
|
+
unit_scope = unit_scope.parent
|
|
1667
|
+
unit_name = unit_scope.name
|
|
1668
|
+
if symbol.kind == SymbolKind.UNIT:
|
|
1669
|
+
return unit_name
|
|
1670
|
+
if scope.kind == ScopeKind.TYPE and scope.owner is not None:
|
|
1671
|
+
return f"{unit_name}.{scope.owner.name}.{symbol.name}"
|
|
1672
|
+
return f"{unit_name}.{symbol.name}"
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _target_group(target: RelationTarget) -> _GroupKey:
|
|
1676
|
+
return (
|
|
1677
|
+
_path_key(target.source_path),
|
|
1678
|
+
_normalized(target.qualified_name),
|
|
1679
|
+
target.kind,
|
|
1680
|
+
_target_signature_identity(target),
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def _target_signature_identity(target: RelationTarget) -> str:
|
|
1685
|
+
if target.kind not in _ROUTINE_KINDS:
|
|
1686
|
+
return ""
|
|
1687
|
+
return _canonical_signature_identity(target.signature)
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
def _symbol_signature_identity(symbol: Symbol) -> str:
|
|
1691
|
+
if symbol.kind.value not in _ROUTINE_KINDS or not isinstance(symbol.type_ref, ProcTypeRef):
|
|
1692
|
+
return ""
|
|
1693
|
+
parameter_symbols = []
|
|
1694
|
+
if symbol.member_scope is not None:
|
|
1695
|
+
parameter_symbols = sorted(
|
|
1696
|
+
(
|
|
1697
|
+
candidate
|
|
1698
|
+
for symbols in symbol.member_scope.symbols.values()
|
|
1699
|
+
for candidate in symbols
|
|
1700
|
+
if candidate.kind == SymbolKind.PARAMETER
|
|
1701
|
+
),
|
|
1702
|
+
key=lambda candidate: (
|
|
1703
|
+
candidate.decl_range.start_line,
|
|
1704
|
+
candidate.decl_range.start_col,
|
|
1705
|
+
candidate.name.casefold(),
|
|
1706
|
+
),
|
|
1707
|
+
)
|
|
1708
|
+
normalized_params: list[str] = []
|
|
1709
|
+
for index, param in enumerate(symbol.type_ref.params):
|
|
1710
|
+
modifier = ""
|
|
1711
|
+
if index < len(parameter_symbols):
|
|
1712
|
+
modifier = _normalized(parameter_symbols[index].attributes.get("modifier", ""))
|
|
1713
|
+
type_name = _normalized(param.display_name()).replace(" ", "")
|
|
1714
|
+
normalized_params.append(f"{modifier}:{type_name}")
|
|
1715
|
+
params = ",".join(normalized_params)
|
|
1716
|
+
signature = f"({params})"
|
|
1717
|
+
if symbol.type_ref.return_type is not None:
|
|
1718
|
+
result = _normalized(symbol.type_ref.return_type.display_name()).replace(" ", "")
|
|
1719
|
+
signature = f"{signature}:{result}"
|
|
1720
|
+
calling_convention = _normalized(symbol.attributes.get("callingconvention", ""))
|
|
1721
|
+
if calling_convention:
|
|
1722
|
+
signature = f"{signature}|cc:{calling_convention}"
|
|
1723
|
+
return _canonical_signature_identity(signature)
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
def _canonical_signature_identity(signature: str) -> str:
|
|
1727
|
+
def split_parameters(content: str) -> list[str]:
|
|
1728
|
+
parameters: list[str] = []
|
|
1729
|
+
start = 0
|
|
1730
|
+
depth = 0
|
|
1731
|
+
for index, char in enumerate(content):
|
|
1732
|
+
if char == "(":
|
|
1733
|
+
depth += 1
|
|
1734
|
+
elif char == ")":
|
|
1735
|
+
depth -= 1
|
|
1736
|
+
elif char in ";," and depth == 0:
|
|
1737
|
+
parameters.append(content[start:index])
|
|
1738
|
+
start = index + 1
|
|
1739
|
+
parameters.append(content[start:])
|
|
1740
|
+
return parameters
|
|
1741
|
+
|
|
1742
|
+
def canonicalize(content: str) -> str:
|
|
1743
|
+
parts: list[str] = []
|
|
1744
|
+
index = 0
|
|
1745
|
+
while index < len(content):
|
|
1746
|
+
if content[index] != "(":
|
|
1747
|
+
parts.append(content[index])
|
|
1748
|
+
index += 1
|
|
1749
|
+
continue
|
|
1750
|
+
depth = 1
|
|
1751
|
+
end = index + 1
|
|
1752
|
+
while end < len(content) and depth:
|
|
1753
|
+
if content[end] == "(":
|
|
1754
|
+
depth += 1
|
|
1755
|
+
elif content[end] == ")":
|
|
1756
|
+
depth -= 1
|
|
1757
|
+
end += 1
|
|
1758
|
+
if depth:
|
|
1759
|
+
parts.append(content[index:])
|
|
1760
|
+
break
|
|
1761
|
+
parameters = split_parameters(content[index + 1 : end - 1])
|
|
1762
|
+
expanded: list[str] = []
|
|
1763
|
+
for parameter in parameters:
|
|
1764
|
+
match = re.fullmatch(r"\s*((?:var|const|out)\s*)?#(\d+):(.*)", parameter, re.DOTALL)
|
|
1765
|
+
if match is None:
|
|
1766
|
+
expanded.append(canonicalize(parameter))
|
|
1767
|
+
continue
|
|
1768
|
+
modifier, count, parameter_type = match.groups()
|
|
1769
|
+
expanded.extend(
|
|
1770
|
+
f"{modifier or ''}:{canonicalize(parameter_type)}"
|
|
1771
|
+
for _ in range(int(count))
|
|
1772
|
+
)
|
|
1773
|
+
parts.append(f"({','.join(expanded)})")
|
|
1774
|
+
index = end
|
|
1775
|
+
return "".join(parts)
|
|
1776
|
+
|
|
1777
|
+
return _normalized(canonicalize(signature).replace(" ", ""))
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def _target_sort_key(target: RelationTarget) -> tuple[object, ...]:
|
|
1781
|
+
return (
|
|
1782
|
+
target.path.casefold(),
|
|
1783
|
+
target.line,
|
|
1784
|
+
target.column,
|
|
1785
|
+
target.qualified_name.casefold(),
|
|
1786
|
+
target.signature.casefold(),
|
|
1787
|
+
target.target_id,
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
def _relation_sort_key(item: Mapping[str, object]) -> tuple[object, ...]:
|
|
1792
|
+
evidence = item.get("evidence")
|
|
1793
|
+
evidence_mapping = evidence if isinstance(evidence, Mapping) else {}
|
|
1794
|
+
return (
|
|
1795
|
+
str(item.get("qualified_name", "")).casefold(),
|
|
1796
|
+
str(item.get("path", "")).casefold(),
|
|
1797
|
+
int(item.get("line", 0)),
|
|
1798
|
+
int(item.get("column", 0)),
|
|
1799
|
+
str(evidence_mapping.get("path", "")).casefold(),
|
|
1800
|
+
int(evidence_mapping.get("line", 0)),
|
|
1801
|
+
int(evidence_mapping.get("column", 0)),
|
|
1802
|
+
str(evidence_mapping.get("kind", "")),
|
|
1803
|
+
str(item.get("target_id", "")),
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
def _freeze_adjacency(
|
|
1808
|
+
groups: Mapping[_GroupKey, list[_AdjacencyValue]],
|
|
1809
|
+
) -> dict[_GroupKey, tuple[_AdjacencyValue, ...]]:
|
|
1810
|
+
return {key: tuple(values) for key, values in groups.items()}
|
|
1811
|
+
|
|
1812
|
+
|
|
1813
|
+
def _dedupe_mappings(items: Iterable[Mapping[str, object]]) -> list[dict[str, object]]:
|
|
1814
|
+
result: list[dict[str, object]] = []
|
|
1815
|
+
seen: set[str] = set()
|
|
1816
|
+
for item in items:
|
|
1817
|
+
mapping = dict(item)
|
|
1818
|
+
key = repr(_freeze(mapping))
|
|
1819
|
+
if key in seen:
|
|
1820
|
+
continue
|
|
1821
|
+
seen.add(key)
|
|
1822
|
+
result.append(mapping)
|
|
1823
|
+
return result
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def _summarize_relation_problems(
|
|
1827
|
+
problems: Iterable[Mapping[str, object]],
|
|
1828
|
+
*,
|
|
1829
|
+
limit: int,
|
|
1830
|
+
) -> tuple[list[dict[str, object]], int]:
|
|
1831
|
+
summaries: list[dict[str, object]] = []
|
|
1832
|
+
seen: set[tuple[str, str, str]] = set()
|
|
1833
|
+
for problem in problems:
|
|
1834
|
+
mapping = dict(problem)
|
|
1835
|
+
key = (
|
|
1836
|
+
str(mapping.get("path", mapping.get("file", ""))),
|
|
1837
|
+
str(mapping.get("kind", "")),
|
|
1838
|
+
str(mapping.get("message", "")),
|
|
1839
|
+
)
|
|
1840
|
+
if key in seen:
|
|
1841
|
+
continue
|
|
1842
|
+
seen.add(key)
|
|
1843
|
+
if len(summaries) < limit:
|
|
1844
|
+
summaries.append(mapping)
|
|
1845
|
+
return summaries, len(seen)
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def _freeze(value: object) -> object:
|
|
1849
|
+
if isinstance(value, Mapping):
|
|
1850
|
+
return tuple(sorted((str(key), _freeze(item)) for key, item in value.items()))
|
|
1851
|
+
if isinstance(value, (list, tuple)):
|
|
1852
|
+
return tuple(_freeze(item) for item in value)
|
|
1853
|
+
return value
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
def _path_key(value: str | Path) -> str:
|
|
1857
|
+
return str(Path(value).expanduser().resolve()).casefold()
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
def _normalized(value: str) -> str:
|
|
1861
|
+
return unicodedata.normalize("NFC", value).casefold()
|
|
1862
|
+
|
|
1863
|
+
|
|
1864
|
+
def _text_contains_reference_token(text: str, value: str) -> bool:
|
|
1865
|
+
needle = _normalized(value)
|
|
1866
|
+
normalized_text = _normalized(text) if not needle.isascii() else text.casefold()
|
|
1867
|
+
if not needle:
|
|
1868
|
+
return False
|
|
1869
|
+
if re.fullmatch(r"[^\W\d]\w*(?:\.[^\W\d]\w*)*", needle) is None:
|
|
1870
|
+
return needle in normalized_text
|
|
1871
|
+
return re.search(
|
|
1872
|
+
rf"(?<!\w){re.escape(needle)}(?!\w)",
|
|
1873
|
+
normalized_text,
|
|
1874
|
+
) is not None
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
def _stable_component(value: str) -> str:
|
|
1878
|
+
return unicodedata.normalize("NFC", value).replace("\\", "_").replace("/", "_") or "unknown"
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
__all__ = ["ProjectRelationIndex", "RelationTarget"]
|