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,822 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+ from pathlib import Path
5
+ import re
6
+ from typing import Iterable, Iterator
7
+
8
+ from ..models import Language, Problem, Reference, Relation, Resolution, SourceRange, Symbol
9
+ from ..workspace import stable_id
10
+ from .delphi_frontend.lsp_server import (
11
+ _mask_delphi_comments_and_strings,
12
+ build_outline_semantic_model,
13
+ outline_source,
14
+ )
15
+ from .delphi_frontend.parser import DelphiParser
16
+ from .delphi_frontend.preprocessor import PreprocessedSource, Preprocessor
17
+ from .delphi_frontend.semantic import Modifier, ReferenceKind, Scope, Symbol as DelphiSymbol
18
+ from .delphi_frontend.semantic_builder import SemanticModel
19
+ from .delphi_frontend.source_reader import read_source_text
20
+ from .tree_sitter import AdapterResult
21
+
22
+
23
+ _DELPHI_SECTION = re.compile(r"\b(interface|implementation)\b", re.IGNORECASE)
24
+ _DELPHI_IMPLEMENTABLE_KINDS = {"constructor", "destructor", "function", "method", "procedure"}
25
+ _DELPHI_INCLUDE = re.compile(
26
+ r"\{\$\s*(?:I|INCLUDE)\b(?P<brace>[^}]*)\}|\(\*\$\s*(?:I|INCLUDE)\b(?P<paren>.*?)\*\)",
27
+ re.IGNORECASE,
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class DelphiDocument:
33
+ path: str
34
+ source: str
35
+ outline_model: SemanticModel
36
+ detail_model: SemanticModel | None = None
37
+ parse_count: int = 0
38
+ dependencies: tuple[str, ...] = ()
39
+
40
+
41
+ class DelphiAdapter:
42
+ language = Language.DELPHI
43
+ extensions: tuple[str, ...] = (".pas", ".dpr", ".dpk", ".inc")
44
+
45
+ def __init__(
46
+ self,
47
+ *,
48
+ include_paths: Iterable[str] = (),
49
+ defines: Iterable[str] = (),
50
+ workspace_root: str | Path | None = None,
51
+ ) -> None:
52
+ self._include_paths = tuple(include_paths)
53
+ self._defines = tuple(defines)
54
+ self._workspace_root = (
55
+ Path(workspace_root).expanduser().resolve() if workspace_root is not None else None
56
+ )
57
+
58
+ def detect(self, path: str | Path) -> bool:
59
+ return Path(path).suffix.casefold() in self.extensions
60
+
61
+ def parse_outline(self, path: str | Path, source: str) -> AdapterResult:
62
+ path_text = str(path)
63
+ preprocessed: PreprocessedSource | None = None
64
+ outline_input = source
65
+ if "{$" in source or "(*$" in source:
66
+ preprocessed = Preprocessor(
67
+ defines=self._defines,
68
+ include_paths=self._include_paths,
69
+ workspace_root=self._workspace_root,
70
+ ).process(source, path_text)
71
+ outline_input = preprocessed.text
72
+ model = build_outline_semantic_model(outline_source(outline_input), path_text)
73
+ document = DelphiDocument(
74
+ path_text,
75
+ source,
76
+ model,
77
+ dependencies=preprocessed.included_files if preprocessed is not None else (),
78
+ )
79
+ symbols, _, _ = self._convert_model(model, source, path_text, preprocessed)
80
+ return AdapterResult(
81
+ symbols=symbols,
82
+ problems=self._preprocessor_problems(preprocessed) if preprocessed is not None else (),
83
+ document=document,
84
+ )
85
+
86
+ def parse_detail(self, path: str | Path, source: str, outline: AdapterResult) -> AdapterResult:
87
+ path_text = str(path)
88
+ current = outline.document
89
+ if not isinstance(current, DelphiDocument) or current.path != path_text or current.source != source:
90
+ current = DelphiDocument(path_text, source, build_outline_semantic_model(outline_source(source), path_text))
91
+ preprocessed = Preprocessor(
92
+ defines=self._defines,
93
+ include_paths=self._include_paths,
94
+ workspace_root=self._workspace_root,
95
+ ).process(source, path_text)
96
+ try:
97
+ parsed = DelphiParser(
98
+ include_paths=self._include_paths,
99
+ defines=self._defines,
100
+ workspace_root=self._workspace_root,
101
+ ).parse(
102
+ source,
103
+ path_text,
104
+ build_semantic=True,
105
+ preprocessed_source=preprocessed,
106
+ )
107
+ assert parsed.semantic is not None
108
+ except Exception as exc:
109
+ syntax_range = self._syntax_error_range(exc, preprocessed)
110
+ problem = Problem(
111
+ "syntax-error",
112
+ f"Delphi parser rejected the source: {exc}",
113
+ "error",
114
+ Language.DELPHI,
115
+ syntax_range,
116
+ metadata={
117
+ "parser": "delphi_frontend",
118
+ "error": type(exc).__name__,
119
+ "path": syntax_range.path if syntax_range is not None else path_text,
120
+ },
121
+ )
122
+ problems = list(outline.problems)
123
+ for preprocessor_problem in self._preprocessor_problems(preprocessed):
124
+ if preprocessor_problem not in problems:
125
+ problems.append(preprocessor_problem)
126
+ if problem not in problems:
127
+ problems.append(problem)
128
+ return replace(
129
+ outline,
130
+ problems=tuple(problems),
131
+ document=replace(current, dependencies=preprocessed.included_files),
132
+ )
133
+
134
+ document = replace(
135
+ current,
136
+ detail_model=parsed.semantic,
137
+ parse_count=current.parse_count + 1,
138
+ dependencies=parsed.preprocessed.included_files,
139
+ )
140
+ symbols, references, relations = self._convert_model(
141
+ parsed.semantic,
142
+ source,
143
+ path_text,
144
+ parsed.preprocessed,
145
+ )
146
+ problems = list(outline.problems)
147
+ for problem in self._preprocessor_problems(parsed.preprocessed):
148
+ if problem not in problems:
149
+ problems.append(problem)
150
+ for semantic_problem in parsed.semantic.problems:
151
+ problems.append(
152
+ Problem(
153
+ "semantic-unresolved",
154
+ semantic_problem.message,
155
+ "warning",
156
+ Language.DELPHI,
157
+ self._mapped_range(semantic_problem.range, parsed.preprocessed),
158
+ )
159
+ )
160
+ return AdapterResult(symbols, references, relations, tuple(problems), document)
161
+
162
+ @staticmethod
163
+ def _preprocessor_problems(preprocessed: PreprocessedSource) -> tuple[Problem, ...]:
164
+ problems: list[Problem] = []
165
+ for item in preprocessed.problems:
166
+ start_line = max(0, item.line - 1)
167
+ start_character = max(0, item.col - 1)
168
+ problems.append(
169
+ Problem(
170
+ f"preprocessor-{item.kind}",
171
+ item.message,
172
+ "warning",
173
+ Language.DELPHI,
174
+ SourceRange(
175
+ item.file_name,
176
+ start_line,
177
+ start_character,
178
+ start_line,
179
+ start_character + 1,
180
+ ),
181
+ metadata={"parser": "delphi_frontend", "path": item.file_name},
182
+ )
183
+ )
184
+ return tuple(problems)
185
+
186
+ @staticmethod
187
+ def _syntax_error_range(
188
+ error: Exception,
189
+ preprocessed: PreprocessedSource,
190
+ ) -> SourceRange | None:
191
+ line = getattr(error, "line", None)
192
+ column = getattr(error, "column", None)
193
+ token = getattr(error, "token", None)
194
+ if not isinstance(line, int) and token is not None:
195
+ line = getattr(token, "line", None)
196
+ if not isinstance(column, int) and token is not None:
197
+ column = getattr(token, "column", None)
198
+ if not isinstance(line, int) or not isinstance(column, int):
199
+ return None
200
+ path, mapped_line, mapped_character = preprocessed.map_position(line, max(0, column - 1))
201
+ if not path:
202
+ return None
203
+ return SourceRange(
204
+ path,
205
+ max(0, mapped_line - 1),
206
+ max(0, mapped_character),
207
+ max(0, mapped_line - 1),
208
+ max(0, mapped_character) + 1,
209
+ )
210
+
211
+ def resolve(self, result: AdapterResult) -> AdapterResult:
212
+ return result
213
+
214
+ def _convert_model(
215
+ self,
216
+ model: SemanticModel,
217
+ source: str,
218
+ path: str,
219
+ preprocessed: PreprocessedSource | None = None,
220
+ ) -> tuple[tuple[Symbol, ...], tuple[Reference, ...], tuple[Relation, ...]]:
221
+ converted: list[Symbol] = []
222
+ source_symbols: dict[int, Symbol] = {}
223
+ parent_by_source: dict[int, str] = {}
224
+ seen: set[int] = set()
225
+ semantic_source = preprocessed.text if preprocessed is not None else source
226
+ interface_position, implementation_position = self._section_lines(semantic_source)
227
+
228
+ def visit(scope: Scope, prefix: tuple[str, ...], parent_id: str) -> None:
229
+ if id(scope) in seen:
230
+ return
231
+ seen.add(id(scope))
232
+ for values in scope.symbols.values():
233
+ for native in values:
234
+ native_id = id(native)
235
+ if native_id in source_symbols:
236
+ continue
237
+ name = native.name
238
+ name_parts = tuple(part for part in name.split(".") if part)
239
+ qualified_parts = name_parts if len(name_parts) > 1 else (*prefix, name)
240
+ qualified = ".".join(qualified_parts)
241
+ declared = self._mapped_range(native.decl_range, preprocessed)
242
+ native_name_range = self._name_range(semantic_source, native)
243
+ name_range = self._mapped_source_range(native_name_range, preprocessed)
244
+ target_id = stable_id(
245
+ "symbol",
246
+ Language.DELPHI.value,
247
+ declared.path,
248
+ qualified,
249
+ str(declared.start_line),
250
+ str(declared.start_character),
251
+ )
252
+ item = Symbol(
253
+ target_id,
254
+ name,
255
+ qualified,
256
+ native.kind.value,
257
+ Language.DELPHI,
258
+ declared,
259
+ "",
260
+ declared.path,
261
+ self._signature(semantic_source, native),
262
+ native.kind.value,
263
+ parent_id,
264
+ name_range,
265
+ self._symbol_role(
266
+ native.kind.value,
267
+ native.decl_range.start_line,
268
+ native.decl_range.start_col,
269
+ interface_position,
270
+ implementation_position,
271
+ native.modifiers,
272
+ parent_id,
273
+ self._routine_has_body(semantic_source, native),
274
+ ),
275
+ )
276
+ converted.append(item)
277
+ source_symbols[native_id] = item
278
+ parent_by_source[native_id] = parent_id
279
+ if native.member_scope is not None:
280
+ visit(native.member_scope, (*prefix, name), target_id)
281
+
282
+ visit(model.unit_scope, (), "")
283
+
284
+ references = tuple(
285
+ self._reference(item, source_symbols, semantic_source, preprocessed)
286
+ for item in model.references
287
+ )
288
+ relations: list[Relation] = []
289
+ for native_id, item in source_symbols.items():
290
+ native = self._native_symbol(model.unit_scope, native_id)
291
+ if native is None:
292
+ continue
293
+ for base in native.base_types:
294
+ target_name = base.display_name()
295
+ target = self._unique_symbol(converted, target_name)
296
+ resolution = Resolution.COMPLETE if target is not None else Resolution.UNRESOLVED
297
+ kind = "implements" if target is not None and target.kind == "interface" else "inherits"
298
+ relations.append(
299
+ Relation(
300
+ item.target_id,
301
+ target.target_id if target else "",
302
+ kind,
303
+ resolution,
304
+ (item.range,),
305
+ target_name,
306
+ )
307
+ )
308
+ if Modifier.EXTERNAL in native.modifiers:
309
+ target_name, target_library = self._external_identity(
310
+ semantic_source,
311
+ replace(item, range=self._range(native.decl_range)),
312
+ native.name.split(".")[-1],
313
+ )
314
+ relations.append(
315
+ Relation(
316
+ item.target_id,
317
+ "",
318
+ "ffi",
319
+ Resolution.SOUND_PARTIAL,
320
+ (item.range,),
321
+ target_name,
322
+ target_library,
323
+ )
324
+ )
325
+ if preprocessed is not None:
326
+ relations.extend(self._export_relations(preprocessed, converted, path, source))
327
+ return tuple(converted), references, tuple(relations)
328
+
329
+ @staticmethod
330
+ def _section_lines(
331
+ source: str,
332
+ ) -> tuple[tuple[int, int] | None, tuple[int, int] | None]:
333
+ interface_position: tuple[int, int] | None = None
334
+ implementation_position: tuple[int, int] | None = None
335
+ comment_state: str | None = None
336
+ for line_number, raw_line in enumerate(source.splitlines(), start=1):
337
+ line, comment_state = _mask_delphi_comments_and_strings(raw_line, comment_state)
338
+ for match in _DELPHI_SECTION.finditer(line):
339
+ position = (line_number, match.start() + 1)
340
+ if match.group(1).casefold() == "interface" and interface_position is None:
341
+ interface_position = position
342
+ elif match.group(1).casefold() == "implementation":
343
+ implementation_position = position
344
+ return interface_position, implementation_position
345
+ return interface_position, implementation_position
346
+
347
+ @staticmethod
348
+ def _symbol_role(
349
+ kind: str,
350
+ line_number: int,
351
+ column_number: int,
352
+ interface_position: tuple[int, int] | None,
353
+ implementation_position: tuple[int, int] | None,
354
+ modifiers: set[Modifier],
355
+ parent_id: str,
356
+ has_body: bool,
357
+ ) -> str:
358
+ if kind not in _DELPHI_IMPLEMENTABLE_KINDS:
359
+ return "definition"
360
+ if has_body:
361
+ return "implementation"
362
+ if parent_id or modifiers.intersection({Modifier.ABSTRACT, Modifier.EXTERNAL, Modifier.FORWARD}):
363
+ return "declaration"
364
+ position = (line_number, column_number)
365
+ if implementation_position is not None and position > implementation_position:
366
+ return "implementation"
367
+ if interface_position is not None and position > interface_position:
368
+ return "declaration"
369
+ return "definition"
370
+
371
+ @staticmethod
372
+ def _routine_has_body(source: str, symbol: DelphiSymbol) -> bool:
373
+ return re.search(
374
+ r"\b(?:begin|asm)\b",
375
+ DelphiAdapter._symbol_text(source, symbol, mask=True),
376
+ re.IGNORECASE,
377
+ ) is not None
378
+
379
+ def _export_relations(
380
+ self,
381
+ preprocessed: PreprocessedSource,
382
+ symbols: list[Symbol],
383
+ path: str,
384
+ original_source: str,
385
+ ) -> list[Relation]:
386
+ source = preprocessed.text
387
+ masked = self._masked_source(source)
388
+ library = re.search(
389
+ r"\blibrary\s+([A-Za-z_][A-Za-z0-9_]*)\s*;",
390
+ masked,
391
+ re.IGNORECASE,
392
+ )
393
+ target_library = library.group(1) if library is not None else ""
394
+ relations: list[Relation] = []
395
+ for exports in re.finditer(r"\bexports\b", masked, re.IGNORECASE):
396
+ statement_end = self._statement_end(masked, exports.end())
397
+ if statement_end is None:
398
+ continue
399
+ for start, end in self._comma_spans(masked, exports.end(), statement_end):
400
+ masked_item = masked[start:end]
401
+ name_match = re.search(r"\b([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\b", masked_item)
402
+ if name_match is None:
403
+ continue
404
+ exported_name = name_match.group(1)
405
+ symbol = self._exported_symbol(symbols, exported_name)
406
+ if symbol is None:
407
+ continue
408
+ target_name = exported_name.rsplit(".", 1)[-1]
409
+ alias_marker = re.search(r"\bname\b", masked_item, re.IGNORECASE)
410
+ if alias_marker is not None:
411
+ original_tail = source[start + alias_marker.end():end]
412
+ alias = re.match(r"\s*'((?:''|[^'])*)'", original_tail)
413
+ if alias is not None:
414
+ target_name = alias.group(1).replace("''", "'")
415
+ name_start = start + name_match.start(1)
416
+ evidence_path, evidence_line, character = self._map_export_position(
417
+ preprocessed,
418
+ original_source,
419
+ path,
420
+ name_start,
421
+ exported_name,
422
+ )
423
+ evidence = SourceRange(
424
+ evidence_path,
425
+ evidence_line,
426
+ character,
427
+ evidence_line,
428
+ character + len(exported_name),
429
+ )
430
+ relations.append(
431
+ Relation(
432
+ symbol.target_id,
433
+ "",
434
+ "ffi",
435
+ Resolution.SOUND_PARTIAL,
436
+ (evidence,),
437
+ target_name,
438
+ target_library,
439
+ )
440
+ )
441
+ return relations
442
+
443
+ def _map_export_position(
444
+ self,
445
+ preprocessed: PreprocessedSource,
446
+ original_source: str,
447
+ path: str,
448
+ offset: int,
449
+ exported_name: str,
450
+ ) -> tuple[str, int, int]:
451
+ line = preprocessed.text.count("\n", 0, offset)
452
+ line_start = preprocessed.text.rfind("\n", 0, offset) + 1
453
+ character = offset - line_start
454
+ mapped_path, mapped_line, mapped_character = preprocessed.map_position(line + 1, character)
455
+ if not mapped_path:
456
+ return path, line, character
457
+ inline_origin = self._inline_include_origin(
458
+ mapped_path,
459
+ mapped_line,
460
+ mapped_character,
461
+ exported_name,
462
+ path,
463
+ original_source,
464
+ )
465
+ if inline_origin is not None:
466
+ return inline_origin
467
+ return mapped_path, max(0, mapped_line - 1), mapped_character
468
+
469
+ def _inline_include_origin(
470
+ self,
471
+ mapped_path: str,
472
+ mapped_line: int,
473
+ mapped_character: int,
474
+ exported_name: str,
475
+ main_path: str,
476
+ main_source: str,
477
+ ) -> tuple[str, int, int] | None:
478
+ source = main_source if mapped_path == main_path else self._read_source(mapped_path)
479
+ if source is None:
480
+ return None
481
+ lines = source.splitlines()
482
+ if mapped_line < 1 or mapped_line > len(lines):
483
+ return None
484
+ candidates: list[tuple[str, int, int]] = []
485
+ for include in _DELPHI_INCLUDE.finditer(lines[mapped_line - 1]):
486
+ include_character = mapped_character - include.start()
487
+ if include_character < 0:
488
+ continue
489
+ include_name = self._include_name(include.group("brace") or include.group("paren") or "")
490
+ resolved = self._read_include(mapped_path, include_name)
491
+ if resolved is None:
492
+ continue
493
+ include_source, include_path = resolved
494
+ first_line = include_source.removeprefix("\ufeff").split("\n", 1)[0]
495
+ end_character = include_character + len(exported_name)
496
+ if first_line[include_character:end_character].casefold() == exported_name.casefold():
497
+ candidates.append((include_path, 0, include_character))
498
+ return candidates[0] if len(candidates) == 1 else None
499
+
500
+ @staticmethod
501
+ def _include_name(parameter: str) -> str:
502
+ text = parameter.strip()
503
+ if not text:
504
+ return ""
505
+ if text[0] in {"'", '"'}:
506
+ end = text.find(text[0], 1)
507
+ return text[1:] if end == -1 else text[1:end]
508
+ return text.split(maxsplit=1)[0]
509
+
510
+ @staticmethod
511
+ def _read_source(path: str) -> str | None:
512
+ source_path = Path(path)
513
+ return read_source_text(source_path) if source_path.exists() else None
514
+
515
+ def _read_include(self, parent_file: str, include_name: str) -> tuple[str, str] | None:
516
+ if not include_name:
517
+ return None
518
+ workspace_root = self._workspace_root or Path(parent_file).expanduser().resolve().parent
519
+ include_path = Path(include_name.replace("\\", "/"))
520
+ parent_path = Path(parent_file).expanduser()
521
+ if not parent_path.is_absolute():
522
+ parent_path = workspace_root / parent_path
523
+ bases = [parent_path.parent]
524
+ for value in self._include_paths:
525
+ base = Path(value).expanduser()
526
+ bases.append(base if base.is_absolute() else workspace_root / base)
527
+ for base in bases:
528
+ candidate = (include_path if include_path.is_absolute() else base / include_path).resolve()
529
+ if candidate != workspace_root and not candidate.is_relative_to(workspace_root):
530
+ continue
531
+ if candidate.exists():
532
+ return read_source_text(candidate), str(candidate)
533
+ return None
534
+
535
+ @classmethod
536
+ def _external_identity(
537
+ cls,
538
+ source: str,
539
+ symbol: Symbol,
540
+ default_name: str,
541
+ ) -> tuple[str, str]:
542
+ lines = source.splitlines(keepends=True)
543
+ if symbol.range.start_line >= len(lines):
544
+ return default_name, ""
545
+ start = sum(len(line) for line in lines[: symbol.range.start_line])
546
+ start += symbol.range.start_character
547
+ segment = source[start:start + 4096]
548
+ masked = cls._masked_source(segment)
549
+ external = re.search(r"\bexternal\b", masked, re.IGNORECASE)
550
+ if external is None:
551
+ return default_name, ""
552
+ end = cls._statement_end(masked, external.end())
553
+ if end is None:
554
+ return default_name, ""
555
+ masked_tail = masked[external.end():end]
556
+ original_tail = segment[external.end():end]
557
+ library_match = re.match(r"\s*'((?:''|[^'])*)'", original_tail)
558
+ library = library_match.group(1).replace("''", "'") if library_match else ""
559
+ name = default_name
560
+ name_marker = re.search(r"\bname\b", masked_tail, re.IGNORECASE)
561
+ if name_marker is not None:
562
+ original_name = original_tail[name_marker.end():]
563
+ name_match = re.match(r"\s*'((?:''|[^'])*)'", original_name)
564
+ if name_match is not None:
565
+ name = name_match.group(1).replace("''", "'")
566
+ return name, library
567
+
568
+ @staticmethod
569
+ def _masked_source(source: str) -> str:
570
+ state: str | None = None
571
+ masked: list[str] = []
572
+ for raw_line in source.splitlines(keepends=True):
573
+ line = raw_line.rstrip("\r\n")
574
+ ending = raw_line[len(line):]
575
+ clean, state = _mask_delphi_comments_and_strings(line, state)
576
+ masked.append(clean + ending)
577
+ return "".join(masked)
578
+
579
+ @staticmethod
580
+ def _statement_end(source: str, start: int) -> int | None:
581
+ depth = 0
582
+ for index in range(start, len(source)):
583
+ character = source[index]
584
+ if character == "(":
585
+ depth += 1
586
+ elif character == ")" and depth:
587
+ depth -= 1
588
+ elif character == ";" and depth == 0:
589
+ return index
590
+ return None
591
+
592
+ @staticmethod
593
+ def _comma_spans(source: str, start: int, end: int) -> Iterator[tuple[int, int]]:
594
+ depth = 0
595
+ item_start = start
596
+ for index in range(start, end):
597
+ character = source[index]
598
+ if character == "(":
599
+ depth += 1
600
+ elif character == ")" and depth:
601
+ depth -= 1
602
+ elif character == "," and depth == 0:
603
+ yield item_start, index
604
+ item_start = index + 1
605
+ yield item_start, end
606
+
607
+ @staticmethod
608
+ def _exported_symbol(symbols: list[Symbol], name: str) -> Symbol | None:
609
+ folded = name.casefold()
610
+ exact = [
611
+ item
612
+ for item in symbols
613
+ if item.qualified_name.casefold() == folded or item.name.casefold() == folded
614
+ ]
615
+ candidates = exact or [
616
+ item
617
+ for item in symbols
618
+ if item.name.rsplit(".", 1)[-1].casefold() == name.rsplit(".", 1)[-1].casefold()
619
+ ]
620
+ for role in ("implementation", "definition", "declaration"):
621
+ preferred = [item for item in candidates if item.role == role]
622
+ if len(preferred) == 1:
623
+ return preferred[0]
624
+ if len(preferred) > 1:
625
+ return None
626
+ return None
627
+
628
+ def _reference(
629
+ self,
630
+ item: object,
631
+ symbols: dict[int, Symbol],
632
+ source_text: str,
633
+ preprocessed: PreprocessedSource | None,
634
+ ) -> Reference:
635
+ native = item
636
+ resolved = getattr(native, "resolved")
637
+ target = symbols.get(id(resolved)) if resolved is not None else None
638
+ owner = getattr(native, "scope").owner
639
+ source = symbols.get(id(owner)) if owner is not None else None
640
+ candidates = [value for value in symbols.values() if value.name.casefold() == getattr(native, "name").casefold()]
641
+ resolution = Resolution.COMPLETE if target is not None else (
642
+ Resolution.AMBIGUOUS if len(candidates) > 1 else Resolution.UNRESOLVED
643
+ )
644
+ kind = getattr(native, "kind")
645
+ original_name = getattr(native, "name")
646
+ name = original_name
647
+ qualifier = ""
648
+ if kind is ReferenceKind.CALL and "." in name:
649
+ qualifier = name.rsplit(".", 1)[0]
650
+ name = name.rsplit(".", 1)[-1]
651
+ ref_range = self._range(getattr(native, "ref_range"))
652
+ simple_name = name.rsplit(".", 1)[-1]
653
+ if ref_range.start_line == ref_range.end_line:
654
+ lines = source_text.splitlines()
655
+ if 0 <= ref_range.start_line < len(lines):
656
+ line_text = lines[ref_range.start_line]
657
+ if ref_range.end_character <= ref_range.start_character:
658
+ segment = line_text[
659
+ ref_range.start_character:ref_range.start_character + len(simple_name)
660
+ ]
661
+ else:
662
+ segment = line_text[ref_range.start_character:ref_range.end_character]
663
+ offset = segment.casefold().rfind(simple_name.casefold())
664
+ if offset >= 0:
665
+ start_character = ref_range.start_character + offset
666
+ ref_range = replace(
667
+ ref_range,
668
+ start_character=start_character,
669
+ end_character=start_character + len(simple_name),
670
+ )
671
+ mapped_ref_range = self._mapped_source_range(ref_range, preprocessed)
672
+ assert mapped_ref_range is not None
673
+ ref_range = mapped_ref_range
674
+ return Reference(
675
+ source.target_id if source else "",
676
+ target.target_id if target else "",
677
+ name,
678
+ Language.DELPHI,
679
+ ref_range,
680
+ resolution,
681
+ "import" if kind is ReferenceKind.UNIT else kind.value,
682
+ qualifier,
683
+ original_name if kind is ReferenceKind.UNIT else "",
684
+ (),
685
+ "",
686
+ kind is ReferenceKind.UNIT,
687
+ )
688
+
689
+ def _native_symbol(self, root: Scope, target_id: int) -> DelphiSymbol | None:
690
+ return next((item for item in self._iter_native_symbols(root) if id(item) == target_id), None)
691
+
692
+ def _iter_native_symbols(self, scope: Scope, seen: set[int] | None = None) -> Iterator[DelphiSymbol]:
693
+ if seen is None:
694
+ seen = set()
695
+ if id(scope) in seen:
696
+ return
697
+ seen.add(id(scope))
698
+ for values in scope.symbols.values():
699
+ for item in values:
700
+ yield item
701
+ if item.member_scope is not None:
702
+ yield from self._iter_native_symbols(item.member_scope, seen)
703
+
704
+ @staticmethod
705
+ def _unique_symbol(symbols: list[Symbol], name: str) -> Symbol | None:
706
+ simple = name.rsplit(".", 1)[-1].casefold()
707
+ matches = [item for item in symbols if item.name.casefold() == simple]
708
+ return matches[0] if len(matches) == 1 else None
709
+
710
+ @staticmethod
711
+ def _range(value: object) -> SourceRange:
712
+ return SourceRange(
713
+ str(getattr(value, "file_name")),
714
+ max(0, int(getattr(value, "start_line")) - 1),
715
+ max(0, int(getattr(value, "start_col")) - 1),
716
+ max(0, int(getattr(value, "end_line")) - 1),
717
+ max(0, int(getattr(value, "end_col")) - 1),
718
+ )
719
+
720
+ @classmethod
721
+ def _mapped_range(
722
+ cls,
723
+ value: object,
724
+ preprocessed: PreprocessedSource | None,
725
+ ) -> SourceRange:
726
+ mapped = cls._mapped_source_range(cls._range(value), preprocessed)
727
+ assert mapped is not None
728
+ return mapped
729
+
730
+ @staticmethod
731
+ def _mapped_source_range(
732
+ value: SourceRange | None,
733
+ preprocessed: PreprocessedSource | None,
734
+ ) -> SourceRange | None:
735
+ if value is None or preprocessed is None:
736
+ return value
737
+ start_path, start_line, start_character = preprocessed.map_position(
738
+ value.start_line + 1,
739
+ value.start_character,
740
+ )
741
+ end_path, end_line, end_character = preprocessed.map_position(
742
+ value.end_line + 1,
743
+ value.end_character,
744
+ )
745
+ if not start_path:
746
+ return value
747
+ if end_path != start_path:
748
+ end_line = start_line
749
+ end_character = start_character
750
+ return SourceRange(
751
+ start_path,
752
+ max(0, start_line - 1),
753
+ max(0, start_character),
754
+ max(0, end_line - 1),
755
+ max(0, end_character),
756
+ )
757
+
758
+ @staticmethod
759
+ def _signature(source: str, symbol: DelphiSymbol) -> str:
760
+ text = DelphiAdapter._symbol_text(source, symbol).strip()
761
+ if (
762
+ symbol.kind.value in _DELPHI_IMPLEMENTABLE_KINDS
763
+ and re.search(
764
+ r"\b(?:constructor|destructor|function|procedure)\b",
765
+ text,
766
+ re.IGNORECASE,
767
+ )
768
+ is None
769
+ ):
770
+ lines = source.splitlines()
771
+ start = max(0, symbol.decl_range.start_line - 1)
772
+ end = min(len(lines), max(start + 1, symbol.decl_range.end_line))
773
+ text = "\n".join(lines[start:end]).strip()
774
+ if "begin" in text.casefold():
775
+ text = text[: text.casefold().find("begin")].rstrip()
776
+ return " ".join(text.split())[:2000]
777
+
778
+ @staticmethod
779
+ def _symbol_text(source: str, symbol: DelphiSymbol, *, mask: bool = False) -> str:
780
+ lines = source.splitlines()
781
+ if mask:
782
+ masked_lines: list[str] = []
783
+ comment_state: str | None = None
784
+ for line in lines:
785
+ clean, comment_state = _mask_delphi_comments_and_strings(line, comment_state)
786
+ masked_lines.append(clean)
787
+ lines = masked_lines
788
+ start_line = max(0, symbol.decl_range.start_line - 1)
789
+ end_line = min(len(lines) - 1, max(start_line, symbol.decl_range.end_line - 1))
790
+ if start_line >= len(lines) or end_line < start_line:
791
+ return ""
792
+ segments: list[str] = []
793
+ for line_number in range(start_line, end_line + 1):
794
+ start = max(0, symbol.decl_range.start_col - 1) if line_number == start_line else 0
795
+ end = (
796
+ max(start, symbol.decl_range.end_col - 1)
797
+ if line_number == end_line
798
+ else len(lines[line_number])
799
+ )
800
+ segments.append(lines[line_number][start:end])
801
+ return "\n".join(segments)
802
+
803
+ def _name_range(self, source: str, symbol: DelphiSymbol) -> SourceRange | None:
804
+ declared = self._range(symbol.decl_range)
805
+ lines = source.splitlines()
806
+ simple_name = symbol.name.rsplit(".", 1)[-1]
807
+ for line_number in range(declared.start_line, min(declared.end_line + 1, len(lines))):
808
+ start = declared.start_character if line_number == declared.start_line else 0
809
+ end = declared.end_character if line_number == declared.end_line else len(lines[line_number])
810
+ if end <= start:
811
+ end = len(lines[line_number])
812
+ segment = lines[line_number][start:end]
813
+ match = re.search(rf"\b{re.escape(simple_name)}\b", segment, re.IGNORECASE)
814
+ if match is not None:
815
+ return SourceRange(
816
+ declared.path,
817
+ line_number,
818
+ start + match.start(),
819
+ line_number,
820
+ start + match.end(),
821
+ )
822
+ return None