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.
Files changed (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,969 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import stat
7
+ import tempfile
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from time import time_ns
12
+
13
+ from .preprocessor import Preprocessor
14
+ from .project_config import ProjectPathConfig
15
+ from .semantic import (
16
+ ArrayTypeRef,
17
+ ClassOfTypeRef,
18
+ ClassTypeRef,
19
+ EnumTypeRef,
20
+ FileTypeRef,
21
+ GenericInstanceTypeRef,
22
+ InterfaceTypeRef,
23
+ Modifier,
24
+ NamedTypeRef,
25
+ PointerTypeRef,
26
+ ProcTypeRef,
27
+ RecordTypeRef,
28
+ ReferenceTypeRef,
29
+ Scope,
30
+ ScopeKind,
31
+ SetTypeRef,
32
+ SourceRange,
33
+ SubrangeTypeRef,
34
+ Symbol,
35
+ SymbolIndex,
36
+ SymbolKind,
37
+ TypeParameterRef,
38
+ TypeRef,
39
+ UnknownTypeRef,
40
+ Visibility,
41
+ )
42
+ from .semantic_builder import SemanticModel, SemanticProblem
43
+ from .source_reader import read_source_text
44
+
45
+ ARTIFACT_SCHEMA_VERSION = 1
46
+ OUTLINE_PRODUCER_VERSION = "outline-model-v3"
47
+ DEFAULT_ARTIFACT_CACHE_BYTES = 2 * 1024**3
48
+
49
+ _MAX_ARTIFACT_BYTES = 256 * 1024**2
50
+ _MAX_SYMBOLS_PER_ARTIFACT = 1_000_000
51
+ _STALE_SHARD_GRACE_NS = 5 * 60 * 1_000_000_000
52
+ _KEY_PREFIX = b"python-delphi-lsp-outline-artifact-v1\0"
53
+
54
+
55
+ class ArtifactFormatError(ValueError):
56
+ """Raised internally when an untrusted artifact has an invalid shape."""
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class ArtifactDependency:
61
+ relative_path: str
62
+ content_hash: str
63
+
64
+ def to_mapping(self) -> dict[str, str]:
65
+ return {
66
+ "relative_path": self.relative_path,
67
+ "content_hash": self.content_hash,
68
+ }
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class ArtifactKey:
73
+ cache_key: str
74
+ relative_path: str
75
+ content_hash: str
76
+ effective_defines: tuple[str, ...]
77
+ effective_defines_hash: str
78
+ includes: tuple[ArtifactDependency, ...]
79
+ producer_version: str
80
+ schema_version: int = ARTIFACT_SCHEMA_VERSION
81
+
82
+ def to_mapping(self) -> dict[str, object]:
83
+ return {
84
+ "relative_path": self.relative_path,
85
+ "content_hash": self.content_hash,
86
+ "effective_defines": list(self.effective_defines),
87
+ "effective_defines_hash": self.effective_defines_hash,
88
+ "includes": [item.to_mapping() for item in self.includes],
89
+ "producer_version": self.producer_version,
90
+ "schema_version": self.schema_version,
91
+ }
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class CachedOutlineArtifact:
96
+ model: SemanticModel
97
+ lines_processed: int
98
+ symbols_discovered: int
99
+
100
+
101
+ def artifact_cache_disabled() -> bool:
102
+ return os.environ.get("DELPHI_LSP_NO_ARTIFACT_CACHE") == "1"
103
+
104
+
105
+ def artifact_key(
106
+ workspace_root: str | Path,
107
+ source_path: str | Path,
108
+ text: str,
109
+ defines: tuple[str, ...],
110
+ *,
111
+ include_paths: tuple[str, ...] = (),
112
+ project_config: ProjectPathConfig | None = None,
113
+ producer_version: str = OUTLINE_PRODUCER_VERSION,
114
+ ) -> ArtifactKey:
115
+ root = Path(workspace_root).expanduser().resolve()
116
+ source = Path(source_path).expanduser().resolve()
117
+ dependencies: dict[str, str] = {}
118
+ preprocessor = Preprocessor(
119
+ defines=defines,
120
+ include_paths=include_paths,
121
+ include_loader=_recording_include_loader(
122
+ root,
123
+ include_paths,
124
+ project_config,
125
+ dependencies,
126
+ ),
127
+ track_source_map=False,
128
+ collect_comments=False,
129
+ )
130
+ effective_defines = tuple(sorted(preprocessor.defines))
131
+ if "{$" in text or "(*$" in text:
132
+ preprocessor.process(text, str(source))
133
+
134
+ define_data = _canonical_json(list(effective_defines))
135
+ define_hash = hashlib.sha256(define_data).hexdigest()
136
+ dependency_items = tuple(
137
+ ArtifactDependency(relative_path=relative_path, content_hash=content_hash)
138
+ for relative_path, content_hash in sorted(dependencies.items())
139
+ )
140
+ relative_path = _relative_path(root, source)
141
+ content_hash = _text_hash(text)
142
+ material = {
143
+ "relative_path": relative_path,
144
+ "content_hash": content_hash,
145
+ "effective_defines_hash": define_hash,
146
+ "includes": [item.to_mapping() for item in dependency_items],
147
+ "producer_version": producer_version,
148
+ "schema_version": ARTIFACT_SCHEMA_VERSION,
149
+ }
150
+ digest = hashlib.sha256()
151
+ digest.update(_KEY_PREFIX)
152
+ digest.update(_canonical_json(material))
153
+ return ArtifactKey(
154
+ cache_key=digest.hexdigest(),
155
+ relative_path=relative_path,
156
+ content_hash=content_hash,
157
+ effective_defines=effective_defines,
158
+ effective_defines_hash=define_hash,
159
+ includes=dependency_items,
160
+ producer_version=producer_version,
161
+ )
162
+
163
+
164
+ def _recording_include_loader(
165
+ root: Path,
166
+ include_paths: tuple[str, ...],
167
+ project_config: ProjectPathConfig | None,
168
+ dependencies: dict[str, str],
169
+ ):
170
+ search_paths = tuple(Path(path).expanduser().resolve() for path in include_paths)
171
+
172
+ def load(parent_file: str, include_name: str) -> tuple[str, str] | None:
173
+ include_path = Path(include_name.replace("\\", "/"))
174
+ parent = Path(parent_file).expanduser().resolve().parent
175
+ for base in (parent, *search_paths):
176
+ candidate = (base / include_path).resolve()
177
+ if (
178
+ project_config is not None
179
+ and project_config.excludes_workspace_path(candidate)
180
+ ):
181
+ continue
182
+ try:
183
+ if not candidate.is_file():
184
+ continue
185
+ content = read_source_text(candidate)
186
+ except (OSError, UnicodeError):
187
+ continue
188
+ dependencies[_relative_path(root, candidate)] = _text_hash(content)
189
+ return content, str(candidate)
190
+ return None
191
+
192
+ return load
193
+
194
+
195
+ class ArtifactStore:
196
+ """Sharded, bounded JSON storage for untrusted outline artifacts."""
197
+
198
+ def __init__(
199
+ self,
200
+ root: str | Path,
201
+ *,
202
+ max_bytes: int = DEFAULT_ARTIFACT_CACHE_BYTES,
203
+ ) -> None:
204
+ if isinstance(max_bytes, bool) or not isinstance(max_bytes, int) or max_bytes < 0:
205
+ raise ValueError("artifact cache byte budget must be non-negative")
206
+ self.root = Path(root)
207
+ self.max_bytes = max_bytes
208
+
209
+ def load(self, cache_key: str) -> Mapping[str, object] | None:
210
+ path = self.path_for_key(cache_key)
211
+ if self._has_unsafe_directory(path.parent):
212
+ return None
213
+ try:
214
+ descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
215
+ try:
216
+ info = os.fstat(descriptor)
217
+ if (
218
+ not stat.S_ISREG(info.st_mode)
219
+ or info.st_size > _MAX_ARTIFACT_BYTES
220
+ or (
221
+ os.name != "nt"
222
+ and (info.st_uid != os.getuid() or info.st_mode & 0o077)
223
+ )
224
+ ):
225
+ return None
226
+ with os.fdopen(descriptor, "rb", closefd=False) as stream:
227
+ raw = stream.read(_MAX_ARTIFACT_BYTES + 1)
228
+ if len(raw) > _MAX_ARTIFACT_BYTES:
229
+ return None
230
+ finally:
231
+ os.close(descriptor)
232
+ payload = json.loads(raw.decode("utf-8"))
233
+ except (OSError, UnicodeError, ValueError, RecursionError):
234
+ return None
235
+ if (
236
+ not isinstance(payload, dict)
237
+ or payload.get("schema_version") != ARTIFACT_SCHEMA_VERSION
238
+ or payload.get("cache_key") != cache_key
239
+ ):
240
+ return None
241
+ return payload
242
+
243
+ def store(self, cache_key: str, payload: Mapping[str, object]) -> None:
244
+ path = self.path_for_key(cache_key)
245
+ self._ensure_directory(path.parent)
246
+ record = {
247
+ **dict(payload),
248
+ "schema_version": ARTIFACT_SCHEMA_VERSION,
249
+ "cache_key": cache_key,
250
+ }
251
+ data = _canonical_json(record)
252
+ if len(data) > _MAX_ARTIFACT_BYTES:
253
+ return
254
+ descriptor, temporary = tempfile.mkstemp(prefix=".artifact-", dir=path.parent)
255
+ try:
256
+ if os.name != "nt":
257
+ os.fchmod(descriptor, 0o600)
258
+ with os.fdopen(descriptor, "wb") as stream:
259
+ stream.write(data)
260
+ stream.flush()
261
+ os.fsync(stream.fileno())
262
+ os.replace(temporary, path)
263
+ if os.name != "nt":
264
+ os.chmod(path, 0o600)
265
+ finally:
266
+ try:
267
+ os.unlink(temporary)
268
+ except FileNotFoundError:
269
+ pass
270
+
271
+ def prune(self, live_keys: set[str]) -> None:
272
+ if not self.root.exists() or self._has_unsafe_directory(self.root):
273
+ return
274
+ retained: list[tuple[int, str, Path, int]] = []
275
+ observed_at = time_ns()
276
+ bucket_paths: list[Path] = []
277
+ try:
278
+ buckets = tuple(self.root.iterdir())
279
+ except OSError:
280
+ return
281
+ for bucket in buckets:
282
+ if bucket.is_symlink() or not bucket.is_dir():
283
+ continue
284
+ bucket_paths.append(bucket)
285
+ try:
286
+ shards = tuple(bucket.iterdir())
287
+ except OSError:
288
+ continue
289
+ for shard in shards:
290
+ if shard.is_symlink() or not shard.is_file() or shard.suffix != ".json":
291
+ continue
292
+ cache_key = shard.stem
293
+ try:
294
+ info = shard.stat(follow_symlinks=False)
295
+ except OSError:
296
+ continue
297
+ if (
298
+ cache_key not in live_keys
299
+ and observed_at - info.st_mtime_ns >= _STALE_SHARD_GRACE_NS
300
+ ):
301
+ try:
302
+ shard.unlink()
303
+ except OSError:
304
+ pass
305
+ continue
306
+ retained.append((info.st_mtime_ns, str(shard), shard, info.st_size))
307
+
308
+ retained_bytes = sum(item[3] for item in retained)
309
+ for _mtime, _name, shard, size in sorted(retained):
310
+ if retained_bytes <= self.max_bytes:
311
+ break
312
+ try:
313
+ shard.unlink()
314
+ except OSError:
315
+ continue
316
+ retained_bytes -= size
317
+ for bucket in bucket_paths:
318
+ try:
319
+ bucket.rmdir()
320
+ except OSError:
321
+ pass
322
+
323
+ def path_for_key(self, cache_key: str) -> Path:
324
+ _validate_cache_key(cache_key)
325
+ return self.root / cache_key[:2] / f"{cache_key}.json"
326
+
327
+ def _has_unsafe_directory(self, directory: Path) -> bool:
328
+ return any(path.is_symlink() for path in (self.root.parent, self.root, directory))
329
+
330
+ def _ensure_directory(self, directory: Path) -> None:
331
+ if self._has_unsafe_directory(directory):
332
+ raise OSError("artifact cache path is unsafe")
333
+ self.root.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
334
+ for path in (self.root, directory):
335
+ if path.exists() and path.is_symlink():
336
+ raise OSError("artifact cache path is unsafe")
337
+ path.mkdir(mode=0o700, parents=True, exist_ok=True)
338
+ if os.name != "nt":
339
+ os.chmod(path, 0o700)
340
+
341
+
342
+ def load_outline_artifact(
343
+ store: ArtifactStore,
344
+ key: ArtifactKey,
345
+ source_path: str,
346
+ ) -> CachedOutlineArtifact | None:
347
+ payload = store.load(key.cache_key)
348
+ if payload is None:
349
+ return None
350
+ try:
351
+ if (
352
+ payload.get("producer_version") != key.producer_version
353
+ or payload.get("key") != key.to_mapping()
354
+ or payload.get("source_path") != source_path
355
+ ):
356
+ return None
357
+ lines_processed = _non_negative_int(payload.get("lines_processed"))
358
+ symbols_discovered = _non_negative_int(payload.get("symbols_discovered"))
359
+ model = semantic_model_from_mapping(_mapping(payload.get("model")))
360
+ except (ArtifactFormatError, KeyError, TypeError, ValueError, RecursionError):
361
+ return None
362
+ return CachedOutlineArtifact(model, lines_processed, symbols_discovered)
363
+
364
+
365
+ def store_outline_artifact(
366
+ store: ArtifactStore,
367
+ key: ArtifactKey,
368
+ source_path: str,
369
+ model: SemanticModel,
370
+ *,
371
+ lines_processed: int,
372
+ symbols_discovered: int,
373
+ ) -> None:
374
+ store.store(
375
+ key.cache_key,
376
+ {
377
+ "producer_version": key.producer_version,
378
+ "key": key.to_mapping(),
379
+ "source_path": source_path,
380
+ "lines_processed": lines_processed,
381
+ "symbols_discovered": symbols_discovered,
382
+ "model": semantic_model_to_mapping(model),
383
+ },
384
+ )
385
+
386
+
387
+ def semantic_model_to_mapping(model: SemanticModel) -> dict[str, object]:
388
+ if model.references:
389
+ raise ValueError("outline artifacts do not support semantic references")
390
+ payload: dict[str, object] = {"unit_scope": _scope_to_mapping(model.unit_scope)}
391
+ if model.problems:
392
+ payload["problems"] = [
393
+ {
394
+ "message": problem.message,
395
+ "range": _range_to_mapping(problem.range),
396
+ }
397
+ for problem in model.problems
398
+ ]
399
+ return payload
400
+
401
+
402
+ def semantic_model_from_mapping(payload: Mapping[str, object]) -> SemanticModel:
403
+ count = [0]
404
+ unit_scope = _scope_from_mapping(
405
+ _mapping(payload.get("unit_scope")),
406
+ parent=None,
407
+ inherited_owner=None,
408
+ symbol_count=count,
409
+ )
410
+ problem_payloads = _optional_list(payload.get("problems"))
411
+ if _optional_list(payload.get("references")):
412
+ raise ArtifactFormatError("outline artifact references must be empty")
413
+ problems = [
414
+ SemanticProblem(
415
+ message=_string(_mapping(item).get("message")),
416
+ range=_range_from_value(_mapping(item).get("range")),
417
+ )
418
+ for item in problem_payloads
419
+ ]
420
+ index = SymbolIndex()
421
+ index.register_unit(unit_scope.name, unit_scope)
422
+ return SemanticModel(unit_scope=unit_scope, index=index, problems=problems)
423
+
424
+
425
+ def _scope_to_mapping(scope: Scope) -> dict[str, object]:
426
+ symbols = [symbol for items in scope.symbols.values() for symbol in items]
427
+ owner_index = next(
428
+ (index for index, symbol in enumerate(symbols) if symbol is scope.owner),
429
+ None,
430
+ )
431
+ payload: dict[str, object] = {
432
+ "kind": scope.kind.value,
433
+ "name": scope.name,
434
+ "symbols": [_symbol_to_mapping(symbol) for symbol in symbols],
435
+ }
436
+ if owner_index is not None:
437
+ payload["owner_index"] = owner_index
438
+ return payload
439
+
440
+
441
+ def _scope_from_mapping(
442
+ payload: Mapping[str, object],
443
+ *,
444
+ parent: Scope | None,
445
+ inherited_owner: Symbol | None,
446
+ symbol_count: list[int],
447
+ ) -> Scope:
448
+ scope = Scope(
449
+ kind=ScopeKind(_string(payload.get("kind"))),
450
+ name=_string(payload.get("name")),
451
+ parent=parent,
452
+ owner=inherited_owner,
453
+ )
454
+ symbol_payloads = [_mapping(item) for item in _list(payload.get("symbols"))]
455
+ symbol_count[0] += len(symbol_payloads)
456
+ if symbol_count[0] > _MAX_SYMBOLS_PER_ARTIFACT:
457
+ raise ArtifactFormatError("outline artifact has too many symbols")
458
+ symbols: list[Symbol] = []
459
+ for item in symbol_payloads:
460
+ symbol = Symbol(
461
+ name=_string(item.get("name")),
462
+ kind=SymbolKind(_string(item.get("kind"))),
463
+ decl_range=(decl_range := _range_from_value(item.get("range"))),
464
+ name_range=(
465
+ decl_range
466
+ if item.get("name_range") is None
467
+ else _range_from_value(item.get("name_range"))
468
+ ),
469
+ scope=scope,
470
+ visibility=Visibility(
471
+ _string(item.get("visibility", Visibility.UNKNOWN.value))
472
+ ),
473
+ type_ref=(
474
+ UnknownTypeRef()
475
+ if item.get("type_ref") is None
476
+ else _type_ref_from_mapping(_mapping(item.get("type_ref")))
477
+ ),
478
+ modifiers={
479
+ Modifier(_string(value))
480
+ for value in _optional_list(item.get("modifiers"))
481
+ },
482
+ attributes=_string_mapping(item.get("attributes")),
483
+ doc=_string(item.get("doc", "")),
484
+ base_types=tuple(
485
+ _type_ref_from_mapping(_mapping(value))
486
+ for value in _optional_list(item.get("base_types"))
487
+ ),
488
+ )
489
+ scope.define(symbol)
490
+ symbols.append(symbol)
491
+
492
+ owner_index = payload.get("owner_index")
493
+ if owner_index is not None:
494
+ owner_ordinal = _integer(owner_index)
495
+ if owner_ordinal < 0 or owner_ordinal >= len(symbols):
496
+ raise ArtifactFormatError("outline scope owner is invalid")
497
+ scope.owner = symbols[owner_ordinal]
498
+
499
+ for symbol, item in zip(symbols, symbol_payloads, strict=True):
500
+ member_scope = item.get("member_scope")
501
+ if member_scope is not None:
502
+ symbol.member_scope = _scope_from_mapping(
503
+ _mapping(member_scope),
504
+ parent=scope,
505
+ inherited_owner=symbol,
506
+ symbol_count=symbol_count,
507
+ )
508
+ type_ref = item.get("type_ref")
509
+ if type_ref is not None:
510
+ symbol.type_ref = _type_ref_from_mapping(
511
+ _mapping(type_ref),
512
+ structured_scope=symbol.member_scope,
513
+ )
514
+ symbol.overloads = [
515
+ _standalone_symbol_from_mapping(
516
+ _mapping(overload),
517
+ scope,
518
+ symbol_count,
519
+ )
520
+ for overload in _optional_list(item.get("overloads"))
521
+ ]
522
+ return scope
523
+
524
+
525
+ def _standalone_symbol_from_mapping(
526
+ payload: Mapping[str, object],
527
+ scope: Scope,
528
+ symbol_count: list[int],
529
+ ) -> Symbol:
530
+ symbol_count[0] += 1
531
+ if symbol_count[0] > _MAX_SYMBOLS_PER_ARTIFACT:
532
+ raise ArtifactFormatError("outline artifact has too many symbols")
533
+ symbol = Symbol(
534
+ name=_string(payload.get("name")),
535
+ kind=SymbolKind(_string(payload.get("kind"))),
536
+ decl_range=(decl_range := _range_from_value(payload.get("range"))),
537
+ name_range=(
538
+ decl_range
539
+ if payload.get("name_range") is None
540
+ else _range_from_value(payload.get("name_range"))
541
+ ),
542
+ scope=scope,
543
+ visibility=Visibility(
544
+ _string(payload.get("visibility", Visibility.UNKNOWN.value))
545
+ ),
546
+ type_ref=(
547
+ UnknownTypeRef()
548
+ if payload.get("type_ref") is None
549
+ else _type_ref_from_mapping(_mapping(payload.get("type_ref")))
550
+ ),
551
+ modifiers={
552
+ Modifier(_string(value))
553
+ for value in _optional_list(payload.get("modifiers"))
554
+ },
555
+ attributes=_string_mapping(payload.get("attributes")),
556
+ doc=_string(payload.get("doc", "")),
557
+ base_types=tuple(
558
+ _type_ref_from_mapping(_mapping(value))
559
+ for value in _optional_list(payload.get("base_types"))
560
+ ),
561
+ )
562
+ member_scope = payload.get("member_scope")
563
+ if member_scope is not None:
564
+ symbol.member_scope = _scope_from_mapping(
565
+ _mapping(member_scope),
566
+ parent=scope,
567
+ inherited_owner=symbol,
568
+ symbol_count=symbol_count,
569
+ )
570
+ type_ref = payload.get("type_ref")
571
+ if type_ref is not None:
572
+ symbol.type_ref = _type_ref_from_mapping(
573
+ _mapping(type_ref),
574
+ structured_scope=symbol.member_scope,
575
+ )
576
+ symbol.overloads = [
577
+ _standalone_symbol_from_mapping(_mapping(value), scope, symbol_count)
578
+ for value in _optional_list(payload.get("overloads"))
579
+ ]
580
+ return symbol
581
+
582
+
583
+ def _symbol_to_mapping(symbol: Symbol) -> dict[str, object]:
584
+ payload: dict[str, object] = {
585
+ "name": symbol.name,
586
+ "kind": symbol.kind.value,
587
+ "range": _range_to_mapping(symbol.decl_range),
588
+ }
589
+ if symbol.name_range != symbol.decl_range:
590
+ payload["name_range"] = _range_to_mapping(symbol.name_range)
591
+ if symbol.visibility is not Visibility.UNKNOWN:
592
+ payload["visibility"] = symbol.visibility.value
593
+ if not isinstance(symbol.type_ref, UnknownTypeRef) or symbol.type_ref.reason:
594
+ payload["type_ref"] = _type_ref_to_mapping(symbol.type_ref)
595
+ if symbol.modifiers:
596
+ payload["modifiers"] = sorted(modifier.value for modifier in symbol.modifiers)
597
+ if symbol.attributes:
598
+ payload["attributes"] = dict(sorted(symbol.attributes.items()))
599
+ if symbol.doc:
600
+ payload["doc"] = symbol.doc
601
+ if symbol.overloads:
602
+ payload["overloads"] = [_symbol_to_mapping(item) for item in symbol.overloads]
603
+ if symbol.member_scope is not None:
604
+ payload["member_scope"] = _scope_to_mapping(symbol.member_scope)
605
+ if symbol.base_types:
606
+ payload["base_types"] = [
607
+ _type_ref_to_mapping(item) for item in symbol.base_types
608
+ ]
609
+ return payload
610
+
611
+
612
+ def _range_to_mapping(source_range: SourceRange) -> list[object]:
613
+ return [
614
+ source_range.file_name,
615
+ source_range.start_line,
616
+ source_range.start_col,
617
+ source_range.end_line,
618
+ source_range.end_col,
619
+ ]
620
+
621
+
622
+ def _range_from_value(value: object) -> SourceRange:
623
+ payload = _list(value)
624
+ if len(payload) != 5:
625
+ raise ArtifactFormatError("outline source range must have five values")
626
+ return SourceRange(
627
+ file_name=_string(payload[0]),
628
+ start_line=_positive_int(payload[1]),
629
+ start_col=_positive_int(payload[2]),
630
+ end_line=_positive_int(payload[3]),
631
+ end_col=_positive_int(payload[4]),
632
+ )
633
+
634
+
635
+ def _type_ref_to_mapping(type_ref: TypeRef) -> dict[str, object]:
636
+ if isinstance(type_ref, UnknownTypeRef):
637
+ return {"kind": "unknown", "reason": type_ref.reason}
638
+ if isinstance(type_ref, NamedTypeRef):
639
+ return {"kind": "named", "name": type_ref.name, "unit_name": type_ref.unit_name}
640
+ if isinstance(type_ref, PointerTypeRef):
641
+ return {"kind": "pointer", "target": _type_ref_to_mapping(type_ref.target)}
642
+ if isinstance(type_ref, ClassOfTypeRef):
643
+ return {"kind": "class_of", "target": _type_ref_to_mapping(type_ref.target)}
644
+ if isinstance(type_ref, ReferenceTypeRef):
645
+ return {"kind": "reference", "target": _type_ref_to_mapping(type_ref.target)}
646
+ if isinstance(type_ref, FileTypeRef):
647
+ return {
648
+ "kind": "file",
649
+ "element_type": (
650
+ None
651
+ if type_ref.element_type is None
652
+ else _type_ref_to_mapping(type_ref.element_type)
653
+ ),
654
+ }
655
+ if isinstance(type_ref, GenericInstanceTypeRef):
656
+ return {
657
+ "kind": "generic",
658
+ "base": _type_ref_to_mapping(type_ref.base),
659
+ "args": [_type_ref_to_mapping(item) for item in type_ref.args],
660
+ }
661
+ if isinstance(type_ref, ArrayTypeRef):
662
+ return {
663
+ "kind": "array",
664
+ "element_type": _type_ref_to_mapping(type_ref.element_type),
665
+ "dimensions": [_type_ref_to_mapping(item) for item in type_ref.dimensions],
666
+ "is_dynamic": type_ref.is_dynamic,
667
+ }
668
+ if isinstance(type_ref, SetTypeRef):
669
+ return {"kind": "set", "element_type": _type_ref_to_mapping(type_ref.element_type)}
670
+ if isinstance(type_ref, SubrangeTypeRef):
671
+ return {"kind": "subrange", "lower": type_ref.lower, "upper": type_ref.upper}
672
+ if isinstance(type_ref, EnumTypeRef):
673
+ return {"kind": "enum", "name": type_ref.name, "members": list(type_ref.members)}
674
+ if isinstance(type_ref, RecordTypeRef):
675
+ return {
676
+ "kind": "record",
677
+ "name": type_ref.name,
678
+ "packed": type_ref.packed,
679
+ "has_scope": type_ref.scope is not None,
680
+ }
681
+ if isinstance(type_ref, ClassTypeRef):
682
+ return {
683
+ "kind": "class",
684
+ "name": type_ref.name,
685
+ "bases": [_type_ref_to_mapping(item) for item in type_ref.bases],
686
+ "is_abstract": type_ref.is_abstract,
687
+ "has_scope": type_ref.scope is not None,
688
+ }
689
+ if isinstance(type_ref, InterfaceTypeRef):
690
+ return {
691
+ "kind": "interface",
692
+ "name": type_ref.name,
693
+ "bases": [_type_ref_to_mapping(item) for item in type_ref.bases],
694
+ "has_scope": type_ref.scope is not None,
695
+ }
696
+ if isinstance(type_ref, TypeParameterRef):
697
+ return {"kind": "type_parameter", "name": type_ref.name}
698
+ if isinstance(type_ref, ProcTypeRef):
699
+ return {
700
+ "kind": "proc",
701
+ "name": type_ref.name,
702
+ "params": [_type_ref_to_mapping(item) for item in type_ref.params],
703
+ "return_type": (
704
+ None
705
+ if type_ref.return_type is None
706
+ else _type_ref_to_mapping(type_ref.return_type)
707
+ ),
708
+ }
709
+ raise ValueError(f"unsupported outline type reference: {type(type_ref).__name__}")
710
+
711
+
712
+ def _type_ref_from_mapping(
713
+ payload: Mapping[str, object],
714
+ *,
715
+ structured_scope: Scope | None = None,
716
+ ) -> TypeRef:
717
+ kind = _string(payload.get("kind"))
718
+ if kind == "unknown":
719
+ return UnknownTypeRef(_string(payload.get("reason")))
720
+ if kind == "named":
721
+ unit_name = payload.get("unit_name")
722
+ return NamedTypeRef(
723
+ _string(payload.get("name")),
724
+ None if unit_name is None else _string(unit_name),
725
+ )
726
+ if kind == "pointer":
727
+ return PointerTypeRef(
728
+ _type_ref_from_mapping(
729
+ _mapping(payload.get("target")),
730
+ structured_scope=structured_scope,
731
+ )
732
+ )
733
+ if kind == "class_of":
734
+ return ClassOfTypeRef(
735
+ _type_ref_from_mapping(
736
+ _mapping(payload.get("target")),
737
+ structured_scope=structured_scope,
738
+ )
739
+ )
740
+ if kind == "reference":
741
+ return ReferenceTypeRef(
742
+ _type_ref_from_mapping(
743
+ _mapping(payload.get("target")),
744
+ structured_scope=structured_scope,
745
+ )
746
+ )
747
+ if kind == "file":
748
+ element = payload.get("element_type")
749
+ return FileTypeRef(
750
+ None
751
+ if element is None
752
+ else _type_ref_from_mapping(
753
+ _mapping(element),
754
+ structured_scope=structured_scope,
755
+ )
756
+ )
757
+ if kind == "generic":
758
+ base = _type_ref_from_mapping(
759
+ _mapping(payload.get("base")),
760
+ structured_scope=structured_scope,
761
+ )
762
+ if not isinstance(base, NamedTypeRef):
763
+ raise ArtifactFormatError("generic base must be a named type")
764
+ return GenericInstanceTypeRef(
765
+ base,
766
+ tuple(
767
+ _type_ref_from_mapping(
768
+ _mapping(item),
769
+ structured_scope=structured_scope,
770
+ )
771
+ for item in _list(payload.get("args"))
772
+ ),
773
+ )
774
+ if kind == "array":
775
+ return ArrayTypeRef(
776
+ element_type=_type_ref_from_mapping(
777
+ _mapping(payload.get("element_type")),
778
+ structured_scope=structured_scope,
779
+ ),
780
+ dimensions=tuple(
781
+ _type_ref_from_mapping(
782
+ _mapping(item),
783
+ structured_scope=structured_scope,
784
+ )
785
+ for item in _list(payload.get("dimensions"))
786
+ ),
787
+ is_dynamic=_boolean(payload.get("is_dynamic")),
788
+ )
789
+ if kind == "set":
790
+ return SetTypeRef(
791
+ _type_ref_from_mapping(
792
+ _mapping(payload.get("element_type")),
793
+ structured_scope=structured_scope,
794
+ )
795
+ )
796
+ if kind == "subrange":
797
+ return SubrangeTypeRef(_string(payload.get("lower")), _string(payload.get("upper")))
798
+ if kind == "enum":
799
+ return EnumTypeRef(
800
+ _string(payload.get("name")),
801
+ tuple(_string(item) for item in _list(payload.get("members"))),
802
+ )
803
+ if kind == "record":
804
+ return RecordTypeRef(
805
+ _string(payload.get("name")),
806
+ scope=(
807
+ structured_scope
808
+ if _boolean(payload.get("has_scope", False))
809
+ else None
810
+ ),
811
+ packed=_boolean(payload.get("packed")),
812
+ )
813
+ if kind == "class":
814
+ return ClassTypeRef(
815
+ _string(payload.get("name")),
816
+ scope=(
817
+ structured_scope
818
+ if _boolean(payload.get("has_scope", False))
819
+ else None
820
+ ),
821
+ bases=tuple(
822
+ _type_ref_from_mapping(
823
+ _mapping(item),
824
+ structured_scope=structured_scope,
825
+ )
826
+ for item in _list(payload.get("bases"))
827
+ ),
828
+ is_abstract=_boolean(payload.get("is_abstract")),
829
+ )
830
+ if kind == "interface":
831
+ return InterfaceTypeRef(
832
+ _string(payload.get("name")),
833
+ scope=(
834
+ structured_scope
835
+ if _boolean(payload.get("has_scope", False))
836
+ else None
837
+ ),
838
+ bases=tuple(
839
+ _type_ref_from_mapping(
840
+ _mapping(item),
841
+ structured_scope=structured_scope,
842
+ )
843
+ for item in _list(payload.get("bases"))
844
+ ),
845
+ )
846
+ if kind == "type_parameter":
847
+ return TypeParameterRef(_string(payload.get("name")))
848
+ if kind == "proc":
849
+ result = payload.get("return_type")
850
+ return ProcTypeRef(
851
+ _string(payload.get("name")),
852
+ params=tuple(
853
+ _type_ref_from_mapping(
854
+ _mapping(item),
855
+ structured_scope=structured_scope,
856
+ )
857
+ for item in _list(payload.get("params"))
858
+ ),
859
+ return_type=(
860
+ None
861
+ if result is None
862
+ else _type_ref_from_mapping(
863
+ _mapping(result),
864
+ structured_scope=structured_scope,
865
+ )
866
+ ),
867
+ )
868
+ raise ArtifactFormatError("outline type reference kind is invalid")
869
+
870
+
871
+ def _relative_path(root: Path, path: Path) -> str:
872
+ try:
873
+ return path.relative_to(root).as_posix()
874
+ except ValueError:
875
+ return Path(os.path.relpath(path, root)).as_posix()
876
+
877
+
878
+ def _text_hash(text: str) -> str:
879
+ return hashlib.sha256(text.encode("utf-8", errors="surrogatepass")).hexdigest()
880
+
881
+
882
+ def _canonical_json(payload: object) -> bytes:
883
+ return json.dumps(
884
+ payload,
885
+ ensure_ascii=False,
886
+ sort_keys=True,
887
+ separators=(",", ":"),
888
+ allow_nan=False,
889
+ ).encode("utf-8", errors="surrogatepass")
890
+
891
+
892
+ def _validate_cache_key(cache_key: str) -> None:
893
+ if (
894
+ len(cache_key) != 64
895
+ or any(character not in "0123456789abcdef" for character in cache_key)
896
+ ):
897
+ raise ValueError("artifact cache key is malformed")
898
+
899
+
900
+ def _mapping(value: object) -> Mapping[str, object]:
901
+ if not isinstance(value, Mapping):
902
+ raise ArtifactFormatError("outline artifact value must be an object")
903
+ return value
904
+
905
+
906
+ def _list(value: object) -> list[object]:
907
+ if not isinstance(value, list):
908
+ raise ArtifactFormatError("outline artifact value must be an array")
909
+ return value
910
+
911
+
912
+ def _optional_list(value: object) -> list[object]:
913
+ return [] if value is None else _list(value)
914
+
915
+
916
+ def _string(value: object) -> str:
917
+ if not isinstance(value, str):
918
+ raise ArtifactFormatError("outline artifact value must be a string")
919
+ return value
920
+
921
+
922
+ def _string_mapping(value: object) -> dict[str, str]:
923
+ if value is None:
924
+ return {}
925
+ mapping = _mapping(value)
926
+ return {_string(key): _string(item) for key, item in mapping.items()}
927
+
928
+
929
+ def _integer(value: object) -> int:
930
+ if isinstance(value, bool) or not isinstance(value, int):
931
+ raise ArtifactFormatError("outline artifact value must be an integer")
932
+ return value
933
+
934
+
935
+ def _positive_int(value: object) -> int:
936
+ result = _integer(value)
937
+ if result < 1:
938
+ raise ArtifactFormatError("outline artifact position must be positive")
939
+ return result
940
+
941
+
942
+ def _non_negative_int(value: object) -> int:
943
+ result = _integer(value)
944
+ if result < 0:
945
+ raise ArtifactFormatError("outline artifact count must be non-negative")
946
+ return result
947
+
948
+
949
+ def _boolean(value: object) -> bool:
950
+ if not isinstance(value, bool):
951
+ raise ArtifactFormatError("outline artifact value must be a boolean")
952
+ return value
953
+
954
+
955
+ __all__ = [
956
+ "ARTIFACT_SCHEMA_VERSION",
957
+ "DEFAULT_ARTIFACT_CACHE_BYTES",
958
+ "OUTLINE_PRODUCER_VERSION",
959
+ "ArtifactDependency",
960
+ "ArtifactKey",
961
+ "ArtifactStore",
962
+ "CachedOutlineArtifact",
963
+ "artifact_cache_disabled",
964
+ "artifact_key",
965
+ "load_outline_artifact",
966
+ "semantic_model_from_mapping",
967
+ "semantic_model_to_mapping",
968
+ "store_outline_artifact",
969
+ ]