python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
from collections.abc import Iterable, Iterator
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .progress import ProgressCallback, ProgressEvent
|
|
12
|
+
from .project_config import (
|
|
13
|
+
ProjectConfigError,
|
|
14
|
+
ProjectPathConfig,
|
|
15
|
+
load_project_path_config,
|
|
16
|
+
)
|
|
17
|
+
from .source_reader import read_source_text
|
|
18
|
+
|
|
19
|
+
SOURCE_EXTENSIONS = (".pas", ".dpr", ".dpk", ".inc")
|
|
20
|
+
PROJECT_EXTENSIONS = (".dpr", ".dpk")
|
|
21
|
+
SKIP_DIRS = {
|
|
22
|
+
".git",
|
|
23
|
+
".hg",
|
|
24
|
+
".svn",
|
|
25
|
+
".venv",
|
|
26
|
+
"venv",
|
|
27
|
+
"env",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
".pytest_cache",
|
|
30
|
+
".ruff_cache",
|
|
31
|
+
".mypy_cache",
|
|
32
|
+
"build",
|
|
33
|
+
"dist",
|
|
34
|
+
".worktrees",
|
|
35
|
+
"node_modules",
|
|
36
|
+
}
|
|
37
|
+
_RAD_STUDIO_ALWAYS_SKIP_DIRS = {"__history", "__recovery"}
|
|
38
|
+
_RAD_STUDIO_PROJECT_OUTPUT_DIRS = {"bin", "obj", "win32", "win64"}
|
|
39
|
+
_DELPHI_PROJECT_MARKER_EXTENSIONS = {".dpr", ".dproj", ".dpk"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class DiscoveryProblem:
|
|
44
|
+
kind: str
|
|
45
|
+
message: str
|
|
46
|
+
origin: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class DelphiProjectDiscovery:
|
|
51
|
+
root: str
|
|
52
|
+
project_files: list[str] = field(default_factory=list)
|
|
53
|
+
config_files: list[str] = field(default_factory=list)
|
|
54
|
+
search_paths: list[str] = field(default_factory=list)
|
|
55
|
+
include_paths: list[str] = field(default_factory=list)
|
|
56
|
+
defines: list[str] = field(default_factory=list)
|
|
57
|
+
source_files: list[str] = field(default_factory=list)
|
|
58
|
+
unit_paths: dict[str, list[str]] = field(default_factory=dict)
|
|
59
|
+
problems: list[DiscoveryProblem] = field(default_factory=list)
|
|
60
|
+
search_path_origins: dict[str, list[str]] = field(default_factory=dict)
|
|
61
|
+
include_path_origins: dict[str, list[str]] = field(default_factory=dict)
|
|
62
|
+
define_origins: dict[str, list[str]] = field(default_factory=dict)
|
|
63
|
+
project_config: ProjectPathConfig | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_DPR_UNIT_RE = re.compile(
|
|
67
|
+
r"\b(?P<name>[A-Za-z_][A-Za-z0-9_.]*)\b\s*(?:in\s*['\"](?P<path>[^'\"]+)['\"])?",
|
|
68
|
+
re.IGNORECASE,
|
|
69
|
+
)
|
|
70
|
+
_DPR_CLAUSE_RE = re.compile(r"\b(?:uses|contains)\b(?P<body>.*?);", re.IGNORECASE | re.DOTALL)
|
|
71
|
+
_CFG_TOKEN_RE = re.compile(r"(?P<option>-[UID])(?P<value>.+)", re.IGNORECASE)
|
|
72
|
+
_MACRO_RE = re.compile(r"\$\(([A-Za-z_][A-Za-z0-9_]*)\)")
|
|
73
|
+
_WINDOWS_ABSOLUTE_RE = re.compile(r"^[A-Za-z]:[\\/]")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def discover_delphi_project(
|
|
77
|
+
root: str | os.PathLike[str],
|
|
78
|
+
*,
|
|
79
|
+
project_file: str | os.PathLike[str] | None = None,
|
|
80
|
+
main_projects_only: bool = False,
|
|
81
|
+
include_paths: Iterable[str | os.PathLike[str]] = (),
|
|
82
|
+
search_paths: Iterable[str | os.PathLike[str]] = (),
|
|
83
|
+
defines: Iterable[str] = (),
|
|
84
|
+
scan_workspace_sources: bool = True,
|
|
85
|
+
on_progress: ProgressCallback | None = None,
|
|
86
|
+
) -> DelphiProjectDiscovery:
|
|
87
|
+
root_path = Path(root).expanduser().resolve()
|
|
88
|
+
project_config = load_project_path_config(root_path)
|
|
89
|
+
project_path = Path(project_file).expanduser() if project_file is not None else None
|
|
90
|
+
if project_path is not None:
|
|
91
|
+
if not project_path.is_absolute():
|
|
92
|
+
project_path = root_path / project_path
|
|
93
|
+
project_path = project_path.resolve()
|
|
94
|
+
if (
|
|
95
|
+
project_config is not None
|
|
96
|
+
and project_config.excludes_workspace_path(project_path)
|
|
97
|
+
):
|
|
98
|
+
raise ProjectConfigError(
|
|
99
|
+
f"{project_config.source}: explicit project {project_path} is "
|
|
100
|
+
"excluded by workspace.exclude."
|
|
101
|
+
)
|
|
102
|
+
discovery = DelphiProjectDiscovery(
|
|
103
|
+
root=str(root_path),
|
|
104
|
+
project_config=project_config,
|
|
105
|
+
)
|
|
106
|
+
_emit_progress(on_progress, "discovery", str(root_path), 0, 0, None, "project discovery started")
|
|
107
|
+
|
|
108
|
+
seen_search: dict[str, str] = {}
|
|
109
|
+
seen_include: dict[str, str] = {}
|
|
110
|
+
seen_defines: set[str] = set()
|
|
111
|
+
seen_projects: set[str] = set()
|
|
112
|
+
seen_configs: set[str] = set()
|
|
113
|
+
project_configs: dict[str, tuple[Path, ...]] = {}
|
|
114
|
+
if project_config is not None:
|
|
115
|
+
config_key = str(project_config.source).casefold()
|
|
116
|
+
seen_configs.add(config_key)
|
|
117
|
+
discovery.config_files.append(str(project_config.source))
|
|
118
|
+
|
|
119
|
+
def add_path(
|
|
120
|
+
target: list[str],
|
|
121
|
+
canonical_by_key: dict[str, str],
|
|
122
|
+
origins: dict[str, list[str]],
|
|
123
|
+
path: Path | str,
|
|
124
|
+
*,
|
|
125
|
+
base: Path,
|
|
126
|
+
origin: str,
|
|
127
|
+
) -> None:
|
|
128
|
+
_add_resolved_path(
|
|
129
|
+
target,
|
|
130
|
+
canonical_by_key,
|
|
131
|
+
origins,
|
|
132
|
+
str(path),
|
|
133
|
+
base=base,
|
|
134
|
+
origin=origin,
|
|
135
|
+
discovery=discovery,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def add_define(raw: str, *, origin: str = "manual define") -> None:
|
|
139
|
+
for item in _split_list(raw):
|
|
140
|
+
define = item.strip()
|
|
141
|
+
if not define:
|
|
142
|
+
continue
|
|
143
|
+
if _MACRO_RE.search(define):
|
|
144
|
+
discovery.problems.append(
|
|
145
|
+
DiscoveryProblem("unresolved_macro", f"Could not resolve {define} in define list", origin)
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
key = define.casefold()
|
|
149
|
+
if key not in seen_defines:
|
|
150
|
+
seen_defines.add(key)
|
|
151
|
+
discovery.defines.append(define)
|
|
152
|
+
exposed_define = next(item for item in discovery.defines if item.casefold() == key)
|
|
153
|
+
_record_origin(discovery.define_origins, exposed_define, origin)
|
|
154
|
+
|
|
155
|
+
for value in search_paths:
|
|
156
|
+
add_path(
|
|
157
|
+
discovery.search_paths,
|
|
158
|
+
seen_search,
|
|
159
|
+
discovery.search_path_origins,
|
|
160
|
+
Path(value),
|
|
161
|
+
base=root_path,
|
|
162
|
+
origin="manual search path",
|
|
163
|
+
)
|
|
164
|
+
for value in include_paths:
|
|
165
|
+
add_path(
|
|
166
|
+
discovery.include_paths,
|
|
167
|
+
seen_include,
|
|
168
|
+
discovery.include_path_origins,
|
|
169
|
+
Path(value),
|
|
170
|
+
base=root_path,
|
|
171
|
+
origin="manual include path",
|
|
172
|
+
)
|
|
173
|
+
for value in defines:
|
|
174
|
+
add_define(value)
|
|
175
|
+
|
|
176
|
+
explicit_dproj: Path | None = None
|
|
177
|
+
if (
|
|
178
|
+
project_path is not None
|
|
179
|
+
and project_path.suffix.casefold() == ".dproj"
|
|
180
|
+
):
|
|
181
|
+
explicit_dproj = project_path
|
|
182
|
+
project_path = _project_entry_from_dproj(
|
|
183
|
+
explicit_dproj,
|
|
184
|
+
discovery,
|
|
185
|
+
)
|
|
186
|
+
if (
|
|
187
|
+
project_path is not None
|
|
188
|
+
and project_config is not None
|
|
189
|
+
and project_config.excludes_workspace_path(project_path)
|
|
190
|
+
):
|
|
191
|
+
raise ProjectConfigError(
|
|
192
|
+
f"{project_config.source}: explicit project {project_path} is "
|
|
193
|
+
"excluded by workspace.exclude."
|
|
194
|
+
)
|
|
195
|
+
candidates = (
|
|
196
|
+
[]
|
|
197
|
+
if explicit_dproj is not None and project_path is None
|
|
198
|
+
else _project_candidates(
|
|
199
|
+
root_path,
|
|
200
|
+
project_path,
|
|
201
|
+
main_projects_only=main_projects_only,
|
|
202
|
+
project_config=project_config,
|
|
203
|
+
project_configs=project_configs,
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
for project in candidates:
|
|
207
|
+
key = str(project).casefold()
|
|
208
|
+
if key not in seen_projects:
|
|
209
|
+
seen_projects.add(key)
|
|
210
|
+
discovery.project_files.append(str(project))
|
|
211
|
+
if project.suffix.casefold() in PROJECT_EXTENSIONS:
|
|
212
|
+
_read_dpr_paths(
|
|
213
|
+
project,
|
|
214
|
+
discovery,
|
|
215
|
+
discovery.search_paths,
|
|
216
|
+
seen_search,
|
|
217
|
+
discovery.search_path_origins,
|
|
218
|
+
)
|
|
219
|
+
dproj_candidates = [
|
|
220
|
+
*project_configs.get(str(project).casefold(), ()),
|
|
221
|
+
project.with_suffix(".dproj"),
|
|
222
|
+
]
|
|
223
|
+
if explicit_dproj is not None:
|
|
224
|
+
dproj_candidates.insert(0, explicit_dproj)
|
|
225
|
+
for dproj in dproj_candidates:
|
|
226
|
+
config_key = str(dproj).casefold()
|
|
227
|
+
if config_key in seen_configs or not dproj.exists():
|
|
228
|
+
continue
|
|
229
|
+
_read_dproj(
|
|
230
|
+
dproj,
|
|
231
|
+
discovery,
|
|
232
|
+
discovery.search_paths,
|
|
233
|
+
seen_search,
|
|
234
|
+
discovery.search_path_origins,
|
|
235
|
+
discovery.include_paths,
|
|
236
|
+
seen_include,
|
|
237
|
+
discovery.include_path_origins,
|
|
238
|
+
add_define,
|
|
239
|
+
)
|
|
240
|
+
seen_configs.add(config_key)
|
|
241
|
+
discovery.config_files.append(str(dproj))
|
|
242
|
+
for cfg in (project.with_suffix(".cfg"), project.with_suffix(".dof")):
|
|
243
|
+
if cfg.exists():
|
|
244
|
+
_read_cfg(
|
|
245
|
+
cfg,
|
|
246
|
+
discovery,
|
|
247
|
+
discovery.search_paths,
|
|
248
|
+
seen_search,
|
|
249
|
+
discovery.search_path_origins,
|
|
250
|
+
discovery.include_paths,
|
|
251
|
+
seen_include,
|
|
252
|
+
discovery.include_path_origins,
|
|
253
|
+
add_define,
|
|
254
|
+
)
|
|
255
|
+
key = str(cfg).casefold()
|
|
256
|
+
if key not in seen_configs:
|
|
257
|
+
seen_configs.add(key)
|
|
258
|
+
discovery.config_files.append(str(cfg))
|
|
259
|
+
|
|
260
|
+
if scan_workspace_sources:
|
|
261
|
+
populate_workspace_sources(discovery, on_progress=on_progress)
|
|
262
|
+
|
|
263
|
+
_emit_progress(
|
|
264
|
+
on_progress,
|
|
265
|
+
"inventory",
|
|
266
|
+
str(root_path),
|
|
267
|
+
len(discovery.source_files),
|
|
268
|
+
0,
|
|
269
|
+
len(discovery.source_files),
|
|
270
|
+
"project discovery complete",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return discovery
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def populate_workspace_sources(
|
|
277
|
+
discovery: DelphiProjectDiscovery,
|
|
278
|
+
*,
|
|
279
|
+
on_progress: ProgressCallback | None = None,
|
|
280
|
+
) -> DelphiProjectDiscovery:
|
|
281
|
+
root_path = Path(discovery.root).expanduser().resolve()
|
|
282
|
+
seen_sources = {source.casefold() for source in discovery.source_files}
|
|
283
|
+
seen_search = {path.casefold(): path for path in discovery.search_paths}
|
|
284
|
+
seen_include = {path.casefold(): path for path in discovery.include_paths}
|
|
285
|
+
|
|
286
|
+
_scan_sources(root_path, discovery, seen_sources, on_progress=on_progress)
|
|
287
|
+
total_sources = len(discovery.source_files)
|
|
288
|
+
_emit_progress(
|
|
289
|
+
on_progress,
|
|
290
|
+
"inventory",
|
|
291
|
+
str(root_path),
|
|
292
|
+
total_sources,
|
|
293
|
+
0,
|
|
294
|
+
total_sources,
|
|
295
|
+
"workspace source inventory complete",
|
|
296
|
+
)
|
|
297
|
+
for source in discovery.source_files:
|
|
298
|
+
path = Path(source)
|
|
299
|
+
unit_key = path.stem.casefold()
|
|
300
|
+
unit_paths = discovery.unit_paths.setdefault(unit_key, [])
|
|
301
|
+
if source not in unit_paths:
|
|
302
|
+
unit_paths.append(source)
|
|
303
|
+
if path.suffix.casefold() in {".pas", ".dpr", ".dpk"}:
|
|
304
|
+
_add_resolved_path(
|
|
305
|
+
discovery.search_paths,
|
|
306
|
+
seen_search,
|
|
307
|
+
discovery.search_path_origins,
|
|
308
|
+
str(path.parent),
|
|
309
|
+
base=root_path,
|
|
310
|
+
origin="workspace source scan",
|
|
311
|
+
discovery=discovery,
|
|
312
|
+
)
|
|
313
|
+
elif path.suffix.casefold() == ".inc":
|
|
314
|
+
_add_resolved_path(
|
|
315
|
+
discovery.include_paths,
|
|
316
|
+
seen_include,
|
|
317
|
+
discovery.include_path_origins,
|
|
318
|
+
str(path.parent),
|
|
319
|
+
base=root_path,
|
|
320
|
+
origin="workspace include scan",
|
|
321
|
+
discovery=discovery,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return discovery
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def discover_workspace_sources(
|
|
328
|
+
root: str | os.PathLike[str],
|
|
329
|
+
*,
|
|
330
|
+
on_progress: ProgressCallback | None = None,
|
|
331
|
+
) -> DelphiProjectDiscovery:
|
|
332
|
+
root_path = Path(root).expanduser().resolve()
|
|
333
|
+
project_config = load_project_path_config(root_path)
|
|
334
|
+
discovery = DelphiProjectDiscovery(
|
|
335
|
+
root=str(root_path),
|
|
336
|
+
project_config=project_config,
|
|
337
|
+
)
|
|
338
|
+
if project_config is not None:
|
|
339
|
+
discovery.config_files.append(str(project_config.source))
|
|
340
|
+
_emit_progress(on_progress, "discovery", str(root_path), 0, 0, None, "workspace discovery started")
|
|
341
|
+
populate_workspace_sources(discovery, on_progress=on_progress)
|
|
342
|
+
_emit_progress(
|
|
343
|
+
on_progress,
|
|
344
|
+
"complete",
|
|
345
|
+
str(root_path),
|
|
346
|
+
len(discovery.source_files),
|
|
347
|
+
len(discovery.source_files),
|
|
348
|
+
len(discovery.source_files),
|
|
349
|
+
"workspace discovery complete",
|
|
350
|
+
)
|
|
351
|
+
return discovery
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _emit_progress(
|
|
355
|
+
callback: ProgressCallback | None,
|
|
356
|
+
phase: str,
|
|
357
|
+
path: str,
|
|
358
|
+
files_discovered: int,
|
|
359
|
+
files_completed: int,
|
|
360
|
+
files_total: int | None,
|
|
361
|
+
detail: str,
|
|
362
|
+
) -> None:
|
|
363
|
+
if callback is not None:
|
|
364
|
+
callback(
|
|
365
|
+
ProgressEvent(
|
|
366
|
+
phase=phase,
|
|
367
|
+
language="delphi",
|
|
368
|
+
path=path,
|
|
369
|
+
files_discovered=files_discovered,
|
|
370
|
+
files_completed=files_completed,
|
|
371
|
+
files_total=files_total,
|
|
372
|
+
lines_processed=0,
|
|
373
|
+
symbols_discovered=0,
|
|
374
|
+
cached_files=0,
|
|
375
|
+
detail=detail,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _project_candidates(
|
|
381
|
+
root: Path,
|
|
382
|
+
explicit: Path | None,
|
|
383
|
+
*,
|
|
384
|
+
main_projects_only: bool = False,
|
|
385
|
+
project_config: ProjectPathConfig | None = None,
|
|
386
|
+
project_configs: dict[str, tuple[Path, ...]] | None = None,
|
|
387
|
+
) -> list[Path]:
|
|
388
|
+
if explicit is not None:
|
|
389
|
+
return [explicit]
|
|
390
|
+
candidates: list[Path] = []
|
|
391
|
+
aliases: dict[str, list[Path]] = {}
|
|
392
|
+
configs_by_entry: dict[str, list[Path]] = {}
|
|
393
|
+
for ext in PROJECT_EXTENSIONS:
|
|
394
|
+
candidates.extend(_walk_sources(root, f"*{ext}", project_config))
|
|
395
|
+
for candidate in candidates:
|
|
396
|
+
key = str(candidate).casefold()
|
|
397
|
+
aliases.setdefault(key, []).append(candidate)
|
|
398
|
+
companion = candidate.with_suffix(".dproj")
|
|
399
|
+
if companion.is_file():
|
|
400
|
+
aliases[key].append(companion)
|
|
401
|
+
configs_by_entry.setdefault(key, []).append(companion)
|
|
402
|
+
for dproj in _walk_sources(root, "*.dproj", project_config):
|
|
403
|
+
main = _main_source_from_dproj(dproj)
|
|
404
|
+
if main is not None:
|
|
405
|
+
entry = (dproj.parent / main).resolve()
|
|
406
|
+
key = str(entry).casefold()
|
|
407
|
+
aliases.setdefault(key, []).extend((entry, dproj))
|
|
408
|
+
configs_by_entry.setdefault(key, []).append(dproj)
|
|
409
|
+
candidates.append(entry)
|
|
410
|
+
candidates = sorted(
|
|
411
|
+
{
|
|
412
|
+
candidate
|
|
413
|
+
for candidate in candidates
|
|
414
|
+
if candidate.exists()
|
|
415
|
+
and candidate.is_file()
|
|
416
|
+
and candidate.is_relative_to(root)
|
|
417
|
+
and (
|
|
418
|
+
project_config is None
|
|
419
|
+
or not project_config.excludes_workspace_path(candidate)
|
|
420
|
+
)
|
|
421
|
+
},
|
|
422
|
+
key=lambda path: str(path).casefold(),
|
|
423
|
+
)
|
|
424
|
+
if project_config is not None and project_config.has_filters:
|
|
425
|
+
candidates = [
|
|
426
|
+
candidate
|
|
427
|
+
for candidate in candidates
|
|
428
|
+
if project_config.selects(
|
|
429
|
+
*aliases.get(
|
|
430
|
+
str(candidate).casefold(),
|
|
431
|
+
(candidate, candidate.with_suffix(".dproj")),
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
]
|
|
435
|
+
if not candidates:
|
|
436
|
+
raise ProjectConfigError(
|
|
437
|
+
f"{project_config.source}: configured [projects] filters matched "
|
|
438
|
+
"no Delphi projects."
|
|
439
|
+
)
|
|
440
|
+
_set_project_configs(project_configs, configs_by_entry, candidates)
|
|
441
|
+
return candidates
|
|
442
|
+
if not main_projects_only or not candidates:
|
|
443
|
+
_set_project_configs(project_configs, configs_by_entry, candidates)
|
|
444
|
+
return candidates
|
|
445
|
+
|
|
446
|
+
primary = [
|
|
447
|
+
candidate
|
|
448
|
+
for candidate in candidates
|
|
449
|
+
if not _is_auxiliary_project(candidate, root)
|
|
450
|
+
]
|
|
451
|
+
configured = [
|
|
452
|
+
candidate
|
|
453
|
+
for candidate in primary
|
|
454
|
+
if candidate.with_suffix(".dproj").is_file()
|
|
455
|
+
]
|
|
456
|
+
root_entries = [
|
|
457
|
+
candidate
|
|
458
|
+
for candidate in primary
|
|
459
|
+
if candidate.parent == root
|
|
460
|
+
]
|
|
461
|
+
scoped = root_entries or configured
|
|
462
|
+
if not scoped:
|
|
463
|
+
return []
|
|
464
|
+
minimum_depth = min(
|
|
465
|
+
len(candidate.parent.relative_to(root).parts)
|
|
466
|
+
for candidate in scoped
|
|
467
|
+
)
|
|
468
|
+
selected = [
|
|
469
|
+
candidate
|
|
470
|
+
for candidate in scoped
|
|
471
|
+
if len(candidate.parent.relative_to(root).parts) == minimum_depth
|
|
472
|
+
]
|
|
473
|
+
_set_project_configs(project_configs, configs_by_entry, selected)
|
|
474
|
+
return selected
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _set_project_configs(
|
|
478
|
+
target: dict[str, tuple[Path, ...]] | None,
|
|
479
|
+
configs_by_entry: dict[str, list[Path]],
|
|
480
|
+
selected: list[Path],
|
|
481
|
+
) -> None:
|
|
482
|
+
if target is None:
|
|
483
|
+
return
|
|
484
|
+
for candidate in selected:
|
|
485
|
+
key = str(candidate).casefold()
|
|
486
|
+
configs = sorted(
|
|
487
|
+
set(configs_by_entry.get(key, ())),
|
|
488
|
+
key=lambda path: str(path).casefold(),
|
|
489
|
+
)
|
|
490
|
+
if configs:
|
|
491
|
+
target[key] = tuple(configs)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
_AUXILIARY_PROJECT_PARTS = {
|
|
495
|
+
"benchmark",
|
|
496
|
+
"benchmarks",
|
|
497
|
+
"demo",
|
|
498
|
+
"demos",
|
|
499
|
+
"ex",
|
|
500
|
+
"example",
|
|
501
|
+
"examples",
|
|
502
|
+
"sample",
|
|
503
|
+
"samples",
|
|
504
|
+
"test",
|
|
505
|
+
"tests",
|
|
506
|
+
"testing",
|
|
507
|
+
"thirdparty",
|
|
508
|
+
"third-party",
|
|
509
|
+
"vendor",
|
|
510
|
+
"vendors",
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _is_auxiliary_project(path: Path, root: Path) -> bool:
|
|
515
|
+
relative = path.relative_to(root)
|
|
516
|
+
directory_parts = {part.casefold() for part in relative.parent.parts}
|
|
517
|
+
if directory_parts & _AUXILIARY_PROJECT_PARTS:
|
|
518
|
+
return True
|
|
519
|
+
stem = path.stem.casefold()
|
|
520
|
+
return (
|
|
521
|
+
"example" in stem
|
|
522
|
+
or "sample" in stem
|
|
523
|
+
or stem.startswith("test")
|
|
524
|
+
or stem.endswith("tests")
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _read_dpr_paths(
|
|
529
|
+
project: Path,
|
|
530
|
+
discovery: DelphiProjectDiscovery,
|
|
531
|
+
search_paths: list[str],
|
|
532
|
+
seen_search: dict[str, str],
|
|
533
|
+
search_path_origins: dict[str, list[str]],
|
|
534
|
+
) -> None:
|
|
535
|
+
try:
|
|
536
|
+
text = read_source_text(project)
|
|
537
|
+
except (OSError, UnicodeError) as exc:
|
|
538
|
+
discovery.problems.append(DiscoveryProblem("cant_read_project", str(exc), str(project)))
|
|
539
|
+
return
|
|
540
|
+
for clause in _DPR_CLAUSE_RE.finditer(text):
|
|
541
|
+
body = clause.group("body")
|
|
542
|
+
for match in _DPR_UNIT_RE.finditer(body):
|
|
543
|
+
unit_path = match.group("path")
|
|
544
|
+
if not unit_path:
|
|
545
|
+
continue
|
|
546
|
+
resolved = _resolve_project_path(unit_path, base=project.parent, origin=str(project), discovery=discovery)
|
|
547
|
+
if resolved is None:
|
|
548
|
+
continue
|
|
549
|
+
_add_resolved_path(
|
|
550
|
+
search_paths,
|
|
551
|
+
seen_search,
|
|
552
|
+
search_path_origins,
|
|
553
|
+
str(resolved.parent),
|
|
554
|
+
base=project.parent,
|
|
555
|
+
origin=str(project),
|
|
556
|
+
discovery=discovery,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _read_dproj(
|
|
561
|
+
path: Path,
|
|
562
|
+
discovery: DelphiProjectDiscovery,
|
|
563
|
+
search_paths: list[str],
|
|
564
|
+
seen_search: dict[str, str],
|
|
565
|
+
search_path_origins: dict[str, list[str]],
|
|
566
|
+
include_paths: list[str],
|
|
567
|
+
seen_include: dict[str, str],
|
|
568
|
+
include_path_origins: dict[str, list[str]],
|
|
569
|
+
add_define,
|
|
570
|
+
) -> None:
|
|
571
|
+
try:
|
|
572
|
+
root = ET.parse(path).getroot()
|
|
573
|
+
except (ET.ParseError, OSError) as exc:
|
|
574
|
+
discovery.problems.append(DiscoveryProblem("cant_read_dproj", str(exc), str(path)))
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
for element in root.iter():
|
|
578
|
+
name = _xml_local_name(element.tag)
|
|
579
|
+
text = (element.text or "").strip()
|
|
580
|
+
if name in {"DCC_UnitSearchPath", "UnitSearchPath"} and text:
|
|
581
|
+
for item in _split_list(text):
|
|
582
|
+
_add_resolved_path(
|
|
583
|
+
search_paths,
|
|
584
|
+
seen_search,
|
|
585
|
+
search_path_origins,
|
|
586
|
+
item,
|
|
587
|
+
base=path.parent,
|
|
588
|
+
origin=str(path),
|
|
589
|
+
discovery=discovery,
|
|
590
|
+
)
|
|
591
|
+
elif name in {"DCC_IncludePath", "IncludePath"} and text:
|
|
592
|
+
for item in _split_list(text):
|
|
593
|
+
_add_resolved_path(
|
|
594
|
+
include_paths,
|
|
595
|
+
seen_include,
|
|
596
|
+
include_path_origins,
|
|
597
|
+
item,
|
|
598
|
+
base=path.parent,
|
|
599
|
+
origin=str(path),
|
|
600
|
+
discovery=discovery,
|
|
601
|
+
)
|
|
602
|
+
elif name in {"DCC_Define", "DefineConstants"} and text:
|
|
603
|
+
add_define(text, origin=str(path))
|
|
604
|
+
|
|
605
|
+
if name == "DCCReference":
|
|
606
|
+
include = element.attrib.get("Include") or element.attrib.get("include")
|
|
607
|
+
if include:
|
|
608
|
+
resolved = _resolve_project_path(include, base=path.parent, origin=str(path), discovery=discovery)
|
|
609
|
+
if resolved is not None:
|
|
610
|
+
_add_resolved_path(
|
|
611
|
+
search_paths,
|
|
612
|
+
seen_search,
|
|
613
|
+
search_path_origins,
|
|
614
|
+
str(resolved.parent),
|
|
615
|
+
base=path.parent,
|
|
616
|
+
origin=str(path),
|
|
617
|
+
discovery=discovery,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _read_cfg(
|
|
622
|
+
path: Path,
|
|
623
|
+
discovery: DelphiProjectDiscovery,
|
|
624
|
+
search_paths: list[str],
|
|
625
|
+
seen_search: dict[str, str],
|
|
626
|
+
search_path_origins: dict[str, list[str]],
|
|
627
|
+
include_paths: list[str],
|
|
628
|
+
seen_include: dict[str, str],
|
|
629
|
+
include_path_origins: dict[str, list[str]],
|
|
630
|
+
add_define,
|
|
631
|
+
) -> None:
|
|
632
|
+
try:
|
|
633
|
+
lines = read_source_text(path).splitlines()
|
|
634
|
+
except (OSError, UnicodeError) as exc:
|
|
635
|
+
discovery.problems.append(DiscoveryProblem("cant_read_config", str(exc), str(path)))
|
|
636
|
+
return
|
|
637
|
+
for line in lines:
|
|
638
|
+
stripped = line.strip()
|
|
639
|
+
if not stripped or stripped.startswith("#"):
|
|
640
|
+
continue
|
|
641
|
+
match = _CFG_TOKEN_RE.match(stripped)
|
|
642
|
+
if match is None:
|
|
643
|
+
continue
|
|
644
|
+
option = match.group("option").casefold()
|
|
645
|
+
value = match.group("value")
|
|
646
|
+
if option == "-u":
|
|
647
|
+
for item in _split_list(value):
|
|
648
|
+
_add_resolved_path(
|
|
649
|
+
search_paths,
|
|
650
|
+
seen_search,
|
|
651
|
+
search_path_origins,
|
|
652
|
+
item,
|
|
653
|
+
base=path.parent,
|
|
654
|
+
origin=str(path),
|
|
655
|
+
discovery=discovery,
|
|
656
|
+
)
|
|
657
|
+
elif option == "-i":
|
|
658
|
+
for item in _split_list(value):
|
|
659
|
+
_add_resolved_path(
|
|
660
|
+
include_paths,
|
|
661
|
+
seen_include,
|
|
662
|
+
include_path_origins,
|
|
663
|
+
item,
|
|
664
|
+
base=path.parent,
|
|
665
|
+
origin=str(path),
|
|
666
|
+
discovery=discovery,
|
|
667
|
+
)
|
|
668
|
+
elif option == "-d":
|
|
669
|
+
add_define(value, origin=str(path))
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _main_source_from_dproj(path: Path) -> str | None:
|
|
673
|
+
try:
|
|
674
|
+
root = ET.parse(path).getroot()
|
|
675
|
+
except (ET.ParseError, OSError):
|
|
676
|
+
return None
|
|
677
|
+
for element in root.iter():
|
|
678
|
+
if _xml_local_name(element.tag) == "MainSource" and element.text:
|
|
679
|
+
return element.text.strip()
|
|
680
|
+
return None
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _project_entry_from_dproj(
|
|
684
|
+
path: Path,
|
|
685
|
+
discovery: DelphiProjectDiscovery,
|
|
686
|
+
) -> Path | None:
|
|
687
|
+
try:
|
|
688
|
+
root = ET.parse(path).getroot()
|
|
689
|
+
except ET.ParseError as exc:
|
|
690
|
+
discovery.problems.append(
|
|
691
|
+
DiscoveryProblem(
|
|
692
|
+
"cant_read_project",
|
|
693
|
+
f"Could not parse Delphi project file: {exc}",
|
|
694
|
+
str(path),
|
|
695
|
+
)
|
|
696
|
+
)
|
|
697
|
+
return None
|
|
698
|
+
except OSError as exc:
|
|
699
|
+
discovery.problems.append(
|
|
700
|
+
DiscoveryProblem(
|
|
701
|
+
"cant_read_project",
|
|
702
|
+
f"Could not read Delphi project file: {exc}",
|
|
703
|
+
str(path),
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
main_source = next(
|
|
709
|
+
(
|
|
710
|
+
(element.text or "").strip()
|
|
711
|
+
for element in root.iter()
|
|
712
|
+
if _xml_local_name(element.tag) == "MainSource"
|
|
713
|
+
and (element.text or "").strip()
|
|
714
|
+
),
|
|
715
|
+
"",
|
|
716
|
+
)
|
|
717
|
+
if not main_source:
|
|
718
|
+
discovery.problems.append(
|
|
719
|
+
DiscoveryProblem(
|
|
720
|
+
"cant_read_project",
|
|
721
|
+
"Delphi project file does not define MainSource.",
|
|
722
|
+
str(path),
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
return None
|
|
726
|
+
|
|
727
|
+
entry = (path.parent / main_source).resolve()
|
|
728
|
+
if entry.suffix.casefold() not in PROJECT_EXTENSIONS:
|
|
729
|
+
discovery.problems.append(
|
|
730
|
+
DiscoveryProblem(
|
|
731
|
+
"cant_read_project",
|
|
732
|
+
"MainSource must reference a .dpr or .dpk entry file.",
|
|
733
|
+
str(path),
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
return None
|
|
737
|
+
if not entry.is_file():
|
|
738
|
+
discovery.problems.append(
|
|
739
|
+
DiscoveryProblem(
|
|
740
|
+
"cant_read_project",
|
|
741
|
+
f"MainSource does not exist: {entry}",
|
|
742
|
+
str(path),
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
return None
|
|
746
|
+
return entry
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _scan_sources(
|
|
750
|
+
root: Path,
|
|
751
|
+
discovery: DelphiProjectDiscovery,
|
|
752
|
+
seen_sources: set[str],
|
|
753
|
+
*,
|
|
754
|
+
on_progress: ProgressCallback | None = None,
|
|
755
|
+
) -> None:
|
|
756
|
+
for path in _walk_files(root, discovery.project_config):
|
|
757
|
+
if path.suffix.casefold() not in SOURCE_EXTENSIONS:
|
|
758
|
+
continue
|
|
759
|
+
resolved = path.resolve()
|
|
760
|
+
key = str(resolved).casefold()
|
|
761
|
+
if key in seen_sources:
|
|
762
|
+
continue
|
|
763
|
+
seen_sources.add(key)
|
|
764
|
+
discovery.source_files.append(str(resolved))
|
|
765
|
+
_emit_progress(
|
|
766
|
+
on_progress,
|
|
767
|
+
"discovery",
|
|
768
|
+
str(resolved),
|
|
769
|
+
len(discovery.source_files),
|
|
770
|
+
0,
|
|
771
|
+
None,
|
|
772
|
+
"source file discovered",
|
|
773
|
+
)
|
|
774
|
+
discovery.source_files.sort(key=lambda item: (item.casefold(), item))
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _walk_sources(
|
|
778
|
+
root: Path,
|
|
779
|
+
pattern: str,
|
|
780
|
+
project_config: ProjectPathConfig | None = None,
|
|
781
|
+
) -> list[Path]:
|
|
782
|
+
results: list[Path] = []
|
|
783
|
+
for path in _walk_files(root, project_config):
|
|
784
|
+
if path.match(pattern):
|
|
785
|
+
results.append(path.resolve())
|
|
786
|
+
return results
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _walk_files(
|
|
790
|
+
root: Path,
|
|
791
|
+
project_config: ProjectPathConfig | None = None,
|
|
792
|
+
) -> Iterator[Path]:
|
|
793
|
+
def handle_error(error: OSError) -> None:
|
|
794
|
+
if _is_path_too_long(error):
|
|
795
|
+
return
|
|
796
|
+
raise error
|
|
797
|
+
|
|
798
|
+
for directory, directory_names, file_names in os.walk(
|
|
799
|
+
root,
|
|
800
|
+
followlinks=False,
|
|
801
|
+
onerror=handle_error,
|
|
802
|
+
):
|
|
803
|
+
current = Path(directory)
|
|
804
|
+
directory_names[:] = sorted(
|
|
805
|
+
name
|
|
806
|
+
for name in directory_names
|
|
807
|
+
if not _skip_walk_directory(name, file_names)
|
|
808
|
+
and (
|
|
809
|
+
project_config is None
|
|
810
|
+
or not project_config.excludes_workspace_path(current / name)
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
for name in sorted(file_names):
|
|
814
|
+
path = current / name
|
|
815
|
+
if (
|
|
816
|
+
project_config is not None
|
|
817
|
+
and project_config.excludes_workspace_path(path)
|
|
818
|
+
):
|
|
819
|
+
continue
|
|
820
|
+
try:
|
|
821
|
+
if path.is_file():
|
|
822
|
+
yield path
|
|
823
|
+
except OSError as error:
|
|
824
|
+
if _is_path_too_long(error):
|
|
825
|
+
continue
|
|
826
|
+
raise
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _skip_walk_directory(name: str, sibling_file_names: list[str]) -> bool:
|
|
830
|
+
if name in SKIP_DIRS:
|
|
831
|
+
return True
|
|
832
|
+
normalized = name.casefold()
|
|
833
|
+
if normalized in _RAD_STUDIO_ALWAYS_SKIP_DIRS:
|
|
834
|
+
return True
|
|
835
|
+
if normalized not in _RAD_STUDIO_PROJECT_OUTPUT_DIRS:
|
|
836
|
+
return False
|
|
837
|
+
return any(
|
|
838
|
+
Path(file_name).suffix.casefold() in _DELPHI_PROJECT_MARKER_EXTENSIONS
|
|
839
|
+
for file_name in sibling_file_names
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _is_path_too_long(error: OSError) -> bool:
|
|
844
|
+
return getattr(error, "winerror", None) == 206 or error.errno == errno.ENAMETOOLONG
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _add_resolved_path(
|
|
848
|
+
target: list[str],
|
|
849
|
+
canonical_by_key: dict[str, str],
|
|
850
|
+
origins: dict[str, list[str]],
|
|
851
|
+
value: str,
|
|
852
|
+
*,
|
|
853
|
+
base: Path,
|
|
854
|
+
origin: str,
|
|
855
|
+
discovery: DelphiProjectDiscovery,
|
|
856
|
+
) -> None:
|
|
857
|
+
resolved = _resolve_project_path(value, base=base, origin=origin, discovery=discovery)
|
|
858
|
+
if resolved is None:
|
|
859
|
+
return
|
|
860
|
+
if (
|
|
861
|
+
discovery.project_config is not None
|
|
862
|
+
and discovery.project_config.excludes_workspace_path(resolved)
|
|
863
|
+
):
|
|
864
|
+
return
|
|
865
|
+
key = str(resolved).casefold()
|
|
866
|
+
exposed_path = canonical_by_key.get(key)
|
|
867
|
+
if exposed_path is None:
|
|
868
|
+
exposed_path = str(resolved)
|
|
869
|
+
canonical_by_key[key] = exposed_path
|
|
870
|
+
target.append(exposed_path)
|
|
871
|
+
_record_origin(origins, exposed_path, origin)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _record_origin(origins: dict[str, list[str]], key: str, origin: str) -> None:
|
|
875
|
+
entries = origins.setdefault(key, [])
|
|
876
|
+
if origin not in entries:
|
|
877
|
+
entries.append(origin)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _resolve_project_path(
|
|
881
|
+
raw: str,
|
|
882
|
+
*,
|
|
883
|
+
base: Path,
|
|
884
|
+
origin: str,
|
|
885
|
+
discovery: DelphiProjectDiscovery,
|
|
886
|
+
) -> Path | None:
|
|
887
|
+
cleaned = raw.strip().strip('"').strip("'")
|
|
888
|
+
if not cleaned:
|
|
889
|
+
return None
|
|
890
|
+
if os.name != "nt" and _WINDOWS_ABSOLUTE_RE.match(cleaned):
|
|
891
|
+
discovery.problems.append(
|
|
892
|
+
DiscoveryProblem("external_path", f"Skipping non-local Windows absolute path: {raw}", origin)
|
|
893
|
+
)
|
|
894
|
+
return None
|
|
895
|
+
normalized = cleaned.replace("\\", os.sep)
|
|
896
|
+
macros = _MACRO_RE.findall(normalized)
|
|
897
|
+
replacements = {
|
|
898
|
+
"PROJECTDIR": str(base),
|
|
899
|
+
"PROJECT_DIR": str(base),
|
|
900
|
+
"MSBUILDPROJECTDIRECTORY": str(base),
|
|
901
|
+
"MSBUILDTHISFILEDIRECTORY": str(base),
|
|
902
|
+
}
|
|
903
|
+
for macro in macros:
|
|
904
|
+
value = replacements.get(macro.upper())
|
|
905
|
+
if value is None:
|
|
906
|
+
discovery.problems.append(
|
|
907
|
+
DiscoveryProblem("unresolved_macro", f"Could not resolve $({macro}) in {raw}", origin)
|
|
908
|
+
)
|
|
909
|
+
return None
|
|
910
|
+
normalized = normalized.replace(f"$({macro})", value)
|
|
911
|
+
normalized = os.path.expandvars(normalized)
|
|
912
|
+
path = Path(normalized)
|
|
913
|
+
if not path.is_absolute():
|
|
914
|
+
path = base / path
|
|
915
|
+
return path.resolve()
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _split_list(raw: str) -> list[str]:
|
|
919
|
+
return [item.strip() for item in raw.replace("\n", ";").split(";") if item.strip()]
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _xml_local_name(tag: str) -> str:
|
|
923
|
+
return tag.rsplit("}", 1)[-1]
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
__all__ = [
|
|
927
|
+
"DelphiProjectDiscovery",
|
|
928
|
+
"DiscoveryProblem",
|
|
929
|
+
"discover_delphi_project",
|
|
930
|
+
"discover_workspace_sources",
|
|
931
|
+
"populate_workspace_sources",
|
|
932
|
+
]
|