python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Linear Delphi/Object Pascal lexer ported from DelphiAST/SimpleParser.
|
|
2
|
+
|
|
3
|
+
The implementation intentionally uses direct character dispatch rather than a
|
|
4
|
+
backtracking regular-expression lexer. Source: DelphiAST, MPL-2.0,
|
|
5
|
+
https://github.com/RomanYankovsky/DelphiAST
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from .delphiast_tokens import Token, TokenKind
|
|
13
|
+
from .lark_tokens import KEYWORDS
|
|
14
|
+
from .lexical_scanner import scan_brace_comment
|
|
15
|
+
|
|
16
|
+
_KEYWORDS = frozenset(value.casefold() for value in KEYWORDS)
|
|
17
|
+
_COMPOUND_SYMBOLS = frozenset(
|
|
18
|
+
{
|
|
19
|
+
":=",
|
|
20
|
+
"<=",
|
|
21
|
+
">=",
|
|
22
|
+
"<>",
|
|
23
|
+
"..",
|
|
24
|
+
"**",
|
|
25
|
+
"+=",
|
|
26
|
+
"-=",
|
|
27
|
+
"*=",
|
|
28
|
+
"/=",
|
|
29
|
+
"><",
|
|
30
|
+
"=>",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
_HEX_DIGITS = frozenset("0123456789abcdefABCDEF")
|
|
34
|
+
_WHITESPACE_RE = re.compile(r"\s+")
|
|
35
|
+
_IDENTIFIER_PART_RE = re.compile(r"\w*")
|
|
36
|
+
_INITIALIZER_SPECIAL_RE = re.compile(r"['/{}\[\]()]")
|
|
37
|
+
_LARGE_INITIALIZER_CANDIDATE_RE = re.compile(r"(?<!:)=\s*\(")
|
|
38
|
+
_SIMPLE_TOKEN_RE = re.compile(
|
|
39
|
+
r"\s+"
|
|
40
|
+
r"|(?:[^\W\d]|_)\w*"
|
|
41
|
+
r"|\d+(?:\.(?!\.)\d*)?(?:[eE][+-]?\d+)?"
|
|
42
|
+
r"|:=|<=|>=|<>|\.\.|\*\*|\+=|-=|\*=|/=|><|=>"
|
|
43
|
+
r"|[;,:.=+\-*/()\[\]^@<>]"
|
|
44
|
+
r"|.",
|
|
45
|
+
re.DOTALL,
|
|
46
|
+
)
|
|
47
|
+
_SIMPLE_SOURCE_MARKERS = ("'", "{", "//", "(*", "#", "$", "%", "&")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DelphiAstLexer:
|
|
51
|
+
def __init__(self, source: str, file_name: str = "") -> None:
|
|
52
|
+
self.source = source
|
|
53
|
+
self.file_name = file_name
|
|
54
|
+
self._length = len(source)
|
|
55
|
+
self._index = 0
|
|
56
|
+
self._line = 1
|
|
57
|
+
self._column = 1
|
|
58
|
+
self._previous_significant = ""
|
|
59
|
+
|
|
60
|
+
def tokenize(self, *, include_trivia: bool = True) -> list[Token]:
|
|
61
|
+
if self._is_simple_source():
|
|
62
|
+
return self._tokenize_simple()
|
|
63
|
+
tokens: list[Token] = []
|
|
64
|
+
while self._index < self._length:
|
|
65
|
+
character = self.source[self._index]
|
|
66
|
+
if character.isspace():
|
|
67
|
+
whitespace = _WHITESPACE_RE.match(self.source, self._index)
|
|
68
|
+
assert whitespace is not None
|
|
69
|
+
self._advance_to(whitespace.end())
|
|
70
|
+
continue
|
|
71
|
+
if not include_trivia:
|
|
72
|
+
trivia_end = self._trivia_end()
|
|
73
|
+
if trivia_end is not None:
|
|
74
|
+
self._advance_to(trivia_end)
|
|
75
|
+
continue
|
|
76
|
+
token = self._next_token()
|
|
77
|
+
if include_trivia or token.kind not in {
|
|
78
|
+
TokenKind.COMMENT,
|
|
79
|
+
TokenKind.DIRECTIVE,
|
|
80
|
+
}:
|
|
81
|
+
tokens.append(token)
|
|
82
|
+
if token.kind not in {TokenKind.COMMENT, TokenKind.DIRECTIVE}:
|
|
83
|
+
self._previous_significant = token.normalized
|
|
84
|
+
if token.end <= token.start: # Defensive forward-progress guard.
|
|
85
|
+
self._advance_to(self._index + 1)
|
|
86
|
+
tokens.append(
|
|
87
|
+
Token(
|
|
88
|
+
TokenKind.EOF,
|
|
89
|
+
"",
|
|
90
|
+
self._line,
|
|
91
|
+
self._column,
|
|
92
|
+
self._index,
|
|
93
|
+
self._index,
|
|
94
|
+
self.file_name,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return tokens
|
|
98
|
+
|
|
99
|
+
def _is_simple_source(self) -> bool:
|
|
100
|
+
return not any(marker in self.source for marker in _SIMPLE_SOURCE_MARKERS) and (
|
|
101
|
+
len(self.source) < 64 * 1024
|
|
102
|
+
or _LARGE_INITIALIZER_CANDIDATE_RE.search(self.source) is None
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _tokenize_simple(self) -> list[Token]:
|
|
106
|
+
tokens: list[Token] = []
|
|
107
|
+
line = 1
|
|
108
|
+
column = 1
|
|
109
|
+
for match in _SIMPLE_TOKEN_RE.finditer(self.source):
|
|
110
|
+
value = match.group()
|
|
111
|
+
if value.isspace():
|
|
112
|
+
newline_count = value.count("\n")
|
|
113
|
+
if newline_count:
|
|
114
|
+
line += newline_count
|
|
115
|
+
column = len(value.rsplit("\n", 1)[-1]) + 1
|
|
116
|
+
else:
|
|
117
|
+
column += len(value)
|
|
118
|
+
continue
|
|
119
|
+
first = value[0]
|
|
120
|
+
if _identifier_start(first):
|
|
121
|
+
kind = (
|
|
122
|
+
TokenKind.KEYWORD
|
|
123
|
+
if value.casefold() in _KEYWORDS
|
|
124
|
+
else TokenKind.IDENTIFIER
|
|
125
|
+
)
|
|
126
|
+
elif first.isdigit():
|
|
127
|
+
kind = TokenKind.NUMBER
|
|
128
|
+
elif value in _COMPOUND_SYMBOLS or first in ";,:.=+-*/()[]^@<>":
|
|
129
|
+
kind = TokenKind.SYMBOL
|
|
130
|
+
else:
|
|
131
|
+
kind = TokenKind.UNKNOWN
|
|
132
|
+
tokens.append(
|
|
133
|
+
Token(
|
|
134
|
+
kind,
|
|
135
|
+
value,
|
|
136
|
+
line,
|
|
137
|
+
column,
|
|
138
|
+
match.start(),
|
|
139
|
+
match.end(),
|
|
140
|
+
self.file_name,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
column += len(value)
|
|
144
|
+
self._index = self._length
|
|
145
|
+
self._line = line
|
|
146
|
+
self._column = column
|
|
147
|
+
tokens.append(
|
|
148
|
+
Token(
|
|
149
|
+
TokenKind.EOF,
|
|
150
|
+
"",
|
|
151
|
+
line,
|
|
152
|
+
column,
|
|
153
|
+
self._length,
|
|
154
|
+
self._length,
|
|
155
|
+
self.file_name,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
return tokens
|
|
159
|
+
|
|
160
|
+
def _trivia_end(self) -> int | None:
|
|
161
|
+
start = self._index
|
|
162
|
+
character = self.source[start]
|
|
163
|
+
following = self.source[start + 1] if start + 1 < self._length else ""
|
|
164
|
+
if character == "/" and following == "/":
|
|
165
|
+
end = self.source.find("\n", start + 2)
|
|
166
|
+
return self._length if end < 0 else end
|
|
167
|
+
if character == "{":
|
|
168
|
+
end, _ = scan_brace_comment(self.source, start)
|
|
169
|
+
return end
|
|
170
|
+
if character == "(" and following == "*":
|
|
171
|
+
close = self.source.find("*)", start + 2)
|
|
172
|
+
return self._length if close < 0 else close + 2
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _next_token(self) -> Token:
|
|
176
|
+
start = self._index
|
|
177
|
+
line = self._line
|
|
178
|
+
column = self._column
|
|
179
|
+
character = self.source[start]
|
|
180
|
+
following = self.source[start + 1] if start + 1 < self._length else ""
|
|
181
|
+
|
|
182
|
+
if character == "/" and following == "/":
|
|
183
|
+
end = self.source.find("\n", start + 2)
|
|
184
|
+
end = self._length if end < 0 else end
|
|
185
|
+
return self._emit(TokenKind.COMMENT, start, end, line, column)
|
|
186
|
+
if character == "{":
|
|
187
|
+
end, _ = scan_brace_comment(self.source, start)
|
|
188
|
+
kind = TokenKind.DIRECTIVE if following == "$" else TokenKind.COMMENT
|
|
189
|
+
return self._emit(kind, start, end, line, column)
|
|
190
|
+
if character == "(" and following == "*":
|
|
191
|
+
end = self.source.find("*)", start + 2)
|
|
192
|
+
end = self._length if end < 0 else end + 2
|
|
193
|
+
directive = start + 2 < self._length and self.source[start + 2] == "$"
|
|
194
|
+
return self._emit(
|
|
195
|
+
TokenKind.DIRECTIVE if directive else TokenKind.COMMENT,
|
|
196
|
+
start,
|
|
197
|
+
end,
|
|
198
|
+
line,
|
|
199
|
+
column,
|
|
200
|
+
)
|
|
201
|
+
if character == "'":
|
|
202
|
+
end = self._quoted_end(start)
|
|
203
|
+
return self._emit(TokenKind.STRING, start, end, line, column)
|
|
204
|
+
if character == "(" and self._previous_significant == "=":
|
|
205
|
+
initializer_end = self._large_initializer_end(start)
|
|
206
|
+
if initializer_end is not None:
|
|
207
|
+
return self._emit(
|
|
208
|
+
TokenKind.UNKNOWN,
|
|
209
|
+
start,
|
|
210
|
+
initializer_end,
|
|
211
|
+
line,
|
|
212
|
+
column,
|
|
213
|
+
value="<large-initializer>",
|
|
214
|
+
)
|
|
215
|
+
if character == "#":
|
|
216
|
+
end = start + 1
|
|
217
|
+
if end < self._length and self.source[end] == "$":
|
|
218
|
+
end += 1
|
|
219
|
+
while end < self._length and self.source[end] in _HEX_DIGITS:
|
|
220
|
+
end += 1
|
|
221
|
+
else:
|
|
222
|
+
while end < self._length and self.source[end].isdigit():
|
|
223
|
+
end += 1
|
|
224
|
+
return self._emit(TokenKind.CHAR_CODE, start, max(end, start + 1), line, column)
|
|
225
|
+
if character == "$":
|
|
226
|
+
end = start + 1
|
|
227
|
+
while end < self._length and self.source[end] in _HEX_DIGITS:
|
|
228
|
+
end += 1
|
|
229
|
+
return self._emit(TokenKind.NUMBER, start, end, line, column)
|
|
230
|
+
if character == "%" and following in {"0", "1"}:
|
|
231
|
+
end = start + 1
|
|
232
|
+
while end < self._length and self.source[end] in {"0", "1"}:
|
|
233
|
+
end += 1
|
|
234
|
+
return self._emit(TokenKind.NUMBER, start, end, line, column)
|
|
235
|
+
if character == "&" and following.isdigit():
|
|
236
|
+
end = start + 1
|
|
237
|
+
while end < self._length and self.source[end] in "01234567":
|
|
238
|
+
end += 1
|
|
239
|
+
return self._emit(TokenKind.NUMBER, start, end, line, column)
|
|
240
|
+
if character == "&" and _identifier_start(following):
|
|
241
|
+
end = start + 2
|
|
242
|
+
identifier_tail = _IDENTIFIER_PART_RE.match(self.source, end)
|
|
243
|
+
assert identifier_tail is not None
|
|
244
|
+
end = identifier_tail.end()
|
|
245
|
+
token = self._emit(TokenKind.IDENTIFIER, start, end, line, column)
|
|
246
|
+
return Token(
|
|
247
|
+
token.kind,
|
|
248
|
+
token.value[1:],
|
|
249
|
+
token.line,
|
|
250
|
+
token.column,
|
|
251
|
+
token.start,
|
|
252
|
+
token.end,
|
|
253
|
+
token.file_name,
|
|
254
|
+
)
|
|
255
|
+
if _identifier_start(character):
|
|
256
|
+
end = start + 1
|
|
257
|
+
identifier_tail = _IDENTIFIER_PART_RE.match(self.source, end)
|
|
258
|
+
assert identifier_tail is not None
|
|
259
|
+
end = identifier_tail.end()
|
|
260
|
+
value = self.source[start:end]
|
|
261
|
+
kind = TokenKind.KEYWORD if value.casefold() in _KEYWORDS else TokenKind.IDENTIFIER
|
|
262
|
+
return self._emit(kind, start, end, line, column)
|
|
263
|
+
if character.isdigit():
|
|
264
|
+
end = self._number_end(start)
|
|
265
|
+
return self._emit(TokenKind.NUMBER, start, end, line, column)
|
|
266
|
+
|
|
267
|
+
compound = self.source[start : start + 2]
|
|
268
|
+
if compound in _COMPOUND_SYMBOLS:
|
|
269
|
+
return self._emit(TokenKind.SYMBOL, start, start + 2, line, column)
|
|
270
|
+
if character in ";,:.=+-*/()[]^@<>":
|
|
271
|
+
return self._emit(TokenKind.SYMBOL, start, start + 1, line, column)
|
|
272
|
+
return self._emit(TokenKind.UNKNOWN, start, start + 1, line, column)
|
|
273
|
+
|
|
274
|
+
def _number_end(self, start: int) -> int:
|
|
275
|
+
end = start
|
|
276
|
+
while end < self._length and self.source[end].isdigit():
|
|
277
|
+
end += 1
|
|
278
|
+
if (
|
|
279
|
+
end < self._length
|
|
280
|
+
and self.source[end] == "."
|
|
281
|
+
and self.source[end : end + 2] != ".."
|
|
282
|
+
):
|
|
283
|
+
end += 1
|
|
284
|
+
while end < self._length and self.source[end].isdigit():
|
|
285
|
+
end += 1
|
|
286
|
+
if end < self._length and self.source[end] in {"e", "E"}:
|
|
287
|
+
exponent = end + 1
|
|
288
|
+
if exponent < self._length and self.source[exponent] in {"+", "-"}:
|
|
289
|
+
exponent += 1
|
|
290
|
+
digit_start = exponent
|
|
291
|
+
while exponent < self._length and self.source[exponent].isdigit():
|
|
292
|
+
exponent += 1
|
|
293
|
+
if exponent > digit_start:
|
|
294
|
+
end = exponent
|
|
295
|
+
return end
|
|
296
|
+
|
|
297
|
+
def _quoted_end(self, start: int) -> int:
|
|
298
|
+
for delimiter in ("'''''", "'''"):
|
|
299
|
+
opening_end = start + len(delimiter)
|
|
300
|
+
if not self.source.startswith(delimiter, start):
|
|
301
|
+
continue
|
|
302
|
+
if self.source[opening_end : opening_end + 1] not in {"\r", "\n"}:
|
|
303
|
+
continue
|
|
304
|
+
close = self.source.find(delimiter, opening_end)
|
|
305
|
+
return self._length if close < 0 else close + len(delimiter)
|
|
306
|
+
end = start + 1
|
|
307
|
+
while end < self._length:
|
|
308
|
+
if self.source[end] in {"\r", "\n"}:
|
|
309
|
+
return end
|
|
310
|
+
if self.source[end] != "'":
|
|
311
|
+
end += 1
|
|
312
|
+
continue
|
|
313
|
+
if end + 1 < self._length and self.source[end + 1] == "'":
|
|
314
|
+
end += 2
|
|
315
|
+
continue
|
|
316
|
+
return end + 1
|
|
317
|
+
return self._length
|
|
318
|
+
|
|
319
|
+
def _large_initializer_end(self, start: int) -> int | None:
|
|
320
|
+
first_special = _INITIALIZER_SPECIAL_RE.search(self.source, start + 1)
|
|
321
|
+
if first_special is not None and first_special.group() == ")":
|
|
322
|
+
end = first_special.end()
|
|
323
|
+
return end if end - start >= 64 * 1024 else None
|
|
324
|
+
stack = [")"]
|
|
325
|
+
index = start + 1
|
|
326
|
+
while index < self._length:
|
|
327
|
+
special = _INITIALIZER_SPECIAL_RE.search(self.source, index)
|
|
328
|
+
if special is None:
|
|
329
|
+
return None
|
|
330
|
+
index = special.start()
|
|
331
|
+
character = special.group()
|
|
332
|
+
following = self.source[index + 1] if index + 1 < self._length else ""
|
|
333
|
+
if character == "'":
|
|
334
|
+
index = self._quoted_end(index)
|
|
335
|
+
continue
|
|
336
|
+
if character == "/" and following == "/":
|
|
337
|
+
newline = self.source.find("\n", index + 2)
|
|
338
|
+
index = self._length if newline < 0 else newline + 1
|
|
339
|
+
continue
|
|
340
|
+
if character == "{":
|
|
341
|
+
index, _ = scan_brace_comment(self.source, index)
|
|
342
|
+
continue
|
|
343
|
+
if character == "(" and following == "*":
|
|
344
|
+
close = self.source.find("*)", index + 2)
|
|
345
|
+
index = self._length if close < 0 else close + 2
|
|
346
|
+
continue
|
|
347
|
+
if character == "(":
|
|
348
|
+
stack.append(")")
|
|
349
|
+
elif character == "[":
|
|
350
|
+
stack.append("]")
|
|
351
|
+
elif stack and character == stack[-1]:
|
|
352
|
+
stack.pop()
|
|
353
|
+
if not stack:
|
|
354
|
+
end = index + 1
|
|
355
|
+
return end if end - start >= 64 * 1024 else None
|
|
356
|
+
index = special.end()
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def _emit(
|
|
360
|
+
self,
|
|
361
|
+
kind: TokenKind,
|
|
362
|
+
start: int,
|
|
363
|
+
end: int,
|
|
364
|
+
line: int,
|
|
365
|
+
column: int,
|
|
366
|
+
*,
|
|
367
|
+
value: str | None = None,
|
|
368
|
+
) -> Token:
|
|
369
|
+
token_value = self.source[start:end] if value is None else value
|
|
370
|
+
self._advance_to(end)
|
|
371
|
+
return Token(kind, token_value, line, column, start, end, self.file_name)
|
|
372
|
+
|
|
373
|
+
def _advance_to(self, end: int) -> None:
|
|
374
|
+
if end <= self._index:
|
|
375
|
+
return
|
|
376
|
+
newline_count = self.source.count("\n", self._index, end)
|
|
377
|
+
if newline_count:
|
|
378
|
+
self._line += newline_count
|
|
379
|
+
last_newline = self.source.rfind("\n", self._index, end)
|
|
380
|
+
self._column = end - last_newline
|
|
381
|
+
else:
|
|
382
|
+
self._column += end - self._index
|
|
383
|
+
self._index = end
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def large_initializer_end(source: str, start: int) -> int | None:
|
|
387
|
+
"""Return the closing offset for an initializer large enough to compact."""
|
|
388
|
+
return DelphiAstLexer(source)._large_initializer_end(start)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _identifier_start(character: str) -> bool:
|
|
392
|
+
return bool(character) and (character == "_" or character.isalpha())
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _identifier_part(character: str) -> bool:
|
|
396
|
+
return character == "_" or character.isalnum()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
__all__ = ["DelphiAstLexer", "large_initializer_end"]
|