avrae-ls 0.4.0__py3-none-any.whl → 0.5.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.
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.5.0.dist-info}/METADATA +31 -11
- avrae_ls-0.5.0.dist-info/RECORD +6 -0
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.5.0.dist-info}/WHEEL +1 -2
- avrae_ls/__init__.py +0 -3
- avrae_ls/__main__.py +0 -108
- avrae_ls/alias_preview.py +0 -180
- avrae_ls/api.py +0 -2014
- avrae_ls/argparser.py +0 -430
- avrae_ls/argument_parsing.py +0 -67
- avrae_ls/completions.py +0 -984
- avrae_ls/config.py +0 -496
- avrae_ls/context.py +0 -229
- avrae_ls/cvars.py +0 -115
- avrae_ls/diagnostics.py +0 -694
- avrae_ls/dice.py +0 -33
- avrae_ls/parser.py +0 -44
- avrae_ls/runtime.py +0 -661
- avrae_ls/server.py +0 -390
- avrae_ls/signature_help.py +0 -201
- avrae_ls/symbols.py +0 -266
- avrae_ls-0.4.0.dist-info/RECORD +0 -32
- avrae_ls-0.4.0.dist-info/top_level.txt +0 -2
- draconic/__init__.py +0 -4
- draconic/exceptions.py +0 -157
- draconic/helpers.py +0 -236
- draconic/interpreter.py +0 -1091
- draconic/string.py +0 -100
- draconic/types.py +0 -364
- draconic/utils.py +0 -78
- draconic/versions.py +0 -4
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.5.0.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.5.0.dist-info}/licenses/LICENSE +0 -0
avrae_ls/completions.py
DELETED
|
@@ -1,984 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
import inspect
|
|
5
|
-
import re
|
|
6
|
-
import typing
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from functools import lru_cache
|
|
9
|
-
from typing import Any, ClassVar, Dict, Iterable, List, Optional
|
|
10
|
-
|
|
11
|
-
from lsprotocol import types
|
|
12
|
-
|
|
13
|
-
from .context import ContextData, GVarResolver
|
|
14
|
-
from .argparser import ParsedArguments
|
|
15
|
-
from .runtime import _default_builtins
|
|
16
|
-
from .api import (
|
|
17
|
-
AliasAction,
|
|
18
|
-
AliasBaseStats,
|
|
19
|
-
AliasCoinpurse,
|
|
20
|
-
AliasContextAPI,
|
|
21
|
-
AliasCustomCounter,
|
|
22
|
-
AliasDeathSaves,
|
|
23
|
-
AliasResistances,
|
|
24
|
-
AliasSaves,
|
|
25
|
-
AliasSkill,
|
|
26
|
-
AliasSkills,
|
|
27
|
-
AliasSpellbook,
|
|
28
|
-
AliasSpellbookSpell,
|
|
29
|
-
AliasAttack,
|
|
30
|
-
AliasAttackList,
|
|
31
|
-
AliasLevels,
|
|
32
|
-
CategoryAPI,
|
|
33
|
-
ChannelAPI,
|
|
34
|
-
CharacterAPI,
|
|
35
|
-
SimpleCombat,
|
|
36
|
-
SimpleCombatant,
|
|
37
|
-
GuildAPI,
|
|
38
|
-
RoleAPI,
|
|
39
|
-
AuthorAPI,
|
|
40
|
-
SimpleEffect,
|
|
41
|
-
SimpleGroup,
|
|
42
|
-
SimpleRollResult,
|
|
43
|
-
)
|
|
44
|
-
from .signature_help import FunctionSig
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class _BuiltinList:
|
|
48
|
-
ATTRS: ClassVar[list[str]] = []
|
|
49
|
-
METHODS: ClassVar[list[str]] = ["append", "extend", "insert", "remove", "pop", "clear", "index", "count", "sort", "reverse", "copy"]
|
|
50
|
-
|
|
51
|
-
def __iter__(self) -> Iterable[Any]:
|
|
52
|
-
return iter([])
|
|
53
|
-
|
|
54
|
-
def append(self, value: Any) -> None: ...
|
|
55
|
-
def extend(self, iterable: Iterable[Any]) -> None: ...
|
|
56
|
-
def insert(self, index: int, value: Any) -> None: ...
|
|
57
|
-
def remove(self, value: Any) -> None: ...
|
|
58
|
-
def pop(self, index: int = -1) -> Any: ...
|
|
59
|
-
def clear(self) -> None: ...
|
|
60
|
-
def index(self, value: Any, start: int = 0, stop: int | None = None) -> int: ...
|
|
61
|
-
def count(self, value: Any) -> int: ...
|
|
62
|
-
def sort(self, *, key=None, reverse: bool = False) -> None: ...
|
|
63
|
-
def reverse(self) -> None: ...
|
|
64
|
-
def copy(self) -> list[Any]: ...
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class _BuiltinDict:
|
|
68
|
-
ATTRS: ClassVar[list[str]] = []
|
|
69
|
-
METHODS: ClassVar[list[str]] = ["get", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "clear", "copy"]
|
|
70
|
-
|
|
71
|
-
def __iter__(self) -> Iterable[Any]:
|
|
72
|
-
return iter({})
|
|
73
|
-
|
|
74
|
-
def get(self, key: Any, default: Any = None) -> Any: ...
|
|
75
|
-
def keys(self) -> Any: ...
|
|
76
|
-
def values(self) -> Any: ...
|
|
77
|
-
def items(self) -> Any: ...
|
|
78
|
-
def pop(self, key: Any, default: Any = None) -> Any: ...
|
|
79
|
-
def popitem(self) -> tuple[Any, Any]: ...
|
|
80
|
-
def update(self, *args, **kwargs) -> None: ...
|
|
81
|
-
def setdefault(self, key: Any, default: Any = None) -> Any: ...
|
|
82
|
-
def clear(self) -> None: ...
|
|
83
|
-
def copy(self) -> dict[Any, Any]: ...
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class _BuiltinStr:
|
|
87
|
-
ATTRS: ClassVar[list[str]] = []
|
|
88
|
-
METHODS: ClassVar[list[str]] = [
|
|
89
|
-
"lower",
|
|
90
|
-
"upper",
|
|
91
|
-
"title",
|
|
92
|
-
"split",
|
|
93
|
-
"join",
|
|
94
|
-
"replace",
|
|
95
|
-
"strip",
|
|
96
|
-
"startswith",
|
|
97
|
-
"endswith",
|
|
98
|
-
"format",
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
def __iter__(self) -> Iterable[str]:
|
|
102
|
-
return iter("")
|
|
103
|
-
|
|
104
|
-
def lower(self) -> str: ...
|
|
105
|
-
def upper(self) -> str: ...
|
|
106
|
-
def title(self) -> str: ...
|
|
107
|
-
def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...
|
|
108
|
-
def join(self, iterable: Iterable[str]) -> str: ...
|
|
109
|
-
def replace(self, old: str, new: str, count: int = -1) -> str: ...
|
|
110
|
-
def strip(self, chars: str | None = None) -> str: ...
|
|
111
|
-
def startswith(self, prefix, start: int = 0, end: int | None = None) -> bool: ...
|
|
112
|
-
def endswith(self, suffix, start: int = 0, end: int | None = None) -> bool: ...
|
|
113
|
-
def format(self, *args, **kwargs) -> str: ...
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
TYPE_MAP: Dict[str, object] = {
|
|
117
|
-
"character": CharacterAPI,
|
|
118
|
-
"combat": SimpleCombat,
|
|
119
|
-
"SimpleCombat": SimpleCombat,
|
|
120
|
-
"ctx": AliasContextAPI,
|
|
121
|
-
"SimpleRollResult": SimpleRollResult,
|
|
122
|
-
"stats": AliasBaseStats,
|
|
123
|
-
"levels": AliasLevels,
|
|
124
|
-
"attacks": AliasAttackList,
|
|
125
|
-
"attack": AliasAttack,
|
|
126
|
-
"skills": AliasSkills,
|
|
127
|
-
"skill": AliasSkill,
|
|
128
|
-
"saves": AliasSaves,
|
|
129
|
-
"resistances": AliasResistances,
|
|
130
|
-
"coinpurse": AliasCoinpurse,
|
|
131
|
-
"custom_counter": AliasCustomCounter,
|
|
132
|
-
"consumable": AliasCustomCounter,
|
|
133
|
-
"death_saves": AliasDeathSaves,
|
|
134
|
-
"action": AliasAction,
|
|
135
|
-
"spellbook": AliasSpellbook,
|
|
136
|
-
"spell": AliasSpellbookSpell,
|
|
137
|
-
"guild": GuildAPI,
|
|
138
|
-
"channel": ChannelAPI,
|
|
139
|
-
"category": CategoryAPI,
|
|
140
|
-
"author": AuthorAPI,
|
|
141
|
-
"role": RoleAPI,
|
|
142
|
-
"combatant": SimpleCombatant,
|
|
143
|
-
"SimpleCombatant": SimpleCombatant,
|
|
144
|
-
"group": SimpleGroup,
|
|
145
|
-
"SimpleGroup": SimpleGroup,
|
|
146
|
-
"effect": SimpleEffect,
|
|
147
|
-
"SimpleEffect": SimpleEffect,
|
|
148
|
-
"list": _BuiltinList,
|
|
149
|
-
"dict": _BuiltinDict,
|
|
150
|
-
"str": _BuiltinStr,
|
|
151
|
-
"ParsedArguments": ParsedArguments,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
IDENT_RE = re.compile(r"[A-Za-z_]\w*$")
|
|
156
|
-
ATTR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.(?:([A-Za-z_]\w*)\s*)?$")
|
|
157
|
-
DICT_GET_RE = re.compile(r"^([A-Za-z_]\w*)\.get\(\s*(['\"])(.+?)\2")
|
|
158
|
-
ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.([A-Za-z_]\w*)")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
@dataclass
|
|
162
|
-
class Suggestion:
|
|
163
|
-
name: str
|
|
164
|
-
kind: types.CompletionItemKind
|
|
165
|
-
detail: str = ""
|
|
166
|
-
documentation: str = ""
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@dataclass
|
|
170
|
-
class AttrMeta:
|
|
171
|
-
doc: str = ""
|
|
172
|
-
type_name: str = ""
|
|
173
|
-
element_type: str = ""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@dataclass
|
|
177
|
-
class MethodMeta:
|
|
178
|
-
signature: str = ""
|
|
179
|
-
doc: str = ""
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@dataclass
|
|
183
|
-
class TypeMeta:
|
|
184
|
-
attrs: Dict[str, AttrMeta]
|
|
185
|
-
methods: Dict[str, MethodMeta]
|
|
186
|
-
element_type: str = ""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def gather_suggestions(
|
|
190
|
-
ctx_data: ContextData,
|
|
191
|
-
resolver: GVarResolver,
|
|
192
|
-
sigs: Dict[str, FunctionSig],
|
|
193
|
-
) -> List[Suggestion]:
|
|
194
|
-
suggestions: list[Suggestion] = []
|
|
195
|
-
|
|
196
|
-
for name, sig in sigs.items():
|
|
197
|
-
suggestions.append(
|
|
198
|
-
Suggestion(
|
|
199
|
-
name=name,
|
|
200
|
-
kind=types.CompletionItemKind.Function,
|
|
201
|
-
detail=sig.label,
|
|
202
|
-
documentation=sig.doc,
|
|
203
|
-
)
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
vars_map = ctx_data.vars.to_initial_names()
|
|
207
|
-
for name in vars_map:
|
|
208
|
-
suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Variable, detail="var"))
|
|
209
|
-
|
|
210
|
-
gvars = resolver.snapshot()
|
|
211
|
-
for name in gvars:
|
|
212
|
-
suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Variable, detail="gvar"))
|
|
213
|
-
|
|
214
|
-
for name in _default_builtins().keys():
|
|
215
|
-
if name not in sigs:
|
|
216
|
-
suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Function))
|
|
217
|
-
|
|
218
|
-
# context helpers
|
|
219
|
-
suggestions.append(Suggestion(name="character", kind=types.CompletionItemKind.Function, detail="Alias character()"))
|
|
220
|
-
suggestions.append(Suggestion(name="combat", kind=types.CompletionItemKind.Function, detail="Alias combat()"))
|
|
221
|
-
suggestions.append(Suggestion(name="ctx", kind=types.CompletionItemKind.Variable, detail="Alias context"))
|
|
222
|
-
|
|
223
|
-
return suggestions
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def completion_items_for_position(
|
|
227
|
-
code: str,
|
|
228
|
-
line: int,
|
|
229
|
-
character: int,
|
|
230
|
-
suggestions: Iterable[Suggestion],
|
|
231
|
-
) -> List[types.CompletionItem]:
|
|
232
|
-
attr_ctx = _attribute_receiver_and_prefix(code, line, character)
|
|
233
|
-
if attr_ctx:
|
|
234
|
-
receiver, attr_prefix = attr_ctx
|
|
235
|
-
sanitized = _sanitize_incomplete_line(code, line, character)
|
|
236
|
-
type_map = _infer_type_map(sanitized)
|
|
237
|
-
return _attribute_completions(receiver, attr_prefix, sanitized, type_map)
|
|
238
|
-
|
|
239
|
-
line_text = _line_text_to_cursor(code, line, character)
|
|
240
|
-
prefix = _current_prefix(line_text)
|
|
241
|
-
items: list[types.CompletionItem] = []
|
|
242
|
-
for sugg in suggestions:
|
|
243
|
-
if prefix and not sugg.name.startswith(prefix):
|
|
244
|
-
continue
|
|
245
|
-
items.append(
|
|
246
|
-
types.CompletionItem(
|
|
247
|
-
label=sugg.name,
|
|
248
|
-
kind=sugg.kind,
|
|
249
|
-
detail=sugg.detail or None,
|
|
250
|
-
documentation=sugg.documentation or None,
|
|
251
|
-
)
|
|
252
|
-
)
|
|
253
|
-
return items
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict[str, str] | None = None) -> List[types.CompletionItem]:
|
|
257
|
-
items: list[types.CompletionItem] = []
|
|
258
|
-
type_key = _resolve_type_name(receiver, code, type_map)
|
|
259
|
-
meta = _type_meta(type_key)
|
|
260
|
-
detail = f"{type_key}()"
|
|
261
|
-
|
|
262
|
-
for name, attr_meta in meta.attrs.items():
|
|
263
|
-
if prefix and not name.startswith(prefix):
|
|
264
|
-
continue
|
|
265
|
-
items.append(
|
|
266
|
-
types.CompletionItem(
|
|
267
|
-
label=name,
|
|
268
|
-
kind=types.CompletionItemKind.Field,
|
|
269
|
-
detail=detail,
|
|
270
|
-
documentation=attr_meta.doc or None,
|
|
271
|
-
)
|
|
272
|
-
)
|
|
273
|
-
for name, method_meta in meta.methods.items():
|
|
274
|
-
if prefix and not name.startswith(prefix):
|
|
275
|
-
continue
|
|
276
|
-
method_detail = method_meta.signature or f"{name}()"
|
|
277
|
-
items.append(
|
|
278
|
-
types.CompletionItem(
|
|
279
|
-
label=name,
|
|
280
|
-
kind=types.CompletionItemKind.Method,
|
|
281
|
-
detail=method_detail,
|
|
282
|
-
documentation=method_meta.doc or None,
|
|
283
|
-
)
|
|
284
|
-
)
|
|
285
|
-
return items
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def hover_for_position(
|
|
289
|
-
code: str,
|
|
290
|
-
line: int,
|
|
291
|
-
character: int,
|
|
292
|
-
sigs: Dict[str, FunctionSig],
|
|
293
|
-
ctx_data: ContextData,
|
|
294
|
-
resolver: GVarResolver,
|
|
295
|
-
) -> Optional[types.Hover]:
|
|
296
|
-
line_text = _line_text(code, line)
|
|
297
|
-
type_map = _infer_type_map(code)
|
|
298
|
-
bindings = _infer_constant_bindings(code, line, ctx_data)
|
|
299
|
-
attr_ctx = _attribute_receiver_and_prefix(code, line, character, capture_full_token=True)
|
|
300
|
-
if attr_ctx:
|
|
301
|
-
receiver, attr_prefix = attr_ctx
|
|
302
|
-
inferred = _resolve_type_name(receiver, code, type_map)
|
|
303
|
-
meta = _type_meta(inferred)
|
|
304
|
-
if attr_prefix in meta.attrs:
|
|
305
|
-
doc = meta.attrs[attr_prefix].doc
|
|
306
|
-
contents = f"```avrae\n{inferred}().{attr_prefix}\n```"
|
|
307
|
-
if doc:
|
|
308
|
-
contents += f"\n\n{doc}"
|
|
309
|
-
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
310
|
-
if attr_prefix in meta.methods:
|
|
311
|
-
method_meta = meta.methods[attr_prefix]
|
|
312
|
-
signature = method_meta.signature or f"{attr_prefix}()"
|
|
313
|
-
doc = method_meta.doc
|
|
314
|
-
contents = f"```avrae\n{signature}\n```"
|
|
315
|
-
if doc:
|
|
316
|
-
contents += f"\n\n{doc}"
|
|
317
|
-
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
318
|
-
|
|
319
|
-
word, _, _ = _word_at_position(line_text, character)
|
|
320
|
-
if not word:
|
|
321
|
-
return None
|
|
322
|
-
if word in bindings:
|
|
323
|
-
return _format_binding_hover(word, bindings[word], "local")
|
|
324
|
-
if word in type_map:
|
|
325
|
-
type_label = _display_type_label(type_map[word])
|
|
326
|
-
contents = f"`{word}` type: `{type_label}`"
|
|
327
|
-
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
328
|
-
if word in sigs:
|
|
329
|
-
sig = sigs[word]
|
|
330
|
-
contents = f"```avrae\n{sig.label}\n```\n\n{sig.doc}"
|
|
331
|
-
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
332
|
-
|
|
333
|
-
vars_map = ctx_data.vars.to_initial_names()
|
|
334
|
-
if word in vars_map:
|
|
335
|
-
return _format_binding_hover(word, vars_map[word], "var")
|
|
336
|
-
|
|
337
|
-
gvars = resolver.snapshot()
|
|
338
|
-
if word in gvars:
|
|
339
|
-
return _format_binding_hover(word, gvars[word], "gvar")
|
|
340
|
-
return None
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def _current_prefix(line_text: str) -> str:
|
|
344
|
-
match = IDENT_RE.search(line_text)
|
|
345
|
-
return match.group(0) if match else ""
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def _word_from_line(text: str, cursor: int) -> str:
|
|
349
|
-
return _word_at_position(text, cursor)[0]
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def _word_at_position(text: str, cursor: int) -> tuple[str, int, int]:
|
|
353
|
-
cursor = max(0, min(cursor, len(text)))
|
|
354
|
-
start = cursor
|
|
355
|
-
while start > 0 and (text[start - 1].isalnum() or text[start - 1] == "_"):
|
|
356
|
-
start -= 1
|
|
357
|
-
end = cursor
|
|
358
|
-
while end < len(text) and (text[end].isalnum() or text[end] == "_"):
|
|
359
|
-
end += 1
|
|
360
|
-
return text[start:end], start, end
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def _line_text_to_cursor(code: str, line: int, character: int) -> str:
|
|
364
|
-
lines = code.splitlines()
|
|
365
|
-
if line >= len(lines):
|
|
366
|
-
return ""
|
|
367
|
-
return lines[line][:character]
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture_full_token: bool = False) -> Optional[tuple[str, str]]:
|
|
371
|
-
lines = code.splitlines()
|
|
372
|
-
if line >= len(lines):
|
|
373
|
-
return None
|
|
374
|
-
line_text = lines[line]
|
|
375
|
-
end = character
|
|
376
|
-
if capture_full_token:
|
|
377
|
-
while end < len(line_text) and (line_text[end].isalnum() or line_text[end] == "_"):
|
|
378
|
-
end += 1
|
|
379
|
-
line_text = line_text[: end]
|
|
380
|
-
dot = line_text.rfind(".")
|
|
381
|
-
if dot == -1:
|
|
382
|
-
return None
|
|
383
|
-
tail = line_text[dot + 1 :]
|
|
384
|
-
prefix_match = re.match(r"\s*([A-Za-z_]\w*)?", tail)
|
|
385
|
-
prefix = prefix_match.group(1) or "" if prefix_match else ""
|
|
386
|
-
suffix = tail[prefix_match.end() if prefix_match else 0 :]
|
|
387
|
-
placeholder = "__COMPLETE__"
|
|
388
|
-
new_line = f"{line_text[:dot]}.{placeholder}{suffix}"
|
|
389
|
-
# Close unmatched parentheses so the temporary code parses.
|
|
390
|
-
paren_balance = new_line.count("(") - new_line.count(")")
|
|
391
|
-
if paren_balance > 0:
|
|
392
|
-
new_line = new_line + (")" * paren_balance)
|
|
393
|
-
mod_lines = list(lines)
|
|
394
|
-
mod_lines[line] = new_line
|
|
395
|
-
mod_code = "\n".join(mod_lines)
|
|
396
|
-
try:
|
|
397
|
-
tree = ast.parse(mod_code)
|
|
398
|
-
except SyntaxError:
|
|
399
|
-
return None
|
|
400
|
-
|
|
401
|
-
receiver_src: Optional[str] = None
|
|
402
|
-
|
|
403
|
-
class Finder(ast.NodeVisitor):
|
|
404
|
-
def visit_Attribute(self, node: ast.Attribute):
|
|
405
|
-
nonlocal receiver_src
|
|
406
|
-
if isinstance(node.attr, str) and node.attr == placeholder:
|
|
407
|
-
try:
|
|
408
|
-
receiver_src = ast.unparse(node.value)
|
|
409
|
-
except Exception:
|
|
410
|
-
receiver_src = None
|
|
411
|
-
self.generic_visit(node)
|
|
412
|
-
|
|
413
|
-
Finder().visit(tree)
|
|
414
|
-
if receiver_src is None:
|
|
415
|
-
return None
|
|
416
|
-
return receiver_src, prefix
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def _sanitize_incomplete_line(code: str, line: int, character: int) -> str:
|
|
420
|
-
lines = code.splitlines()
|
|
421
|
-
if 0 <= line < len(lines):
|
|
422
|
-
prefix = lines[line][:character]
|
|
423
|
-
trimmed = prefix.rstrip()
|
|
424
|
-
if trimmed.endswith("."):
|
|
425
|
-
prefix = trimmed[:-1]
|
|
426
|
-
else:
|
|
427
|
-
dot = prefix.rfind(".")
|
|
428
|
-
if dot != -1:
|
|
429
|
-
after = prefix[dot + 1 :]
|
|
430
|
-
if not re.match(r"\s*[A-Za-z_]", after):
|
|
431
|
-
prefix = prefix[:dot] + after
|
|
432
|
-
lines[line] = prefix
|
|
433
|
-
candidate = "\n".join(lines)
|
|
434
|
-
try:
|
|
435
|
-
ast.parse(candidate)
|
|
436
|
-
except SyntaxError:
|
|
437
|
-
indent = re.match(r"[ \t]*", lines[line]).group(0)
|
|
438
|
-
lines[line] = indent + "pass"
|
|
439
|
-
return "\n".join(lines)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def _line_text(code: str, line: int) -> str:
|
|
443
|
-
lines = code.splitlines()
|
|
444
|
-
if line < 0 or line >= len(lines):
|
|
445
|
-
return ""
|
|
446
|
-
return lines[line]
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
def _display_type_label(type_key: str) -> str:
|
|
450
|
-
if type_key in TYPE_MAP:
|
|
451
|
-
return TYPE_MAP[type_key].__name__
|
|
452
|
-
return type_key
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
def _infer_receiver_type(code: str, name: str) -> Optional[str]:
|
|
456
|
-
return _infer_type_map(code).get(name)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
def _infer_type_map(code: str) -> Dict[str, str]:
|
|
460
|
-
try:
|
|
461
|
-
tree = ast.parse(code)
|
|
462
|
-
except SyntaxError:
|
|
463
|
-
return {}
|
|
464
|
-
type_map: dict[str, str] = {}
|
|
465
|
-
|
|
466
|
-
class Visitor(ast.NodeVisitor):
|
|
467
|
-
def visit_Assign(self, node: ast.Assign):
|
|
468
|
-
val_type, elem_type = self._value_type(node.value)
|
|
469
|
-
for target in node.targets:
|
|
470
|
-
if not isinstance(target, ast.Name):
|
|
471
|
-
continue
|
|
472
|
-
if val_type:
|
|
473
|
-
type_map[target.id] = val_type
|
|
474
|
-
if elem_type:
|
|
475
|
-
type_map[f"{target.id}.__element__"] = elem_type
|
|
476
|
-
self._record_dict_key_types(target.id, node.value)
|
|
477
|
-
self.generic_visit(node)
|
|
478
|
-
|
|
479
|
-
def visit_For(self, node: ast.For):
|
|
480
|
-
iter_type, elem_type = self._value_type(node.iter)
|
|
481
|
-
if not elem_type and isinstance(node.iter, ast.Name):
|
|
482
|
-
elem_type = type_map.get(f"{node.iter.id}.__element__")
|
|
483
|
-
if elem_type and isinstance(node.target, ast.Name):
|
|
484
|
-
type_map[node.target.id] = elem_type
|
|
485
|
-
self.generic_visit(node)
|
|
486
|
-
|
|
487
|
-
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
488
|
-
val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
|
|
489
|
-
if isinstance(node.target, ast.Name):
|
|
490
|
-
if val_type:
|
|
491
|
-
type_map[node.target.id] = val_type
|
|
492
|
-
if elem_type:
|
|
493
|
-
type_map[f"{node.target.id}.__element__"] = elem_type
|
|
494
|
-
self._record_dict_key_types(node.target.id, node.value)
|
|
495
|
-
self.generic_visit(node)
|
|
496
|
-
|
|
497
|
-
def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
|
|
498
|
-
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
|
|
499
|
-
if value.func.id in {"character", "combat"}:
|
|
500
|
-
return value.func.id, None
|
|
501
|
-
if value.func.id == "vroll":
|
|
502
|
-
return "SimpleRollResult", None
|
|
503
|
-
if value.func.id == "argparse":
|
|
504
|
-
return "ParsedArguments", None
|
|
505
|
-
if value.func.id in {"list", "dict", "str"}:
|
|
506
|
-
return value.func.id, None
|
|
507
|
-
if isinstance(value, ast.List):
|
|
508
|
-
return "list", None
|
|
509
|
-
if isinstance(value, ast.Dict):
|
|
510
|
-
return "dict", None
|
|
511
|
-
if isinstance(value, ast.Constant):
|
|
512
|
-
if isinstance(value.value, str):
|
|
513
|
-
return "str", None
|
|
514
|
-
if isinstance(value, ast.Name):
|
|
515
|
-
if value.id in type_map:
|
|
516
|
-
return type_map[value.id], type_map.get(f"{value.id}.__element__")
|
|
517
|
-
if value.id in {"character", "combat", "ctx"}:
|
|
518
|
-
return value.id, None
|
|
519
|
-
if isinstance(value, ast.Attribute):
|
|
520
|
-
attr_name = value.attr
|
|
521
|
-
base_type = None
|
|
522
|
-
base_elem = None
|
|
523
|
-
if isinstance(value.value, ast.Name):
|
|
524
|
-
base_type = type_map.get(value.value.id)
|
|
525
|
-
base_elem = type_map.get(f"{value.value.id}.__element__")
|
|
526
|
-
if base_type is None:
|
|
527
|
-
base_type, base_elem = self._value_type(value.value)
|
|
528
|
-
if base_type:
|
|
529
|
-
meta = _type_meta(base_type)
|
|
530
|
-
attr_meta = meta.attrs.get(attr_name)
|
|
531
|
-
if attr_meta:
|
|
532
|
-
if attr_meta.type_name:
|
|
533
|
-
return attr_meta.type_name, attr_meta.element_type or None
|
|
534
|
-
if attr_meta.element_type:
|
|
535
|
-
return base_type, attr_meta.element_type
|
|
536
|
-
if base_elem:
|
|
537
|
-
return base_elem, None
|
|
538
|
-
if attr_name in TYPE_MAP:
|
|
539
|
-
return attr_name, None
|
|
540
|
-
return None, None
|
|
541
|
-
return None, None
|
|
542
|
-
|
|
543
|
-
def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
|
|
544
|
-
if not isinstance(value, ast.Dict):
|
|
545
|
-
return
|
|
546
|
-
for key_node, val_node in zip(value.keys or [], value.values or []):
|
|
547
|
-
if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
|
|
548
|
-
val_type, elem_type = self._value_type(val_node)
|
|
549
|
-
if val_type:
|
|
550
|
-
type_map[f"{var_name}.{key_node.value}"] = val_type
|
|
551
|
-
if elem_type:
|
|
552
|
-
type_map[f"{var_name}.{key_node.value}.__element__"] = elem_type
|
|
553
|
-
|
|
554
|
-
Visitor().visit(tree)
|
|
555
|
-
return type_map
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
|
|
559
|
-
mapping = type_map or _infer_type_map(code)
|
|
560
|
-
get_match = DICT_GET_RE.match(receiver)
|
|
561
|
-
if get_match:
|
|
562
|
-
base, _, key = get_match.groups()
|
|
563
|
-
dict_key = f"{base}.{key}"
|
|
564
|
-
if dict_key in mapping:
|
|
565
|
-
return mapping[dict_key]
|
|
566
|
-
bracket = receiver.rfind("[")
|
|
567
|
-
if bracket != -1 and receiver.endswith("]"):
|
|
568
|
-
base_expr = receiver[:bracket]
|
|
569
|
-
elem_hint = mapping.get(f"{base_expr}.__element__")
|
|
570
|
-
if elem_hint:
|
|
571
|
-
return elem_hint
|
|
572
|
-
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
573
|
-
if base_type:
|
|
574
|
-
base_meta = _type_meta(base_type)
|
|
575
|
-
if base_meta.element_type:
|
|
576
|
-
return base_meta.element_type
|
|
577
|
-
return base_type
|
|
578
|
-
receiver = receiver.rstrip("()")
|
|
579
|
-
if "." in receiver:
|
|
580
|
-
base_expr, attr_name = receiver.rsplit(".", 1)
|
|
581
|
-
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
582
|
-
if base_type:
|
|
583
|
-
meta = _type_meta(base_type)
|
|
584
|
-
attr_key = attr_name.split("[", 1)[0]
|
|
585
|
-
attr_meta = meta.attrs.get(attr_key)
|
|
586
|
-
if attr_meta:
|
|
587
|
-
if attr_meta.element_type:
|
|
588
|
-
return attr_meta.element_type
|
|
589
|
-
if attr_meta.type_name:
|
|
590
|
-
return attr_meta.type_name
|
|
591
|
-
|
|
592
|
-
if receiver in mapping:
|
|
593
|
-
return mapping[receiver]
|
|
594
|
-
elem_key = f"{receiver}.__element__"
|
|
595
|
-
if elem_key in mapping:
|
|
596
|
-
return mapping[elem_key]
|
|
597
|
-
if receiver in TYPE_MAP:
|
|
598
|
-
return receiver
|
|
599
|
-
tail = receiver.split(".")[-1].split("[", 1)[0]
|
|
600
|
-
if tail in TYPE_MAP:
|
|
601
|
-
return tail
|
|
602
|
-
return receiver
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
def _type_meta(type_name: str) -> TypeMeta:
|
|
606
|
-
return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}, element_type=""))
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
@lru_cache()
|
|
610
|
-
def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
611
|
-
meta: dict[str, TypeMeta] = {}
|
|
612
|
-
reverse_type_map: dict[type, str] = {}
|
|
613
|
-
for key, cls in TYPE_MAP.items():
|
|
614
|
-
reverse_type_map[cls] = key
|
|
615
|
-
|
|
616
|
-
def _iter_element_for_type_name(type_name: str) -> str:
|
|
617
|
-
cls = TYPE_MAP.get(type_name)
|
|
618
|
-
if not cls:
|
|
619
|
-
return ""
|
|
620
|
-
return _element_type_from_iterable(cls, reverse_type_map)
|
|
621
|
-
|
|
622
|
-
for type_name, cls in TYPE_MAP.items():
|
|
623
|
-
attrs: dict[str, AttrMeta] = {}
|
|
624
|
-
methods: dict[str, MethodMeta] = {}
|
|
625
|
-
iterable_element = _iter_element_for_type_name(type_name)
|
|
626
|
-
|
|
627
|
-
for attr in getattr(cls, "ATTRS", []):
|
|
628
|
-
doc = ""
|
|
629
|
-
type_name_hint = ""
|
|
630
|
-
element_type_hint = ""
|
|
631
|
-
try:
|
|
632
|
-
attr_obj = getattr(cls, attr)
|
|
633
|
-
except Exception:
|
|
634
|
-
attr_obj = None
|
|
635
|
-
if isinstance(attr_obj, property) and attr_obj.fget:
|
|
636
|
-
doc = (attr_obj.fget.__doc__ or "").strip()
|
|
637
|
-
ann = _return_annotation(attr_obj.fget, cls)
|
|
638
|
-
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
639
|
-
elif attr_obj is not None:
|
|
640
|
-
doc = (getattr(attr_obj, "__doc__", "") or "").strip()
|
|
641
|
-
if not type_name_hint and not element_type_hint:
|
|
642
|
-
ann = _class_annotation(cls, attr)
|
|
643
|
-
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
644
|
-
if type_name_hint and not element_type_hint:
|
|
645
|
-
element_type_hint = _iter_element_for_type_name(type_name_hint)
|
|
646
|
-
attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
|
|
647
|
-
|
|
648
|
-
for meth in getattr(cls, "METHODS", []):
|
|
649
|
-
doc = ""
|
|
650
|
-
sig_label = ""
|
|
651
|
-
try:
|
|
652
|
-
meth_obj = getattr(cls, meth)
|
|
653
|
-
except Exception:
|
|
654
|
-
meth_obj = None
|
|
655
|
-
if callable(meth_obj):
|
|
656
|
-
sig_label = _format_method_signature(meth, meth_obj)
|
|
657
|
-
doc = (meth_obj.__doc__ or "").strip()
|
|
658
|
-
methods[meth] = MethodMeta(signature=sig_label, doc=doc)
|
|
659
|
-
|
|
660
|
-
meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=iterable_element)
|
|
661
|
-
return meta
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
def _format_method_signature(name: str, obj: Any) -> str:
|
|
665
|
-
try:
|
|
666
|
-
sig = inspect.signature(obj)
|
|
667
|
-
except (TypeError, ValueError):
|
|
668
|
-
return f"{name}()"
|
|
669
|
-
params = list(sig.parameters.values())
|
|
670
|
-
if params and params[0].name in {"self", "cls"}:
|
|
671
|
-
params = params[1:]
|
|
672
|
-
sig = sig.replace(parameters=params)
|
|
673
|
-
return f"{name}{sig}"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
def _return_annotation(func: Any, cls: type) -> Any:
|
|
677
|
-
try:
|
|
678
|
-
module = inspect.getmodule(func) or inspect.getmodule(cls)
|
|
679
|
-
globalns = module.__dict__ if module else None
|
|
680
|
-
hints = typing.get_type_hints(func, globalns=globalns, include_extras=False)
|
|
681
|
-
return hints.get("return")
|
|
682
|
-
except Exception:
|
|
683
|
-
return getattr(func, "__annotations__", {}).get("return")
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
def _class_annotation(cls: type, attr: str) -> Any:
|
|
687
|
-
try:
|
|
688
|
-
module = inspect.getmodule(cls)
|
|
689
|
-
globalns = module.__dict__ if module else None
|
|
690
|
-
hints = typing.get_type_hints(cls, globalns=globalns, include_extras=False)
|
|
691
|
-
if attr in hints:
|
|
692
|
-
return hints[attr]
|
|
693
|
-
except Exception:
|
|
694
|
-
pass
|
|
695
|
-
return getattr(getattr(cls, "__annotations__", {}), "get", lambda _k: None)(attr)
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) -> tuple[str, str]:
|
|
699
|
-
if ann is None:
|
|
700
|
-
return "", ""
|
|
701
|
-
if isinstance(ann, str):
|
|
702
|
-
return "", ""
|
|
703
|
-
try:
|
|
704
|
-
origin = getattr(ann, "__origin__", None)
|
|
705
|
-
except Exception:
|
|
706
|
-
origin = None
|
|
707
|
-
args = getattr(ann, "__args__", ()) if origin else ()
|
|
708
|
-
|
|
709
|
-
if ann in reverse_type_map:
|
|
710
|
-
return reverse_type_map[ann], ""
|
|
711
|
-
|
|
712
|
-
# handle list/sequence typing to detect element type
|
|
713
|
-
iterable_origins = {list, List, Iterable, typing.Sequence, typing.Iterable}
|
|
714
|
-
try:
|
|
715
|
-
from collections.abc import Iterable as ABCIterable, Sequence as ABCSequence
|
|
716
|
-
iterable_origins.update({ABCIterable, ABCSequence})
|
|
717
|
-
except Exception:
|
|
718
|
-
pass
|
|
719
|
-
if origin in iterable_origins:
|
|
720
|
-
if args:
|
|
721
|
-
elem = args[0]
|
|
722
|
-
elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
|
|
723
|
-
container_name = reverse_type_map.get(origin) or "list"
|
|
724
|
-
return container_name, elem_name
|
|
725
|
-
return reverse_type_map.get(origin) or "list", ""
|
|
726
|
-
|
|
727
|
-
if isinstance(ann, type) and ann in reverse_type_map:
|
|
728
|
-
return reverse_type_map[ann], ""
|
|
729
|
-
return "", ""
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) -> str:
|
|
733
|
-
try:
|
|
734
|
-
hints = typing.get_type_hints(cls.__iter__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
|
|
735
|
-
ret_ann = hints.get("return")
|
|
736
|
-
_, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
|
|
737
|
-
return elem
|
|
738
|
-
except Exception:
|
|
739
|
-
return ""
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
def _attribute_at_position(line_text: str, cursor: int) -> tuple[Optional[str], Optional[str]]:
|
|
743
|
-
cursor = max(0, min(cursor, len(line_text)))
|
|
744
|
-
for match in ATTR_AT_CURSOR_RE.finditer(line_text):
|
|
745
|
-
start, end = match.span(2)
|
|
746
|
-
if start <= cursor <= end:
|
|
747
|
-
return match.group(1), match.group(2)
|
|
748
|
-
return None, None
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
def _infer_constant_bindings(code: str, upto_line: int | None, ctx_data: ContextData) -> Dict[str, Any]:
|
|
752
|
-
try:
|
|
753
|
-
tree = ast.parse(code)
|
|
754
|
-
except SyntaxError:
|
|
755
|
-
return {}
|
|
756
|
-
bindings: dict[str, Any] = {}
|
|
757
|
-
limit = None if upto_line is None else upto_line + 1
|
|
758
|
-
|
|
759
|
-
def _value_for(node: ast.AST) -> Any | None:
|
|
760
|
-
value = _literal_value(node)
|
|
761
|
-
if value is None:
|
|
762
|
-
value = _evaluated_value(node, ctx_data, bindings)
|
|
763
|
-
return value
|
|
764
|
-
|
|
765
|
-
def _loop_binding(node: ast.AST) -> Any | None:
|
|
766
|
-
value = _value_for(node)
|
|
767
|
-
if value is None:
|
|
768
|
-
return _LoopVarBinding()
|
|
769
|
-
try:
|
|
770
|
-
iterator = iter(value)
|
|
771
|
-
except TypeError:
|
|
772
|
-
return _LoopVarBinding()
|
|
773
|
-
try:
|
|
774
|
-
return next(iterator)
|
|
775
|
-
except StopIteration:
|
|
776
|
-
return _LoopVarBinding()
|
|
777
|
-
|
|
778
|
-
class Visitor(ast.NodeVisitor):
|
|
779
|
-
def visit_Assign(self, node: ast.Assign):
|
|
780
|
-
if limit is not None and node.lineno > limit:
|
|
781
|
-
return
|
|
782
|
-
value = _value_for(node.value)
|
|
783
|
-
if value is None:
|
|
784
|
-
self.generic_visit(node)
|
|
785
|
-
return
|
|
786
|
-
for name in _names_from_target(node.targets):
|
|
787
|
-
bindings[name] = value
|
|
788
|
-
|
|
789
|
-
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
790
|
-
if limit is not None and node.lineno > limit:
|
|
791
|
-
return
|
|
792
|
-
if node.value is None:
|
|
793
|
-
return
|
|
794
|
-
value = _value_for(node.value)
|
|
795
|
-
if value is None:
|
|
796
|
-
self.generic_visit(node)
|
|
797
|
-
return
|
|
798
|
-
for name in _names_from_target([node.target]):
|
|
799
|
-
bindings[name] = value
|
|
800
|
-
|
|
801
|
-
def visit_For(self, node: ast.For):
|
|
802
|
-
if limit is not None and node.lineno > limit:
|
|
803
|
-
return
|
|
804
|
-
loop_val = _loop_binding(node.iter)
|
|
805
|
-
for name in _names_from_target([node.target]):
|
|
806
|
-
bindings[name] = loop_val
|
|
807
|
-
self.generic_visit(node)
|
|
808
|
-
|
|
809
|
-
def visit_AsyncFor(self, node: ast.AsyncFor):
|
|
810
|
-
if limit is not None and node.lineno > limit:
|
|
811
|
-
return
|
|
812
|
-
loop_val = _loop_binding(node.iter)
|
|
813
|
-
for name in _names_from_target([node.target]):
|
|
814
|
-
bindings[name] = loop_val
|
|
815
|
-
self.generic_visit(node)
|
|
816
|
-
|
|
817
|
-
Visitor().visit(tree)
|
|
818
|
-
return bindings
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
def _names_from_target(targets: Iterable[ast.expr]) -> List[str]:
|
|
822
|
-
names: list[str] = []
|
|
823
|
-
for target in targets:
|
|
824
|
-
if isinstance(target, ast.Name):
|
|
825
|
-
names.append(target.id)
|
|
826
|
-
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
827
|
-
names.extend(_names_from_target(target.elts))
|
|
828
|
-
return names
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
def _literal_value(node: ast.AST) -> Any | None:
|
|
832
|
-
if isinstance(node, ast.Constant):
|
|
833
|
-
return node.value
|
|
834
|
-
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
|
835
|
-
val = _literal_value(node.operand)
|
|
836
|
-
if isinstance(val, (int, float, complex)):
|
|
837
|
-
return val if isinstance(node.op, ast.UAdd) else -val
|
|
838
|
-
return None
|
|
839
|
-
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
840
|
-
items = []
|
|
841
|
-
for elt in node.elts:
|
|
842
|
-
val = _literal_value(elt)
|
|
843
|
-
if val is None:
|
|
844
|
-
return None
|
|
845
|
-
items.append(val)
|
|
846
|
-
if isinstance(node, ast.List):
|
|
847
|
-
return items
|
|
848
|
-
if isinstance(node, ast.Tuple):
|
|
849
|
-
return tuple(items)
|
|
850
|
-
return set(items)
|
|
851
|
-
if isinstance(node, ast.Dict):
|
|
852
|
-
keys = []
|
|
853
|
-
values = []
|
|
854
|
-
for k, v in zip(node.keys, node.values):
|
|
855
|
-
key_val = _literal_value(k) if k is not None else None
|
|
856
|
-
val_val = _literal_value(v)
|
|
857
|
-
if key_val is None or val_val is None:
|
|
858
|
-
return None
|
|
859
|
-
keys.append(key_val)
|
|
860
|
-
values.append(val_val)
|
|
861
|
-
return dict(zip(keys, values))
|
|
862
|
-
return None
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
def _evaluated_value(node: ast.AST, ctx_data: ContextData, bindings: Dict[str, Any] | None = None) -> Any | None:
|
|
866
|
-
bindings = bindings or {}
|
|
867
|
-
try:
|
|
868
|
-
return _eval_node(node, ctx_data, bindings)
|
|
869
|
-
except Exception:
|
|
870
|
-
return None
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
def _eval_node(node: ast.AST, ctx_data: ContextData, bindings: Dict[str, Any]) -> Any | None:
|
|
874
|
-
if isinstance(node, ast.Attribute):
|
|
875
|
-
base = _eval_node(node.value, ctx_data, bindings)
|
|
876
|
-
if base is None:
|
|
877
|
-
return None
|
|
878
|
-
return getattr(base, node.attr, None)
|
|
879
|
-
if isinstance(node, ast.Call):
|
|
880
|
-
if isinstance(node.func, ast.Name):
|
|
881
|
-
if node.func.id == "character":
|
|
882
|
-
return CharacterAPI(ctx_data.character)
|
|
883
|
-
if node.func.id == "combat":
|
|
884
|
-
return SimpleCombat(ctx_data.combat)
|
|
885
|
-
if node.func.id == "range":
|
|
886
|
-
args = []
|
|
887
|
-
for arg in node.args:
|
|
888
|
-
val = _literal_value(arg)
|
|
889
|
-
if val is None:
|
|
890
|
-
return None
|
|
891
|
-
args.append(val)
|
|
892
|
-
try:
|
|
893
|
-
return range(*args)
|
|
894
|
-
except Exception:
|
|
895
|
-
return None
|
|
896
|
-
if isinstance(node.func, ast.Attribute):
|
|
897
|
-
base = _eval_node(node.func.value, ctx_data, bindings)
|
|
898
|
-
if base is None:
|
|
899
|
-
return None
|
|
900
|
-
method_name = node.func.attr
|
|
901
|
-
if not _is_safe_call(base, method_name):
|
|
902
|
-
return None
|
|
903
|
-
args = []
|
|
904
|
-
for arg in node.args:
|
|
905
|
-
val = _literal_value(arg)
|
|
906
|
-
if val is None:
|
|
907
|
-
val = _eval_node(arg, ctx_data, bindings)
|
|
908
|
-
if val is None:
|
|
909
|
-
return None
|
|
910
|
-
args.append(val)
|
|
911
|
-
kwargs = {}
|
|
912
|
-
for kw in node.keywords:
|
|
913
|
-
if kw.arg is None:
|
|
914
|
-
return None
|
|
915
|
-
val = _literal_value(kw.value)
|
|
916
|
-
if val is None:
|
|
917
|
-
val = _eval_node(kw.value, ctx_data, bindings)
|
|
918
|
-
if val is None:
|
|
919
|
-
return None
|
|
920
|
-
kwargs[kw.arg] = val
|
|
921
|
-
callee = getattr(base, method_name, None)
|
|
922
|
-
if not callable(callee):
|
|
923
|
-
return None
|
|
924
|
-
try:
|
|
925
|
-
return callee(*args, **kwargs)
|
|
926
|
-
except Exception:
|
|
927
|
-
return None
|
|
928
|
-
if isinstance(node, ast.Name):
|
|
929
|
-
if node.id in bindings:
|
|
930
|
-
return bindings[node.id]
|
|
931
|
-
if node.id == "ctx":
|
|
932
|
-
return AliasContextAPI(ctx_data.ctx)
|
|
933
|
-
return None
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
def _is_safe_call(base: Any, method: str) -> bool:
|
|
937
|
-
for cls, allowed in SAFE_METHODS.items():
|
|
938
|
-
if isinstance(base, cls) and method in allowed:
|
|
939
|
-
return True
|
|
940
|
-
return False
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
def _format_binding_hover(name: str, value: Any, label: str) -> types.Hover:
|
|
944
|
-
type_name = _describe_type(value)
|
|
945
|
-
preview = _preview_value(value)
|
|
946
|
-
contents = f"**{label}** `{name}`\n\nType: `{type_name}`\nValue: `{preview}`"
|
|
947
|
-
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
def _describe_type(value: Any) -> str:
|
|
951
|
-
# Provide light element-type hints for common iterables so hover shows list[Foo].
|
|
952
|
-
def _iterable_type(iterable: Iterable[Any], container: str) -> str:
|
|
953
|
-
try:
|
|
954
|
-
seen = {type(item).__name__ for item in iterable if item is not None}
|
|
955
|
-
except Exception:
|
|
956
|
-
return container
|
|
957
|
-
return f"{container}[{seen.pop()}]" if len(seen) == 1 else container
|
|
958
|
-
|
|
959
|
-
try:
|
|
960
|
-
if isinstance(value, list):
|
|
961
|
-
return _iterable_type(value, "list")
|
|
962
|
-
if isinstance(value, tuple):
|
|
963
|
-
return _iterable_type(value, "tuple")
|
|
964
|
-
if isinstance(value, set):
|
|
965
|
-
return _iterable_type(value, "set")
|
|
966
|
-
except Exception:
|
|
967
|
-
pass
|
|
968
|
-
return type(value).__name__
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
def _preview_value(value: Any) -> str:
|
|
972
|
-
text = repr(value)
|
|
973
|
-
if len(text) > 120:
|
|
974
|
-
text = text[:117] + "..."
|
|
975
|
-
return text
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
class _LoopVarBinding:
|
|
979
|
-
def __repr__(self) -> str:
|
|
980
|
-
return "<loop item>"
|
|
981
|
-
SAFE_METHODS: dict[type, set[str]] = {
|
|
982
|
-
AliasLevels: {"get"},
|
|
983
|
-
dict: {"get"},
|
|
984
|
-
}
|