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,447 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import keyword
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
from urllib.parse import unquote, urlparse
|
|
9
|
+
|
|
10
|
+
from .indexer import build_workspace_snapshot
|
|
11
|
+
from .models import Language, Problem, Resolution, SourceRange, Symbol
|
|
12
|
+
from .workspace import WorkspaceSnapshot
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_IDENTIFIER_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
16
|
+
_DELPHI_LINKED_ROLES = frozenset({"declaration", "implementation"})
|
|
17
|
+
_RUST_RESERVED_WORDS = frozenset(
|
|
18
|
+
"""
|
|
19
|
+
Self _ abstract as async await become box break const continue crate do dyn else enum extern false final fn for gen
|
|
20
|
+
if impl in let loop macro match mod move mut override priv pub ref return self static struct super trait true try
|
|
21
|
+
type typeof unsafe unsized use virtual where while yield
|
|
22
|
+
""".split()
|
|
23
|
+
)
|
|
24
|
+
_CSHARP_RESERVED_WORDS = frozenset(
|
|
25
|
+
"""
|
|
26
|
+
abstract as base bool break byte case catch char checked class const continue decimal default delegate do double
|
|
27
|
+
else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is
|
|
28
|
+
lock long namespace new null object operator out override params private protected public readonly ref return sbyte
|
|
29
|
+
sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe
|
|
30
|
+
ushort using virtual void volatile while
|
|
31
|
+
""".split()
|
|
32
|
+
)
|
|
33
|
+
_CPP_RESERVED_WORDS = frozenset(
|
|
34
|
+
"""
|
|
35
|
+
alignas alignof and and_eq asm auto bitand bitor bool break case catch char char8_t char16_t char32_t class
|
|
36
|
+
compl concept const consteval constexpr constinit const_cast continue contract_assert co_await co_return co_yield
|
|
37
|
+
decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if
|
|
38
|
+
inline int long mutable namespace new noexcept not not_eq nullptr operator or or_eq private protected public
|
|
39
|
+
register reinterpret_cast requires return short signed sizeof static static_assert static_cast struct switch template
|
|
40
|
+
this thread_local throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while
|
|
41
|
+
xor xor_eq
|
|
42
|
+
""".split()
|
|
43
|
+
)
|
|
44
|
+
_DELPHI_RESERVED_WORDS = frozenset(
|
|
45
|
+
"""
|
|
46
|
+
and array as asm at automated begin case class const constructor destructor dispinterface div do downto else end
|
|
47
|
+
except exports file finalization finally for function goto if implementation in inherited initialization inline
|
|
48
|
+
interface is label library mod nil not object of on or packed private procedure program property protected public
|
|
49
|
+
published raise record repeat resourcestring set shl shr string then threadvar to try type unit until uses var while
|
|
50
|
+
with xor
|
|
51
|
+
""".split()
|
|
52
|
+
)
|
|
53
|
+
_RESERVED_WORDS = {
|
|
54
|
+
Language.RUST: _RUST_RESERVED_WORDS,
|
|
55
|
+
Language.CSHARP: _CSHARP_RESERVED_WORDS,
|
|
56
|
+
Language.CPP: _CPP_RESERVED_WORDS,
|
|
57
|
+
Language.DELPHI: _DELPHI_RESERVED_WORDS,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class TextEdit:
|
|
63
|
+
range: SourceRange
|
|
64
|
+
new_text: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SafeRenameError(ValueError):
|
|
68
|
+
"""Raised when a rename cannot be proven safe from indexed evidence."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LanguageService:
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
root: str | Path,
|
|
75
|
+
snapshot: WorkspaceSnapshot | None = None,
|
|
76
|
+
languages: Iterable[Language] | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
self.root = Path(root).expanduser().resolve()
|
|
79
|
+
self.languages = tuple(languages) if languages is not None else None
|
|
80
|
+
self._overrides: dict[str, str] = {}
|
|
81
|
+
self.snapshot = snapshot or build_workspace_snapshot(self.root, self.languages)
|
|
82
|
+
|
|
83
|
+
def update_document(self, path: str | Path, source: str) -> None:
|
|
84
|
+
relative = self.relative_path(path)
|
|
85
|
+
self._overrides[relative] = source
|
|
86
|
+
self.snapshot = build_workspace_snapshot(self.root, self.languages, self._overrides)
|
|
87
|
+
|
|
88
|
+
def close_document(self, path: str | Path) -> None:
|
|
89
|
+
relative = self.relative_path(path)
|
|
90
|
+
if relative in self._overrides:
|
|
91
|
+
del self._overrides[relative]
|
|
92
|
+
self.snapshot = build_workspace_snapshot(self.root, self.languages, self._overrides)
|
|
93
|
+
|
|
94
|
+
def definition(self, path: str | Path, line: int, character: int) -> SourceRange | None:
|
|
95
|
+
symbol = self.symbol_at(path, line, character)
|
|
96
|
+
return (symbol.name_range or symbol.range) if symbol is not None else None
|
|
97
|
+
|
|
98
|
+
def hover(self, path: str | Path, line: int, character: int) -> str | None:
|
|
99
|
+
symbol = self.symbol_at(path, line, character)
|
|
100
|
+
if symbol is None:
|
|
101
|
+
return None
|
|
102
|
+
signature = symbol.signature or symbol.qualified_name
|
|
103
|
+
return f"```{symbol.language.value}\n{signature}\n```\n{symbol.kind} in `{symbol.range.path}`"
|
|
104
|
+
|
|
105
|
+
def references(
|
|
106
|
+
self,
|
|
107
|
+
path: str | Path,
|
|
108
|
+
line: int,
|
|
109
|
+
character: int,
|
|
110
|
+
*,
|
|
111
|
+
include_declaration: bool = True,
|
|
112
|
+
) -> tuple[SourceRange, ...]:
|
|
113
|
+
symbol = self.symbol_at(path, line, character)
|
|
114
|
+
if symbol is None:
|
|
115
|
+
return ()
|
|
116
|
+
linked_symbols = self._linked_rename_symbols(symbol, require_unique=False)
|
|
117
|
+
linked_target_ids = {item.target_id for item in linked_symbols}
|
|
118
|
+
ranges = [
|
|
119
|
+
item.range
|
|
120
|
+
for item in self.snapshot.references
|
|
121
|
+
if item.target_id in linked_target_ids
|
|
122
|
+
]
|
|
123
|
+
if include_declaration:
|
|
124
|
+
ranges[0:0] = [item.name_range or item.range for item in linked_symbols]
|
|
125
|
+
return tuple(dict.fromkeys(ranges))
|
|
126
|
+
|
|
127
|
+
def rename(
|
|
128
|
+
self,
|
|
129
|
+
path: str | Path,
|
|
130
|
+
line: int,
|
|
131
|
+
character: int,
|
|
132
|
+
new_name: str,
|
|
133
|
+
) -> dict[str, tuple[TextEdit, ...]]:
|
|
134
|
+
symbol = self.symbol_at(path, line, character)
|
|
135
|
+
if symbol is None:
|
|
136
|
+
raise SafeRenameError("no symbol at the requested position")
|
|
137
|
+
if symbol.name_range is None:
|
|
138
|
+
raise SafeRenameError("the declaration has no exact editable name range")
|
|
139
|
+
if not self._valid_identifier(symbol.language, new_name):
|
|
140
|
+
raise SafeRenameError(f"{new_name!r} is not a valid {symbol.language.value} identifier")
|
|
141
|
+
rename_symbols = self._linked_rename_symbols(symbol)
|
|
142
|
+
rename_target_ids = {item.target_id for item in rename_symbols}
|
|
143
|
+
rename_scopes = {(item.file_id, item.parent_id) for item in rename_symbols}
|
|
144
|
+
if any(
|
|
145
|
+
item.target_id not in rename_target_ids
|
|
146
|
+
and (item.file_id, item.parent_id) in rename_scopes
|
|
147
|
+
and item.language is symbol.language
|
|
148
|
+
and item.name.casefold() == new_name.casefold()
|
|
149
|
+
for item in self.snapshot.symbols
|
|
150
|
+
):
|
|
151
|
+
raise SafeRenameError("the new name collides with a symbol in the same scope")
|
|
152
|
+
|
|
153
|
+
references = [item for item in self.snapshot.references if item.target_id in rename_target_ids]
|
|
154
|
+
matching_names = {self._simple_identifier(item.name).casefold() for item in rename_symbols}
|
|
155
|
+
uncertain = [
|
|
156
|
+
item
|
|
157
|
+
for item in self.snapshot.references
|
|
158
|
+
if item.language is symbol.language
|
|
159
|
+
and self._simple_identifier(item.name).casefold() in matching_names
|
|
160
|
+
and item.resolution is not Resolution.COMPLETE
|
|
161
|
+
]
|
|
162
|
+
if uncertain or any(item.resolution is not Resolution.COMPLETE for item in references):
|
|
163
|
+
raise SafeRenameError("not every matching reference is uniquely resolved")
|
|
164
|
+
|
|
165
|
+
ranges: dict[SourceRange, str] = {}
|
|
166
|
+
for rename_symbol in rename_symbols:
|
|
167
|
+
if rename_symbol.name_range is None:
|
|
168
|
+
raise SafeRenameError("a linked declaration has no exact editable name range")
|
|
169
|
+
ranges[rename_symbol.name_range] = self._simple_identifier(rename_symbol.name)
|
|
170
|
+
for reference in references:
|
|
171
|
+
ranges[reference.range] = self._simple_identifier(reference.name)
|
|
172
|
+
|
|
173
|
+
edits: dict[str, list[TextEdit]] = {}
|
|
174
|
+
for source_range, expected_name in ranges.items():
|
|
175
|
+
if not self._identifier_text_matches(symbol.language, self._range_text(source_range), expected_name):
|
|
176
|
+
raise SafeRenameError("an indexed edit range no longer matches the symbol name")
|
|
177
|
+
edits.setdefault(source_range.path, []).append(TextEdit(source_range, new_name))
|
|
178
|
+
return {
|
|
179
|
+
file_name: tuple(sorted(values, key=lambda item: self._range_key(item.range), reverse=True))
|
|
180
|
+
for file_name, values in edits.items()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def document_symbols(self, path: str | Path) -> tuple[Symbol, ...]:
|
|
184
|
+
relative = self.relative_path(path)
|
|
185
|
+
selected: dict[tuple[object, ...], Symbol] = {}
|
|
186
|
+
for item in self.snapshot.symbols:
|
|
187
|
+
if item.kind == "module" or self.relative_path(item.range.path) != relative:
|
|
188
|
+
continue
|
|
189
|
+
anchor = item.name_range or item.range
|
|
190
|
+
key = (
|
|
191
|
+
item.qualified_name,
|
|
192
|
+
item.kind,
|
|
193
|
+
anchor.start_line,
|
|
194
|
+
anchor.start_character,
|
|
195
|
+
anchor.end_line,
|
|
196
|
+
anchor.end_character,
|
|
197
|
+
)
|
|
198
|
+
selected.setdefault(key, item)
|
|
199
|
+
return tuple(selected.values())
|
|
200
|
+
|
|
201
|
+
def workspace_symbols(self, query: str = "") -> tuple[Symbol, ...]:
|
|
202
|
+
folded = query.casefold().strip()
|
|
203
|
+
return tuple(
|
|
204
|
+
item
|
|
205
|
+
for item in self.snapshot.symbols
|
|
206
|
+
if item.kind != "module" and (not folded or folded in item.qualified_name.casefold())
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def completions(self, path: str | Path, prefix: str = "") -> tuple[Symbol, ...]:
|
|
210
|
+
relative = self.relative_path(path)
|
|
211
|
+
source_file = next((item for item in self.snapshot.files if item.path == relative), None)
|
|
212
|
+
if source_file is None:
|
|
213
|
+
return ()
|
|
214
|
+
folded = prefix.casefold()
|
|
215
|
+
seen: set[str] = set()
|
|
216
|
+
result: list[Symbol] = []
|
|
217
|
+
for item in self.snapshot.symbols:
|
|
218
|
+
if item.project_id != source_file.project_id or item.language is not source_file.language:
|
|
219
|
+
continue
|
|
220
|
+
if item.kind == "module" or (folded and not item.name.casefold().startswith(folded)):
|
|
221
|
+
continue
|
|
222
|
+
key = item.name.casefold()
|
|
223
|
+
if key not in seen:
|
|
224
|
+
seen.add(key)
|
|
225
|
+
result.append(item)
|
|
226
|
+
return tuple(result)
|
|
227
|
+
|
|
228
|
+
def diagnostics(self, path: str | Path) -> tuple[Problem, ...]:
|
|
229
|
+
relative = self.relative_path(path)
|
|
230
|
+
return tuple(
|
|
231
|
+
item
|
|
232
|
+
for item in self.snapshot.problems
|
|
233
|
+
if (
|
|
234
|
+
item.range is not None and self.relative_path(item.range.path) == relative
|
|
235
|
+
or item.range is None and item.metadata.get("path") == relative
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def symbol_at(self, path: str | Path, line: int, character: int) -> Symbol | None:
|
|
240
|
+
relative = self.relative_path(path)
|
|
241
|
+
references = [
|
|
242
|
+
item
|
|
243
|
+
for item in self.snapshot.references
|
|
244
|
+
if self.relative_path(item.range.path) == relative and self._contains(item.range, line, character)
|
|
245
|
+
]
|
|
246
|
+
if references:
|
|
247
|
+
reference = min(references, key=lambda item: self._span(item.range))
|
|
248
|
+
return self.snapshot.symbols_by_id.get(reference.target_id) if reference.target_id else None
|
|
249
|
+
symbols = [
|
|
250
|
+
item
|
|
251
|
+
for item in self.snapshot.symbols
|
|
252
|
+
if self.relative_path(item.range.path) == relative
|
|
253
|
+
and self._contains(item.name_range or item.range, line, character)
|
|
254
|
+
]
|
|
255
|
+
return min(symbols, key=lambda item: self._span(item.name_range or item.range)) if symbols else None
|
|
256
|
+
|
|
257
|
+
def relative_path(self, path: str | Path) -> str:
|
|
258
|
+
text = str(path)
|
|
259
|
+
parsed = urlparse(text)
|
|
260
|
+
if parsed.scheme == "file":
|
|
261
|
+
text = unquote(parsed.path)
|
|
262
|
+
if re.match(r"^/[A-Za-z]:/", text):
|
|
263
|
+
text = text[1:]
|
|
264
|
+
candidate = Path(text)
|
|
265
|
+
if candidate.is_absolute():
|
|
266
|
+
try:
|
|
267
|
+
return candidate.resolve().relative_to(self.root).as_posix()
|
|
268
|
+
except ValueError:
|
|
269
|
+
return candidate.as_posix()
|
|
270
|
+
return candidate.as_posix()
|
|
271
|
+
|
|
272
|
+
def path_uri(self, path: str) -> str:
|
|
273
|
+
candidate = Path(path)
|
|
274
|
+
return (candidate if candidate.is_absolute() else self.root / candidate).resolve().as_uri()
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _contains(source_range: SourceRange, line: int, character: int) -> bool:
|
|
278
|
+
position = (line, character)
|
|
279
|
+
return (source_range.start_line, source_range.start_character) <= position < (
|
|
280
|
+
source_range.end_line,
|
|
281
|
+
source_range.end_character,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _span(source_range: SourceRange) -> tuple[int, int]:
|
|
286
|
+
return (
|
|
287
|
+
source_range.end_line - source_range.start_line,
|
|
288
|
+
source_range.end_character - source_range.start_character,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def _range_key(source_range: SourceRange) -> tuple[int, int, int, int]:
|
|
293
|
+
return (
|
|
294
|
+
source_range.start_line,
|
|
295
|
+
source_range.start_character,
|
|
296
|
+
source_range.end_line,
|
|
297
|
+
source_range.end_character,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _range_text(self, source_range: SourceRange) -> str:
|
|
301
|
+
source = self._overrides.get(source_range.path)
|
|
302
|
+
if source is None:
|
|
303
|
+
source = (self.root / source_range.path).read_text(encoding="utf-8", errors="replace")
|
|
304
|
+
lines = source.splitlines(keepends=True)
|
|
305
|
+
if not lines:
|
|
306
|
+
lines = [""]
|
|
307
|
+
elif source.endswith(("\r", "\n")):
|
|
308
|
+
lines.append("")
|
|
309
|
+
if (
|
|
310
|
+
source_range.start_line < 0
|
|
311
|
+
or source_range.end_line < source_range.start_line
|
|
312
|
+
or source_range.end_line >= len(lines)
|
|
313
|
+
):
|
|
314
|
+
return ""
|
|
315
|
+
|
|
316
|
+
start_line = lines[source_range.start_line]
|
|
317
|
+
end_line = lines[source_range.end_line]
|
|
318
|
+
start_content = self._without_line_ending(start_line)
|
|
319
|
+
end_content = self._without_line_ending(end_line)
|
|
320
|
+
start = self._utf16_character_to_index(start_content, source_range.start_character)
|
|
321
|
+
end = self._utf16_character_to_index(end_content, source_range.end_character)
|
|
322
|
+
if start is None or end is None:
|
|
323
|
+
return ""
|
|
324
|
+
if source_range.start_line == source_range.end_line:
|
|
325
|
+
return start_content[start:end]
|
|
326
|
+
return "".join(
|
|
327
|
+
(
|
|
328
|
+
start_line[start:],
|
|
329
|
+
*lines[source_range.start_line + 1 : source_range.end_line],
|
|
330
|
+
end_content[:end],
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _utf16_character_to_index(line: str, character: int) -> int | None:
|
|
336
|
+
if character < 0:
|
|
337
|
+
return None
|
|
338
|
+
utf16_character = 0
|
|
339
|
+
for index, value in enumerate(line):
|
|
340
|
+
if utf16_character == character:
|
|
341
|
+
return index
|
|
342
|
+
utf16_character += 2 if ord(value) > 0xFFFF else 1
|
|
343
|
+
if utf16_character > character:
|
|
344
|
+
return None
|
|
345
|
+
return len(line) if utf16_character == character else None
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def _without_line_ending(line: str) -> str:
|
|
349
|
+
if line.endswith("\r\n"):
|
|
350
|
+
return line[:-2]
|
|
351
|
+
if line.endswith(("\r", "\n")):
|
|
352
|
+
return line[:-1]
|
|
353
|
+
return line
|
|
354
|
+
|
|
355
|
+
def _linked_rename_symbols(
|
|
356
|
+
self,
|
|
357
|
+
symbol: Symbol,
|
|
358
|
+
*,
|
|
359
|
+
require_unique: bool = True,
|
|
360
|
+
) -> tuple[Symbol, ...]:
|
|
361
|
+
if symbol.language is not Language.DELPHI:
|
|
362
|
+
return (symbol,)
|
|
363
|
+
|
|
364
|
+
simple_name = self._simple_identifier(symbol.name).casefold()
|
|
365
|
+
signature_key = self._normalized_delphi_signature(symbol)
|
|
366
|
+
selected_ids = {symbol.target_id}
|
|
367
|
+
while True:
|
|
368
|
+
expanded_ids = set(selected_ids)
|
|
369
|
+
for target_id in selected_ids:
|
|
370
|
+
expanded_ids.update(
|
|
371
|
+
item.target_id
|
|
372
|
+
for item in self.snapshot.symbol_aliases_by_id.get(target_id, ())
|
|
373
|
+
)
|
|
374
|
+
if symbol.role in _DELPHI_LINKED_ROLES:
|
|
375
|
+
context_file_ids = {
|
|
376
|
+
item.file_id
|
|
377
|
+
for item in self.snapshot.symbols
|
|
378
|
+
if item.target_id in expanded_ids
|
|
379
|
+
}
|
|
380
|
+
expanded_ids.update(
|
|
381
|
+
item.target_id
|
|
382
|
+
for item in self.snapshot.symbols
|
|
383
|
+
if item.language is Language.DELPHI
|
|
384
|
+
and item.file_id in context_file_ids
|
|
385
|
+
and item.qualified_name.casefold() == symbol.qualified_name.casefold()
|
|
386
|
+
and item.kind == symbol.kind
|
|
387
|
+
and item.role in _DELPHI_LINKED_ROLES
|
|
388
|
+
and self._simple_identifier(item.name).casefold() == simple_name
|
|
389
|
+
and self._normalized_delphi_signature(item) == signature_key
|
|
390
|
+
)
|
|
391
|
+
if expanded_ids == selected_ids:
|
|
392
|
+
break
|
|
393
|
+
selected_ids = expanded_ids
|
|
394
|
+
|
|
395
|
+
candidates = tuple(
|
|
396
|
+
item for item in self.snapshot.symbols if item.target_id in selected_ids
|
|
397
|
+
)
|
|
398
|
+
if require_unique:
|
|
399
|
+
role_counts: dict[tuple[str, str], int] = {}
|
|
400
|
+
for item in candidates:
|
|
401
|
+
if item.role not in _DELPHI_LINKED_ROLES:
|
|
402
|
+
continue
|
|
403
|
+
key = (item.file_id, item.role)
|
|
404
|
+
role_counts[key] = role_counts.get(key, 0) + 1
|
|
405
|
+
if any(count > 1 for count in role_counts.values()):
|
|
406
|
+
raise SafeRenameError(
|
|
407
|
+
"the Delphi declaration and implementation cannot be linked uniquely"
|
|
408
|
+
)
|
|
409
|
+
return candidates
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def _normalized_delphi_signature(symbol: Symbol) -> str:
|
|
413
|
+
signature = " ".join(symbol.signature.casefold().split())
|
|
414
|
+
indexed_name = symbol.name.casefold()
|
|
415
|
+
simple_name = symbol.qualified_name.rsplit(".", 1)[-1].casefold()
|
|
416
|
+
signature = signature.replace(indexed_name, simple_name, 1)
|
|
417
|
+
|
|
418
|
+
depth = 0
|
|
419
|
+
for index, character in enumerate(signature):
|
|
420
|
+
if character == "(":
|
|
421
|
+
depth += 1
|
|
422
|
+
elif character == ")" and depth:
|
|
423
|
+
depth -= 1
|
|
424
|
+
elif character == ";" and depth == 0:
|
|
425
|
+
signature = signature[:index]
|
|
426
|
+
break
|
|
427
|
+
return "".join(signature.split())
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def _simple_identifier(name: str) -> str:
|
|
431
|
+
return name.rsplit(".", 1)[-1]
|
|
432
|
+
|
|
433
|
+
@staticmethod
|
|
434
|
+
def _identifier_text_matches(language: Language, actual: str, expected: str) -> bool:
|
|
435
|
+
if language is Language.DELPHI:
|
|
436
|
+
return actual.casefold() == expected.casefold()
|
|
437
|
+
return actual == expected
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def _valid_identifier(language: Language, name: str) -> bool:
|
|
441
|
+
if language is Language.PYTHON:
|
|
442
|
+
return name.isidentifier() and not keyword.iskeyword(name)
|
|
443
|
+
if _IDENTIFIER_PATTERN.fullmatch(name) is None:
|
|
444
|
+
return False
|
|
445
|
+
if language is Language.DELPHI:
|
|
446
|
+
return name.casefold() not in _RESERVED_WORDS[language]
|
|
447
|
+
return name not in _RESERVED_WORDS[language]
|