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,470 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from pathlib import Path
7
+
8
+ from .consts import AttributeName, SyntaxNodeType
9
+ from .nodes import SyntaxNode
10
+ from .parser import DelphiParser
11
+ from .parser_backend import ParserBackend, ParserMode, normalize_backend, normalize_mode
12
+ from .preprocessor import IncludeLoader, PreprocessedSource
13
+ from .progress import ProgressCallback, ProgressEvent
14
+ from .project_config import ProjectPathConfig
15
+ from .source_reader import read_source_text
16
+
17
+
18
+ class ProjectProblemType(str, Enum):
19
+ CANT_FIND_FILE = 'cant_find_file'
20
+ CANT_OPEN_FILE = 'cant_open_file'
21
+ CANT_PARSE_FILE = 'cant_parse_file'
22
+
23
+
24
+ @dataclass
25
+ class ProjectProblem:
26
+ problem_type: ProjectProblemType
27
+ file_name: str
28
+ description: str
29
+
30
+
31
+ @dataclass
32
+ class UnitErrorInfo:
33
+ line: int = 0
34
+ col: int = 0
35
+ error: str = ''
36
+
37
+
38
+ @dataclass
39
+ class UnitInfo:
40
+ name: str
41
+ path: str
42
+ syntax_tree: SyntaxNode | None
43
+ preprocessed: PreprocessedSource | None = None
44
+ has_error: bool = False
45
+ error_info: UnitErrorInfo = field(default_factory=UnitErrorInfo)
46
+
47
+
48
+ @dataclass
49
+ class IncludeFileInfo:
50
+ name: str
51
+ path: str
52
+
53
+
54
+ @dataclass
55
+ class ProjectIndexResult:
56
+ parsed_units: list[UnitInfo]
57
+ include_files: list[IncludeFileInfo]
58
+ problems: list[ProjectProblem]
59
+ not_found_units: list[str]
60
+
61
+
62
+ GetUnitSyntaxHook = Callable[[str], tuple[SyntaxNode | None, bool, bool]]
63
+ UnitParsedHook = Callable[[str, str, SyntaxNode, bool], bool]
64
+ SourceTransform = Callable[[str], str]
65
+
66
+
67
+ class ProjectIndexer:
68
+ def __init__(
69
+ self,
70
+ *,
71
+ search_paths: Iterable[str] = (),
72
+ include_paths: Iterable[str] = (),
73
+ defines: Iterable[str] = (),
74
+ include_loader: IncludeLoader | None = None,
75
+ on_get_unit_syntax: GetUnitSyntaxHook | None = None,
76
+ on_unit_parsed: UnitParsedHook | None = None,
77
+ source_transform: SourceTransform | None = None,
78
+ retain_syntax: bool = True,
79
+ on_progress: ProgressCallback | None = None,
80
+ backend: ParserBackend | str = ParserBackend.DELPHIAST,
81
+ mode: ParserMode | str = ParserMode.STRICT,
82
+ source_roots: Iterable[str | Path] = (),
83
+ project_config: ProjectPathConfig | None = None,
84
+ ) -> None:
85
+ self.search_paths = [Path(path) for path in search_paths]
86
+ self.include_paths = [Path(path) for path in include_paths]
87
+ self.defines = list(defines)
88
+ self.include_loader = include_loader
89
+ self.on_get_unit_syntax = on_get_unit_syntax
90
+ self.on_unit_parsed = on_unit_parsed
91
+ self.source_transform = source_transform
92
+ self.retain_syntax = retain_syntax
93
+ self.on_progress = on_progress
94
+ self.backend = normalize_backend(backend)
95
+ self.mode = normalize_mode(mode)
96
+ self.source_roots = tuple(
97
+ Path(path).expanduser().resolve() for path in source_roots
98
+ )
99
+ self.project_config = project_config
100
+
101
+ self._parsed_units: dict[str, UnitInfo] = {}
102
+ self._problems: list[ProjectProblem] = []
103
+ self._not_found_units: set[str] = set()
104
+ self._include_files: dict[str, IncludeFileInfo] = {}
105
+ self._project_folder: Path = Path('.')
106
+ self._aborting: bool = False
107
+ self._files_discovered = 0
108
+ self._files_completed = 0
109
+ self._lines_processed = 0
110
+
111
+ def index(self, file_name: str) -> ProjectIndexResult:
112
+ self._parsed_units = {}
113
+ self._problems = []
114
+ self._not_found_units = set()
115
+ self._include_files = {}
116
+ self._aborting = False
117
+ self._files_discovered = 0
118
+ self._files_completed = 0
119
+ self._lines_processed = 0
120
+
121
+ entry = Path(file_name).expanduser().resolve()
122
+ self._project_folder = entry.parent
123
+ self._emit_progress("discovery", str(entry), "project index started")
124
+
125
+ is_project = entry.suffix.casefold() in {'.dpr', '.dpk'}
126
+ self._parse_unit(entry.stem, entry, is_project=is_project)
127
+
128
+ parsed_units = sorted(
129
+ self._parsed_units.values(),
130
+ key=lambda item: (
131
+ item.name.casefold(),
132
+ item.name,
133
+ item.path.casefold(),
134
+ item.path,
135
+ ),
136
+ )
137
+ include_files = sorted(
138
+ self._include_files.values(),
139
+ key=lambda item: (
140
+ item.name.casefold(),
141
+ item.name,
142
+ item.path.casefold(),
143
+ item.path,
144
+ ),
145
+ )
146
+ problems = list(self._problems)
147
+ not_found = sorted(
148
+ self._not_found_units,
149
+ key=lambda value: (value.casefold(), value),
150
+ )
151
+
152
+ result = ProjectIndexResult(
153
+ parsed_units=parsed_units,
154
+ include_files=include_files,
155
+ problems=problems,
156
+ not_found_units=not_found,
157
+ )
158
+ self._emit_progress("complete", str(entry), "project index complete")
159
+ return result
160
+
161
+ def _parse_unit(self, unit_name: str, file_path: Path, *, is_project: bool) -> None:
162
+ if self._aborting:
163
+ return
164
+ if not self._source_is_allowed(file_path):
165
+ self._not_found_units.add(unit_name)
166
+ return
167
+
168
+ normalized_name = unit_name.casefold()
169
+ if normalized_name in self._parsed_units:
170
+ return
171
+ self._files_discovered += 1
172
+ self._emit_progress("inventory", str(file_path), "unit discovered")
173
+
174
+ hook_tree: SyntaxNode | None = None
175
+ do_parse_unit = True
176
+ if self.on_get_unit_syntax is not None:
177
+ hook_tree, do_parse_unit, do_abort = self.on_get_unit_syntax(str(file_path))
178
+ if do_abort:
179
+ self._aborting = True
180
+ return
181
+
182
+ syntax_tree: SyntaxNode | None = hook_tree
183
+ preprocessed: PreprocessedSource | None = None
184
+ from_parser = False
185
+ parser_error = UnitErrorInfo()
186
+
187
+ if syntax_tree is None and do_parse_unit:
188
+ source = self._read_file(file_path)
189
+ if source is None:
190
+ self._files_completed += 1
191
+ self._emit_progress("detail", str(file_path), "unit unavailable")
192
+ return
193
+ self._lines_processed += source.count("\n") + (
194
+ 0 if not source or source.endswith("\n") else 1
195
+ )
196
+ parser = DelphiParser(
197
+ include_paths=[str(path) for path in self.include_paths],
198
+ defines=self.defines,
199
+ include_loader=self._build_include_loader(),
200
+ backend=self.backend,
201
+ mode=self.mode,
202
+ )
203
+ try:
204
+ if self.source_transform is not None:
205
+ source = self.source_transform(source)
206
+ result = parser.parse(source, str(file_path), build_semantic=False)
207
+ syntax_tree = result.root
208
+ preprocessed = result.preprocessed
209
+ from_parser = True
210
+ if result.problems:
211
+ first = result.problems[0]
212
+ parser_error = UnitErrorInfo(
213
+ line=first.line,
214
+ col=first.column,
215
+ error=first.message,
216
+ )
217
+ self._problems.append(
218
+ ProjectProblem(
219
+ problem_type=ProjectProblemType.CANT_PARSE_FILE,
220
+ file_name=str(file_path),
221
+ description=(
222
+ f"Partial DelphiAST parse at {first.line}:{first.column}: "
223
+ f"{first.message}"
224
+ ),
225
+ )
226
+ )
227
+ except Exception as exc: # pragma: no cover - parser error branch
228
+ self._problems.append(
229
+ ProjectProblem(
230
+ problem_type=ProjectProblemType.CANT_PARSE_FILE,
231
+ file_name=str(file_path),
232
+ description=str(exc),
233
+ )
234
+ )
235
+ self._parsed_units[normalized_name] = UnitInfo(
236
+ name=unit_name,
237
+ path=str(file_path),
238
+ syntax_tree=None,
239
+ has_error=True,
240
+ error_info=UnitErrorInfo(error=str(exc)),
241
+ )
242
+ self._files_completed += 1
243
+ self._emit_progress("detail", str(file_path), "unit parse failed")
244
+ return
245
+
246
+ if syntax_tree is None:
247
+ self._files_completed += 1
248
+ self._emit_progress("detail", str(file_path), "unit skipped")
249
+ return
250
+
251
+ actual_name = syntax_tree.get_attribute(AttributeName.anName) or unit_name
252
+ normalized_name = actual_name.casefold()
253
+ unit_info = UnitInfo(
254
+ name=actual_name,
255
+ path=str(file_path),
256
+ syntax_tree=syntax_tree if self.retain_syntax else None,
257
+ preprocessed=preprocessed if self.retain_syntax else None,
258
+ has_error=bool(parser_error.error),
259
+ error_info=parser_error,
260
+ )
261
+ self._parsed_units[normalized_name] = unit_info
262
+ self._files_completed += 1
263
+ self._emit_progress("detail", str(file_path), "unit parsed")
264
+
265
+ if self.on_unit_parsed is not None:
266
+ do_abort = self.on_unit_parsed(actual_name, str(file_path), syntax_tree, from_parser)
267
+ if do_abort:
268
+ self._aborting = True
269
+ return
270
+
271
+ dependencies = self._collect_dependencies(
272
+ syntax_tree,
273
+ file_path,
274
+ is_project=is_project,
275
+ )
276
+ if not self.retain_syntax:
277
+ syntax_tree = None
278
+ preprocessed = None
279
+ hook_tree = None
280
+ if from_parser:
281
+ del result
282
+ del parser
283
+ source = ""
284
+
285
+ for dep_name, dep_path in dependencies:
286
+ if self._aborting:
287
+ return
288
+ resolved = self._resolve_unit_path(dep_name, dep_path, relative_to=file_path.parent)
289
+ if resolved is None:
290
+ self._not_found_units.add(dep_name)
291
+ self._problems.append(
292
+ ProjectProblem(
293
+ problem_type=ProjectProblemType.CANT_FIND_FILE,
294
+ file_name=dep_name,
295
+ description=f'Unit not found: {dep_name}',
296
+ )
297
+ )
298
+ continue
299
+ self._parse_unit(dep_name, resolved, is_project=False)
300
+
301
+ def _read_file(self, file_path: Path) -> str | None:
302
+ try:
303
+ return read_source_text(file_path)
304
+ except (OSError, UnicodeError) as exc:
305
+ self._problems.append(
306
+ ProjectProblem(
307
+ problem_type=ProjectProblemType.CANT_OPEN_FILE,
308
+ file_name=str(file_path),
309
+ description=str(exc),
310
+ )
311
+ )
312
+ return None
313
+
314
+ def _collect_dependencies(
315
+ self,
316
+ syntax_tree: SyntaxNode,
317
+ file_path: Path,
318
+ *,
319
+ is_project: bool,
320
+ ) -> list[tuple[str, str | None]]:
321
+ deps: list[tuple[str, str | None]] = []
322
+ if is_project:
323
+ self._append_units_from_node(syntax_tree, SyntaxNodeType.ntUses, deps)
324
+ self._append_units_from_node(syntax_tree, SyntaxNodeType.ntContains, deps)
325
+ return deps
326
+
327
+ intf = syntax_tree.find_node(SyntaxNodeType.ntInterface)
328
+ impl = syntax_tree.find_node(SyntaxNodeType.ntImplementation)
329
+ if intf is not None:
330
+ self._append_units_from_node(intf, SyntaxNodeType.ntUses, deps)
331
+ if impl is not None:
332
+ self._append_units_from_node(impl, SyntaxNodeType.ntUses, deps)
333
+ return deps
334
+
335
+ def _append_units_from_node(
336
+ self,
337
+ root: SyntaxNode,
338
+ node_type: SyntaxNodeType,
339
+ deps: list[tuple[str, str | None]],
340
+ ) -> None:
341
+ target = root.find_node(node_type)
342
+ if target is None:
343
+ return
344
+ for child in target.child_nodes:
345
+ if child.typ != SyntaxNodeType.ntUnit:
346
+ continue
347
+ name = child.get_attribute(AttributeName.anName)
348
+ if not name:
349
+ continue
350
+ dep_path = child.get_attribute(AttributeName.anPath) or None
351
+ deps.append((name, dep_path))
352
+
353
+ def _resolve_unit_path(self, unit_name: str, unit_path: str | None, *, relative_to: Path) -> Path | None:
354
+ candidates: list[Path] = []
355
+
356
+ if unit_path:
357
+ hint = Path(unit_path.replace('\\', '/'))
358
+ if hint.is_absolute():
359
+ candidates.append(hint)
360
+ else:
361
+ candidates.append((relative_to / hint).resolve())
362
+
363
+ unit_file_names: list[str]
364
+ normalized_unit_name = unit_name.replace('\\', '/')
365
+ if Path(normalized_unit_name).suffix.casefold() in {'.pas', '.dpr', '.dpk'}:
366
+ unit_file_names = [normalized_unit_name]
367
+ else:
368
+ unit_file_names = [
369
+ f'{normalized_unit_name}.pas',
370
+ f'{normalized_unit_name}.dpr',
371
+ f'{normalized_unit_name}.dpk',
372
+ ]
373
+
374
+ search_roots = [relative_to, self._project_folder, *self.search_paths, *self.include_paths]
375
+ for root in search_roots:
376
+ for file_name in unit_file_names:
377
+ candidates.append((root / file_name).resolve())
378
+
379
+ seen: set[str] = set()
380
+ for candidate in candidates:
381
+ key = str(candidate).casefold()
382
+ if key in seen:
383
+ continue
384
+ seen.add(key)
385
+ if (
386
+ candidate.exists()
387
+ and candidate.is_file()
388
+ and self._source_is_allowed(candidate)
389
+ ):
390
+ return candidate
391
+ return None
392
+
393
+ def _build_include_loader(self) -> IncludeLoader:
394
+ base_loader = self.include_loader or self._default_include_loader
395
+
396
+ def wrapped(parent_file: str, include_name: str) -> tuple[str, str] | None:
397
+ resolved = base_loader(parent_file, include_name)
398
+ if resolved is None:
399
+ return None
400
+ content, resolved_path = resolved
401
+ if not self._source_is_allowed(Path(resolved_path)):
402
+ return None
403
+ cache_key = f'{include_name.casefold()}::{resolved_path.casefold()}'
404
+ self._include_files[cache_key] = IncludeFileInfo(name=include_name, path=resolved_path)
405
+ return content, resolved_path
406
+
407
+ return wrapped
408
+
409
+ def _source_is_allowed(self, path: Path) -> bool:
410
+ try:
411
+ resolved = path.expanduser().resolve()
412
+ except OSError:
413
+ return False
414
+ if (
415
+ self.project_config is not None
416
+ and self.project_config.excludes_workspace_path(resolved)
417
+ ):
418
+ return False
419
+ if not self.source_roots:
420
+ return True
421
+ return any(resolved.is_relative_to(root) for root in self.source_roots)
422
+
423
+ def _emit_progress(self, phase: str, path: str, detail: str) -> None:
424
+ if self.on_progress is not None:
425
+ self.on_progress(
426
+ ProgressEvent(
427
+ phase=phase,
428
+ language="delphi",
429
+ path=path,
430
+ files_discovered=self._files_discovered,
431
+ files_completed=self._files_completed,
432
+ files_total=self._files_discovered,
433
+ lines_processed=self._lines_processed,
434
+ symbols_discovered=0,
435
+ cached_files=0,
436
+ detail=detail,
437
+ )
438
+ )
439
+
440
+ def _default_include_loader(self, parent_file: str, include_name: str) -> tuple[str, str] | None:
441
+ parent = Path(parent_file).resolve().parent
442
+ include_path = Path(include_name.replace('\\', '/'))
443
+ search_roots = [parent, self._project_folder, *self.include_paths, *self.search_paths]
444
+ for root in search_roots:
445
+ candidate = (root / include_path).resolve()
446
+ if (
447
+ candidate.exists()
448
+ and candidate.is_file()
449
+ and self._source_is_allowed(candidate)
450
+ ):
451
+ try:
452
+ content = read_source_text(candidate)
453
+ except OSError:
454
+ continue
455
+ return content, str(candidate)
456
+ return None
457
+
458
+
459
+ __all__ = [
460
+ 'GetUnitSyntaxHook',
461
+ 'IncludeFileInfo',
462
+ 'ProjectIndexResult',
463
+ 'ProjectIndexer',
464
+ 'ProjectProblem',
465
+ 'ProjectProblemType',
466
+ 'SourceTransform',
467
+ 'UnitErrorInfo',
468
+ 'UnitInfo',
469
+ 'UnitParsedHook',
470
+ ]
delphi_lsp/py.typed ADDED
@@ -0,0 +1 @@
1
+