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/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
- }