agent-codinglanguage-mapper 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_codinglanguage_mapper/__init__.py +20 -0
- agent_codinglanguage_mapper/__main__.py +4 -0
- agent_codinglanguage_mapper/_version.py +2 -0
- agent_codinglanguage_mapper/adapters/__init__.py +4 -0
- agent_codinglanguage_mapper/adapters/delphi.py +822 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
- agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
- agent_codinglanguage_mapper/cli.py +138 -0
- agent_codinglanguage_mapper/discovery.py +1328 -0
- agent_codinglanguage_mapper/indexer.py +1102 -0
- agent_codinglanguage_mapper/integration.py +186 -0
- agent_codinglanguage_mapper/lsp_server.py +413 -0
- agent_codinglanguage_mapper/lsp_service.py +447 -0
- agent_codinglanguage_mapper/mapper.py +596 -0
- agent_codinglanguage_mapper/models.py +101 -0
- agent_codinglanguage_mapper/protocol.py +152 -0
- agent_codinglanguage_mapper/rendering.py +153 -0
- agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
- agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
- agent_codinglanguage_mapper/worker.py +29 -0
- agent_codinglanguage_mapper/workspace.py +114 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SOURCE_EXTENSIONS = (".pas", ".dpr", ".dpk", ".inc")
|
|
12
|
+
PROJECT_EXTENSIONS = (".dpr", ".dpk")
|
|
13
|
+
SKIP_DIRS = {
|
|
14
|
+
".git",
|
|
15
|
+
".hg",
|
|
16
|
+
".svn",
|
|
17
|
+
".venv",
|
|
18
|
+
"venv",
|
|
19
|
+
"env",
|
|
20
|
+
"__pycache__",
|
|
21
|
+
".pytest_cache",
|
|
22
|
+
".ruff_cache",
|
|
23
|
+
".mypy_cache",
|
|
24
|
+
"build",
|
|
25
|
+
"dist",
|
|
26
|
+
".worktrees",
|
|
27
|
+
"node_modules",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class DiscoveryProblem:
|
|
33
|
+
kind: str
|
|
34
|
+
message: str
|
|
35
|
+
origin: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DelphiProjectDiscovery:
|
|
40
|
+
root: str
|
|
41
|
+
project_files: list[str] = field(default_factory=list)
|
|
42
|
+
config_files: list[str] = field(default_factory=list)
|
|
43
|
+
search_paths: list[str] = field(default_factory=list)
|
|
44
|
+
include_paths: list[str] = field(default_factory=list)
|
|
45
|
+
defines: list[str] = field(default_factory=list)
|
|
46
|
+
source_files: list[str] = field(default_factory=list)
|
|
47
|
+
unit_paths: dict[str, list[str]] = field(default_factory=dict)
|
|
48
|
+
problems: list[DiscoveryProblem] = field(default_factory=list)
|
|
49
|
+
search_path_origins: dict[str, list[str]] = field(default_factory=dict)
|
|
50
|
+
include_path_origins: dict[str, list[str]] = field(default_factory=dict)
|
|
51
|
+
define_origins: dict[str, list[str]] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_DPR_UNIT_RE = re.compile(
|
|
55
|
+
r"\b(?P<name>[A-Za-z_][A-Za-z0-9_.]*)\b\s*(?:in\s*['\"](?P<path>[^'\"]+)['\"])?",
|
|
56
|
+
re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
_DPR_CLAUSE_RE = re.compile(r"\b(?:uses|contains)\b(?P<body>.*?);", re.IGNORECASE | re.DOTALL)
|
|
59
|
+
_CFG_TOKEN_RE = re.compile(r"(?P<option>-[UID])(?P<value>.+)", re.IGNORECASE)
|
|
60
|
+
_MACRO_RE = re.compile(r"\$\(([A-Za-z_][A-Za-z0-9_]*)\)")
|
|
61
|
+
_WINDOWS_ABSOLUTE_RE = re.compile(r"^[A-Za-z]:[\\/]")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def discover_delphi_project(
|
|
65
|
+
root: str | os.PathLike[str],
|
|
66
|
+
*,
|
|
67
|
+
project_file: str | os.PathLike[str] | None = None,
|
|
68
|
+
include_paths: Iterable[str | os.PathLike[str]] = (),
|
|
69
|
+
search_paths: Iterable[str | os.PathLike[str]] = (),
|
|
70
|
+
defines: Iterable[str] = (),
|
|
71
|
+
scan_workspace_sources: bool = True,
|
|
72
|
+
) -> DelphiProjectDiscovery:
|
|
73
|
+
root_path = Path(root).expanduser().resolve()
|
|
74
|
+
project_path = Path(project_file).expanduser().resolve() if project_file is not None else None
|
|
75
|
+
discovery = DelphiProjectDiscovery(root=str(root_path))
|
|
76
|
+
|
|
77
|
+
seen_search: set[str] = set()
|
|
78
|
+
seen_include: set[str] = set()
|
|
79
|
+
seen_defines: set[str] = set()
|
|
80
|
+
seen_projects: set[str] = set()
|
|
81
|
+
seen_configs: set[str] = set()
|
|
82
|
+
|
|
83
|
+
def add_path(
|
|
84
|
+
target: list[str],
|
|
85
|
+
seen: set[str],
|
|
86
|
+
origins: dict[str, list[str]],
|
|
87
|
+
path: Path | str,
|
|
88
|
+
*,
|
|
89
|
+
base: Path,
|
|
90
|
+
origin: str,
|
|
91
|
+
) -> None:
|
|
92
|
+
_add_resolved_path(
|
|
93
|
+
target,
|
|
94
|
+
seen,
|
|
95
|
+
origins,
|
|
96
|
+
str(path),
|
|
97
|
+
base=base,
|
|
98
|
+
origin=origin,
|
|
99
|
+
discovery=discovery,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def add_define(raw: str, *, origin: str = "manual define") -> None:
|
|
103
|
+
for item in _split_list(raw):
|
|
104
|
+
define = item.strip()
|
|
105
|
+
if not define:
|
|
106
|
+
continue
|
|
107
|
+
if _MACRO_RE.search(define):
|
|
108
|
+
discovery.problems.append(
|
|
109
|
+
DiscoveryProblem("unresolved_macro", f"Could not resolve {define} in define list", origin)
|
|
110
|
+
)
|
|
111
|
+
continue
|
|
112
|
+
key = define.casefold()
|
|
113
|
+
if key not in seen_defines:
|
|
114
|
+
seen_defines.add(key)
|
|
115
|
+
discovery.defines.append(define)
|
|
116
|
+
exposed_define = next(item for item in discovery.defines if item.casefold() == key)
|
|
117
|
+
_record_origin(discovery.define_origins, exposed_define, origin)
|
|
118
|
+
|
|
119
|
+
for value in search_paths:
|
|
120
|
+
add_path(
|
|
121
|
+
discovery.search_paths,
|
|
122
|
+
seen_search,
|
|
123
|
+
discovery.search_path_origins,
|
|
124
|
+
Path(value),
|
|
125
|
+
base=root_path,
|
|
126
|
+
origin="manual search path",
|
|
127
|
+
)
|
|
128
|
+
for value in include_paths:
|
|
129
|
+
add_path(
|
|
130
|
+
discovery.include_paths,
|
|
131
|
+
seen_include,
|
|
132
|
+
discovery.include_path_origins,
|
|
133
|
+
Path(value),
|
|
134
|
+
base=root_path,
|
|
135
|
+
origin="manual include path",
|
|
136
|
+
)
|
|
137
|
+
for value in defines:
|
|
138
|
+
add_define(value)
|
|
139
|
+
|
|
140
|
+
candidates = _project_candidates(root_path, project_path)
|
|
141
|
+
for project in candidates:
|
|
142
|
+
key = str(project).casefold()
|
|
143
|
+
if key not in seen_projects:
|
|
144
|
+
seen_projects.add(key)
|
|
145
|
+
discovery.project_files.append(str(project))
|
|
146
|
+
if project.suffix.casefold() in PROJECT_EXTENSIONS:
|
|
147
|
+
_read_dpr_paths(
|
|
148
|
+
project,
|
|
149
|
+
discovery,
|
|
150
|
+
discovery.search_paths,
|
|
151
|
+
seen_search,
|
|
152
|
+
discovery.search_path_origins,
|
|
153
|
+
)
|
|
154
|
+
dproj = project.with_suffix(".dproj")
|
|
155
|
+
if dproj.exists():
|
|
156
|
+
_read_dproj(
|
|
157
|
+
dproj,
|
|
158
|
+
discovery,
|
|
159
|
+
discovery.search_paths,
|
|
160
|
+
seen_search,
|
|
161
|
+
discovery.search_path_origins,
|
|
162
|
+
discovery.include_paths,
|
|
163
|
+
seen_include,
|
|
164
|
+
discovery.include_path_origins,
|
|
165
|
+
add_define,
|
|
166
|
+
)
|
|
167
|
+
seen_configs.add(str(dproj).casefold())
|
|
168
|
+
discovery.config_files.append(str(dproj))
|
|
169
|
+
for cfg in (project.with_suffix(".cfg"), project.with_suffix(".dof")):
|
|
170
|
+
if cfg.exists():
|
|
171
|
+
_read_cfg(
|
|
172
|
+
cfg,
|
|
173
|
+
discovery,
|
|
174
|
+
discovery.search_paths,
|
|
175
|
+
seen_search,
|
|
176
|
+
discovery.search_path_origins,
|
|
177
|
+
discovery.include_paths,
|
|
178
|
+
seen_include,
|
|
179
|
+
discovery.include_path_origins,
|
|
180
|
+
add_define,
|
|
181
|
+
)
|
|
182
|
+
key = str(cfg).casefold()
|
|
183
|
+
if key not in seen_configs:
|
|
184
|
+
seen_configs.add(key)
|
|
185
|
+
discovery.config_files.append(str(cfg))
|
|
186
|
+
|
|
187
|
+
if scan_workspace_sources:
|
|
188
|
+
populate_workspace_sources(discovery)
|
|
189
|
+
|
|
190
|
+
return discovery
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def populate_workspace_sources(discovery: DelphiProjectDiscovery) -> DelphiProjectDiscovery:
|
|
194
|
+
root_path = Path(discovery.root).expanduser().resolve()
|
|
195
|
+
seen_sources = {source.casefold() for source in discovery.source_files}
|
|
196
|
+
seen_search = {path.casefold() for path in discovery.search_paths}
|
|
197
|
+
seen_include = {path.casefold() for path in discovery.include_paths}
|
|
198
|
+
|
|
199
|
+
_scan_sources(root_path, discovery, seen_sources)
|
|
200
|
+
for source in discovery.source_files:
|
|
201
|
+
path = Path(source)
|
|
202
|
+
unit_key = path.stem.casefold()
|
|
203
|
+
unit_paths = discovery.unit_paths.setdefault(unit_key, [])
|
|
204
|
+
if source not in unit_paths:
|
|
205
|
+
unit_paths.append(source)
|
|
206
|
+
if path.suffix.casefold() in {".pas", ".dpr", ".dpk"}:
|
|
207
|
+
_add_resolved_path(
|
|
208
|
+
discovery.search_paths,
|
|
209
|
+
seen_search,
|
|
210
|
+
discovery.search_path_origins,
|
|
211
|
+
str(path.parent),
|
|
212
|
+
base=root_path,
|
|
213
|
+
origin="workspace source scan",
|
|
214
|
+
discovery=discovery,
|
|
215
|
+
)
|
|
216
|
+
elif path.suffix.casefold() == ".inc":
|
|
217
|
+
_add_resolved_path(
|
|
218
|
+
discovery.include_paths,
|
|
219
|
+
seen_include,
|
|
220
|
+
discovery.include_path_origins,
|
|
221
|
+
str(path.parent),
|
|
222
|
+
base=root_path,
|
|
223
|
+
origin="workspace include scan",
|
|
224
|
+
discovery=discovery,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return discovery
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def discover_workspace_sources(root: str | os.PathLike[str]) -> DelphiProjectDiscovery:
|
|
231
|
+
root_path = Path(root).expanduser().resolve()
|
|
232
|
+
discovery = DelphiProjectDiscovery(root=str(root_path))
|
|
233
|
+
return populate_workspace_sources(discovery)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _project_candidates(root: Path, explicit: Path | None) -> list[Path]:
|
|
237
|
+
if explicit is not None:
|
|
238
|
+
return [explicit]
|
|
239
|
+
candidates: list[Path] = []
|
|
240
|
+
for ext in PROJECT_EXTENSIONS:
|
|
241
|
+
candidates.extend(_walk_sources(root, f"*{ext}"))
|
|
242
|
+
dproj_mains: list[Path] = []
|
|
243
|
+
for dproj in _walk_sources(root, "*.dproj"):
|
|
244
|
+
main = _main_source_from_dproj(dproj)
|
|
245
|
+
if main is not None:
|
|
246
|
+
dproj_mains.append((dproj.parent / main).resolve())
|
|
247
|
+
for candidate in [*dproj_mains, *candidates]:
|
|
248
|
+
if candidate.exists() and candidate.is_file() and candidate not in candidates:
|
|
249
|
+
candidates.append(candidate)
|
|
250
|
+
return sorted(candidates, key=lambda path: str(path).casefold())
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _read_dpr_paths(
|
|
254
|
+
project: Path,
|
|
255
|
+
discovery: DelphiProjectDiscovery,
|
|
256
|
+
search_paths: list[str],
|
|
257
|
+
seen_search: set[str],
|
|
258
|
+
search_path_origins: dict[str, list[str]],
|
|
259
|
+
) -> None:
|
|
260
|
+
try:
|
|
261
|
+
text = project.read_text(encoding="utf-8")
|
|
262
|
+
except UnicodeDecodeError:
|
|
263
|
+
text = project.read_text(encoding="latin-1")
|
|
264
|
+
except OSError as exc:
|
|
265
|
+
discovery.problems.append(DiscoveryProblem("cant_read_project", str(exc), str(project)))
|
|
266
|
+
return
|
|
267
|
+
for clause in _DPR_CLAUSE_RE.finditer(text):
|
|
268
|
+
body = clause.group("body")
|
|
269
|
+
for match in _DPR_UNIT_RE.finditer(body):
|
|
270
|
+
unit_path = match.group("path")
|
|
271
|
+
if not unit_path:
|
|
272
|
+
continue
|
|
273
|
+
resolved = _resolve_project_path(unit_path, base=project.parent, origin=str(project), discovery=discovery)
|
|
274
|
+
if resolved is None:
|
|
275
|
+
continue
|
|
276
|
+
_add_resolved_path(
|
|
277
|
+
search_paths,
|
|
278
|
+
seen_search,
|
|
279
|
+
search_path_origins,
|
|
280
|
+
str(resolved.parent),
|
|
281
|
+
base=project.parent,
|
|
282
|
+
origin=str(project),
|
|
283
|
+
discovery=discovery,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _read_dproj(
|
|
288
|
+
path: Path,
|
|
289
|
+
discovery: DelphiProjectDiscovery,
|
|
290
|
+
search_paths: list[str],
|
|
291
|
+
seen_search: set[str],
|
|
292
|
+
search_path_origins: dict[str, list[str]],
|
|
293
|
+
include_paths: list[str],
|
|
294
|
+
seen_include: set[str],
|
|
295
|
+
include_path_origins: dict[str, list[str]],
|
|
296
|
+
add_define,
|
|
297
|
+
) -> None:
|
|
298
|
+
try:
|
|
299
|
+
root = ET.parse(path).getroot()
|
|
300
|
+
except (ET.ParseError, OSError) as exc:
|
|
301
|
+
discovery.problems.append(DiscoveryProblem("cant_read_dproj", str(exc), str(path)))
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
for element in root.iter():
|
|
305
|
+
name = _xml_local_name(element.tag)
|
|
306
|
+
text = (element.text or "").strip()
|
|
307
|
+
if name in {"DCC_UnitSearchPath", "UnitSearchPath"} and text:
|
|
308
|
+
for item in _split_list(text):
|
|
309
|
+
_add_resolved_path(
|
|
310
|
+
search_paths,
|
|
311
|
+
seen_search,
|
|
312
|
+
search_path_origins,
|
|
313
|
+
item,
|
|
314
|
+
base=path.parent,
|
|
315
|
+
origin=str(path),
|
|
316
|
+
discovery=discovery,
|
|
317
|
+
)
|
|
318
|
+
elif name in {"DCC_IncludePath", "IncludePath"} and text:
|
|
319
|
+
for item in _split_list(text):
|
|
320
|
+
_add_resolved_path(
|
|
321
|
+
include_paths,
|
|
322
|
+
seen_include,
|
|
323
|
+
include_path_origins,
|
|
324
|
+
item,
|
|
325
|
+
base=path.parent,
|
|
326
|
+
origin=str(path),
|
|
327
|
+
discovery=discovery,
|
|
328
|
+
)
|
|
329
|
+
elif name in {"DCC_Define", "DefineConstants"} and text:
|
|
330
|
+
add_define(text, origin=str(path))
|
|
331
|
+
|
|
332
|
+
if name == "DCCReference":
|
|
333
|
+
include = element.attrib.get("Include") or element.attrib.get("include")
|
|
334
|
+
if include:
|
|
335
|
+
resolved = _resolve_project_path(include, base=path.parent, origin=str(path), discovery=discovery)
|
|
336
|
+
if resolved is not None:
|
|
337
|
+
_add_resolved_path(
|
|
338
|
+
search_paths,
|
|
339
|
+
seen_search,
|
|
340
|
+
search_path_origins,
|
|
341
|
+
str(resolved.parent),
|
|
342
|
+
base=path.parent,
|
|
343
|
+
origin=str(path),
|
|
344
|
+
discovery=discovery,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _read_cfg(
|
|
349
|
+
path: Path,
|
|
350
|
+
discovery: DelphiProjectDiscovery,
|
|
351
|
+
search_paths: list[str],
|
|
352
|
+
seen_search: set[str],
|
|
353
|
+
search_path_origins: dict[str, list[str]],
|
|
354
|
+
include_paths: list[str],
|
|
355
|
+
seen_include: set[str],
|
|
356
|
+
include_path_origins: dict[str, list[str]],
|
|
357
|
+
add_define,
|
|
358
|
+
) -> None:
|
|
359
|
+
try:
|
|
360
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
361
|
+
except UnicodeDecodeError:
|
|
362
|
+
lines = path.read_text(encoding="latin-1").splitlines()
|
|
363
|
+
except OSError as exc:
|
|
364
|
+
discovery.problems.append(DiscoveryProblem("cant_read_config", str(exc), str(path)))
|
|
365
|
+
return
|
|
366
|
+
for line in lines:
|
|
367
|
+
stripped = line.strip()
|
|
368
|
+
if not stripped or stripped.startswith("#"):
|
|
369
|
+
continue
|
|
370
|
+
match = _CFG_TOKEN_RE.match(stripped)
|
|
371
|
+
if match is None:
|
|
372
|
+
continue
|
|
373
|
+
option = match.group("option").casefold()
|
|
374
|
+
value = match.group("value")
|
|
375
|
+
if option == "-u":
|
|
376
|
+
for item in _split_list(value):
|
|
377
|
+
_add_resolved_path(
|
|
378
|
+
search_paths,
|
|
379
|
+
seen_search,
|
|
380
|
+
search_path_origins,
|
|
381
|
+
item,
|
|
382
|
+
base=path.parent,
|
|
383
|
+
origin=str(path),
|
|
384
|
+
discovery=discovery,
|
|
385
|
+
)
|
|
386
|
+
elif option == "-i":
|
|
387
|
+
for item in _split_list(value):
|
|
388
|
+
_add_resolved_path(
|
|
389
|
+
include_paths,
|
|
390
|
+
seen_include,
|
|
391
|
+
include_path_origins,
|
|
392
|
+
item,
|
|
393
|
+
base=path.parent,
|
|
394
|
+
origin=str(path),
|
|
395
|
+
discovery=discovery,
|
|
396
|
+
)
|
|
397
|
+
elif option == "-d":
|
|
398
|
+
add_define(value, origin=str(path))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _main_source_from_dproj(path: Path) -> str | None:
|
|
402
|
+
try:
|
|
403
|
+
root = ET.parse(path).getroot()
|
|
404
|
+
except (ET.ParseError, OSError):
|
|
405
|
+
return None
|
|
406
|
+
for element in root.iter():
|
|
407
|
+
if _xml_local_name(element.tag) == "MainSource" and element.text:
|
|
408
|
+
return element.text.strip()
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _scan_sources(root: Path, discovery: DelphiProjectDiscovery, seen_sources: set[str]) -> None:
|
|
413
|
+
for path in root.rglob("*"):
|
|
414
|
+
if path.suffix.casefold() not in SOURCE_EXTENSIONS:
|
|
415
|
+
continue
|
|
416
|
+
if any(part in SKIP_DIRS for part in path.parts):
|
|
417
|
+
continue
|
|
418
|
+
if not path.is_file():
|
|
419
|
+
continue
|
|
420
|
+
resolved = path.resolve()
|
|
421
|
+
key = str(resolved).casefold()
|
|
422
|
+
if key in seen_sources:
|
|
423
|
+
continue
|
|
424
|
+
seen_sources.add(key)
|
|
425
|
+
discovery.source_files.append(str(resolved))
|
|
426
|
+
discovery.source_files.sort(key=lambda item: (item.casefold(), item))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _walk_sources(root: Path, pattern: str) -> list[Path]:
|
|
430
|
+
results: list[Path] = []
|
|
431
|
+
for path in root.rglob(pattern):
|
|
432
|
+
if any(part in SKIP_DIRS for part in path.parts):
|
|
433
|
+
continue
|
|
434
|
+
if path.is_file():
|
|
435
|
+
results.append(path.resolve())
|
|
436
|
+
return results
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _add_resolved_path(
|
|
440
|
+
target: list[str],
|
|
441
|
+
seen: set[str],
|
|
442
|
+
origins: dict[str, list[str]],
|
|
443
|
+
value: str,
|
|
444
|
+
*,
|
|
445
|
+
base: Path,
|
|
446
|
+
origin: str,
|
|
447
|
+
discovery: DelphiProjectDiscovery,
|
|
448
|
+
) -> None:
|
|
449
|
+
resolved = _resolve_project_path(value, base=base, origin=origin, discovery=discovery)
|
|
450
|
+
if resolved is None:
|
|
451
|
+
return
|
|
452
|
+
key = str(resolved).casefold()
|
|
453
|
+
if key not in seen:
|
|
454
|
+
seen.add(key)
|
|
455
|
+
target.append(str(resolved))
|
|
456
|
+
exposed_path = next(item for item in target if item.casefold() == key)
|
|
457
|
+
_record_origin(origins, exposed_path, origin)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _record_origin(origins: dict[str, list[str]], key: str, origin: str) -> None:
|
|
461
|
+
entries = origins.setdefault(key, [])
|
|
462
|
+
if origin not in entries:
|
|
463
|
+
entries.append(origin)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _resolve_project_path(
|
|
467
|
+
raw: str,
|
|
468
|
+
*,
|
|
469
|
+
base: Path,
|
|
470
|
+
origin: str,
|
|
471
|
+
discovery: DelphiProjectDiscovery,
|
|
472
|
+
) -> Path | None:
|
|
473
|
+
cleaned = raw.strip().strip('"').strip("'")
|
|
474
|
+
if not cleaned:
|
|
475
|
+
return None
|
|
476
|
+
if os.name != "nt" and _WINDOWS_ABSOLUTE_RE.match(cleaned):
|
|
477
|
+
discovery.problems.append(
|
|
478
|
+
DiscoveryProblem("external_path", f"Skipping non-local Windows absolute path: {raw}", origin)
|
|
479
|
+
)
|
|
480
|
+
return None
|
|
481
|
+
normalized = cleaned.replace("\\", os.sep)
|
|
482
|
+
macros = _MACRO_RE.findall(normalized)
|
|
483
|
+
replacements = {
|
|
484
|
+
"PROJECTDIR": str(base),
|
|
485
|
+
"PROJECT_DIR": str(base),
|
|
486
|
+
"MSBUILDPROJECTDIRECTORY": str(base),
|
|
487
|
+
"MSBUILDTHISFILEDIRECTORY": str(base),
|
|
488
|
+
}
|
|
489
|
+
for macro in macros:
|
|
490
|
+
value = replacements.get(macro.upper())
|
|
491
|
+
if value is None:
|
|
492
|
+
discovery.problems.append(
|
|
493
|
+
DiscoveryProblem("unresolved_macro", f"Could not resolve $({macro}) in {raw}", origin)
|
|
494
|
+
)
|
|
495
|
+
return None
|
|
496
|
+
normalized = normalized.replace(f"$({macro})", value)
|
|
497
|
+
normalized = os.path.expandvars(normalized)
|
|
498
|
+
path = Path(normalized)
|
|
499
|
+
if not path.is_absolute():
|
|
500
|
+
path = base / path
|
|
501
|
+
return path.resolve()
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _split_list(raw: str) -> list[str]:
|
|
505
|
+
return [item.strip() for item in raw.replace("\n", ";").split(";") if item.strip()]
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _xml_local_name(tag: str) -> str:
|
|
509
|
+
return tag.rsplit("}", 1)[-1]
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
__all__ = [
|
|
513
|
+
"DelphiProjectDiscovery",
|
|
514
|
+
"DiscoveryProblem",
|
|
515
|
+
"discover_delphi_project",
|
|
516
|
+
"discover_workspace_sources",
|
|
517
|
+
"populate_workspace_sources",
|
|
518
|
+
]
|