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.
Files changed (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,300 @@
1
+ """Reconstruct SyntaxNode trees from the Rust parser's serialized output.
2
+
3
+ The Rust parser (``parse_to_serialized``) produces a deterministic text
4
+ representation identical to Python's ``serialize_syntax_tree``. This module
5
+ provides the inverse: parsing that text back into real
6
+ ``delphi_lsp.nodes.SyntaxNode`` objects so the rest of the LSP can operate on
7
+ native AST nodes instead of strings.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import cast
13
+
14
+ from .consts import ATTRIBUTE_NAME_STRINGS, AttributeName, SyntaxNodeType
15
+ from .nodes import CommentNode, SyntaxNode, ValuedSyntaxNode
16
+
17
+
18
+ def _unescape_value(value: str) -> str:
19
+ """Unescape a serialized node value (reverse of ``_escape_value``)."""
20
+ result: list[str] = []
21
+ i = 0
22
+ while i < len(value):
23
+ if value[i] == "\\" and i + 1 < len(value):
24
+ nxt = value[i + 1]
25
+ if nxt == "\\":
26
+ result.append("\\")
27
+ i += 2
28
+ elif nxt == "n":
29
+ result.append("\n")
30
+ i += 2
31
+ elif nxt == "r":
32
+ result.append("\r")
33
+ i += 2
34
+ elif nxt == "t":
35
+ result.append("\t")
36
+ i += 2
37
+ else:
38
+ result.append(value[i])
39
+ i += 1
40
+ else:
41
+ result.append(value[i])
42
+ i += 1
43
+ return "".join(result)
44
+
45
+
46
+ def _syntax_node_type_from_name(name: str) -> SyntaxNodeType:
47
+ """Map a name string like ``'ntUnit'`` back to the ``SyntaxNodeType`` enum."""
48
+ try:
49
+ return SyntaxNodeType[name]
50
+ except KeyError:
51
+ if name.startswith("ntUnknown(") and name.endswith(")"):
52
+ try:
53
+ val = int(name[len("ntUnknown("):-1])
54
+ return SyntaxNodeType(val)
55
+ except (ValueError, OverflowError):
56
+ return SyntaxNodeType.ntUnknown
57
+ return SyntaxNodeType.ntUnknown
58
+
59
+
60
+ def _parse_kv_pairs(token: str) -> list[tuple[str, str]]:
61
+ """Parse ``key=value`` pairs from a token string.
62
+
63
+ Values may contain spaces but are delimited by the next whitespace
64
+ followed by a ``key=`` pattern.
65
+ """
66
+ pairs: list[tuple[str, str]] = []
67
+ i = 0
68
+ length = len(token)
69
+ while i < length:
70
+ while i < length and token[i].isspace():
71
+ i += 1
72
+ if i >= length:
73
+ break
74
+
75
+ eq_pos = token.find("=", i)
76
+ if eq_pos == -1:
77
+ break
78
+ key = token[i:eq_pos]
79
+ i = eq_pos + 1
80
+
81
+ while i < length and token[i].isspace():
82
+ i += 1
83
+
84
+ if i >= length:
85
+ pairs.append((key, ""))
86
+ break
87
+
88
+ value_start = i
89
+ while i < length:
90
+ if token[i].isspace():
91
+ j = i
92
+ while j < length and token[j].isspace():
93
+ j += 1
94
+ if j < length and token[j].isalpha():
95
+ k = j
96
+ while k < length and (token[k].isalnum() or token[k] == "_"):
97
+ k += 1
98
+ if k < length and token[k] == "=":
99
+ break
100
+ i += 1
101
+ else:
102
+ i += 1
103
+
104
+ value = token[value_start:i]
105
+ pairs.append((key, value))
106
+
107
+ return pairs
108
+
109
+
110
+ def _parse_node_line(line: str) -> tuple[str, list[tuple[str, str]]]:
111
+ """Parse a node line into (type_name, key_value_pairs).
112
+
113
+ The first whitespace-delimited token that does not contain ``=``
114
+ is the type name. Everything after that is parsed as key=value pairs.
115
+ """
116
+ stripped = line.lstrip(" ")
117
+ # Find the first whitespace-delimited token
118
+ first_space = stripped.find(" ")
119
+ if first_space == -1:
120
+ # Only the type name, no key-value pairs
121
+ return stripped, []
122
+
123
+ first_token = stripped[:first_space]
124
+ if "=" not in first_token:
125
+ # First token is the type name
126
+ rest = stripped[first_space:]
127
+ kv_pairs = _parse_kv_pairs(rest)
128
+ return first_token, kv_pairs
129
+
130
+ # First token contains '=' — fall back to treating everything as kv pairs
131
+ kv_pairs = _parse_kv_pairs(stripped)
132
+ return kv_pairs[0][0] if kv_pairs else "", kv_pairs[1:] if len(kv_pairs) > 1 else []
133
+
134
+
135
+ def _attribute_name_to_index(name: str) -> int:
136
+ """Map an attribute name string to its AttributeName int value."""
137
+ for i, s in enumerate(ATTRIBUTE_NAME_STRINGS):
138
+ if s == name:
139
+ return i
140
+ return len(ATTRIBUTE_NAME_STRINGS)
141
+
142
+
143
+ def _looks_like_subclass_field(line: str) -> bool:
144
+ """Check if a line looks like a subclass field (value= or text=)."""
145
+ return line.startswith(("value=", "text="))
146
+
147
+
148
+ def _apply_subclass_field(node: SyntaxNode, line: str) -> SyntaxNode:
149
+ """Apply a subclass field (value= or text=) to a node.
150
+
151
+ If the node is a plain SyntaxNode and needs a subclass attribute,
152
+ convert it to the appropriate subclass. Returns the (possibly new) node.
153
+ """
154
+ if line.startswith("value="):
155
+ # Convert to ValuedSyntaxNode if needed
156
+ if isinstance(node, SyntaxNode) and not isinstance(node, ValuedSyntaxNode):
157
+ vnode: ValuedSyntaxNode = ValuedSyntaxNode(node.typ)
158
+ vnode._col = node._col
159
+ vnode._line = node._line
160
+ vnode._file_name = node._file_name
161
+ vnode._attributes = list(node._attributes)
162
+ vnode._child_nodes = list(node._child_nodes)
163
+ vnode._source_positions = node._source_positions
164
+ vnode._token_end_line = node._token_end_line
165
+ vnode._token_end_col = node._token_end_col
166
+ else:
167
+ vnode = cast(ValuedSyntaxNode, node)
168
+ vnode.value = _unescape_value(line[len("value="):])
169
+ return vnode
170
+ elif line.startswith("text="):
171
+ # Convert to CommentNode if needed
172
+ if isinstance(node, SyntaxNode) and not isinstance(node, CommentNode):
173
+ cnode: CommentNode = CommentNode(node.typ)
174
+ cnode._col = node._col
175
+ cnode._line = node._line
176
+ cnode._file_name = node._file_name
177
+ cnode._attributes = list(node._attributes)
178
+ cnode._child_nodes = list(node._child_nodes)
179
+ cnode._source_positions = node._source_positions
180
+ cnode._token_end_line = node._token_end_line
181
+ cnode._token_end_col = node._token_end_col
182
+ else:
183
+ cnode = cast(CommentNode, node)
184
+ cnode.text = _unescape_value(line[len("text="):])
185
+ return cnode
186
+ return node
187
+
188
+
189
+ def _parse_single_node(line: str, parent_file_name: str) -> tuple[SyntaxNode, int]:
190
+ """Parse a single node line and return (node, next_line_index)."""
191
+ type_name, kv_pairs = _parse_node_line(line.lstrip(" "))
192
+ if not type_name:
193
+ raise ValueError(f"Cannot parse node line: {line!r}")
194
+
195
+ typ = _syntax_node_type_from_name(type_name)
196
+
197
+ node = SyntaxNode(typ)
198
+
199
+ for key, value in kv_pairs:
200
+ if key == "line":
201
+ node.line = int(value)
202
+ elif key == "col":
203
+ node.col = int(value)
204
+ elif key == "end_line":
205
+ node.token_end_line = int(value)
206
+ elif key == "end_col":
207
+ node.token_end_col = int(value)
208
+ elif key == "file_name":
209
+ node.file_name = _unescape_value(value)
210
+ elif key == "source_positions":
211
+ positions: list[tuple[int, int]] = []
212
+ for pos in value.split(","):
213
+ if ":" in pos:
214
+ l, c = pos.split(":", 1)
215
+ positions.append((int(l), int(c)))
216
+ node.source_positions = tuple(positions)
217
+ else:
218
+ attr_idx = _attribute_name_to_index(key)
219
+ node.set_attribute(AttributeName(attr_idx), _unescape_value(value))
220
+
221
+ return node, 0
222
+
223
+
224
+ def _parse_node_block(
225
+ lines: list[str],
226
+ start_idx: int,
227
+ expected_indent: int,
228
+ parent_file_name: str,
229
+ ) -> tuple[SyntaxNode, int]:
230
+ """Parse a block of nodes at a given indentation level."""
231
+ if start_idx >= len(lines):
232
+ raise ValueError(f"Expected node at line index {start_idx}, but no lines remain")
233
+
234
+ first_line = lines[start_idx]
235
+ if not first_line.strip():
236
+ raise ValueError(f"Empty line at index {start_idx}")
237
+
238
+ node, _ = _parse_single_node(first_line, parent_file_name)
239
+
240
+ child_indent = expected_indent + 2
241
+ idx = start_idx + 1
242
+
243
+ while idx < len(lines):
244
+ line = lines[idx]
245
+ if not line.strip():
246
+ idx += 1
247
+ continue
248
+
249
+ stripped = line.lstrip(" ")
250
+ current_indent = len(line) - len(stripped)
251
+
252
+ if current_indent < child_indent:
253
+ break
254
+
255
+ if current_indent == child_indent:
256
+ if _looks_like_subclass_field(stripped):
257
+ node = _apply_subclass_field(node, stripped)
258
+ idx += 1
259
+ else:
260
+ # Parse the current line as a child and recurse into its subtree
261
+ child_node, next_idx = _parse_node_block(lines, idx, child_indent, parent_file_name)
262
+ node.add_child(child_node)
263
+ idx = next_idx
264
+ else:
265
+ break
266
+
267
+ return node, idx
268
+
269
+
270
+ def reconstruct_syntax_tree(serialized: str) -> SyntaxNode:
271
+ """Reconstruct a ``SyntaxNode`` tree from a serialized string.
272
+
273
+ The input format matches exactly the output of
274
+ ``serialize_syntax_tree`` / the Rust ``parse_to_serialized`` function:
275
+
276
+ * Header line: ``# file_name=<value>``
277
+ * One line per node: `` TYPE line=N col=M end_line=N end_col=M [attr=val …]``
278
+ * Subclass fields: `` value=<val>`` or `` text=<text>``
279
+ * Children are indented one level deeper.
280
+
281
+ Returns
282
+ -------
283
+ SyntaxNode
284
+ The root node of the reconstructed tree.
285
+ """
286
+ lines = serialized.split("\n") if serialized else []
287
+
288
+ root_file_name = ""
289
+ line_idx = 0
290
+ if lines and lines[0].startswith("# file_name="):
291
+ root_file_name = _unescape_value(lines[0][len("# file_name="):])
292
+ line_idx = 1
293
+
294
+ root, _ = _parse_node_block(lines, line_idx, 0, root_file_name)
295
+ # Set file_name on the root node so re-serialization includes the header
296
+ root.file_name = root_file_name
297
+ return root
298
+
299
+
300
+ __all__ = ["reconstruct_syntax_tree"]
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import stat
7
+ import tempfile
8
+ from collections.abc import Mapping
9
+ from pathlib import Path
10
+
11
+ NAVIGATION_SHARD_SCHEMA = 4
12
+ _MAX_SHARD_BYTES = 256 * 1024**2
13
+ _KEY_PREFIX = b"python-delphi-lsp-navigation-shard-v4\0"
14
+
15
+
16
+ def navigation_cache_key(text: str, defines: tuple[str, ...]) -> str:
17
+ digest = hashlib.sha256()
18
+ digest.update(_KEY_PREFIX)
19
+ digest.update(
20
+ json.dumps(
21
+ sorted(define.casefold() for define in defines),
22
+ ensure_ascii=False,
23
+ separators=(",", ":"),
24
+ ).encode("utf-8")
25
+ )
26
+ digest.update(b"\0")
27
+ digest.update(text.encode("utf-8"))
28
+ return digest.hexdigest()
29
+
30
+
31
+ class NavigationShardStore:
32
+ """Content-addressed JSON storage for detached navigation symbols."""
33
+
34
+ def __init__(self, root: str | Path) -> None:
35
+ self.root = Path(root)
36
+
37
+ def load(self, cache_key: str) -> Mapping[str, object] | None:
38
+ path = self._path(cache_key)
39
+ if self.root.is_symlink() or path.parent.is_symlink():
40
+ return None
41
+ try:
42
+ descriptor = os.open(
43
+ path,
44
+ os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0),
45
+ )
46
+ try:
47
+ info = os.fstat(descriptor)
48
+ if (
49
+ not stat.S_ISREG(info.st_mode)
50
+ or info.st_size > _MAX_SHARD_BYTES
51
+ or (
52
+ os.name != "nt"
53
+ and (
54
+ info.st_uid != os.getuid()
55
+ or info.st_mode & 0o077
56
+ )
57
+ )
58
+ ):
59
+ return None
60
+ with os.fdopen(descriptor, "rb", closefd=False) as stream:
61
+ raw = stream.read(_MAX_SHARD_BYTES + 1)
62
+ if len(raw) > _MAX_SHARD_BYTES:
63
+ return None
64
+ finally:
65
+ os.close(descriptor)
66
+ payload = json.loads(raw.decode("utf-8"))
67
+ except (OSError, UnicodeError, ValueError):
68
+ return None
69
+ if (
70
+ not isinstance(payload, dict)
71
+ or payload.get("schema") != NAVIGATION_SHARD_SCHEMA
72
+ or payload.get("cache_key") != cache_key
73
+ or not isinstance(payload.get("symbols"), list)
74
+ ):
75
+ return None
76
+ return payload
77
+
78
+ def store(self, cache_key: str, payload: Mapping[str, object]) -> None:
79
+ path = self._path(cache_key)
80
+ self._ensure_directory(path.parent)
81
+ record = {
82
+ **dict(payload),
83
+ "schema": NAVIGATION_SHARD_SCHEMA,
84
+ "cache_key": cache_key,
85
+ }
86
+ data = json.dumps(
87
+ record,
88
+ ensure_ascii=False,
89
+ sort_keys=True,
90
+ separators=(",", ":"),
91
+ allow_nan=False,
92
+ ).encode("utf-8")
93
+ if len(data) > _MAX_SHARD_BYTES:
94
+ return
95
+ descriptor, temporary = tempfile.mkstemp(prefix=".navigation-", dir=path.parent)
96
+ try:
97
+ if os.name != "nt":
98
+ os.fchmod(descriptor, 0o600)
99
+ with os.fdopen(descriptor, "wb") as stream:
100
+ stream.write(data)
101
+ stream.flush()
102
+ os.fsync(stream.fileno())
103
+ os.replace(temporary, path)
104
+ if os.name != "nt":
105
+ os.chmod(path, 0o600)
106
+ finally:
107
+ try:
108
+ os.unlink(temporary)
109
+ except FileNotFoundError:
110
+ pass
111
+
112
+ def prune(self, live_keys: set[str], max_bytes: int) -> None:
113
+ if isinstance(max_bytes, bool) or not isinstance(max_bytes, int) or max_bytes < 0:
114
+ raise ValueError("navigation cache byte budget must be non-negative")
115
+ if not self.root.exists() or self.root.is_symlink():
116
+ return
117
+ retained: list[tuple[int, str, Path, int]] = []
118
+ bucket_paths: list[Path] = []
119
+ try:
120
+ buckets = tuple(self.root.iterdir())
121
+ except OSError:
122
+ return
123
+ for bucket in buckets:
124
+ if bucket.is_symlink() or not bucket.is_dir():
125
+ continue
126
+ bucket_paths.append(bucket)
127
+ try:
128
+ shards = tuple(bucket.iterdir())
129
+ except OSError:
130
+ continue
131
+ for shard in shards:
132
+ if (
133
+ shard.is_symlink()
134
+ or not shard.is_file()
135
+ or shard.suffix != ".json"
136
+ ):
137
+ continue
138
+ cache_key = shard.stem
139
+ try:
140
+ info = shard.stat(follow_symlinks=False)
141
+ except OSError:
142
+ continue
143
+ if cache_key not in live_keys:
144
+ try:
145
+ shard.unlink()
146
+ except OSError:
147
+ pass
148
+ continue
149
+ retained.append(
150
+ (info.st_mtime_ns, str(shard), shard, info.st_size)
151
+ )
152
+
153
+ retained_bytes = sum(item[3] for item in retained)
154
+ for _mtime, _name, shard, size in sorted(retained):
155
+ if retained_bytes <= max_bytes:
156
+ break
157
+ try:
158
+ shard.unlink()
159
+ except OSError:
160
+ continue
161
+ retained_bytes -= size
162
+ for bucket in bucket_paths:
163
+ try:
164
+ bucket.rmdir()
165
+ except OSError:
166
+ pass
167
+
168
+ def _path(self, cache_key: str) -> Path:
169
+ if (
170
+ len(cache_key) != 64
171
+ or any(character not in "0123456789abcdef" for character in cache_key)
172
+ ):
173
+ raise ValueError("navigation cache key is malformed")
174
+ return self.root / cache_key[:2] / f"{cache_key}.json"
175
+
176
+ def _ensure_directory(self, directory: Path) -> None:
177
+ for path in (self.root, directory):
178
+ if path.exists() and path.is_symlink():
179
+ raise OSError("navigation cache path is unsafe")
180
+ path.mkdir(mode=0o700, parents=True, exist_ok=True)
181
+ if os.name != "nt":
182
+ os.chmod(path, 0o700)
183
+
184
+
185
+ __all__ = [
186
+ "NAVIGATION_SHARD_SCHEMA",
187
+ "NavigationShardStore",
188
+ "navigation_cache_key",
189
+ ]