agent-codinglanguage-mapper 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_codinglanguage_mapper/__init__.py +20 -0
- agent_codinglanguage_mapper/__main__.py +4 -0
- agent_codinglanguage_mapper/_version.py +2 -0
- agent_codinglanguage_mapper/adapters/__init__.py +4 -0
- agent_codinglanguage_mapper/adapters/delphi.py +822 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
- agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
- agent_codinglanguage_mapper/cli.py +138 -0
- agent_codinglanguage_mapper/discovery.py +1328 -0
- agent_codinglanguage_mapper/indexer.py +1102 -0
- agent_codinglanguage_mapper/integration.py +186 -0
- agent_codinglanguage_mapper/lsp_server.py +413 -0
- agent_codinglanguage_mapper/lsp_service.py +447 -0
- agent_codinglanguage_mapper/mapper.py +596 -0
- agent_codinglanguage_mapper/models.py +101 -0
- agent_codinglanguage_mapper/protocol.py +152 -0
- agent_codinglanguage_mapper/rendering.py +153 -0
- agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
- agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
- agent_codinglanguage_mapper/worker.py +29 -0
- agent_codinglanguage_mapper/workspace.py +114 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
import re
|
|
9
|
+
from typing import Iterable, Mapping
|
|
10
|
+
|
|
11
|
+
from .adapters import AdapterResult, DelphiAdapter, LanguageAdapter, adapter_for
|
|
12
|
+
from .adapters.delphi_frontend.project_discovery import DelphiProjectDiscovery, discover_delphi_project
|
|
13
|
+
from .discovery import discover_workspace, workspace_directory_inventory, workspace_inventory
|
|
14
|
+
from .models import Language, Problem, Reference, Relation, Resolution, SourceFile, SourceRange, Symbol
|
|
15
|
+
from .workspace import WorkspaceSnapshot, stable_id
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class _CachedParse:
|
|
20
|
+
project_id: str
|
|
21
|
+
file_id: str
|
|
22
|
+
language: Language
|
|
23
|
+
source: str
|
|
24
|
+
source_digest: str
|
|
25
|
+
source_stamp: tuple[int, int, int] | None
|
|
26
|
+
context: tuple[str, ...]
|
|
27
|
+
adapter: LanguageAdapter
|
|
28
|
+
outline: AdapterResult
|
|
29
|
+
result: AdapterResult
|
|
30
|
+
dependency_stamps: tuple[tuple[str, tuple[int, int, int] | None], ...] = ()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class _ImportRule:
|
|
35
|
+
project_id: str
|
|
36
|
+
path: str
|
|
37
|
+
language: Language
|
|
38
|
+
import_path: str
|
|
39
|
+
imported_symbols: tuple[tuple[str, str], ...]
|
|
40
|
+
module_alias: str
|
|
41
|
+
wildcard: bool
|
|
42
|
+
project_wide: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class WorkspaceIndexCache:
|
|
47
|
+
entries: dict[str, _CachedParse] = field(default_factory=dict)
|
|
48
|
+
inventory: tuple[tuple[str, int, int, str], ...] = ()
|
|
49
|
+
directories: tuple[tuple[str, int], ...] = ()
|
|
50
|
+
|
|
51
|
+
def sources_changed(self, root: Path) -> bool:
|
|
52
|
+
if any(
|
|
53
|
+
not _dependencies_unchanged(cached.dependency_stamps)
|
|
54
|
+
for cached in self.entries.values()
|
|
55
|
+
):
|
|
56
|
+
return True
|
|
57
|
+
for relative, cached in self.entries.items():
|
|
58
|
+
path = root / relative
|
|
59
|
+
if _source_stamp(path) != cached.source_stamp:
|
|
60
|
+
return True
|
|
61
|
+
for relative, modified, size, digest in self.inventory:
|
|
62
|
+
if relative in self.entries:
|
|
63
|
+
continue
|
|
64
|
+
path = root / relative
|
|
65
|
+
if digest:
|
|
66
|
+
if _inventory_stamp(path) != (modified, size, digest):
|
|
67
|
+
return True
|
|
68
|
+
elif (stamp := _source_stamp(path)) is None or stamp[:2] != (modified, size):
|
|
69
|
+
return True
|
|
70
|
+
if all(
|
|
71
|
+
_directory_stamp(root if relative == "." else root / relative) == modified
|
|
72
|
+
for relative, modified in self.directories
|
|
73
|
+
):
|
|
74
|
+
return False
|
|
75
|
+
new_inventory = workspace_inventory(root)
|
|
76
|
+
self.directories = workspace_directory_inventory(root)
|
|
77
|
+
return new_inventory != self.inventory
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_workspace_snapshot(
|
|
81
|
+
root: str | Path,
|
|
82
|
+
languages: Iterable[Language] | None = None,
|
|
83
|
+
source_overrides: Mapping[str, str] | None = None,
|
|
84
|
+
detail_files: Iterable[str] | None = None,
|
|
85
|
+
base_snapshot: WorkspaceSnapshot | None = None,
|
|
86
|
+
index_cache: WorkspaceIndexCache | None = None,
|
|
87
|
+
) -> WorkspaceSnapshot:
|
|
88
|
+
workspace = Path(root).expanduser().resolve()
|
|
89
|
+
discovery = discover_workspace(workspace, languages)
|
|
90
|
+
files: list[SourceFile] = []
|
|
91
|
+
symbols: list[Symbol] = []
|
|
92
|
+
references: list[Reference] = []
|
|
93
|
+
relations: list[Relation] = []
|
|
94
|
+
problems: list[Problem] = list(discovery.problems)
|
|
95
|
+
source_digests: list[tuple[str, str]] = []
|
|
96
|
+
base_digests = dict(base_snapshot.source_digests) if base_snapshot is not None else {}
|
|
97
|
+
projects_by_id = {item.project_id: item for item in discovery.projects}
|
|
98
|
+
delphi_configs: dict[str, DelphiProjectDiscovery] = {}
|
|
99
|
+
selected_detail_files = None if detail_files is None else {Path(item).as_posix() for item in detail_files}
|
|
100
|
+
incremental_detail = base_snapshot is not None and selected_detail_files is not None
|
|
101
|
+
base_files_by_path = (
|
|
102
|
+
{item.path: item for item in base_snapshot.files}
|
|
103
|
+
if incremental_detail and base_snapshot is not None
|
|
104
|
+
else {}
|
|
105
|
+
)
|
|
106
|
+
active_cache_paths: set[str] = set()
|
|
107
|
+
if incremental_detail and base_snapshot is not None:
|
|
108
|
+
problems.extend(
|
|
109
|
+
item
|
|
110
|
+
for item in base_snapshot.problems
|
|
111
|
+
if not _problem_source_path(item) and item not in problems
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
for found in discovery.files:
|
|
115
|
+
relative = found.path.relative_to(workspace).as_posix()
|
|
116
|
+
include_detail = (
|
|
117
|
+
(selected_detail_files is None or relative in selected_detail_files)
|
|
118
|
+
and found.path.suffix.casefold() != ".inc"
|
|
119
|
+
)
|
|
120
|
+
file_id = stable_id("file", found.project_id, relative)
|
|
121
|
+
project = projects_by_id[found.project_id]
|
|
122
|
+
if found.language is Language.DELPHI:
|
|
123
|
+
config = delphi_configs.get(project.project_id)
|
|
124
|
+
if config is None:
|
|
125
|
+
config = discover_delphi_project(
|
|
126
|
+
project.root,
|
|
127
|
+
project_file=_delphi_project_file(project.root, project.name),
|
|
128
|
+
scan_workspace_sources=False,
|
|
129
|
+
)
|
|
130
|
+
delphi_configs[project.project_id] = config
|
|
131
|
+
problems.extend(
|
|
132
|
+
Problem(
|
|
133
|
+
f"delphi-project-{item.kind}",
|
|
134
|
+
item.message,
|
|
135
|
+
"warning",
|
|
136
|
+
Language.DELPHI,
|
|
137
|
+
metadata={"origin": item.origin},
|
|
138
|
+
)
|
|
139
|
+
for item in config.problems
|
|
140
|
+
)
|
|
141
|
+
candidate_adapter: LanguageAdapter = DelphiAdapter(
|
|
142
|
+
include_paths=(*config.include_paths, *config.search_paths),
|
|
143
|
+
defines=config.defines,
|
|
144
|
+
workspace_root=workspace,
|
|
145
|
+
)
|
|
146
|
+
adapter_context = (
|
|
147
|
+
found.language.value,
|
|
148
|
+
project.root,
|
|
149
|
+
*(str(item) for item in (*config.include_paths, *config.search_paths)),
|
|
150
|
+
"\0defines",
|
|
151
|
+
*config.defines,
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
candidate_adapter = adapter_for(
|
|
155
|
+
found.language,
|
|
156
|
+
workspace_root=workspace,
|
|
157
|
+
include_paths=found.include_paths,
|
|
158
|
+
)
|
|
159
|
+
adapter_context = (
|
|
160
|
+
found.language.value,
|
|
161
|
+
str(workspace),
|
|
162
|
+
*(str(item) for item in found.include_paths),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
has_override = source_overrides is not None and relative in source_overrides
|
|
166
|
+
source_override = source_overrides[relative] if has_override and source_overrides is not None else ""
|
|
167
|
+
if has_override:
|
|
168
|
+
adapter_context = (*adapter_context, "\0override", _source_digest(source_override))
|
|
169
|
+
current_stamp = None if has_override else _source_stamp(found.path)
|
|
170
|
+
cached = index_cache.entries.get(relative) if index_cache is not None else None
|
|
171
|
+
cache_matches = (
|
|
172
|
+
cached is not None
|
|
173
|
+
and cached.project_id == found.project_id
|
|
174
|
+
and cached.file_id == file_id
|
|
175
|
+
and cached.language is found.language
|
|
176
|
+
and cached.context == adapter_context
|
|
177
|
+
and (
|
|
178
|
+
cached.source_digest == _source_digest(source_override)
|
|
179
|
+
if has_override
|
|
180
|
+
else current_stamp is not None and cached.source_stamp == current_stamp
|
|
181
|
+
)
|
|
182
|
+
and _dependencies_unchanged(cached.dependency_stamps)
|
|
183
|
+
)
|
|
184
|
+
existing_file = base_files_by_path.get(relative)
|
|
185
|
+
if (
|
|
186
|
+
incremental_detail
|
|
187
|
+
and not include_detail
|
|
188
|
+
and base_snapshot is not None
|
|
189
|
+
and existing_file is not None
|
|
190
|
+
and existing_file.file_id == file_id
|
|
191
|
+
and existing_file.language is found.language
|
|
192
|
+
and existing_file.project_id == found.project_id
|
|
193
|
+
and cache_matches
|
|
194
|
+
):
|
|
195
|
+
files.append(existing_file)
|
|
196
|
+
digest = base_digests.get(relative)
|
|
197
|
+
if digest is not None:
|
|
198
|
+
source_digests.append((relative, digest))
|
|
199
|
+
existing_symbols = base_snapshot.symbols_by_file.get(existing_file.file_id, ())
|
|
200
|
+
existing_symbol_ids = {item.target_id for item in existing_symbols}
|
|
201
|
+
symbols.extend(existing_symbols)
|
|
202
|
+
references.extend(item for item in base_snapshot.references if item.range.path == relative)
|
|
203
|
+
relations.extend(
|
|
204
|
+
item
|
|
205
|
+
for item in base_snapshot.relations
|
|
206
|
+
if item.source_id in existing_symbol_ids
|
|
207
|
+
or (not item.source_id and any(evidence.path == relative for evidence in item.evidence))
|
|
208
|
+
)
|
|
209
|
+
problems.extend(
|
|
210
|
+
item for item in base_snapshot.problems if _problem_source_path(item) == relative
|
|
211
|
+
)
|
|
212
|
+
if index_cache is not None and relative in index_cache.entries:
|
|
213
|
+
active_cache_paths.add(relative)
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
files.append(SourceFile(file_id, relative, found.language, found.project_id))
|
|
217
|
+
if cache_matches and cached is not None:
|
|
218
|
+
source = cached.source
|
|
219
|
+
source_digest = cached.source_digest
|
|
220
|
+
adapter = cached.adapter
|
|
221
|
+
source_stamp = cached.source_stamp
|
|
222
|
+
outline = cached.outline
|
|
223
|
+
else:
|
|
224
|
+
try:
|
|
225
|
+
if has_override:
|
|
226
|
+
source = source_override
|
|
227
|
+
source_digest = _source_digest(source)
|
|
228
|
+
source_stamp = None
|
|
229
|
+
else:
|
|
230
|
+
source, source_digest, source_stamp = _read_stable_source(found.path)
|
|
231
|
+
except OSError as exc:
|
|
232
|
+
if index_cache is not None:
|
|
233
|
+
index_cache.entries.pop(relative, None)
|
|
234
|
+
problems.append(
|
|
235
|
+
Problem(
|
|
236
|
+
"source-read-failed",
|
|
237
|
+
f"Could not read {relative}: {exc}",
|
|
238
|
+
"error",
|
|
239
|
+
found.language,
|
|
240
|
+
metadata={"path": relative, "error": type(exc).__name__},
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
continue
|
|
244
|
+
adapter = candidate_adapter
|
|
245
|
+
outline = adapter.parse_outline(relative, source)
|
|
246
|
+
result = (
|
|
247
|
+
adapter.resolve(adapter.parse_detail(relative, source, outline))
|
|
248
|
+
if include_detail
|
|
249
|
+
else outline
|
|
250
|
+
)
|
|
251
|
+
if index_cache is not None:
|
|
252
|
+
index_cache.entries[relative] = _CachedParse(
|
|
253
|
+
found.project_id,
|
|
254
|
+
file_id,
|
|
255
|
+
found.language,
|
|
256
|
+
source,
|
|
257
|
+
source_digest,
|
|
258
|
+
source_stamp,
|
|
259
|
+
adapter_context,
|
|
260
|
+
adapter,
|
|
261
|
+
outline,
|
|
262
|
+
result,
|
|
263
|
+
_dependency_stamps(workspace, result.document),
|
|
264
|
+
)
|
|
265
|
+
active_cache_paths.add(relative)
|
|
266
|
+
source_digests.append((relative, source_digest))
|
|
267
|
+
normalized_symbols = tuple(
|
|
268
|
+
replace(
|
|
269
|
+
item,
|
|
270
|
+
range=_workspace_required_range(workspace, item.range),
|
|
271
|
+
name_range=_workspace_range(workspace, item.name_range),
|
|
272
|
+
)
|
|
273
|
+
for item in result.symbols
|
|
274
|
+
)
|
|
275
|
+
ids = {
|
|
276
|
+
item.target_id: _workspace_symbol_id(found.project_id, file_id, normalized)
|
|
277
|
+
for item, normalized in zip(result.symbols, normalized_symbols)
|
|
278
|
+
}
|
|
279
|
+
symbols.extend(
|
|
280
|
+
Symbol(
|
|
281
|
+
ids[item.target_id],
|
|
282
|
+
item.name,
|
|
283
|
+
item.qualified_name,
|
|
284
|
+
item.kind,
|
|
285
|
+
item.language,
|
|
286
|
+
normalized.range,
|
|
287
|
+
found.project_id,
|
|
288
|
+
file_id,
|
|
289
|
+
item.signature,
|
|
290
|
+
item.native_kind,
|
|
291
|
+
ids.get(item.parent_id, ""),
|
|
292
|
+
normalized.name_range,
|
|
293
|
+
item.role,
|
|
294
|
+
)
|
|
295
|
+
for item, normalized in zip(result.symbols, normalized_symbols)
|
|
296
|
+
)
|
|
297
|
+
references.extend(
|
|
298
|
+
replace(
|
|
299
|
+
item,
|
|
300
|
+
source_id=ids.get(item.source_id, ""),
|
|
301
|
+
target_id=ids.get(item.target_id, ""),
|
|
302
|
+
range=_workspace_required_range(workspace, item.range),
|
|
303
|
+
)
|
|
304
|
+
for item in result.references
|
|
305
|
+
)
|
|
306
|
+
local_symbols_by_id = {item.target_id: item for item in result.symbols}
|
|
307
|
+
for item in result.relations:
|
|
308
|
+
relation_library = item.target_library
|
|
309
|
+
relation_source = local_symbols_by_id.get(item.source_id)
|
|
310
|
+
if (
|
|
311
|
+
not relation_library
|
|
312
|
+
and item.kind == "ffi"
|
|
313
|
+
and relation_source is not None
|
|
314
|
+
and _is_abi_export_symbol(relation_source)
|
|
315
|
+
and len(found.output_names) == 1
|
|
316
|
+
):
|
|
317
|
+
relation_library = found.output_names[0]
|
|
318
|
+
relations.append(
|
|
319
|
+
replace(
|
|
320
|
+
item,
|
|
321
|
+
source_id=ids.get(item.source_id, ""),
|
|
322
|
+
target_id=ids.get(item.target_id, ""),
|
|
323
|
+
target_library=relation_library,
|
|
324
|
+
evidence=tuple(
|
|
325
|
+
_workspace_required_range(workspace, value) for value in item.evidence
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
problems.extend(_workspace_problem(workspace, item, relative) for item in result.problems)
|
|
330
|
+
|
|
331
|
+
inventory = workspace_inventory(workspace)
|
|
332
|
+
if index_cache is not None:
|
|
333
|
+
for cached_path in set(index_cache.entries) - active_cache_paths:
|
|
334
|
+
del index_cache.entries[cached_path]
|
|
335
|
+
index_cache.inventory = inventory
|
|
336
|
+
index_cache.directories = workspace_directory_inventory(workspace)
|
|
337
|
+
|
|
338
|
+
symbols, references, relations = _discard_shadowed_fragment_results(
|
|
339
|
+
symbols,
|
|
340
|
+
references,
|
|
341
|
+
relations,
|
|
342
|
+
files,
|
|
343
|
+
)
|
|
344
|
+
problems = _deduplicate_problems(problems)
|
|
345
|
+
resolved_references, resolved_relations = _resolve_workspace(tuple(symbols), references, relations)
|
|
346
|
+
return WorkspaceSnapshot(
|
|
347
|
+
projects=discovery.projects,
|
|
348
|
+
files=tuple(files),
|
|
349
|
+
symbols=tuple(symbols),
|
|
350
|
+
references=resolved_references,
|
|
351
|
+
relations=resolved_relations,
|
|
352
|
+
problems=tuple(problems),
|
|
353
|
+
source_digests=tuple(source_digests),
|
|
354
|
+
metadata_digests=tuple(
|
|
355
|
+
(relative, digest)
|
|
356
|
+
for relative, _modified, _size, digest in inventory
|
|
357
|
+
if digest
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _source_digest(source: str) -> str:
|
|
363
|
+
return hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _dependency_stamps(
|
|
367
|
+
workspace: Path,
|
|
368
|
+
document: object,
|
|
369
|
+
) -> tuple[tuple[str, tuple[int, int, int] | None], ...]:
|
|
370
|
+
dependencies = getattr(document, "dependencies", ())
|
|
371
|
+
if not isinstance(dependencies, tuple):
|
|
372
|
+
return ()
|
|
373
|
+
stamped: list[tuple[str, tuple[int, int, int] | None]] = []
|
|
374
|
+
for value in dependencies:
|
|
375
|
+
if not isinstance(value, str):
|
|
376
|
+
continue
|
|
377
|
+
path = Path(value).expanduser()
|
|
378
|
+
if not path.is_absolute():
|
|
379
|
+
path = workspace / path
|
|
380
|
+
resolved = path.resolve()
|
|
381
|
+
stamped.append((str(resolved), _source_stamp(resolved)))
|
|
382
|
+
return tuple(stamped)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _dependencies_unchanged(
|
|
386
|
+
dependencies: tuple[tuple[str, tuple[int, int, int] | None], ...],
|
|
387
|
+
) -> bool:
|
|
388
|
+
return all(_source_stamp(Path(path)) == stamp for path, stamp in dependencies)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _workspace_range(workspace: Path, value: SourceRange | None) -> SourceRange | None:
|
|
392
|
+
if value is None:
|
|
393
|
+
return None
|
|
394
|
+
path = _workspace_path(workspace, value.path)
|
|
395
|
+
return replace(value, path=path)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _workspace_path(workspace: Path, value: str) -> str:
|
|
399
|
+
source_path = Path(value).expanduser()
|
|
400
|
+
if source_path.is_absolute():
|
|
401
|
+
try:
|
|
402
|
+
return source_path.resolve().relative_to(workspace).as_posix()
|
|
403
|
+
except ValueError:
|
|
404
|
+
return str(source_path.resolve())
|
|
405
|
+
return source_path.as_posix()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _workspace_required_range(workspace: Path, value: SourceRange) -> SourceRange:
|
|
409
|
+
normalized = _workspace_range(workspace, value)
|
|
410
|
+
assert normalized is not None
|
|
411
|
+
return normalized
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _workspace_problem(workspace: Path, problem: Problem, fallback_path: str) -> Problem:
|
|
415
|
+
metadata = dict(problem.metadata)
|
|
416
|
+
metadata_path = metadata.get("path")
|
|
417
|
+
if isinstance(metadata_path, str):
|
|
418
|
+
metadata["path"] = _workspace_path(workspace, metadata_path)
|
|
419
|
+
normalized = replace(
|
|
420
|
+
problem,
|
|
421
|
+
range=_workspace_range(workspace, problem.range),
|
|
422
|
+
metadata=metadata,
|
|
423
|
+
)
|
|
424
|
+
return _problem_for_file(normalized, fallback_path)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _discard_shadowed_fragment_results(
|
|
428
|
+
symbols: list[Symbol],
|
|
429
|
+
references: list[Reference],
|
|
430
|
+
relations: list[Relation],
|
|
431
|
+
files: list[SourceFile],
|
|
432
|
+
) -> tuple[list[Symbol], list[Reference], list[Relation]]:
|
|
433
|
+
file_paths = {item.file_id: item.path for item in files}
|
|
434
|
+
|
|
435
|
+
def source_key(symbol: Symbol) -> tuple[object, ...]:
|
|
436
|
+
anchor = symbol.name_range or symbol.range
|
|
437
|
+
return (
|
|
438
|
+
symbol.project_id,
|
|
439
|
+
symbol.language,
|
|
440
|
+
symbol.qualified_name,
|
|
441
|
+
symbol.kind,
|
|
442
|
+
anchor.path,
|
|
443
|
+
anchor.start_line,
|
|
444
|
+
anchor.start_character,
|
|
445
|
+
anchor.end_line,
|
|
446
|
+
anchor.end_character,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
contextual_keys = {
|
|
450
|
+
source_key(symbol)
|
|
451
|
+
for symbol in symbols
|
|
452
|
+
if file_paths.get(symbol.file_id, "") != (symbol.name_range or symbol.range).path
|
|
453
|
+
}
|
|
454
|
+
discarded_ids: set[str] = set()
|
|
455
|
+
for symbol in symbols:
|
|
456
|
+
anchor = symbol.name_range or symbol.range
|
|
457
|
+
owner_path = file_paths.get(symbol.file_id, "")
|
|
458
|
+
if (
|
|
459
|
+
Path(owner_path).suffix.casefold() == ".inc"
|
|
460
|
+
and owner_path == anchor.path
|
|
461
|
+
and (
|
|
462
|
+
source_key(symbol) in contextual_keys
|
|
463
|
+
or (
|
|
464
|
+
symbol.language is Language.DELPHI
|
|
465
|
+
and symbol.kind == "unit"
|
|
466
|
+
and symbol.name_range is None
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
):
|
|
470
|
+
discarded_ids.add(symbol.target_id)
|
|
471
|
+
kept_symbols = [item for item in symbols if item.target_id not in discarded_ids]
|
|
472
|
+
kept_references = [
|
|
473
|
+
item
|
|
474
|
+
for item in references
|
|
475
|
+
if item.source_id not in discarded_ids and item.target_id not in discarded_ids
|
|
476
|
+
]
|
|
477
|
+
kept_relations = [
|
|
478
|
+
item
|
|
479
|
+
for item in relations
|
|
480
|
+
if item.source_id not in discarded_ids and item.target_id not in discarded_ids
|
|
481
|
+
]
|
|
482
|
+
return kept_symbols, kept_references, kept_relations
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _deduplicate_problems(problems: list[Problem]) -> list[Problem]:
|
|
486
|
+
selected: dict[tuple[object, ...], Problem] = {}
|
|
487
|
+
for problem in problems:
|
|
488
|
+
key = (
|
|
489
|
+
problem.code,
|
|
490
|
+
problem.message,
|
|
491
|
+
problem.severity,
|
|
492
|
+
problem.language,
|
|
493
|
+
problem.range,
|
|
494
|
+
)
|
|
495
|
+
selected.setdefault(key, problem)
|
|
496
|
+
return list(selected.values())
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _read_stable_source(
|
|
500
|
+
path: Path,
|
|
501
|
+
*,
|
|
502
|
+
attempts: int = 3,
|
|
503
|
+
) -> tuple[str, str, tuple[int, int, int]]:
|
|
504
|
+
for _ in range(attempts):
|
|
505
|
+
before = _source_stamp(path)
|
|
506
|
+
if before is None:
|
|
507
|
+
raise OSError(f"source is unavailable: {path}")
|
|
508
|
+
encoded = path.read_bytes()
|
|
509
|
+
after = _source_stamp(path)
|
|
510
|
+
if after is not None and before == after:
|
|
511
|
+
if _requires_source_byte_verification():
|
|
512
|
+
verified = path.read_bytes()
|
|
513
|
+
verified_stamp = _source_stamp(path)
|
|
514
|
+
if verified_stamp is None or after != verified_stamp or encoded != verified:
|
|
515
|
+
continue
|
|
516
|
+
encoded = verified
|
|
517
|
+
after = verified_stamp
|
|
518
|
+
source = encoded.decode("utf-8-sig", "replace")
|
|
519
|
+
return source, _source_digest(source), after
|
|
520
|
+
raise OSError(f"source changed while it was being read: {path}")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _requires_source_byte_verification() -> bool:
|
|
524
|
+
return os.name == "nt"
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _workspace_symbol_id(project_id: str, file_id: str, symbol: Symbol) -> str:
|
|
528
|
+
anchor = symbol.name_range or symbol.range
|
|
529
|
+
return stable_id(
|
|
530
|
+
"symbol",
|
|
531
|
+
project_id,
|
|
532
|
+
file_id,
|
|
533
|
+
symbol.qualified_name,
|
|
534
|
+
str(anchor.start_line),
|
|
535
|
+
str(anchor.start_character),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _source_stamp(path: Path) -> tuple[int, int, int] | None:
|
|
540
|
+
try:
|
|
541
|
+
stat = path.stat()
|
|
542
|
+
except OSError:
|
|
543
|
+
return None
|
|
544
|
+
change_time = _windows_change_time(path)
|
|
545
|
+
return stat.st_mtime_ns, stat.st_size, change_time if change_time is not None else stat.st_ctime_ns
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _windows_change_time(path: Path) -> int | None:
|
|
549
|
+
if os.name != "nt":
|
|
550
|
+
return None
|
|
551
|
+
try:
|
|
552
|
+
import ctypes
|
|
553
|
+
|
|
554
|
+
class _FileBasicInfo(ctypes.Structure):
|
|
555
|
+
_fields_ = (
|
|
556
|
+
("CreationTime", ctypes.c_longlong),
|
|
557
|
+
("LastAccessTime", ctypes.c_longlong),
|
|
558
|
+
("LastWriteTime", ctypes.c_longlong),
|
|
559
|
+
("ChangeTime", ctypes.c_longlong),
|
|
560
|
+
("FileAttributes", ctypes.c_ulong),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined]
|
|
564
|
+
create_file = kernel32.CreateFileW
|
|
565
|
+
create_file.argtypes = (
|
|
566
|
+
ctypes.c_wchar_p,
|
|
567
|
+
ctypes.c_ulong,
|
|
568
|
+
ctypes.c_ulong,
|
|
569
|
+
ctypes.c_void_p,
|
|
570
|
+
ctypes.c_ulong,
|
|
571
|
+
ctypes.c_ulong,
|
|
572
|
+
ctypes.c_void_p,
|
|
573
|
+
)
|
|
574
|
+
create_file.restype = ctypes.c_void_p
|
|
575
|
+
handle = create_file(str(path), 0, 0x7, None, 3, 0x02000000, None)
|
|
576
|
+
if handle in (None, ctypes.c_void_p(-1).value):
|
|
577
|
+
return None
|
|
578
|
+
try:
|
|
579
|
+
info = _FileBasicInfo()
|
|
580
|
+
get_info = kernel32.GetFileInformationByHandleEx
|
|
581
|
+
get_info.argtypes = (
|
|
582
|
+
ctypes.c_void_p,
|
|
583
|
+
ctypes.c_int,
|
|
584
|
+
ctypes.c_void_p,
|
|
585
|
+
ctypes.c_ulong,
|
|
586
|
+
)
|
|
587
|
+
get_info.restype = ctypes.c_int
|
|
588
|
+
if not get_info(handle, 0, ctypes.byref(info), ctypes.sizeof(info)):
|
|
589
|
+
return None
|
|
590
|
+
return int(info.ChangeTime)
|
|
591
|
+
finally:
|
|
592
|
+
close_handle = kernel32.CloseHandle
|
|
593
|
+
close_handle.argtypes = (ctypes.c_void_p,)
|
|
594
|
+
close_handle.restype = ctypes.c_int
|
|
595
|
+
close_handle(handle)
|
|
596
|
+
except (AttributeError, OSError):
|
|
597
|
+
return None
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _inventory_stamp(path: Path) -> tuple[int, int, str] | None:
|
|
601
|
+
try:
|
|
602
|
+
stat = path.stat()
|
|
603
|
+
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
604
|
+
except OSError:
|
|
605
|
+
return None
|
|
606
|
+
return stat.st_mtime_ns, stat.st_size, digest
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _directory_stamp(path: Path) -> int | None:
|
|
610
|
+
try:
|
|
611
|
+
return path.stat().st_mtime_ns
|
|
612
|
+
except OSError:
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _problem_for_file(problem: Problem, path: str) -> Problem:
|
|
617
|
+
if problem.range is not None or isinstance(problem.metadata.get("path"), str):
|
|
618
|
+
return problem
|
|
619
|
+
return replace(problem, metadata={**problem.metadata, "path": path})
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _problem_source_path(problem: Problem) -> str:
|
|
623
|
+
if problem.range is not None:
|
|
624
|
+
return problem.range.path
|
|
625
|
+
path = problem.metadata.get("path")
|
|
626
|
+
return path if isinstance(path, str) else ""
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _delphi_project_file(root: str, project_name: str) -> Path | None:
|
|
630
|
+
project_root = Path(root)
|
|
631
|
+
try:
|
|
632
|
+
entries = tuple(project_root.iterdir())
|
|
633
|
+
except OSError:
|
|
634
|
+
return None
|
|
635
|
+
for suffix in (".dpr", ".dpk"):
|
|
636
|
+
matches = sorted(
|
|
637
|
+
(
|
|
638
|
+
candidate
|
|
639
|
+
for candidate in entries
|
|
640
|
+
if candidate.is_file()
|
|
641
|
+
and candidate.stem.casefold() == project_name.casefold()
|
|
642
|
+
and candidate.suffix.casefold() == suffix
|
|
643
|
+
),
|
|
644
|
+
key=lambda candidate: candidate.name,
|
|
645
|
+
)
|
|
646
|
+
if matches:
|
|
647
|
+
return matches[0]
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _resolve_workspace(
|
|
652
|
+
symbols: tuple[Symbol, ...],
|
|
653
|
+
references: list[Reference],
|
|
654
|
+
relations: list[Relation],
|
|
655
|
+
) -> tuple[tuple[Reference, ...], tuple[Relation, ...]]:
|
|
656
|
+
by_name: dict[tuple[Language, str], list[Symbol]] = defaultdict(list)
|
|
657
|
+
by_id = {item.target_id: item for item in symbols}
|
|
658
|
+
for symbol in symbols:
|
|
659
|
+
if symbol.kind == "module":
|
|
660
|
+
continue
|
|
661
|
+
by_name[(symbol.language, _name_key(symbol.language, symbol.name))].append(symbol)
|
|
662
|
+
if symbol.qualified_name != symbol.name:
|
|
663
|
+
by_name[(symbol.language, _name_key(symbol.language, symbol.qualified_name))].append(symbol)
|
|
664
|
+
|
|
665
|
+
visibility = _workspace_visibility(references, by_id)
|
|
666
|
+
|
|
667
|
+
abi_exports: dict[tuple[str, str], list[Symbol]] = defaultdict(list)
|
|
668
|
+
for relation in relations:
|
|
669
|
+
source = by_id.get(relation.source_id)
|
|
670
|
+
if (
|
|
671
|
+
relation.kind == "ffi"
|
|
672
|
+
and relation.target_name
|
|
673
|
+
and source is not None
|
|
674
|
+
and _is_abi_export_symbol(source)
|
|
675
|
+
and all(
|
|
676
|
+
item.target_id != source.target_id
|
|
677
|
+
for item in abi_exports[
|
|
678
|
+
(_normalize_library(relation.target_library), relation.target_name)
|
|
679
|
+
]
|
|
680
|
+
)
|
|
681
|
+
):
|
|
682
|
+
library = _normalize_library(relation.target_library)
|
|
683
|
+
if library:
|
|
684
|
+
abi_exports[(library, relation.target_name)].append(source)
|
|
685
|
+
|
|
686
|
+
resolved_references = tuple(
|
|
687
|
+
_resolve_reference(item, by_name, by_id, visibility) for item in references
|
|
688
|
+
)
|
|
689
|
+
resolved_relations = tuple(
|
|
690
|
+
_resolve_relation(item, by_name, abi_exports, by_id, visibility) for item in relations
|
|
691
|
+
)
|
|
692
|
+
return resolved_references, resolved_relations
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _resolve_reference(
|
|
696
|
+
reference: Reference,
|
|
697
|
+
by_name: dict[tuple[Language, str], list[Symbol]],
|
|
698
|
+
by_id: dict[str, Symbol],
|
|
699
|
+
visibility: tuple[_ImportRule, ...],
|
|
700
|
+
) -> Reference:
|
|
701
|
+
if reference.reference_kind == "import":
|
|
702
|
+
return replace(reference, target_id="", resolution=Resolution.UNRESOLVED)
|
|
703
|
+
if reference.target_id and reference.target_id in by_id:
|
|
704
|
+
return replace(reference, resolution=Resolution.COMPLETE)
|
|
705
|
+
if reference.target_id:
|
|
706
|
+
reference = replace(reference, target_id="", resolution=Resolution.UNRESOLVED)
|
|
707
|
+
|
|
708
|
+
source = by_id.get(reference.source_id)
|
|
709
|
+
if source is None or source.kind in {"module", "unit"}:
|
|
710
|
+
return replace(
|
|
711
|
+
reference,
|
|
712
|
+
target_id="",
|
|
713
|
+
resolution=_preserve_incomplete_resolution(
|
|
714
|
+
reference.resolution,
|
|
715
|
+
Resolution.UNRESOLVED,
|
|
716
|
+
),
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
rules = _rules_for_source(source, visibility)
|
|
720
|
+
candidate_names = {reference.name}
|
|
721
|
+
candidate_names.update(
|
|
722
|
+
original
|
|
723
|
+
for rule in rules
|
|
724
|
+
for original, alias in rule.imported_symbols
|
|
725
|
+
if _identifier_equal(reference.language, alias, reference.name)
|
|
726
|
+
)
|
|
727
|
+
candidates = [
|
|
728
|
+
candidate
|
|
729
|
+
for name in candidate_names
|
|
730
|
+
for candidate in by_name.get(
|
|
731
|
+
(reference.language, _name_key(reference.language, name)),
|
|
732
|
+
[],
|
|
733
|
+
)
|
|
734
|
+
]
|
|
735
|
+
visible = _visible_candidates(
|
|
736
|
+
source,
|
|
737
|
+
candidates,
|
|
738
|
+
rules,
|
|
739
|
+
by_id,
|
|
740
|
+
reference.name,
|
|
741
|
+
qualifier=reference.qualifier,
|
|
742
|
+
)
|
|
743
|
+
target = _unique_logical_candidate(visible)
|
|
744
|
+
if target is not None:
|
|
745
|
+
return replace(reference, target_id=target.target_id, resolution=Resolution.COMPLETE)
|
|
746
|
+
if len(visible) > 1:
|
|
747
|
+
return replace(reference, target_id="", resolution=Resolution.AMBIGUOUS)
|
|
748
|
+
return replace(
|
|
749
|
+
reference,
|
|
750
|
+
target_id="",
|
|
751
|
+
resolution=_preserve_incomplete_resolution(reference.resolution, Resolution.UNRESOLVED),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _resolve_relation(
|
|
756
|
+
relation: Relation,
|
|
757
|
+
by_name: dict[tuple[Language, str], list[Symbol]],
|
|
758
|
+
abi_exports: dict[tuple[str, str], list[Symbol]],
|
|
759
|
+
by_id: dict[str, Symbol],
|
|
760
|
+
visibility: tuple[_ImportRule, ...],
|
|
761
|
+
) -> Relation:
|
|
762
|
+
if relation.kind == "ffi":
|
|
763
|
+
source = by_id.get(relation.source_id)
|
|
764
|
+
if source is not None and _is_abi_export_symbol(source):
|
|
765
|
+
return replace(
|
|
766
|
+
relation,
|
|
767
|
+
target_id="",
|
|
768
|
+
resolution=_preserve_incomplete_resolution(
|
|
769
|
+
relation.resolution,
|
|
770
|
+
Resolution.SOUND_PARTIAL,
|
|
771
|
+
),
|
|
772
|
+
)
|
|
773
|
+
library = _normalize_library(relation.target_library)
|
|
774
|
+
candidates = abi_exports.get((library, relation.target_name), []) if library else []
|
|
775
|
+
if len(candidates) == 1:
|
|
776
|
+
return replace(
|
|
777
|
+
relation,
|
|
778
|
+
target_id=candidates[0].target_id,
|
|
779
|
+
resolution=Resolution.COMPLETE,
|
|
780
|
+
)
|
|
781
|
+
if len(candidates) > 1:
|
|
782
|
+
return replace(relation, target_id="", resolution=Resolution.AMBIGUOUS)
|
|
783
|
+
return replace(
|
|
784
|
+
relation,
|
|
785
|
+
target_id="",
|
|
786
|
+
resolution=_preserve_incomplete_resolution(
|
|
787
|
+
relation.resolution,
|
|
788
|
+
Resolution.SOUND_PARTIAL,
|
|
789
|
+
),
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
target = by_id.get(relation.target_id)
|
|
793
|
+
if target is None and relation.target_name:
|
|
794
|
+
source = by_id.get(relation.source_id)
|
|
795
|
+
if source is not None:
|
|
796
|
+
candidates = by_name.get(
|
|
797
|
+
(source.language, _name_key(source.language, relation.target_name)),
|
|
798
|
+
[],
|
|
799
|
+
)
|
|
800
|
+
visible = _visible_candidates(
|
|
801
|
+
source,
|
|
802
|
+
candidates,
|
|
803
|
+
_rules_for_source(source, visibility),
|
|
804
|
+
by_id,
|
|
805
|
+
relation.target_name,
|
|
806
|
+
for_relation=True,
|
|
807
|
+
)
|
|
808
|
+
target = _unique_logical_candidate(visible)
|
|
809
|
+
else:
|
|
810
|
+
visible = []
|
|
811
|
+
if target is None and len(visible) > 1:
|
|
812
|
+
return replace(relation, target_id="", resolution=Resolution.AMBIGUOUS)
|
|
813
|
+
if target is None:
|
|
814
|
+
return replace(
|
|
815
|
+
relation,
|
|
816
|
+
target_id="",
|
|
817
|
+
resolution=_preserve_incomplete_resolution(
|
|
818
|
+
relation.resolution,
|
|
819
|
+
Resolution.UNRESOLVED,
|
|
820
|
+
),
|
|
821
|
+
)
|
|
822
|
+
kind = "implements" if relation.kind == "inherits" and target.kind == "interface" else relation.kind
|
|
823
|
+
return replace(relation, target_id=target.target_id, kind=kind, resolution=Resolution.COMPLETE)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _workspace_visibility(
|
|
827
|
+
references: list[Reference],
|
|
828
|
+
by_id: dict[str, Symbol],
|
|
829
|
+
) -> tuple[_ImportRule, ...]:
|
|
830
|
+
visibility: list[_ImportRule] = []
|
|
831
|
+
for reference in references:
|
|
832
|
+
owner = by_id.get(reference.source_id)
|
|
833
|
+
if (
|
|
834
|
+
owner is None
|
|
835
|
+
or reference.reference_kind != "import"
|
|
836
|
+
or owner.kind not in {"module", "namespace", "unit"}
|
|
837
|
+
):
|
|
838
|
+
continue
|
|
839
|
+
visibility.append(
|
|
840
|
+
_ImportRule(
|
|
841
|
+
owner.project_id,
|
|
842
|
+
reference.range.path,
|
|
843
|
+
reference.language,
|
|
844
|
+
_scope_key(reference.language, reference.import_path),
|
|
845
|
+
reference.imported_symbols,
|
|
846
|
+
reference.module_alias,
|
|
847
|
+
reference.wildcard_import,
|
|
848
|
+
reference.project_wide,
|
|
849
|
+
)
|
|
850
|
+
)
|
|
851
|
+
return tuple(visibility)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _rules_for_source(
|
|
855
|
+
source: Symbol,
|
|
856
|
+
visibility: tuple[_ImportRule, ...],
|
|
857
|
+
) -> tuple[_ImportRule, ...]:
|
|
858
|
+
return tuple(
|
|
859
|
+
rule
|
|
860
|
+
for rule in visibility
|
|
861
|
+
if rule.project_id == source.project_id
|
|
862
|
+
and rule.language is source.language
|
|
863
|
+
and (rule.project_wide or rule.path == source.range.path)
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _visible_candidates(
|
|
868
|
+
source: Symbol,
|
|
869
|
+
candidates: Iterable[Symbol],
|
|
870
|
+
rules: tuple[_ImportRule, ...],
|
|
871
|
+
by_id: dict[str, Symbol],
|
|
872
|
+
reference_name: str,
|
|
873
|
+
*,
|
|
874
|
+
qualifier: str = "",
|
|
875
|
+
for_relation: bool = False,
|
|
876
|
+
) -> list[Symbol]:
|
|
877
|
+
same_project = [item for item in candidates if item.project_id == source.project_id]
|
|
878
|
+
local = [item for item in same_project if item.file_id == source.file_id]
|
|
879
|
+
if local:
|
|
880
|
+
return local
|
|
881
|
+
if source.language is Language.CSHARP:
|
|
882
|
+
same_namespace = [
|
|
883
|
+
item
|
|
884
|
+
for item in same_project
|
|
885
|
+
if _csharp_same_namespace_visible(
|
|
886
|
+
source,
|
|
887
|
+
item,
|
|
888
|
+
by_id,
|
|
889
|
+
qualifier=qualifier,
|
|
890
|
+
for_relation=for_relation,
|
|
891
|
+
)
|
|
892
|
+
]
|
|
893
|
+
if same_namespace:
|
|
894
|
+
return same_namespace
|
|
895
|
+
return [
|
|
896
|
+
item
|
|
897
|
+
for item in same_project
|
|
898
|
+
if any(
|
|
899
|
+
_rule_exposes_candidate(
|
|
900
|
+
rule,
|
|
901
|
+
item,
|
|
902
|
+
by_id,
|
|
903
|
+
reference_name,
|
|
904
|
+
qualifier=qualifier,
|
|
905
|
+
for_relation=for_relation,
|
|
906
|
+
)
|
|
907
|
+
for rule in rules
|
|
908
|
+
)
|
|
909
|
+
]
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _csharp_namespace(symbol: Symbol, by_id: dict[str, Symbol]) -> str:
|
|
913
|
+
current: Symbol | None = symbol
|
|
914
|
+
while current is not None:
|
|
915
|
+
if current.kind == "namespace":
|
|
916
|
+
qualified = current.qualified_name
|
|
917
|
+
module_prefix = f"{PurePosixPath(current.range.path).stem}."
|
|
918
|
+
return qualified[len(module_prefix):] if qualified.startswith(module_prefix) else qualified
|
|
919
|
+
current = by_id.get(current.parent_id)
|
|
920
|
+
return ""
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _symbol_module_keys(symbol: Symbol) -> set[str]:
|
|
924
|
+
path = PurePosixPath(symbol.range.path)
|
|
925
|
+
without_suffix = path.with_suffix("").as_posix()
|
|
926
|
+
return {
|
|
927
|
+
_scope_key(symbol.language, value)
|
|
928
|
+
for value in {
|
|
929
|
+
path.name,
|
|
930
|
+
path.stem,
|
|
931
|
+
without_suffix,
|
|
932
|
+
without_suffix.replace("/", "."),
|
|
933
|
+
}
|
|
934
|
+
if value
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _rule_exposes_candidate(
|
|
939
|
+
rule: _ImportRule,
|
|
940
|
+
candidate: Symbol,
|
|
941
|
+
by_id: dict[str, Symbol],
|
|
942
|
+
reference_name: str,
|
|
943
|
+
*,
|
|
944
|
+
qualifier: str,
|
|
945
|
+
for_relation: bool,
|
|
946
|
+
) -> bool:
|
|
947
|
+
module_matches = _import_path_matches(rule, candidate, by_id)
|
|
948
|
+
for original, alias in rule.imported_symbols:
|
|
949
|
+
if (
|
|
950
|
+
_identifier_equal(rule.language, alias, reference_name)
|
|
951
|
+
and _identifier_equal(rule.language, original, candidate.name)
|
|
952
|
+
and module_matches
|
|
953
|
+
):
|
|
954
|
+
return True
|
|
955
|
+
|
|
956
|
+
normalized_qualifier = _scope_key(rule.language, qualifier)
|
|
957
|
+
normalized_alias = _scope_key(rule.language, rule.module_alias)
|
|
958
|
+
if normalized_alias and normalized_qualifier:
|
|
959
|
+
qualifier_parts = normalized_qualifier.split(".")
|
|
960
|
+
alias_parts = normalized_alias.split(".")
|
|
961
|
+
if qualifier_parts[: len(alias_parts)] == alias_parts and module_matches:
|
|
962
|
+
remainder = ".".join(qualifier_parts[len(alias_parts):])
|
|
963
|
+
return not remainder or _candidate_parent_scope(candidate, by_id).endswith(remainder)
|
|
964
|
+
|
|
965
|
+
if not rule.wildcard or not module_matches:
|
|
966
|
+
return False
|
|
967
|
+
if rule.language is Language.DELPHI:
|
|
968
|
+
return candidate.role != "implementation"
|
|
969
|
+
if rule.language is not Language.CSHARP:
|
|
970
|
+
return True
|
|
971
|
+
|
|
972
|
+
candidate_parent = _candidate_parent_scope(candidate, by_id)
|
|
973
|
+
if normalized_qualifier:
|
|
974
|
+
expected_parent = ".".join(
|
|
975
|
+
part for part in (rule.import_path, normalized_qualifier) if part
|
|
976
|
+
)
|
|
977
|
+
return candidate_parent == expected_parent
|
|
978
|
+
if rule.module_alias:
|
|
979
|
+
return candidate_parent == rule.import_path
|
|
980
|
+
return for_relation and candidate.kind in {
|
|
981
|
+
"class",
|
|
982
|
+
"delegate",
|
|
983
|
+
"enum",
|
|
984
|
+
"interface",
|
|
985
|
+
"record",
|
|
986
|
+
"struct",
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _import_path_matches(
|
|
991
|
+
rule: _ImportRule,
|
|
992
|
+
candidate: Symbol,
|
|
993
|
+
by_id: dict[str, Symbol],
|
|
994
|
+
) -> bool:
|
|
995
|
+
if rule.language is Language.CSHARP:
|
|
996
|
+
scope = _candidate_scope(candidate, by_id)
|
|
997
|
+
return scope == rule.import_path or scope.startswith(f"{rule.import_path}.")
|
|
998
|
+
return rule.import_path in _symbol_module_keys(candidate)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _candidate_scope(symbol: Symbol, by_id: dict[str, Symbol]) -> str:
|
|
1002
|
+
namespace = _csharp_namespace(symbol, by_id)
|
|
1003
|
+
parents: list[str] = []
|
|
1004
|
+
current = by_id.get(symbol.parent_id)
|
|
1005
|
+
while current is not None and current.kind not in {"module", "namespace"}:
|
|
1006
|
+
parents.append(current.name)
|
|
1007
|
+
current = by_id.get(current.parent_id)
|
|
1008
|
+
return ".".join(part for part in (namespace, *reversed(parents), symbol.name) if part)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _candidate_parent_scope(symbol: Symbol, by_id: dict[str, Symbol]) -> str:
|
|
1012
|
+
scope = _candidate_scope(symbol, by_id)
|
|
1013
|
+
return scope.rsplit(".", 1)[0] if "." in scope else ""
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _csharp_same_namespace_visible(
|
|
1017
|
+
source: Symbol,
|
|
1018
|
+
candidate: Symbol,
|
|
1019
|
+
by_id: dict[str, Symbol],
|
|
1020
|
+
*,
|
|
1021
|
+
qualifier: str,
|
|
1022
|
+
for_relation: bool,
|
|
1023
|
+
) -> bool:
|
|
1024
|
+
if _csharp_namespace(source, by_id) != _csharp_namespace(candidate, by_id):
|
|
1025
|
+
return False
|
|
1026
|
+
if for_relation:
|
|
1027
|
+
return candidate.kind in {"class", "delegate", "enum", "interface", "record", "struct"}
|
|
1028
|
+
if qualifier:
|
|
1029
|
+
return _candidate_parent_scope(candidate, by_id).endswith(_scope_key(Language.CSHARP, qualifier))
|
|
1030
|
+
return _candidate_parent_scope(source, by_id) == _candidate_parent_scope(candidate, by_id)
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _scope_key(language: Language, value: str) -> str:
|
|
1034
|
+
normalized = value.strip().strip(";<>\"'").replace("\\", "/")
|
|
1035
|
+
normalized = normalized.replace("::", ".").replace("/", ".")
|
|
1036
|
+
normalized = re.sub(r"^(?:crate|self)\.", "", normalized)
|
|
1037
|
+
return _name_key(language, normalized.strip("."))
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _identifier_equal(language: Language, left: str, right: str) -> bool:
|
|
1041
|
+
return _name_key(language, left) == _name_key(language, right)
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def _unique_logical_candidate(candidates: Iterable[Symbol]) -> Symbol | None:
|
|
1045
|
+
unique = {item.target_id: item for item in candidates}
|
|
1046
|
+
if len(unique) == 1:
|
|
1047
|
+
return next(iter(unique.values()))
|
|
1048
|
+
values = list(unique.values())
|
|
1049
|
+
if not values or any(item.language is not Language.DELPHI for item in values):
|
|
1050
|
+
return None
|
|
1051
|
+
logical_keys = {
|
|
1052
|
+
(
|
|
1053
|
+
item.project_id,
|
|
1054
|
+
item.file_id,
|
|
1055
|
+
item.qualified_name.casefold(),
|
|
1056
|
+
item.kind,
|
|
1057
|
+
" ".join(item.signature.casefold().split()),
|
|
1058
|
+
)
|
|
1059
|
+
for item in values
|
|
1060
|
+
}
|
|
1061
|
+
if len(logical_keys) != 1:
|
|
1062
|
+
return None
|
|
1063
|
+
role_order = {"declaration": 0, "definition": 1, "implementation": 2}
|
|
1064
|
+
return min(
|
|
1065
|
+
values,
|
|
1066
|
+
key=lambda item: (
|
|
1067
|
+
role_order.get(item.role, 3),
|
|
1068
|
+
item.range.start_line,
|
|
1069
|
+
item.range.start_character,
|
|
1070
|
+
),
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _name_key(language: Language, name: str) -> str:
|
|
1075
|
+
return name.casefold() if language is Language.DELPHI else name
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _normalize_library(value: str) -> str:
|
|
1079
|
+
name = PurePosixPath(value.replace("\\", "/")).name.casefold()
|
|
1080
|
+
name = re.sub(r"\.(?:dll|dylib|so)(?:\.\d+)*$", "", name)
|
|
1081
|
+
if name.startswith("lib") and len(name) > 3:
|
|
1082
|
+
name = name[3:]
|
|
1083
|
+
return name
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _is_abi_export_symbol(symbol: Symbol) -> bool:
|
|
1087
|
+
if symbol.language is Language.CPP:
|
|
1088
|
+
return symbol.kind == "function" and symbol.native_kind == "function_definition"
|
|
1089
|
+
if symbol.language is Language.RUST:
|
|
1090
|
+
return symbol.kind == "function" and symbol.native_kind == "function_item"
|
|
1091
|
+
if symbol.language is Language.DELPHI:
|
|
1092
|
+
return symbol.kind in {"constructor", "destructor", "function", "method", "procedure"} and (
|
|
1093
|
+
symbol.role == "implementation"
|
|
1094
|
+
)
|
|
1095
|
+
return False
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _preserve_incomplete_resolution(
|
|
1099
|
+
resolution: Resolution,
|
|
1100
|
+
fallback: Resolution,
|
|
1101
|
+
) -> Resolution:
|
|
1102
|
+
return fallback if resolution is Resolution.COMPLETE else resolution
|