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,1328 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import sys
|
|
10
|
+
import xml.etree.ElementTree as element_tree
|
|
11
|
+
from dataclasses import dataclass, replace
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Iterable, Iterator
|
|
14
|
+
|
|
15
|
+
import pathspec
|
|
16
|
+
|
|
17
|
+
if sys.version_info >= (3, 11):
|
|
18
|
+
import tomllib
|
|
19
|
+
else: # pragma: no cover - Python 3.10 dependency fallback
|
|
20
|
+
import tomli as tomllib
|
|
21
|
+
|
|
22
|
+
from .adapters import adapters_for
|
|
23
|
+
from .models import Language, Problem, Project
|
|
24
|
+
from .workspace import stable_id
|
|
25
|
+
|
|
26
|
+
_IGNORED = {
|
|
27
|
+
".git", ".hg", ".mypy_cache", ".pytest_cache", ".svn", ".tox", "__pycache__", "build", "dist", "node_modules",
|
|
28
|
+
"target", ".venv", "venv",
|
|
29
|
+
}
|
|
30
|
+
_MARKERS = {
|
|
31
|
+
"pyproject.toml": (Language.PYTHON, "python"), "setup.cfg": (Language.PYTHON, "python"),
|
|
32
|
+
"setup.py": (Language.PYTHON, "python"), "Cargo.toml": (Language.RUST, "rust"),
|
|
33
|
+
".sln": (Language.CSHARP, "csharp-solution"), ".csproj": (Language.CSHARP, "csharp"),
|
|
34
|
+
".dpr": (Language.DELPHI, "delphi"), ".dpk": (Language.DELPHI, "delphi-package"),
|
|
35
|
+
".dproj": (Language.DELPHI, "delphi"),
|
|
36
|
+
".groupproj": (Language.DELPHI, "delphi-group"), "compile_commands.json": (Language.CPP, "compile-commands"),
|
|
37
|
+
"CMakeLists.txt": (Language.CPP, "cmake"), ".vcxproj": (Language.CPP, "visual-cpp"),
|
|
38
|
+
}
|
|
39
|
+
_VARIABLE = re.compile(
|
|
40
|
+
r"\$\([^)]*\)|\$\{[^}]*\}|\$ENV\{[^}]*\}|\$(?![({<])[A-Za-z_][A-Za-z0-9_]*|%\([^)]*\)|%[^%]+%"
|
|
41
|
+
)
|
|
42
|
+
_GENERATOR_EXPRESSION = re.compile(r"\$<[^>]*>")
|
|
43
|
+
_CMAKE_CALL = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(")
|
|
44
|
+
_CMAKE_INCLUDE_MARKERS = {"AFTER", "BEFORE", "INTERFACE", "PRIVATE", "PUBLIC", "SYSTEM"}
|
|
45
|
+
_CMAKE_BRACKET_COMMENT_START = re.compile(r"#\[(?P<equals>=*)\[")
|
|
46
|
+
_CMAKE_ESCAPED_SEMICOLON = "\0"
|
|
47
|
+
_CMAKE_LIBRARY_KINDS = {"SHARED", "MODULE"}
|
|
48
|
+
_CMAKE_TARGET_MARKERS = {
|
|
49
|
+
"ALIAS", "EXCLUDE_FROM_ALL", "GLOBAL", "IMPORTED", "INTERFACE", "MODULE", "OBJECT", "SHARED", "STATIC", "UNKNOWN",
|
|
50
|
+
}
|
|
51
|
+
_DELPHI_IN = re.compile(r"\b(?:in|include)\s*['\"]([^'\"]+)['\"]", re.IGNORECASE)
|
|
52
|
+
_SOLUTION_PROJECT = re.compile(
|
|
53
|
+
r'^Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+)"',
|
|
54
|
+
re.MULTILINE,
|
|
55
|
+
)
|
|
56
|
+
_NAMED_PROJECT_SUFFIXES = {".csproj", ".dpk", ".dpr", ".dproj", ".groupproj", ".sln", ".vcxproj"}
|
|
57
|
+
_PROJECT_OWNERSHIP_PRIORITY = {
|
|
58
|
+
".csproj": 3,
|
|
59
|
+
".dpr": 3,
|
|
60
|
+
".dpk": 3,
|
|
61
|
+
".vcxproj": 3,
|
|
62
|
+
".dproj": 2,
|
|
63
|
+
".groupproj": 1,
|
|
64
|
+
".sln": 1,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class DiscoveredFile:
|
|
70
|
+
path: Path
|
|
71
|
+
language: Language
|
|
72
|
+
project_id: str
|
|
73
|
+
include_paths: tuple[Path, ...] = ()
|
|
74
|
+
output_names: tuple[str, ...] = ()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def name(self) -> str:
|
|
78
|
+
return self.path.name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class DiscoveryResult:
|
|
83
|
+
projects: tuple[Project, ...]
|
|
84
|
+
files: tuple[DiscoveredFile, ...]
|
|
85
|
+
problems: tuple[Problem, ...] = ()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class _Definition:
|
|
90
|
+
root: Path
|
|
91
|
+
language: Language
|
|
92
|
+
kind: str
|
|
93
|
+
metadata_path: Path
|
|
94
|
+
source_roots: tuple[Path, ...] = ()
|
|
95
|
+
source_files: tuple[Path, ...] = ()
|
|
96
|
+
include_paths: tuple[Path, ...] = ()
|
|
97
|
+
translation_unit_include_paths: tuple[tuple[Path, tuple[Path, ...]], ...] = ()
|
|
98
|
+
translation_unit_output_names: tuple[tuple[Path, tuple[str, ...]], ...] = ()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class _MetadataPath:
|
|
103
|
+
value: str
|
|
104
|
+
base: Path
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class _CompileCommand:
|
|
109
|
+
source_file: str
|
|
110
|
+
include_paths: tuple[_MetadataPath, ...]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class _CMakeConditionalFrame:
|
|
115
|
+
parent_active: bool
|
|
116
|
+
parent_possible: bool
|
|
117
|
+
branch_taken: bool
|
|
118
|
+
uncertain: bool
|
|
119
|
+
active: bool
|
|
120
|
+
possible: bool
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def discover_workspace(root: str | Path, languages: Iterable[Language] | None = None) -> DiscoveryResult:
|
|
124
|
+
workspace = Path(root).expanduser().resolve()
|
|
125
|
+
allowed = set(languages) if languages is not None else set(Language)
|
|
126
|
+
paths = tuple(_walk(workspace))
|
|
127
|
+
definitions, problems = _definitions(workspace, paths)
|
|
128
|
+
projects: dict[str, Project] = {}
|
|
129
|
+
for definition in definitions:
|
|
130
|
+
if (
|
|
131
|
+
definition.language in allowed
|
|
132
|
+
and definition.metadata_path.suffix.casefold() in _NAMED_PROJECT_SUFFIXES
|
|
133
|
+
):
|
|
134
|
+
project = _project_from_definition(definition, definitions)
|
|
135
|
+
projects[project.project_id] = _merge_project(projects.get(project.project_id), project)
|
|
136
|
+
files: list[DiscoveredFile] = []
|
|
137
|
+
for path in paths:
|
|
138
|
+
marker = _MARKERS.get(path.name) or _MARKERS.get(path.suffix.casefold())
|
|
139
|
+
if marker and path.suffix.casefold() not in {".dpk", ".dpr"}:
|
|
140
|
+
continue
|
|
141
|
+
detected = [language for language in _languages_for(path) if language in allowed]
|
|
142
|
+
if not detected:
|
|
143
|
+
continue
|
|
144
|
+
if _excluded_by_layout(path, definitions, detected[0]):
|
|
145
|
+
continue
|
|
146
|
+
project, ownership_problem = _owner(path, definitions, detected[0], workspace)
|
|
147
|
+
if ownership_problem is not None:
|
|
148
|
+
problems.append(ownership_problem)
|
|
149
|
+
project = _merge_project(projects.get(project.project_id), project)
|
|
150
|
+
projects[project.project_id] = project
|
|
151
|
+
files.append(
|
|
152
|
+
DiscoveredFile(
|
|
153
|
+
path,
|
|
154
|
+
detected[0],
|
|
155
|
+
project.project_id,
|
|
156
|
+
_source_include_paths(path, definitions, detected[0], project),
|
|
157
|
+
_source_output_names(path, definitions, detected[0], project),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return DiscoveryResult(
|
|
161
|
+
tuple(sorted(projects.values(), key=lambda item: (item.root, item.name, item.project_id))),
|
|
162
|
+
tuple(files),
|
|
163
|
+
tuple(problems),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def workspace_inventory(root: str | Path) -> tuple[tuple[str, int, int, str], ...]:
|
|
168
|
+
workspace = Path(root).expanduser().resolve()
|
|
169
|
+
tracked: list[tuple[str, int, int, str]] = []
|
|
170
|
+
for path in _walk(workspace):
|
|
171
|
+
relative = path.relative_to(workspace).as_posix()
|
|
172
|
+
if path.name != ".gitignore" and not (
|
|
173
|
+
_MARKERS.get(path.name) or _MARKERS.get(path.suffix.casefold()) or _languages_for(path)
|
|
174
|
+
):
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
stat = path.stat()
|
|
178
|
+
digest = (
|
|
179
|
+
""
|
|
180
|
+
if _languages_for(path)
|
|
181
|
+
else hashlib.sha256(path.read_bytes()).hexdigest()
|
|
182
|
+
)
|
|
183
|
+
except OSError:
|
|
184
|
+
continue
|
|
185
|
+
tracked.append((relative, stat.st_mtime_ns, stat.st_size, digest))
|
|
186
|
+
return tuple(sorted(tracked))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def workspace_directory_inventory(root: str | Path) -> tuple[tuple[str, int], ...]:
|
|
190
|
+
"""Capture included directory mtimes without following directory symlinks."""
|
|
191
|
+
workspace = Path(root).expanduser().resolve()
|
|
192
|
+
tracked: list[tuple[str, int]] = []
|
|
193
|
+
pending: list[tuple[Path, tuple[tuple[Path, pathspec.GitIgnoreSpec], ...]]] = [
|
|
194
|
+
(workspace, ())
|
|
195
|
+
]
|
|
196
|
+
while pending:
|
|
197
|
+
directory, inherited_rules = pending.pop()
|
|
198
|
+
try:
|
|
199
|
+
stat = directory.stat()
|
|
200
|
+
entries = sorted(directory.iterdir(), reverse=True)
|
|
201
|
+
except OSError:
|
|
202
|
+
continue
|
|
203
|
+
relative = directory.relative_to(workspace).as_posix()
|
|
204
|
+
tracked.append((relative or ".", stat.st_mtime_ns))
|
|
205
|
+
local_ignore = _gitignore(directory)
|
|
206
|
+
rules = inherited_rules + (((directory, local_ignore),) if local_ignore is not None else ())
|
|
207
|
+
for path in entries:
|
|
208
|
+
path_relative = path.relative_to(workspace)
|
|
209
|
+
if any(part in _IGNORED for part in path_relative.parts):
|
|
210
|
+
continue
|
|
211
|
+
if (
|
|
212
|
+
path.is_dir()
|
|
213
|
+
and not path.is_symlink()
|
|
214
|
+
and not _ignored_by_rules(path, rules, directory=True)
|
|
215
|
+
):
|
|
216
|
+
pending.append((path, rules))
|
|
217
|
+
return tuple(sorted(tracked))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _walk(root: Path) -> Iterator[Path]:
|
|
221
|
+
"""Yield included regular files without following directory symlinks."""
|
|
222
|
+
pending: list[tuple[Path, tuple[tuple[Path, pathspec.GitIgnoreSpec], ...]]] = [(root, ())]
|
|
223
|
+
while pending:
|
|
224
|
+
directory, inherited_rules = pending.pop()
|
|
225
|
+
try:
|
|
226
|
+
local_ignore = _gitignore(directory)
|
|
227
|
+
entries = sorted(directory.iterdir(), reverse=True)
|
|
228
|
+
except OSError:
|
|
229
|
+
continue
|
|
230
|
+
rules = inherited_rules + (((directory, local_ignore),) if local_ignore is not None else ())
|
|
231
|
+
for path in entries:
|
|
232
|
+
relative = path.relative_to(root)
|
|
233
|
+
if any(part in _IGNORED for part in relative.parts):
|
|
234
|
+
continue
|
|
235
|
+
if path.is_dir():
|
|
236
|
+
if not path.is_symlink() and not _ignored_by_rules(path, rules, directory=True):
|
|
237
|
+
pending.append((path, rules))
|
|
238
|
+
continue
|
|
239
|
+
if _included(root, path, rules):
|
|
240
|
+
yield path
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _included(
|
|
244
|
+
root: Path,
|
|
245
|
+
path: Path,
|
|
246
|
+
rules: tuple[tuple[Path, pathspec.GitIgnoreSpec], ...],
|
|
247
|
+
) -> bool:
|
|
248
|
+
if not path.is_file():
|
|
249
|
+
return False
|
|
250
|
+
try:
|
|
251
|
+
path.resolve().relative_to(root)
|
|
252
|
+
except ValueError:
|
|
253
|
+
return False
|
|
254
|
+
if path.name == ".gitignore":
|
|
255
|
+
return True
|
|
256
|
+
return not _ignored_by_rules(path, rules)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _ignored_by_rules(
|
|
260
|
+
path: Path,
|
|
261
|
+
rules: tuple[tuple[Path, pathspec.GitIgnoreSpec], ...],
|
|
262
|
+
*,
|
|
263
|
+
directory: bool = False,
|
|
264
|
+
) -> bool:
|
|
265
|
+
ignored = False
|
|
266
|
+
for base, rule in rules:
|
|
267
|
+
relative = path.relative_to(base).as_posix()
|
|
268
|
+
if directory:
|
|
269
|
+
relative += "/"
|
|
270
|
+
decision = rule.check_file(relative).include
|
|
271
|
+
if decision is not None:
|
|
272
|
+
ignored = decision
|
|
273
|
+
return ignored
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _gitignore(root: Path) -> pathspec.GitIgnoreSpec | None:
|
|
277
|
+
path = root / ".gitignore"
|
|
278
|
+
return pathspec.GitIgnoreSpec.from_lines(path.read_text(errors="replace").splitlines()) if path.is_file() else None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _definitions(root: Path, paths: tuple[Path, ...]) -> tuple[list[_Definition], list[Problem]]:
|
|
282
|
+
definitions: list[_Definition] = []
|
|
283
|
+
problems: list[Problem] = []
|
|
284
|
+
for path in paths:
|
|
285
|
+
marker = _MARKERS.get(path.name) or _MARKERS.get(path.suffix.casefold())
|
|
286
|
+
if marker is None:
|
|
287
|
+
continue
|
|
288
|
+
language, kind = marker
|
|
289
|
+
definition, reported = _parse_definition(root, path, language, kind)
|
|
290
|
+
definitions.append(definition)
|
|
291
|
+
problems.extend(reported)
|
|
292
|
+
return definitions, problems
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _parse_definition(workspace: Path, path: Path, language: Language, kind: str) -> tuple[_Definition, list[Problem]]:
|
|
296
|
+
root = path.parent
|
|
297
|
+
roots: list[str] = []
|
|
298
|
+
source_files: list[str] = []
|
|
299
|
+
include_paths: list[_MetadataPath] = []
|
|
300
|
+
compile_commands: list[_CompileCommand] = []
|
|
301
|
+
output_mappings: list[tuple[str, str]] = []
|
|
302
|
+
metadata_problems: list[Problem] = []
|
|
303
|
+
if path.name == "pyproject.toml":
|
|
304
|
+
roots = _pyproject_roots(path)
|
|
305
|
+
elif path.name == "setup.cfg":
|
|
306
|
+
roots = _setup_cfg_roots(path)
|
|
307
|
+
elif path.name == "Cargo.toml":
|
|
308
|
+
roots, source_files, output_mappings = _cargo_sources(path)
|
|
309
|
+
elif path.suffix.casefold() == ".sln":
|
|
310
|
+
source_files, metadata_problems = _solution_sources(workspace, path)
|
|
311
|
+
elif path.suffix.casefold() in {".csproj", ".vcxproj"}:
|
|
312
|
+
source_files = _xml_includes(path, {"Compile", "ClCompile", "ClInclude"})
|
|
313
|
+
if path.suffix.casefold() == ".vcxproj":
|
|
314
|
+
include_paths = _vcxproj_include_paths(path)
|
|
315
|
+
elif path.suffix.casefold() in {".dpk", ".dpr", ".dproj", ".groupproj"}:
|
|
316
|
+
roots, source_files = _delphi_sources(path)
|
|
317
|
+
if path.suffix.casefold() in {".dpk", ".dpr"}:
|
|
318
|
+
source_files.insert(0, path.name)
|
|
319
|
+
elif path.name == "compile_commands.json":
|
|
320
|
+
compile_commands = _compile_commands(path)
|
|
321
|
+
elif path.name == "CMakeLists.txt":
|
|
322
|
+
source_files, include_paths, output_mappings, uncertain_conditions = _cmake_metadata(path)
|
|
323
|
+
metadata_problems.extend(
|
|
324
|
+
_cmake_condition_problem(condition, path) for condition in uncertain_conditions
|
|
325
|
+
)
|
|
326
|
+
definition_roots, root_problems = _resolve_roots(workspace, root, language, path, roots)
|
|
327
|
+
translation_unit_includes: list[tuple[Path, tuple[Path, ...]]] = []
|
|
328
|
+
if compile_commands:
|
|
329
|
+
definition_files, translation_unit_includes, compile_problems = _resolve_compile_commands(
|
|
330
|
+
workspace,
|
|
331
|
+
language,
|
|
332
|
+
path,
|
|
333
|
+
compile_commands,
|
|
334
|
+
)
|
|
335
|
+
definition_includes: list[Path] = []
|
|
336
|
+
file_problems = compile_problems
|
|
337
|
+
include_problems: list[Problem] = []
|
|
338
|
+
else:
|
|
339
|
+
definition_files, file_problems = _resolve_files(workspace, root, language, path, source_files)
|
|
340
|
+
definition_includes, include_problems = _resolve_include_paths(workspace, language, path, include_paths)
|
|
341
|
+
output_names_by_file: dict[Path, list[str]] = {}
|
|
342
|
+
for output_name, source_file in output_mappings:
|
|
343
|
+
if _has_unresolved_static_value(output_name):
|
|
344
|
+
metadata_problems.append(_output_problem(output_name, language, path))
|
|
345
|
+
continue
|
|
346
|
+
expanded = _expand_project_variables(source_file, root)
|
|
347
|
+
if _has_unresolved_static_value(expanded):
|
|
348
|
+
continue
|
|
349
|
+
candidate = Path(expanded)
|
|
350
|
+
candidate = (candidate if candidate.is_absolute() else root / candidate).resolve()
|
|
351
|
+
if candidate not in definition_files and not any(
|
|
352
|
+
candidate.is_relative_to(source_root) for source_root in definition_roots
|
|
353
|
+
):
|
|
354
|
+
continue
|
|
355
|
+
names = output_names_by_file.setdefault(candidate, [])
|
|
356
|
+
if output_name not in names:
|
|
357
|
+
names.append(output_name)
|
|
358
|
+
return (
|
|
359
|
+
_Definition(
|
|
360
|
+
root,
|
|
361
|
+
language,
|
|
362
|
+
kind,
|
|
363
|
+
path,
|
|
364
|
+
tuple(definition_roots),
|
|
365
|
+
tuple(definition_files),
|
|
366
|
+
tuple(definition_includes),
|
|
367
|
+
tuple(translation_unit_includes),
|
|
368
|
+
tuple((source_file, tuple(names)) for source_file, names in output_names_by_file.items()),
|
|
369
|
+
),
|
|
370
|
+
root_problems + file_problems + include_problems + metadata_problems,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _pyproject_roots(path: Path) -> list[str]:
|
|
375
|
+
try:
|
|
376
|
+
data = tomllib.loads(path.read_text(errors="replace"))
|
|
377
|
+
except (OSError, ValueError):
|
|
378
|
+
return []
|
|
379
|
+
tool = data.get("tool", {})
|
|
380
|
+
setuptools = tool.get("setuptools", {}) if isinstance(tool, dict) else {}
|
|
381
|
+
result: list[str] = []
|
|
382
|
+
package_dir = setuptools.get("package-dir", {}) if isinstance(setuptools, dict) else {}
|
|
383
|
+
if isinstance(package_dir, dict):
|
|
384
|
+
result.extend(value for value in package_dir.values() if isinstance(value, str))
|
|
385
|
+
packages = setuptools.get("packages", {}) if isinstance(setuptools, dict) else {}
|
|
386
|
+
find = packages.get("find", {}) if isinstance(packages, dict) else {}
|
|
387
|
+
if isinstance(find, dict):
|
|
388
|
+
result.extend(value for value in find.get("where", []) if isinstance(value, str))
|
|
389
|
+
poetry = tool.get("poetry", {}) if isinstance(tool, dict) else {}
|
|
390
|
+
if isinstance(poetry, dict):
|
|
391
|
+
result.extend(item["from"] for item in poetry.get("packages", []) if isinstance(item, dict) and isinstance(item.get("from"), str))
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _setup_cfg_roots(path: Path) -> list[str]:
|
|
396
|
+
parser = configparser.ConfigParser()
|
|
397
|
+
try:
|
|
398
|
+
parser.read_string(path.read_text(errors="replace"))
|
|
399
|
+
except (configparser.Error, OSError):
|
|
400
|
+
return []
|
|
401
|
+
result: list[str] = []
|
|
402
|
+
if parser.has_option("options", "package_dir"):
|
|
403
|
+
result.extend(line.split("=", 1)[1].strip() for line in parser.get("options", "package_dir").splitlines() if "=" in line)
|
|
404
|
+
if parser.has_option("options.packages.find", "where"):
|
|
405
|
+
result.extend(value.strip() for value in parser.get("options.packages.find", "where").splitlines() if value.strip())
|
|
406
|
+
return result
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _cargo_sources(path: Path) -> tuple[list[str], list[str], list[tuple[str, str]]]:
|
|
410
|
+
try:
|
|
411
|
+
data = tomllib.loads(path.read_text(errors="replace"))
|
|
412
|
+
except (OSError, ValueError):
|
|
413
|
+
return [], [], []
|
|
414
|
+
package = data.get("package")
|
|
415
|
+
if not isinstance(package, dict):
|
|
416
|
+
return [], [], []
|
|
417
|
+
files: list[str] = []
|
|
418
|
+
output_mappings: list[tuple[str, str]] = []
|
|
419
|
+
lib = data.get("lib", {})
|
|
420
|
+
if isinstance(lib, dict):
|
|
421
|
+
library_path = lib.get("path")
|
|
422
|
+
if isinstance(library_path, str):
|
|
423
|
+
files.append(library_path)
|
|
424
|
+
else:
|
|
425
|
+
library_path = "src/lib.rs"
|
|
426
|
+
crate_types = lib.get("crate-type", [])
|
|
427
|
+
if isinstance(crate_types, str):
|
|
428
|
+
crate_types = [crate_types]
|
|
429
|
+
if (
|
|
430
|
+
isinstance(crate_types, list)
|
|
431
|
+
and any(item in {"cdylib", "dylib"} for item in crate_types)
|
|
432
|
+
and isinstance(package.get("name"), str)
|
|
433
|
+
):
|
|
434
|
+
library_name = lib.get("name", package["name"])
|
|
435
|
+
if isinstance(library_name, str):
|
|
436
|
+
output_mappings.append((library_name.replace("-", "_"), library_path))
|
|
437
|
+
bins = data.get("bin", [])
|
|
438
|
+
if isinstance(bins, list):
|
|
439
|
+
files.extend(
|
|
440
|
+
item["path"]
|
|
441
|
+
for item in bins
|
|
442
|
+
if isinstance(item, dict) and isinstance(item.get("path"), str)
|
|
443
|
+
)
|
|
444
|
+
sources = ["src"] if (path.parent / "src").is_dir() or not files else []
|
|
445
|
+
return sources, files, output_mappings
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _solution_sources(workspace: Path, path: Path) -> tuple[list[str], list[Problem]]:
|
|
449
|
+
try:
|
|
450
|
+
project_files = _SOLUTION_PROJECT.findall(path.read_text(errors="replace"))
|
|
451
|
+
except OSError:
|
|
452
|
+
return [], []
|
|
453
|
+
sources: list[str] = []
|
|
454
|
+
problems: list[Problem] = []
|
|
455
|
+
for project_file in project_files:
|
|
456
|
+
expanded = _expand_project_variables(project_file, path.parent)
|
|
457
|
+
if _has_unresolved_static_value(expanded):
|
|
458
|
+
problems.append(_project_file_problem("unresolved-project-file", project_file, path))
|
|
459
|
+
continue
|
|
460
|
+
candidate = Path(expanded)
|
|
461
|
+
candidate = candidate if candidate.is_absolute() else path.parent / candidate
|
|
462
|
+
candidate = candidate.resolve()
|
|
463
|
+
try:
|
|
464
|
+
candidate.relative_to(workspace)
|
|
465
|
+
except ValueError:
|
|
466
|
+
problems.append(_project_file_problem("workspace-escaping-project-file", project_file, path))
|
|
467
|
+
continue
|
|
468
|
+
if candidate.suffix.casefold() != ".csproj":
|
|
469
|
+
continue
|
|
470
|
+
if not candidate.is_file():
|
|
471
|
+
problems.append(_project_file_problem("missing-project-file", project_file, path))
|
|
472
|
+
continue
|
|
473
|
+
for source_file in _xml_includes(candidate, {"Compile"}):
|
|
474
|
+
expanded_source = _expand_project_variables(source_file, candidate.parent)
|
|
475
|
+
if _has_unresolved_static_value(expanded_source):
|
|
476
|
+
problems.append(_problem("unresolved-source-file", source_file, Language.CSHARP, candidate))
|
|
477
|
+
continue
|
|
478
|
+
source_path = Path(expanded_source)
|
|
479
|
+
source_path = source_path if source_path.is_absolute() else candidate.parent / source_path
|
|
480
|
+
normalized = str(source_path.resolve())
|
|
481
|
+
if normalized not in sources:
|
|
482
|
+
sources.append(normalized)
|
|
483
|
+
return sources, problems
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _xml_includes(path: Path, names: set[str]) -> list[str]:
|
|
487
|
+
try:
|
|
488
|
+
tree = element_tree.parse(path)
|
|
489
|
+
except (OSError, element_tree.ParseError):
|
|
490
|
+
return []
|
|
491
|
+
return [element.attrib["Include"] for element in tree.iter() if element.tag.split("}")[-1] in names and "Include" in element.attrib]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _vcxproj_include_paths(path: Path) -> list[_MetadataPath]:
|
|
495
|
+
try:
|
|
496
|
+
tree = element_tree.parse(path)
|
|
497
|
+
except (OSError, element_tree.ParseError):
|
|
498
|
+
return []
|
|
499
|
+
result: list[_MetadataPath] = []
|
|
500
|
+
for element in tree.iter():
|
|
501
|
+
if element.tag.split("}")[-1] != "AdditionalIncludeDirectories" or not element.text:
|
|
502
|
+
continue
|
|
503
|
+
result.extend(
|
|
504
|
+
_MetadataPath(value.strip(), path.parent)
|
|
505
|
+
for value in element.text.split(";")
|
|
506
|
+
if value.strip()
|
|
507
|
+
)
|
|
508
|
+
return result
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _delphi_sources(path: Path) -> tuple[list[str], list[str]]:
|
|
512
|
+
text = path.read_text(errors="replace")
|
|
513
|
+
source_files = (
|
|
514
|
+
_delphi_project_sources(text)
|
|
515
|
+
if path.suffix.casefold() in {".dpk", ".dpr"}
|
|
516
|
+
else _DELPHI_IN.findall(text)
|
|
517
|
+
)
|
|
518
|
+
roots: list[str] = []
|
|
519
|
+
if path.suffix.casefold() in {".dproj", ".groupproj"}:
|
|
520
|
+
try:
|
|
521
|
+
tree = element_tree.fromstring(text)
|
|
522
|
+
except element_tree.ParseError:
|
|
523
|
+
return roots, source_files
|
|
524
|
+
for element in tree.iter():
|
|
525
|
+
if element.tag.split("}")[-1] in {"DCC_UnitSearchPath", "DCC_IncludePath", "DCC_ObjPath", "DCC_SourcePath"} and element.text:
|
|
526
|
+
roots.extend(part.strip() for part in element.text.split(";") if part.strip())
|
|
527
|
+
return roots, source_files
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _delphi_project_sources(text: str) -> list[str]:
|
|
531
|
+
sources: list[str] = []
|
|
532
|
+
position = 0
|
|
533
|
+
while position < len(text):
|
|
534
|
+
if text.startswith("//", position):
|
|
535
|
+
newline = text.find("\n", position + 2)
|
|
536
|
+
position = len(text) if newline < 0 else newline + 1
|
|
537
|
+
continue
|
|
538
|
+
if text.startswith("(*", position):
|
|
539
|
+
closing = text.find("*)", position + 2)
|
|
540
|
+
position = len(text) if closing < 0 else closing + 2
|
|
541
|
+
continue
|
|
542
|
+
if text[position] == "{":
|
|
543
|
+
closing = text.find("}", position + 1)
|
|
544
|
+
position = len(text) if closing < 0 else closing + 1
|
|
545
|
+
continue
|
|
546
|
+
if text[position] in {"'", '"'}:
|
|
547
|
+
_, position = _delphi_quoted_value(text, position)
|
|
548
|
+
continue
|
|
549
|
+
if text[position].isalpha() or text[position] == "_":
|
|
550
|
+
end = position + 1
|
|
551
|
+
while end < len(text) and (text[end].isalnum() or text[end] == "_"):
|
|
552
|
+
end += 1
|
|
553
|
+
keyword = text[position:end].casefold()
|
|
554
|
+
value_start = end
|
|
555
|
+
while value_start < len(text) and text[value_start].isspace():
|
|
556
|
+
value_start += 1
|
|
557
|
+
if keyword in {"in", "include"} and value_start < len(text) and text[value_start] in {"'", '"'}:
|
|
558
|
+
value, position = _delphi_quoted_value(text, value_start)
|
|
559
|
+
if value:
|
|
560
|
+
sources.append(value)
|
|
561
|
+
continue
|
|
562
|
+
position = end
|
|
563
|
+
continue
|
|
564
|
+
position += 1
|
|
565
|
+
return sources
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _delphi_quoted_value(text: str, position: int) -> tuple[str, int]:
|
|
569
|
+
quote = text[position]
|
|
570
|
+
value: list[str] = []
|
|
571
|
+
position += 1
|
|
572
|
+
while position < len(text):
|
|
573
|
+
character = text[position]
|
|
574
|
+
if character != quote:
|
|
575
|
+
value.append(character)
|
|
576
|
+
position += 1
|
|
577
|
+
continue
|
|
578
|
+
if position + 1 < len(text) and text[position + 1] == quote:
|
|
579
|
+
value.append(quote)
|
|
580
|
+
position += 2
|
|
581
|
+
continue
|
|
582
|
+
return "".join(value), position + 1
|
|
583
|
+
return "".join(value), position
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _compile_commands(path: Path) -> list[_CompileCommand]:
|
|
587
|
+
try:
|
|
588
|
+
data = json.loads(path.read_text(errors="replace"))
|
|
589
|
+
except (OSError, ValueError):
|
|
590
|
+
return []
|
|
591
|
+
if not isinstance(data, list):
|
|
592
|
+
return []
|
|
593
|
+
commands: list[_CompileCommand] = []
|
|
594
|
+
for item in data:
|
|
595
|
+
if not isinstance(item, dict):
|
|
596
|
+
continue
|
|
597
|
+
directory_value = item.get("directory", str(path.parent))
|
|
598
|
+
directory = Path(directory_value) if isinstance(directory_value, str) else path.parent
|
|
599
|
+
directory = directory if directory.is_absolute() else path.parent / directory
|
|
600
|
+
source_file = item.get("file")
|
|
601
|
+
if not isinstance(source_file, str):
|
|
602
|
+
continue
|
|
603
|
+
source_path = Path(source_file)
|
|
604
|
+
source_path = source_path if source_path.is_absolute() else directory / source_path
|
|
605
|
+
commands.append(
|
|
606
|
+
_CompileCommand(
|
|
607
|
+
str(source_path),
|
|
608
|
+
tuple(_MetadataPath(value, directory) for value in _compile_include_paths(item)),
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
return commands
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _compile_include_paths(
|
|
615
|
+
item: dict[str, object],
|
|
616
|
+
*,
|
|
617
|
+
windows: bool | None = None,
|
|
618
|
+
) -> list[str]:
|
|
619
|
+
raw_arguments = item.get("arguments")
|
|
620
|
+
if isinstance(raw_arguments, list) and all(isinstance(value, str) for value in raw_arguments):
|
|
621
|
+
arguments = list(raw_arguments)
|
|
622
|
+
else:
|
|
623
|
+
command = item.get("command")
|
|
624
|
+
if not isinstance(command, str):
|
|
625
|
+
return []
|
|
626
|
+
try:
|
|
627
|
+
arguments = _split_command(command, windows=windows)
|
|
628
|
+
except ValueError:
|
|
629
|
+
return []
|
|
630
|
+
result: list[str] = []
|
|
631
|
+
index = 0
|
|
632
|
+
while index < len(arguments):
|
|
633
|
+
argument = _unquote_argument(arguments[index])
|
|
634
|
+
if argument in {"-I", "-isystem", "/I"}:
|
|
635
|
+
index += 1
|
|
636
|
+
if index < len(arguments):
|
|
637
|
+
value = _unquote_argument(arguments[index])
|
|
638
|
+
if value:
|
|
639
|
+
result.append(value)
|
|
640
|
+
elif argument.startswith("-isystem") and len(argument) > len("-isystem"):
|
|
641
|
+
value = _unquote_argument(argument[len("-isystem"):])
|
|
642
|
+
if value:
|
|
643
|
+
result.append(value)
|
|
644
|
+
elif argument.startswith("-I") and len(argument) > 2:
|
|
645
|
+
value = _unquote_argument(argument[2:])
|
|
646
|
+
if value:
|
|
647
|
+
result.append(value)
|
|
648
|
+
elif argument.startswith("/I") and len(argument) > 2:
|
|
649
|
+
value = _unquote_argument(argument[2:])
|
|
650
|
+
if value:
|
|
651
|
+
result.append(value)
|
|
652
|
+
index += 1
|
|
653
|
+
return result
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _split_command(command: str, *, windows: bool | None = None) -> list[str]:
|
|
657
|
+
use_windows_rules = os.name == "nt" if windows is None else windows
|
|
658
|
+
arguments = shlex.split(command, posix=not use_windows_rules)
|
|
659
|
+
if not use_windows_rules:
|
|
660
|
+
return arguments
|
|
661
|
+
result: list[str] = []
|
|
662
|
+
pending: list[str] = []
|
|
663
|
+
quoted = False
|
|
664
|
+
for argument in arguments:
|
|
665
|
+
pending.append(argument)
|
|
666
|
+
if argument.count('"') % 2:
|
|
667
|
+
quoted = not quoted
|
|
668
|
+
if not quoted:
|
|
669
|
+
result.append(" ".join(pending))
|
|
670
|
+
pending = []
|
|
671
|
+
result.extend(pending)
|
|
672
|
+
return result
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _unquote_argument(value: str) -> str:
|
|
676
|
+
value = value.strip()
|
|
677
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
678
|
+
return value[1:-1]
|
|
679
|
+
return value
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _cmake_metadata(
|
|
683
|
+
path: Path,
|
|
684
|
+
) -> tuple[list[str], list[_MetadataPath], list[tuple[str, str]], list[str]]:
|
|
685
|
+
sources: list[str] = []
|
|
686
|
+
include_paths: list[_MetadataPath] = []
|
|
687
|
+
loadable_targets: list[tuple[str, list[str]]] = []
|
|
688
|
+
output_names: dict[str, str] = {}
|
|
689
|
+
uncertain_conditions: list[str] = []
|
|
690
|
+
conditional_frames: list[_CMakeConditionalFrame] = []
|
|
691
|
+
opaque_blocks: list[str] = []
|
|
692
|
+
opaque_openers = {
|
|
693
|
+
"foreach": "endforeach",
|
|
694
|
+
"function": "endfunction",
|
|
695
|
+
"macro": "endmacro",
|
|
696
|
+
"while": "endwhile",
|
|
697
|
+
}
|
|
698
|
+
for name, arguments in _cmake_calls(path.read_text(errors="replace")):
|
|
699
|
+
try:
|
|
700
|
+
tokens = shlex.split(
|
|
701
|
+
arguments.replace(r"\;", _CMAKE_ESCAPED_SEMICOLON),
|
|
702
|
+
comments=False,
|
|
703
|
+
posix=True,
|
|
704
|
+
)
|
|
705
|
+
except ValueError:
|
|
706
|
+
continue
|
|
707
|
+
if opaque_blocks:
|
|
708
|
+
if name in opaque_openers:
|
|
709
|
+
opaque_blocks.append(opaque_openers[name])
|
|
710
|
+
elif name == opaque_blocks[-1]:
|
|
711
|
+
opaque_blocks.pop()
|
|
712
|
+
continue
|
|
713
|
+
if name in opaque_openers:
|
|
714
|
+
opaque_blocks.append(opaque_openers[name])
|
|
715
|
+
continue
|
|
716
|
+
if name == "if":
|
|
717
|
+
parent_active = all(frame.active for frame in conditional_frames)
|
|
718
|
+
parent_possible = all(frame.possible for frame in conditional_frames)
|
|
719
|
+
condition = _cmake_static_condition(tokens)
|
|
720
|
+
if parent_possible and condition is None and arguments.strip() not in uncertain_conditions:
|
|
721
|
+
uncertain_conditions.append(arguments.strip())
|
|
722
|
+
conditional_frames.append(
|
|
723
|
+
_CMakeConditionalFrame(
|
|
724
|
+
parent_active,
|
|
725
|
+
parent_possible,
|
|
726
|
+
condition is True,
|
|
727
|
+
condition is None,
|
|
728
|
+
parent_active and condition is True,
|
|
729
|
+
parent_possible and condition is not False,
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
continue
|
|
733
|
+
if name == "elseif":
|
|
734
|
+
if conditional_frames:
|
|
735
|
+
frame = conditional_frames[-1]
|
|
736
|
+
condition = _cmake_static_condition(tokens)
|
|
737
|
+
if (
|
|
738
|
+
frame.parent_possible
|
|
739
|
+
and not frame.branch_taken
|
|
740
|
+
and condition is None
|
|
741
|
+
and arguments.strip() not in uncertain_conditions
|
|
742
|
+
):
|
|
743
|
+
uncertain_conditions.append(arguments.strip())
|
|
744
|
+
frame.active = (
|
|
745
|
+
frame.parent_active
|
|
746
|
+
and not frame.branch_taken
|
|
747
|
+
and not frame.uncertain
|
|
748
|
+
and condition is True
|
|
749
|
+
)
|
|
750
|
+
frame.possible = (
|
|
751
|
+
frame.parent_possible
|
|
752
|
+
and not frame.branch_taken
|
|
753
|
+
and condition is not False
|
|
754
|
+
)
|
|
755
|
+
frame.branch_taken = frame.branch_taken or condition is True
|
|
756
|
+
frame.uncertain = frame.uncertain or condition is None
|
|
757
|
+
continue
|
|
758
|
+
if name == "else":
|
|
759
|
+
if conditional_frames:
|
|
760
|
+
frame = conditional_frames[-1]
|
|
761
|
+
frame.active = frame.parent_active and not frame.branch_taken and not frame.uncertain
|
|
762
|
+
frame.possible = frame.parent_possible and not frame.branch_taken
|
|
763
|
+
frame.branch_taken = True
|
|
764
|
+
continue
|
|
765
|
+
if name == "endif":
|
|
766
|
+
if conditional_frames:
|
|
767
|
+
conditional_frames.pop()
|
|
768
|
+
continue
|
|
769
|
+
possible = all(frame.possible for frame in conditional_frames)
|
|
770
|
+
if not possible:
|
|
771
|
+
continue
|
|
772
|
+
active = all(frame.active for frame in conditional_frames)
|
|
773
|
+
if name in {"add_executable", "add_library"}:
|
|
774
|
+
output_name = tokens[0].replace(_CMAKE_ESCAPED_SEMICOLON, ";") if tokens else ""
|
|
775
|
+
target_kind = next(
|
|
776
|
+
(
|
|
777
|
+
item.upper()
|
|
778
|
+
for item in tokens[1:]
|
|
779
|
+
if item.upper() in _CMAKE_TARGET_MARKERS
|
|
780
|
+
),
|
|
781
|
+
"",
|
|
782
|
+
)
|
|
783
|
+
target_sources = [
|
|
784
|
+
item
|
|
785
|
+
for item in _cmake_list_items(tokens[1:])
|
|
786
|
+
if item.upper() not in _CMAKE_TARGET_MARKERS
|
|
787
|
+
and (
|
|
788
|
+
_has_unresolved_static_value(item)
|
|
789
|
+
or Path(item).suffix.casefold()
|
|
790
|
+
in {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"}
|
|
791
|
+
)
|
|
792
|
+
]
|
|
793
|
+
sources.extend(target_sources)
|
|
794
|
+
if active and name == "add_library" and target_kind in _CMAKE_LIBRARY_KINDS:
|
|
795
|
+
loadable_targets.append((output_name, target_sources))
|
|
796
|
+
elif name == "set_target_properties":
|
|
797
|
+
properties_index = next(
|
|
798
|
+
(index for index, item in enumerate(tokens) if item.upper() == "PROPERTIES"),
|
|
799
|
+
-1,
|
|
800
|
+
)
|
|
801
|
+
if properties_index > 0:
|
|
802
|
+
targets = tokens[:properties_index]
|
|
803
|
+
properties = tokens[properties_index + 1 :]
|
|
804
|
+
for index in range(0, len(properties) - 1, 2):
|
|
805
|
+
if properties[index].upper() == "OUTPUT_NAME":
|
|
806
|
+
for target in targets:
|
|
807
|
+
output_names[target] = (
|
|
808
|
+
properties[index + 1]
|
|
809
|
+
if active
|
|
810
|
+
else "${OUTPUT_NAME_DEPENDS_ON_CMAKE_CONDITION}"
|
|
811
|
+
)
|
|
812
|
+
elif name == "set_property":
|
|
813
|
+
property_index = next(
|
|
814
|
+
(index for index, item in enumerate(tokens) if item.upper() == "PROPERTY"),
|
|
815
|
+
-1,
|
|
816
|
+
)
|
|
817
|
+
if (
|
|
818
|
+
tokens
|
|
819
|
+
and tokens[0].upper() == "TARGET"
|
|
820
|
+
and property_index > 1
|
|
821
|
+
and len(tokens) > property_index + 2
|
|
822
|
+
and tokens[property_index + 1].upper() == "OUTPUT_NAME"
|
|
823
|
+
):
|
|
824
|
+
targets = [
|
|
825
|
+
target
|
|
826
|
+
for target in tokens[1:property_index]
|
|
827
|
+
if target.upper() not in {"APPEND", "APPEND_STRING"}
|
|
828
|
+
]
|
|
829
|
+
modifiers = {item.upper() for item in tokens[1:property_index]}
|
|
830
|
+
values = tokens[property_index + 2 :]
|
|
831
|
+
for target in targets:
|
|
832
|
+
if not active:
|
|
833
|
+
output_names[target] = "${OUTPUT_NAME_DEPENDS_ON_CMAKE_CONDITION}"
|
|
834
|
+
elif "APPEND_STRING" in modifiers:
|
|
835
|
+
output_names[target] = output_names.get(target, "") + "".join(values)
|
|
836
|
+
elif "APPEND" in modifiers:
|
|
837
|
+
output_names[target] = "${OUTPUT_NAME_APPEND_REQUIRES_CMAKE}"
|
|
838
|
+
else:
|
|
839
|
+
output_names[target] = values[0]
|
|
840
|
+
elif name == "include_directories":
|
|
841
|
+
if not active:
|
|
842
|
+
continue
|
|
843
|
+
include_paths.extend(
|
|
844
|
+
_MetadataPath(item, path.parent)
|
|
845
|
+
for item in _cmake_list_items(tokens)
|
|
846
|
+
if item not in _CMAKE_INCLUDE_MARKERS
|
|
847
|
+
)
|
|
848
|
+
elif name == "target_include_directories":
|
|
849
|
+
if not active:
|
|
850
|
+
continue
|
|
851
|
+
include_paths.extend(
|
|
852
|
+
_MetadataPath(item, path.parent)
|
|
853
|
+
for item in _cmake_list_items(tokens[1:])
|
|
854
|
+
if item not in _CMAKE_INCLUDE_MARKERS
|
|
855
|
+
)
|
|
856
|
+
output_mappings = [
|
|
857
|
+
(output_names.get(target, target), source)
|
|
858
|
+
for target, target_sources in loadable_targets
|
|
859
|
+
if target
|
|
860
|
+
for source in target_sources
|
|
861
|
+
]
|
|
862
|
+
return sources, include_paths, output_mappings, uncertain_conditions
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _cmake_static_condition(tokens: list[str]) -> bool | None:
|
|
866
|
+
if len(tokens) == 2 and tokens[0].upper() == "NOT":
|
|
867
|
+
nested = _cmake_static_condition(tokens[1:])
|
|
868
|
+
return None if nested is None else not nested
|
|
869
|
+
if len(tokens) != 1:
|
|
870
|
+
return None
|
|
871
|
+
value = tokens[0].strip().upper()
|
|
872
|
+
if not value or value in {"0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND"}:
|
|
873
|
+
return False
|
|
874
|
+
if value.endswith("-NOTFOUND"):
|
|
875
|
+
return False
|
|
876
|
+
if value in {"1", "ON", "YES", "TRUE", "Y"}:
|
|
877
|
+
return True
|
|
878
|
+
try:
|
|
879
|
+
return float(value) != 0
|
|
880
|
+
except ValueError:
|
|
881
|
+
return None
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _cmake_list_items(tokens: Iterable[str]) -> Iterator[str]:
|
|
885
|
+
for token in tokens:
|
|
886
|
+
yield from (
|
|
887
|
+
item.replace(_CMAKE_ESCAPED_SEMICOLON, ";")
|
|
888
|
+
for item in token.split(";")
|
|
889
|
+
if item
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def _cmake_calls(text: str) -> Iterator[tuple[str, str]]:
|
|
894
|
+
text = _strip_cmake_comments(text)
|
|
895
|
+
position = 0
|
|
896
|
+
while match := _CMAKE_CALL.search(text, position):
|
|
897
|
+
depth = 1
|
|
898
|
+
quote = ""
|
|
899
|
+
escaped = False
|
|
900
|
+
cursor = match.end()
|
|
901
|
+
while cursor < len(text) and depth:
|
|
902
|
+
character = text[cursor]
|
|
903
|
+
if escaped:
|
|
904
|
+
escaped = False
|
|
905
|
+
elif character == "\\":
|
|
906
|
+
escaped = True
|
|
907
|
+
elif quote:
|
|
908
|
+
if character == quote:
|
|
909
|
+
quote = ""
|
|
910
|
+
elif character in {'"', "'"}:
|
|
911
|
+
quote = character
|
|
912
|
+
elif character == "(":
|
|
913
|
+
depth += 1
|
|
914
|
+
elif character == ")":
|
|
915
|
+
depth -= 1
|
|
916
|
+
cursor += 1
|
|
917
|
+
if depth:
|
|
918
|
+
return
|
|
919
|
+
yield match.group(1).casefold(), text[match.end():cursor - 1]
|
|
920
|
+
position = cursor
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _strip_cmake_comments(text: str) -> str:
|
|
924
|
+
result: list[str] = []
|
|
925
|
+
quote = ""
|
|
926
|
+
escaped = False
|
|
927
|
+
position = 0
|
|
928
|
+
while position < len(text):
|
|
929
|
+
character = text[position]
|
|
930
|
+
if escaped:
|
|
931
|
+
escaped = False
|
|
932
|
+
result.append(character)
|
|
933
|
+
elif character == "\\":
|
|
934
|
+
escaped = True
|
|
935
|
+
result.append(character)
|
|
936
|
+
elif quote:
|
|
937
|
+
if character == quote:
|
|
938
|
+
quote = ""
|
|
939
|
+
result.append(character)
|
|
940
|
+
elif character in {'"', "'"}:
|
|
941
|
+
quote = character
|
|
942
|
+
result.append(character)
|
|
943
|
+
elif character == "#":
|
|
944
|
+
bracket = _CMAKE_BRACKET_COMMENT_START.match(text, position)
|
|
945
|
+
if bracket is not None:
|
|
946
|
+
closing = f"]{bracket.group('equals')}]"
|
|
947
|
+
closing_start = text.find(closing, bracket.end())
|
|
948
|
+
comment_end = len(text) if closing_start < 0 else closing_start + len(closing)
|
|
949
|
+
result.extend("\n" * text[position:comment_end].count("\n"))
|
|
950
|
+
position = comment_end
|
|
951
|
+
continue
|
|
952
|
+
newline = text.find("\n", position)
|
|
953
|
+
position = len(text) if newline < 0 else newline
|
|
954
|
+
continue
|
|
955
|
+
else:
|
|
956
|
+
result.append(character)
|
|
957
|
+
position += 1
|
|
958
|
+
return "".join(result)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _resolve_roots(workspace: Path, base: Path, language: Language, path: Path, roots: list[str]) -> tuple[list[Path], list[Problem]]:
|
|
962
|
+
resolved: list[Path] = []
|
|
963
|
+
problems: list[Problem] = []
|
|
964
|
+
for item in roots:
|
|
965
|
+
expanded = _expand_project_variables(item, base)
|
|
966
|
+
if _has_unresolved_static_value(expanded):
|
|
967
|
+
problems.append(_problem("unresolved-source-root", item, language, path))
|
|
968
|
+
continue
|
|
969
|
+
candidate = (base / expanded).resolve()
|
|
970
|
+
try:
|
|
971
|
+
candidate.relative_to(workspace)
|
|
972
|
+
except ValueError:
|
|
973
|
+
problems.append(_problem("workspace-escaping-source-root", item, language, path))
|
|
974
|
+
continue
|
|
975
|
+
if not candidate.is_dir():
|
|
976
|
+
problems.append(_problem("missing-source-root", item, language, path))
|
|
977
|
+
continue
|
|
978
|
+
resolved.append(candidate)
|
|
979
|
+
return resolved, problems
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _resolve_files(workspace: Path, base: Path, language: Language, path: Path, files: list[str]) -> tuple[list[Path], list[Problem]]:
|
|
983
|
+
resolved: list[Path] = []
|
|
984
|
+
problems: list[Problem] = []
|
|
985
|
+
for item in files:
|
|
986
|
+
expanded = _expand_project_variables(item, base)
|
|
987
|
+
if _has_unresolved_static_value(expanded):
|
|
988
|
+
problems.append(_problem("unresolved-source-file", item, language, path))
|
|
989
|
+
continue
|
|
990
|
+
candidate = Path(expanded)
|
|
991
|
+
candidate = candidate if candidate.is_absolute() else base / candidate
|
|
992
|
+
candidate = candidate.resolve()
|
|
993
|
+
try:
|
|
994
|
+
candidate.relative_to(workspace)
|
|
995
|
+
except ValueError:
|
|
996
|
+
problems.append(_problem("workspace-escaping-source-file", item, language, path))
|
|
997
|
+
continue
|
|
998
|
+
if not candidate.is_file():
|
|
999
|
+
problems.append(_problem("missing-source-file", item, language, path))
|
|
1000
|
+
continue
|
|
1001
|
+
resolved.append(candidate)
|
|
1002
|
+
return resolved, problems
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def _resolve_compile_commands(
|
|
1006
|
+
workspace: Path,
|
|
1007
|
+
language: Language,
|
|
1008
|
+
path: Path,
|
|
1009
|
+
commands: list[_CompileCommand],
|
|
1010
|
+
) -> tuple[list[Path], list[tuple[Path, tuple[Path, ...]]], list[Problem]]:
|
|
1011
|
+
resolved_files: list[Path] = []
|
|
1012
|
+
translation_unit_includes: list[tuple[Path, tuple[Path, ...]]] = []
|
|
1013
|
+
problems: list[Problem] = []
|
|
1014
|
+
seen_files: set[Path] = set()
|
|
1015
|
+
mapped_files: set[Path] = set()
|
|
1016
|
+
reported_include_problems: set[tuple[str, str]] = set()
|
|
1017
|
+
for command in commands:
|
|
1018
|
+
command_files, file_problems = _resolve_files(
|
|
1019
|
+
workspace,
|
|
1020
|
+
path.parent,
|
|
1021
|
+
language,
|
|
1022
|
+
path,
|
|
1023
|
+
[command.source_file],
|
|
1024
|
+
)
|
|
1025
|
+
command_includes, include_problems = _resolve_include_paths(
|
|
1026
|
+
workspace,
|
|
1027
|
+
language,
|
|
1028
|
+
path,
|
|
1029
|
+
list(command.include_paths),
|
|
1030
|
+
reported_include_problems,
|
|
1031
|
+
)
|
|
1032
|
+
problems.extend(file_problems)
|
|
1033
|
+
problems.extend(include_problems)
|
|
1034
|
+
if not command_files:
|
|
1035
|
+
continue
|
|
1036
|
+
source_file = command_files[0]
|
|
1037
|
+
if source_file not in seen_files:
|
|
1038
|
+
seen_files.add(source_file)
|
|
1039
|
+
resolved_files.append(source_file)
|
|
1040
|
+
if source_file not in mapped_files:
|
|
1041
|
+
mapped_files.add(source_file)
|
|
1042
|
+
translation_unit_includes.append((source_file, tuple(command_includes)))
|
|
1043
|
+
return resolved_files, translation_unit_includes, problems
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def _resolve_include_paths(
|
|
1047
|
+
workspace: Path,
|
|
1048
|
+
language: Language,
|
|
1049
|
+
path: Path,
|
|
1050
|
+
include_paths: list[_MetadataPath],
|
|
1051
|
+
reported_problems: set[tuple[str, str]] | None = None,
|
|
1052
|
+
) -> tuple[list[Path], list[Problem]]:
|
|
1053
|
+
resolved: list[Path] = []
|
|
1054
|
+
problems: list[Problem] = []
|
|
1055
|
+
seen: set[Path] = set()
|
|
1056
|
+
seen_problems = reported_problems if reported_problems is not None else set()
|
|
1057
|
+
|
|
1058
|
+
def report(code: str, identity: str, include_path: _MetadataPath) -> None:
|
|
1059
|
+
key = (code, identity)
|
|
1060
|
+
if key in seen_problems:
|
|
1061
|
+
return
|
|
1062
|
+
seen_problems.add(key)
|
|
1063
|
+
problems.append(_include_problem(code, include_path.value, language, path))
|
|
1064
|
+
|
|
1065
|
+
for include_path in include_paths:
|
|
1066
|
+
expanded = _expand_project_variables(include_path.value, include_path.base)
|
|
1067
|
+
expanded_base = _expand_project_variables(str(include_path.base), path.parent)
|
|
1068
|
+
if _has_unresolved_static_value(expanded) or _has_unresolved_static_value(expanded_base):
|
|
1069
|
+
report("unresolved-include-path", f"{expanded_base}\0{expanded}", include_path)
|
|
1070
|
+
continue
|
|
1071
|
+
candidate = Path(expanded)
|
|
1072
|
+
candidate = candidate if candidate.is_absolute() else Path(expanded_base) / candidate
|
|
1073
|
+
candidate = candidate.resolve()
|
|
1074
|
+
try:
|
|
1075
|
+
candidate.relative_to(workspace)
|
|
1076
|
+
except ValueError:
|
|
1077
|
+
report("workspace-escaping-include-path", str(candidate), include_path)
|
|
1078
|
+
continue
|
|
1079
|
+
if not candidate.is_dir():
|
|
1080
|
+
report("missing-include-path", str(candidate), include_path)
|
|
1081
|
+
continue
|
|
1082
|
+
if candidate not in seen:
|
|
1083
|
+
seen.add(candidate)
|
|
1084
|
+
resolved.append(candidate)
|
|
1085
|
+
return resolved, problems
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _has_unresolved_static_value(value: str) -> bool:
|
|
1089
|
+
return _VARIABLE.search(value) is not None or _GENERATOR_EXPRESSION.search(value) is not None
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _expand_project_variables(value: str, base: Path) -> str:
|
|
1093
|
+
directory = f"{base}/"
|
|
1094
|
+
replacements = {
|
|
1095
|
+
"PROJECTDIR": directory,
|
|
1096
|
+
"PROJECT_DIR": directory,
|
|
1097
|
+
"MSBUILDPROJECTDIRECTORY": directory,
|
|
1098
|
+
"MSBUILDTHISFILEDIRECTORY": directory,
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
def replace_variable(match: re.Match[str]) -> str:
|
|
1102
|
+
token = match.group(0)
|
|
1103
|
+
if not token.startswith("$("):
|
|
1104
|
+
return token
|
|
1105
|
+
return replacements.get(token[2:-1].upper(), token)
|
|
1106
|
+
|
|
1107
|
+
return _VARIABLE.sub(replace_variable, value).replace("\\", "/")
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _problem(code: str, item: str, language: Language, path: Path) -> Problem:
|
|
1111
|
+
return Problem(code, f"Could not resolve static metadata entry: {item}", language=language, metadata={"source_root": item, "metadata_path": str(path)})
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def _include_problem(code: str, item: str, language: Language, path: Path) -> Problem:
|
|
1115
|
+
return Problem(
|
|
1116
|
+
code,
|
|
1117
|
+
f"Could not resolve static include path: {item}",
|
|
1118
|
+
language=language,
|
|
1119
|
+
metadata={"include_path": item, "metadata_path": str(path)},
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _output_problem(item: str, language: Language, path: Path) -> Problem:
|
|
1124
|
+
return Problem(
|
|
1125
|
+
"unresolved-output-name",
|
|
1126
|
+
f"Could not resolve static output name: {item}",
|
|
1127
|
+
language=language,
|
|
1128
|
+
metadata={"output_name": item, "metadata_path": str(path)},
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _cmake_condition_problem(condition: str, path: Path) -> Problem:
|
|
1133
|
+
return Problem(
|
|
1134
|
+
"unresolved-cmake-condition",
|
|
1135
|
+
f"Could not statically resolve CMake condition: {condition}",
|
|
1136
|
+
language=Language.CPP,
|
|
1137
|
+
metadata={"condition": condition, "metadata_path": str(path)},
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _project_file_problem(code: str, item: str, path: Path) -> Problem:
|
|
1142
|
+
return Problem(
|
|
1143
|
+
code,
|
|
1144
|
+
f"Could not resolve static solution project entry: {item}",
|
|
1145
|
+
language=Language.CSHARP,
|
|
1146
|
+
metadata={"project_file": item, "metadata_path": str(path)},
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _languages_for(path: Path) -> tuple[Language, ...]:
|
|
1151
|
+
detected = adapters_for(path)
|
|
1152
|
+
return (Language.DELPHI,) if path.suffix.casefold() in {".pas", ".dpr", ".dpk"} else detected
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _owner(
|
|
1156
|
+
path: Path,
|
|
1157
|
+
definitions: list[_Definition],
|
|
1158
|
+
language: Language,
|
|
1159
|
+
root: Path,
|
|
1160
|
+
) -> tuple[Project, Problem | None]:
|
|
1161
|
+
candidates = [item for item in definitions if item.language is language and _owns(item, path)]
|
|
1162
|
+
if not candidates:
|
|
1163
|
+
return _synthetic_project(root, language), None
|
|
1164
|
+
best_rank = max(_ownership_rank(item, path) for item in candidates)
|
|
1165
|
+
winners = [item for item in candidates if _ownership_rank(item, path) == best_rank]
|
|
1166
|
+
winner_ids = {_definition_project_id(item, definitions) for item in winners}
|
|
1167
|
+
if len(winner_ids) > 1:
|
|
1168
|
+
metadata_paths = sorted(str(item.metadata_path) for item in winners)
|
|
1169
|
+
return (
|
|
1170
|
+
_synthetic_project(root, language),
|
|
1171
|
+
Problem(
|
|
1172
|
+
"ambiguous-project-ownership",
|
|
1173
|
+
f"Multiple project files can own {path}: {', '.join(metadata_paths)}",
|
|
1174
|
+
"warning",
|
|
1175
|
+
language,
|
|
1176
|
+
metadata={"path": str(path.relative_to(root)), "project_files": metadata_paths},
|
|
1177
|
+
),
|
|
1178
|
+
)
|
|
1179
|
+
return _project_from_definition(winners[0], definitions), None
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def _synthetic_project(root: Path, language: Language) -> Project:
|
|
1183
|
+
return Project(
|
|
1184
|
+
stable_id("project", str(root), "synthetic"),
|
|
1185
|
+
root.name,
|
|
1186
|
+
str(root),
|
|
1187
|
+
(language,),
|
|
1188
|
+
"synthetic",
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _project_from_definition(definition: _Definition, definitions: list[_Definition]) -> Project:
|
|
1193
|
+
project_id = _definition_project_id(definition, definitions)
|
|
1194
|
+
canonical = _canonical_project_definition(definition, definitions)
|
|
1195
|
+
include_paths = tuple(
|
|
1196
|
+
dict.fromkeys(
|
|
1197
|
+
str(include_path)
|
|
1198
|
+
for candidate in definitions
|
|
1199
|
+
if candidate.language is definition.language
|
|
1200
|
+
and _definition_project_id(candidate, definitions) == project_id
|
|
1201
|
+
for include_path in candidate.include_paths
|
|
1202
|
+
)
|
|
1203
|
+
)
|
|
1204
|
+
return Project(
|
|
1205
|
+
project_id,
|
|
1206
|
+
_definition_project_name(definition),
|
|
1207
|
+
str(definition.root),
|
|
1208
|
+
(definition.language,),
|
|
1209
|
+
canonical.kind,
|
|
1210
|
+
include_paths,
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _merge_project(previous: Project | None, project: Project) -> Project:
|
|
1215
|
+
if previous is None:
|
|
1216
|
+
return project
|
|
1217
|
+
selected_languages = {*previous.languages, *project.languages}
|
|
1218
|
+
return replace(
|
|
1219
|
+
previous,
|
|
1220
|
+
languages=tuple(language for language in Language if language in selected_languages),
|
|
1221
|
+
include_paths=tuple(dict.fromkeys((*previous.include_paths, *project.include_paths))),
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _source_include_paths(
|
|
1226
|
+
path: Path,
|
|
1227
|
+
definitions: list[_Definition],
|
|
1228
|
+
language: Language,
|
|
1229
|
+
project: Project,
|
|
1230
|
+
) -> tuple[Path, ...]:
|
|
1231
|
+
project_root = Path(project.root)
|
|
1232
|
+
candidates = [
|
|
1233
|
+
item
|
|
1234
|
+
for item in definitions
|
|
1235
|
+
if item.language is language
|
|
1236
|
+
and item.root == project_root
|
|
1237
|
+
and _definition_project_id(item, definitions) == project.project_id
|
|
1238
|
+
and _owns(item, path)
|
|
1239
|
+
]
|
|
1240
|
+
for candidate in candidates:
|
|
1241
|
+
for source_file, include_paths in candidate.translation_unit_include_paths:
|
|
1242
|
+
if source_file == path:
|
|
1243
|
+
return include_paths
|
|
1244
|
+
return tuple(Path(include_path) for include_path in project.include_paths)
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def _source_output_names(
|
|
1248
|
+
path: Path,
|
|
1249
|
+
definitions: list[_Definition],
|
|
1250
|
+
language: Language,
|
|
1251
|
+
project: Project,
|
|
1252
|
+
) -> tuple[str, ...]:
|
|
1253
|
+
project_root = Path(project.root)
|
|
1254
|
+
names: list[str] = []
|
|
1255
|
+
for candidate in definitions:
|
|
1256
|
+
if (
|
|
1257
|
+
candidate.language is not language
|
|
1258
|
+
or candidate.root != project_root
|
|
1259
|
+
or _definition_project_id(candidate, definitions) != project.project_id
|
|
1260
|
+
or not _owns(candidate, path)
|
|
1261
|
+
):
|
|
1262
|
+
continue
|
|
1263
|
+
for source_file, output_names in candidate.translation_unit_output_names:
|
|
1264
|
+
if source_file == path:
|
|
1265
|
+
names.extend(name for name in output_names if name not in names)
|
|
1266
|
+
return tuple(names)
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def _excluded_by_layout(path: Path, definitions: list[_Definition], language: Language) -> bool:
|
|
1270
|
+
scoped = [item for item in definitions if item.language is language and (item.source_roots or item.source_files)]
|
|
1271
|
+
return any(path.is_relative_to(item.root) for item in scoped) and not any(_owns(item, path) for item in scoped)
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def _owns(definition: _Definition, path: Path) -> bool:
|
|
1275
|
+
if path in definition.source_files:
|
|
1276
|
+
return True
|
|
1277
|
+
roots = definition.source_roots
|
|
1278
|
+
if roots:
|
|
1279
|
+
return any(path.is_relative_to(root) for root in roots)
|
|
1280
|
+
return not definition.source_files and path.is_relative_to(definition.root)
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def _ownership_rank(definition: _Definition, path: Path) -> tuple[int, int, int, int, int]:
|
|
1284
|
+
matching_root_depth = max(
|
|
1285
|
+
(len(root.parts) for root in definition.source_roots if path.is_relative_to(root)),
|
|
1286
|
+
default=-1,
|
|
1287
|
+
)
|
|
1288
|
+
return (
|
|
1289
|
+
int(path in definition.source_files),
|
|
1290
|
+
int(definition.metadata_path.stem.casefold() == path.stem.casefold()),
|
|
1291
|
+
_PROJECT_OWNERSHIP_PRIORITY.get(definition.metadata_path.suffix.casefold(), 0),
|
|
1292
|
+
matching_root_depth,
|
|
1293
|
+
len(definition.root.parts),
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def _definition_project_id(definition: _Definition, definitions: list[_Definition]) -> str:
|
|
1298
|
+
canonical = _canonical_project_definition(definition, definitions)
|
|
1299
|
+
if canonical.metadata_path.suffix.casefold() in _NAMED_PROJECT_SUFFIXES:
|
|
1300
|
+
return stable_id("project", str(canonical.metadata_path.with_suffix("")), canonical.kind)
|
|
1301
|
+
return stable_id("project", str(definition.root), definition.language.value)
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def _canonical_project_definition(
|
|
1305
|
+
definition: _Definition,
|
|
1306
|
+
definitions: list[_Definition],
|
|
1307
|
+
) -> _Definition:
|
|
1308
|
+
if definition.metadata_path.suffix.casefold() != ".dproj":
|
|
1309
|
+
return definition
|
|
1310
|
+
project_path = definition.metadata_path.with_suffix("")
|
|
1311
|
+
companions = [
|
|
1312
|
+
candidate
|
|
1313
|
+
for candidate in definitions
|
|
1314
|
+
if candidate.metadata_path.with_suffix("") == project_path
|
|
1315
|
+
and candidate.metadata_path.suffix.casefold() in {".dpk", ".dpr"}
|
|
1316
|
+
]
|
|
1317
|
+
if not companions:
|
|
1318
|
+
return definition
|
|
1319
|
+
return max(
|
|
1320
|
+
companions,
|
|
1321
|
+
key=lambda candidate: (candidate.metadata_path.suffix.casefold() == ".dpr", candidate.kind),
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def _definition_project_name(definition: _Definition) -> str:
|
|
1326
|
+
if definition.metadata_path.suffix.casefold() in _NAMED_PROJECT_SUFFIXES:
|
|
1327
|
+
return definition.metadata_path.stem
|
|
1328
|
+
return definition.root.name
|