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,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
|