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.
Files changed (42) hide show
  1. agent_codinglanguage_mapper/__init__.py +20 -0
  2. agent_codinglanguage_mapper/__main__.py +4 -0
  3. agent_codinglanguage_mapper/_version.py +2 -0
  4. agent_codinglanguage_mapper/adapters/__init__.py +4 -0
  5. agent_codinglanguage_mapper/adapters/delphi.py +822 -0
  6. agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
  7. agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
  8. agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
  9. agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
  10. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
  11. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
  12. agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
  13. agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
  14. agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
  15. agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
  16. agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
  17. agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
  18. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
  19. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
  20. agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
  21. agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
  22. agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
  23. agent_codinglanguage_mapper/cli.py +138 -0
  24. agent_codinglanguage_mapper/discovery.py +1328 -0
  25. agent_codinglanguage_mapper/indexer.py +1102 -0
  26. agent_codinglanguage_mapper/integration.py +186 -0
  27. agent_codinglanguage_mapper/lsp_server.py +413 -0
  28. agent_codinglanguage_mapper/lsp_service.py +447 -0
  29. agent_codinglanguage_mapper/mapper.py +596 -0
  30. agent_codinglanguage_mapper/models.py +101 -0
  31. agent_codinglanguage_mapper/protocol.py +152 -0
  32. agent_codinglanguage_mapper/rendering.py +153 -0
  33. agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
  34. agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
  35. agent_codinglanguage_mapper/worker.py +29 -0
  36. agent_codinglanguage_mapper/workspace.py +114 -0
  37. agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
  38. agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
  39. agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
  40. agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
  41. agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
  42. 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]