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.
Files changed (42) hide show
  1. agent_codinglanguage_mapper/__init__.py +20 -0
  2. agent_codinglanguage_mapper/__main__.py +4 -0
  3. agent_codinglanguage_mapper/_version.py +2 -0
  4. agent_codinglanguage_mapper/adapters/__init__.py +4 -0
  5. agent_codinglanguage_mapper/adapters/delphi.py +822 -0
  6. agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
  7. agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
  8. agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
  9. agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
  10. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
  11. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
  12. agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
  13. agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
  14. agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
  15. agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
  16. agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
  17. agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
  18. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
  19. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
  20. agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
  21. agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
  22. agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
  23. agent_codinglanguage_mapper/cli.py +138 -0
  24. agent_codinglanguage_mapper/discovery.py +1328 -0
  25. agent_codinglanguage_mapper/indexer.py +1102 -0
  26. agent_codinglanguage_mapper/integration.py +186 -0
  27. agent_codinglanguage_mapper/lsp_server.py +413 -0
  28. agent_codinglanguage_mapper/lsp_service.py +447 -0
  29. agent_codinglanguage_mapper/mapper.py +596 -0
  30. agent_codinglanguage_mapper/models.py +101 -0
  31. agent_codinglanguage_mapper/protocol.py +152 -0
  32. agent_codinglanguage_mapper/rendering.py +153 -0
  33. agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
  34. agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
  35. agent_codinglanguage_mapper/worker.py +29 -0
  36. agent_codinglanguage_mapper/workspace.py +114 -0
  37. agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
  38. agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
  39. agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
  40. agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
  41. agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
  42. 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