avrae-ls 0.4.1__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,1391 +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
- "AliasSkills": AliasSkills,
128
- "skill": AliasSkill,
129
- "AliasSkill": AliasSkill,
130
- "saves": AliasSaves,
131
- "resistances": AliasResistances,
132
- "coinpurse": AliasCoinpurse,
133
- "custom_counter": AliasCustomCounter,
134
- "consumable": AliasCustomCounter,
135
- "death_saves": AliasDeathSaves,
136
- "action": AliasAction,
137
- "spellbook": AliasSpellbook,
138
- "spell": AliasSpellbookSpell,
139
- "guild": GuildAPI,
140
- "channel": ChannelAPI,
141
- "category": CategoryAPI,
142
- "author": AuthorAPI,
143
- "role": RoleAPI,
144
- "combatant": SimpleCombatant,
145
- "SimpleCombatant": SimpleCombatant,
146
- "group": SimpleGroup,
147
- "SimpleGroup": SimpleGroup,
148
- "effect": SimpleEffect,
149
- "SimpleEffect": SimpleEffect,
150
- "list": _BuiltinList,
151
- "dict": _BuiltinDict,
152
- "str": _BuiltinStr,
153
- "ParsedArguments": ParsedArguments,
154
- }
155
-
156
-
157
- IDENT_RE = re.compile(r"[A-Za-z_]\w*$")
158
- ATTR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.(?:([A-Za-z_]\w*)\s*)?$")
159
- DICT_GET_RE = re.compile(r"^([A-Za-z_]\w*)\.get\(\s*(['\"])(.+?)\2")
160
- ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.([A-Za-z_]\w*)")
161
-
162
-
163
- @dataclass
164
- class Suggestion:
165
- name: str
166
- kind: types.CompletionItemKind
167
- detail: str = ""
168
- documentation: str = ""
169
-
170
-
171
- @dataclass
172
- class AttrMeta:
173
- doc: str = ""
174
- type_name: str = ""
175
- element_type: str = ""
176
-
177
-
178
- @dataclass
179
- class MethodMeta:
180
- signature: str = ""
181
- doc: str = ""
182
-
183
-
184
- @dataclass
185
- class TypeMeta:
186
- attrs: Dict[str, AttrMeta]
187
- methods: Dict[str, MethodMeta]
188
- element_type: str = ""
189
-
190
-
191
- _SKILL_DOCS: dict[str, str] = {
192
- "acrobatics": "Acrobatics skill bonus.",
193
- "animalHandling": "Animal Handling skill bonus.",
194
- "arcana": "Arcana skill bonus.",
195
- "athletics": "Athletics skill bonus.",
196
- "deception": "Deception skill bonus.",
197
- "history": "History skill bonus.",
198
- "initiative": "Initiative modifier.",
199
- "insight": "Insight skill bonus.",
200
- "intimidation": "Intimidation skill bonus.",
201
- "investigation": "Investigation skill bonus.",
202
- "medicine": "Medicine skill bonus.",
203
- "nature": "Nature skill bonus.",
204
- "perception": "Perception skill bonus.",
205
- "performance": "Performance skill bonus.",
206
- "persuasion": "Persuasion skill bonus.",
207
- "religion": "Religion skill bonus.",
208
- "sleightOfHand": "Sleight of Hand skill bonus.",
209
- "stealth": "Stealth skill bonus.",
210
- "survival": "Survival skill bonus.",
211
- "strength": "Strength ability score for this skill block.",
212
- "dexterity": "Dexterity ability score for this skill block.",
213
- "constitution": "Constitution ability score for this skill block.",
214
- "intelligence": "Intelligence ability score for this skill block.",
215
- "wisdom": "Wisdom ability score for this skill block.",
216
- "charisma": "Charisma ability score for this skill block.",
217
- }
218
-
219
- _COUNTER_DOCS: dict[str, str] = {
220
- "name": "Internal name of the counter.",
221
- "title": "Display title for the counter.",
222
- "desc": "Description text for the counter.",
223
- "value": "Current counter value.",
224
- "max": "Maximum value for the counter.",
225
- "min": "Minimum value for the counter.",
226
- "reset_on": "Reset cadence for the counter (e.g., long/short rest).",
227
- "display_type": "Display style for the counter.",
228
- "reset_to": "Value to reset the counter to.",
229
- "reset_by": "Increment applied when the counter resets.",
230
- }
231
-
232
- _EFFECT_DOCS: dict[str, str] = {
233
- "name": "Effect name.",
234
- "duration": "Configured duration for the effect.",
235
- "remaining": "Remaining duration for the effect.",
236
- "effect": "Raw effect payload.",
237
- "attacks": "Attack data attached to the effect, if any.",
238
- "buttons": "Buttons provided by the effect.",
239
- "conc": "Whether the effect requires concentration.",
240
- "desc": "Effect description text.",
241
- "ticks_on_end": "Whether the effect ticks when it ends.",
242
- "combatant_name": "Name of the owning combatant.",
243
- "parent": "Parent effect, if nested.",
244
- "children": "Child effects nested under this effect.",
245
- }
246
-
247
- _ATTR_DOC_OVERRIDES: dict[str, dict[str, str]] = {
248
- "SimpleRollResult": {
249
- "dice": "Markdown representation of the dice that were rolled.",
250
- "total": "Numeric total of the resolved roll.",
251
- "full": "Rendered roll result string.",
252
- "result": "Underlying d20 RollResult object.",
253
- "raw": "Original d20 expression for the roll.",
254
- },
255
- "stats": {
256
- "prof_bonus": "Proficiency bonus for the character.",
257
- "strength": "Strength ability score.",
258
- "dexterity": "Dexterity ability score.",
259
- "constitution": "Constitution ability score.",
260
- "intelligence": "Intelligence ability score.",
261
- "wisdom": "Wisdom ability score.",
262
- "charisma": "Charisma ability score.",
263
- },
264
- "AliasBaseStats": {
265
- "prof_bonus": "Proficiency bonus for the character.",
266
- "strength": "Strength ability score.",
267
- "dexterity": "Dexterity ability score.",
268
- "constitution": "Constitution ability score.",
269
- "intelligence": "Intelligence ability score.",
270
- "wisdom": "Wisdom ability score.",
271
- "charisma": "Charisma ability score.",
272
- },
273
- "levels": {
274
- "total_level": "Sum of all class levels.",
275
- },
276
- "AliasLevels": {
277
- "total_level": "Sum of all class levels.",
278
- },
279
- "attack": {
280
- "name": "Attack name.",
281
- "verb": "Attack verb or action phrase.",
282
- "proper": "Whether the attack name is treated as proper.",
283
- "activation_type": "Activation type identifier for this attack.",
284
- "raw": "Raw attack payload from the statblock.",
285
- },
286
- "AliasAttack": {
287
- "name": "Attack name.",
288
- "verb": "Attack verb or action phrase.",
289
- "proper": "Whether the attack name is treated as proper.",
290
- "activation_type": "Activation type identifier for this attack.",
291
- "raw": "Raw attack payload from the statblock.",
292
- },
293
- "skills": _SKILL_DOCS,
294
- "AliasSkills": _SKILL_DOCS,
295
- "skill": {
296
- "value": "Total modifier for the skill.",
297
- "prof": "Proficiency value applied to the skill.",
298
- "bonus": "Base bonus before rolling.",
299
- "adv": "Advantage state for the skill roll (True/False/None).",
300
- },
301
- "AliasSkill": {
302
- "value": "Total modifier for the skill.",
303
- "prof": "Proficiency value applied to the skill.",
304
- "bonus": "Base bonus before rolling.",
305
- "adv": "Advantage state for the skill roll (True/False/None).",
306
- },
307
- "resistances": {
308
- "resist": "Damage types resisted.",
309
- "vuln": "Damage types this target is vulnerable to.",
310
- "immune": "Damage types the target is immune to.",
311
- "neutral": "Damage types with no modifiers.",
312
- },
313
- "AliasResistances": {
314
- "resist": "Damage types resisted.",
315
- "vuln": "Damage types this target is vulnerable to.",
316
- "immune": "Damage types the target is immune to.",
317
- "neutral": "Damage types with no modifiers.",
318
- },
319
- "coinpurse": {
320
- "pp": "Platinum pieces carried.",
321
- "gp": "Gold pieces carried.",
322
- "ep": "Electrum pieces carried.",
323
- "sp": "Silver pieces carried.",
324
- "cp": "Copper pieces carried.",
325
- "total": "Total value of all coins.",
326
- },
327
- "AliasCoinpurse": {
328
- "pp": "Platinum pieces carried.",
329
- "gp": "Gold pieces carried.",
330
- "ep": "Electrum pieces carried.",
331
- "sp": "Silver pieces carried.",
332
- "cp": "Copper pieces carried.",
333
- "total": "Total value of all coins.",
334
- },
335
- "custom_counter": _COUNTER_DOCS,
336
- "consumable": _COUNTER_DOCS,
337
- "AliasCustomCounter": _COUNTER_DOCS,
338
- "death_saves": {
339
- "successes": "Number of successful death saves.",
340
- "fails": "Number of failed death saves.",
341
- },
342
- "AliasDeathSaves": {
343
- "successes": "Number of successful death saves.",
344
- "fails": "Number of failed death saves.",
345
- },
346
- "spellbook": {
347
- "dc": "Save DC for spells in this spellbook.",
348
- "sab": "Spell attack bonus for this spellbook.",
349
- "caster_level": "Caster level used for the spellbook.",
350
- "spell_mod": "Spellcasting ability modifier.",
351
- "spells": "Spells grouped by level.",
352
- "pact_slot_level": "Level of pact slots, if any.",
353
- "num_pact_slots": "Number of pact slots available.",
354
- "max_pact_slots": "Maximum pact slots available.",
355
- },
356
- "AliasSpellbook": {
357
- "dc": "Save DC for spells in this spellbook.",
358
- "sab": "Spell attack bonus for this spellbook.",
359
- "caster_level": "Caster level used for the spellbook.",
360
- "spell_mod": "Spellcasting ability modifier.",
361
- "spells": "Spells grouped by level.",
362
- "pact_slot_level": "Level of pact slots, if any.",
363
- "num_pact_slots": "Number of pact slots available.",
364
- "max_pact_slots": "Maximum pact slots available.",
365
- },
366
- "spell": {
367
- "name": "Spell name.",
368
- "dc": "Save DC for this spell.",
369
- "sab": "Spell attack bonus for this spell.",
370
- "mod": "Spellcasting modifier applied to the spell.",
371
- "prepared": "Whether the spell is prepared/known.",
372
- },
373
- "AliasSpellbookSpell": {
374
- "name": "Spell name.",
375
- "dc": "Save DC for this spell.",
376
- "sab": "Spell attack bonus for this spell.",
377
- "mod": "Spellcasting modifier applied to the spell.",
378
- "prepared": "Whether the spell is prepared/known.",
379
- },
380
- "guild": {
381
- "name": "Guild (server) name.",
382
- "id": "Guild (server) id.",
383
- },
384
- "channel": {
385
- "name": "Channel name.",
386
- "id": "Channel id.",
387
- "topic": "Channel topic, if set.",
388
- "category": "Parent category for the channel.",
389
- "parent": "Parent channel, if present.",
390
- },
391
- "category": {
392
- "name": "Category name.",
393
- "id": "Category id.",
394
- },
395
- "author": {
396
- "name": "User name for the invoking author.",
397
- "id": "User id for the invoking author.",
398
- "discriminator": "User discriminator/tag.",
399
- "display_name": "Display name for the author.",
400
- "roles": "Roles held by the author.",
401
- },
402
- "role": {
403
- "name": "Role name.",
404
- "id": "Role id.",
405
- },
406
- "effect": _EFFECT_DOCS,
407
- "SimpleEffect": _EFFECT_DOCS,
408
- }
409
-
410
-
411
- def gather_suggestions(
412
- ctx_data: ContextData,
413
- resolver: GVarResolver,
414
- sigs: Dict[str, FunctionSig],
415
- ) -> List[Suggestion]:
416
- suggestions: list[Suggestion] = []
417
-
418
- for name, sig in sigs.items():
419
- suggestions.append(
420
- Suggestion(
421
- name=name,
422
- kind=types.CompletionItemKind.Function,
423
- detail=sig.label,
424
- documentation=sig.doc,
425
- )
426
- )
427
-
428
- vars_map = ctx_data.vars.to_initial_names()
429
- for name in vars_map:
430
- suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Variable, detail="var"))
431
-
432
- gvars = resolver.snapshot()
433
- for name in gvars:
434
- suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Variable, detail="gvar"))
435
-
436
- for name in _default_builtins().keys():
437
- if name not in sigs:
438
- suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Function))
439
-
440
- # context helpers
441
- suggestions.append(Suggestion(name="character", kind=types.CompletionItemKind.Function, detail="Alias character()"))
442
- suggestions.append(Suggestion(name="combat", kind=types.CompletionItemKind.Function, detail="Alias combat()"))
443
- suggestions.append(Suggestion(name="ctx", kind=types.CompletionItemKind.Variable, detail="Alias context"))
444
-
445
- return suggestions
446
-
447
-
448
- def completion_items_for_position(
449
- code: str,
450
- line: int,
451
- character: int,
452
- suggestions: Iterable[Suggestion],
453
- ) -> List[types.CompletionItem]:
454
- attr_ctx = _attribute_receiver_and_prefix(code, line, character)
455
- if attr_ctx:
456
- receiver, attr_prefix = attr_ctx
457
- sanitized = _sanitize_incomplete_line(code, line, character)
458
- type_map = _infer_type_map(sanitized)
459
- return _attribute_completions(receiver, attr_prefix, sanitized, type_map)
460
-
461
- line_text = _line_text_to_cursor(code, line, character)
462
- prefix = _current_prefix(line_text)
463
- items: list[types.CompletionItem] = []
464
- for sugg in suggestions:
465
- if prefix and not sugg.name.startswith(prefix):
466
- continue
467
- items.append(
468
- types.CompletionItem(
469
- label=sugg.name,
470
- kind=sugg.kind,
471
- detail=sugg.detail or None,
472
- documentation=sugg.documentation or None,
473
- )
474
- )
475
- return items
476
-
477
-
478
- def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict[str, str] | None = None) -> List[types.CompletionItem]:
479
- items: list[types.CompletionItem] = []
480
- type_key = _resolve_type_name(receiver, code, type_map)
481
- meta = _type_meta(type_key)
482
- detail = f"{type_key}()"
483
-
484
- for name, attr_meta in meta.attrs.items():
485
- if prefix and not name.startswith(prefix):
486
- continue
487
- items.append(
488
- types.CompletionItem(
489
- label=name,
490
- kind=types.CompletionItemKind.Field,
491
- detail=detail,
492
- documentation=attr_meta.doc or None,
493
- )
494
- )
495
- for name, method_meta in meta.methods.items():
496
- if prefix and not name.startswith(prefix):
497
- continue
498
- method_detail = method_meta.signature or f"{name}()"
499
- items.append(
500
- types.CompletionItem(
501
- label=name,
502
- kind=types.CompletionItemKind.Method,
503
- detail=method_detail,
504
- documentation=method_meta.doc or None,
505
- )
506
- )
507
- return items
508
-
509
-
510
- def hover_for_position(
511
- code: str,
512
- line: int,
513
- character: int,
514
- sigs: Dict[str, FunctionSig],
515
- ctx_data: ContextData,
516
- resolver: GVarResolver,
517
- ) -> Optional[types.Hover]:
518
- line_text = _line_text(code, line)
519
- type_map = _infer_type_map(code)
520
- bindings = _infer_constant_bindings(code, line, ctx_data)
521
- attr_ctx = _attribute_receiver_and_prefix(code, line, character, capture_full_token=True)
522
- if attr_ctx:
523
- receiver, attr_prefix = attr_ctx
524
- inferred = _resolve_type_name(receiver, code, type_map)
525
- meta = _type_meta(inferred)
526
- if attr_prefix in meta.attrs:
527
- doc = meta.attrs[attr_prefix].doc
528
- contents = f"```avrae\n{inferred}().{attr_prefix}\n```"
529
- if doc:
530
- contents += f"\n\n{doc}"
531
- return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
532
- if attr_prefix in meta.methods:
533
- method_meta = meta.methods[attr_prefix]
534
- signature = method_meta.signature or f"{attr_prefix}()"
535
- doc = method_meta.doc
536
- contents = f"```avrae\n{signature}\n```"
537
- if doc:
538
- contents += f"\n\n{doc}"
539
- return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
540
-
541
- word, _, _ = _word_at_position(line_text, character)
542
- if not word:
543
- return None
544
- if word in bindings:
545
- return _format_binding_hover(word, bindings[word], "local")
546
- if word in type_map:
547
- type_label = _display_type_label(type_map[word])
548
- contents = f"`{word}` type: `{type_label}`"
549
- return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
550
- if word in sigs:
551
- sig = sigs[word]
552
- contents = f"```avrae\n{sig.label}\n```\n\n{sig.doc}"
553
- return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
554
-
555
- vars_map = ctx_data.vars.to_initial_names()
556
- if word in vars_map:
557
- return _format_binding_hover(word, vars_map[word], "var")
558
-
559
- gvars = resolver.snapshot()
560
- if word in gvars:
561
- return _format_binding_hover(word, gvars[word], "gvar")
562
- return None
563
-
564
-
565
- def _current_prefix(line_text: str) -> str:
566
- match = IDENT_RE.search(line_text)
567
- return match.group(0) if match else ""
568
-
569
-
570
- def _word_from_line(text: str, cursor: int) -> str:
571
- return _word_at_position(text, cursor)[0]
572
-
573
-
574
- def _word_at_position(text: str, cursor: int) -> tuple[str, int, int]:
575
- cursor = max(0, min(cursor, len(text)))
576
- start = cursor
577
- while start > 0 and (text[start - 1].isalnum() or text[start - 1] == "_"):
578
- start -= 1
579
- end = cursor
580
- while end < len(text) and (text[end].isalnum() or text[end] == "_"):
581
- end += 1
582
- return text[start:end], start, end
583
-
584
-
585
- def _line_text_to_cursor(code: str, line: int, character: int) -> str:
586
- lines = code.splitlines()
587
- if line >= len(lines):
588
- return ""
589
- return lines[line][:character]
590
-
591
-
592
- def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture_full_token: bool = False) -> Optional[tuple[str, str]]:
593
- lines = code.splitlines()
594
- if line >= len(lines):
595
- return None
596
- line_text = lines[line]
597
- end = character
598
- if capture_full_token:
599
- while end < len(line_text) and (line_text[end].isalnum() or line_text[end] == "_"):
600
- end += 1
601
- line_text = line_text[: end]
602
- dot = line_text.rfind(".")
603
- if dot == -1:
604
- return None
605
- tail = line_text[dot + 1 :]
606
- prefix_match = re.match(r"\s*([A-Za-z_]\w*)?", tail)
607
- prefix = prefix_match.group(1) or "" if prefix_match else ""
608
- suffix = tail[prefix_match.end() if prefix_match else 0 :]
609
- placeholder = "__COMPLETE__"
610
- new_line = f"{line_text[:dot]}.{placeholder}{suffix}"
611
- # Close unmatched parentheses so the temporary code parses.
612
- paren_balance = new_line.count("(") - new_line.count(")")
613
- if paren_balance > 0:
614
- new_line = new_line + (")" * paren_balance)
615
- mod_lines = list(lines)
616
- mod_lines[line] = new_line
617
- mod_code = "\n".join(mod_lines)
618
- try:
619
- tree = ast.parse(mod_code)
620
- except SyntaxError:
621
- return None
622
-
623
- receiver_src: Optional[str] = None
624
-
625
- class Finder(ast.NodeVisitor):
626
- def visit_Attribute(self, node: ast.Attribute):
627
- nonlocal receiver_src
628
- if isinstance(node.attr, str) and node.attr == placeholder:
629
- try:
630
- receiver_src = ast.unparse(node.value)
631
- except Exception:
632
- receiver_src = None
633
- self.generic_visit(node)
634
-
635
- Finder().visit(tree)
636
- if receiver_src is None:
637
- return None
638
- return receiver_src, prefix
639
-
640
-
641
- def _sanitize_incomplete_line(code: str, line: int, character: int) -> str:
642
- lines = code.splitlines()
643
- if 0 <= line < len(lines):
644
- prefix = lines[line][:character]
645
- trimmed = prefix.rstrip()
646
- if trimmed.endswith("."):
647
- prefix = trimmed[:-1]
648
- else:
649
- dot = prefix.rfind(".")
650
- if dot != -1:
651
- after = prefix[dot + 1 :]
652
- if not re.match(r"\s*[A-Za-z_]", after):
653
- prefix = prefix[:dot] + after
654
- lines[line] = prefix
655
- candidate = "\n".join(lines)
656
- try:
657
- ast.parse(candidate)
658
- except SyntaxError:
659
- indent = re.match(r"[ \t]*", lines[line]).group(0)
660
- lines[line] = indent + "pass"
661
- return "\n".join(lines)
662
-
663
-
664
- def _line_text(code: str, line: int) -> str:
665
- lines = code.splitlines()
666
- if line < 0 or line >= len(lines):
667
- return ""
668
- return lines[line]
669
-
670
-
671
- def _display_type_label(type_key: str) -> str:
672
- if type_key in TYPE_MAP:
673
- return TYPE_MAP[type_key].__name__
674
- return type_key
675
-
676
-
677
- def _infer_receiver_type(code: str, name: str) -> Optional[str]:
678
- return _infer_type_map(code).get(name)
679
-
680
-
681
- def _infer_type_map(code: str) -> Dict[str, str]:
682
- try:
683
- tree = ast.parse(code)
684
- except SyntaxError:
685
- return {}
686
- visitor = _TypeInferencer(code)
687
- visitor.visit(tree)
688
- return visitor.type_map
689
-
690
-
691
- class _TypeInferencer(ast.NodeVisitor):
692
- def __init__(self, code: str) -> None:
693
- self.code = code
694
- self.type_map: dict[str, str] = {}
695
-
696
- def visit_Assign(self, node: ast.Assign):
697
- val_type, elem_type = self._value_type(node.value)
698
- for target in node.targets:
699
- self._bind_target(target, val_type, elem_type, node.value)
700
- self.generic_visit(node)
701
-
702
- def visit_AnnAssign(self, node: ast.AnnAssign):
703
- val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
704
- self._bind_target(node.target, val_type, elem_type, node.value)
705
- self.generic_visit(node)
706
-
707
- def visit_AugAssign(self, node: ast.AugAssign):
708
- val_type, elem_type = self._value_type(node.value)
709
- self._bind_target(
710
- node.target,
711
- val_type or self._existing_type(node.target),
712
- elem_type or self._existing_element(node.target),
713
- None,
714
- )
715
- self.generic_visit(node)
716
-
717
- def visit_For(self, node: ast.For):
718
- _, elem_type = self._value_type(node.iter)
719
- if not elem_type and isinstance(node.iter, ast.Name):
720
- elem_type = self.type_map.get(f"{node.iter.id}.__element__")
721
- self._bind_target(node.target, elem_type, None, None)
722
- self.generic_visit(node)
723
-
724
- def visit_AsyncFor(self, node: ast.AsyncFor):
725
- _, elem_type = self._value_type(node.iter)
726
- if not elem_type and isinstance(node.iter, ast.Name):
727
- elem_type = self.type_map.get(f"{node.iter.id}.__element__")
728
- self._bind_target(node.target, elem_type, None, None)
729
- self.generic_visit(node)
730
-
731
- def visit_If(self, node: ast.If):
732
- self.visit(node.test)
733
- base_map = self.type_map.copy()
734
- body_map = self._visit_block(node.body, base_map.copy())
735
- orelse_seed = base_map.copy()
736
- orelse_map = self._visit_block(node.orelse, orelse_seed) if node.orelse else orelse_seed
737
- self.type_map = self._merge_branch_types(base_map, body_map, orelse_map)
738
-
739
- def _visit_block(self, nodes: Iterable[ast.stmt], seed: dict[str, str]) -> dict[str, str]:
740
- walker = _TypeInferencer(self.code)
741
- walker.type_map = seed
742
- for stmt in nodes:
743
- walker.visit(stmt)
744
- return walker.type_map
745
-
746
- def _merge_branch_types(self, base: dict[str, str], left: dict[str, str], right: dict[str, str]) -> dict[str, str]:
747
- merged = base.copy()
748
- for key in set(left) | set(right):
749
- l_val = left.get(key)
750
- r_val = right.get(key)
751
- if l_val and r_val and l_val == r_val:
752
- merged[key] = l_val
753
- elif key in base:
754
- merged[key] = base[key]
755
- elif l_val and not r_val:
756
- merged[key] = l_val
757
- elif r_val and not l_val:
758
- merged[key] = r_val
759
- elif key in merged:
760
- merged.pop(key, None)
761
- return merged
762
-
763
- def _bind_target(self, target: ast.AST, val_type: Optional[str], elem_type: Optional[str], source: ast.AST | None):
764
- if isinstance(target, ast.Name):
765
- if val_type:
766
- self.type_map[target.id] = val_type
767
- if elem_type:
768
- self.type_map[f"{target.id}.__element__"] = elem_type
769
- if source is not None:
770
- self._record_dict_key_types(target.id, source)
771
- elif isinstance(target, (ast.Tuple, ast.List)):
772
- for elt in target.elts:
773
- self._bind_target(elt, val_type, elem_type, source)
774
-
775
- def _existing_type(self, target: ast.AST) -> Optional[str]:
776
- if isinstance(target, ast.Name):
777
- return self.type_map.get(target.id)
778
- return None
779
-
780
- def _existing_element(self, target: ast.AST) -> Optional[str]:
781
- if isinstance(target, ast.Name):
782
- return self.type_map.get(f"{target.id}.__element__")
783
- return None
784
-
785
- def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
786
- if isinstance(value, ast.Call):
787
- if isinstance(value.func, ast.Name):
788
- if value.func.id in {"character", "combat"}:
789
- return value.func.id, None
790
- if value.func.id == "vroll":
791
- return "SimpleRollResult", None
792
- if value.func.id == "argparse":
793
- return "ParsedArguments", None
794
- if value.func.id == "range":
795
- return "range", "int"
796
- if value.func.id in {"list", "dict", "str"}:
797
- return value.func.id, None
798
- if isinstance(value.func, ast.Attribute):
799
- base_type, base_elem = self._value_type(value.func.value)
800
- if value.func.attr == "get" and value.args:
801
- key_literal = self._literal_key(value.args[0])
802
- val_type, elem_type = self._subscript_type(value.func.value, key_literal, base_type, base_elem)
803
- if val_type:
804
- return val_type, elem_type
805
- if base_elem:
806
- return base_elem, None
807
- if isinstance(value, ast.List):
808
- elem_type, _ = self._iterable_element_from_values(value.elts)
809
- return "list", elem_type
810
- if isinstance(value, ast.Tuple):
811
- elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
812
- return "tuple", elem_type
813
- if isinstance(value, ast.Set):
814
- elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
815
- return "set", elem_type
816
- if isinstance(value, ast.ListComp):
817
- comp_type, comp_elem = self._value_type(value.elt)
818
- return "list", comp_type or comp_elem
819
- if isinstance(value, ast.Dict):
820
- elem_type, _ = self._iterable_element_from_values(value.values or [])
821
- return "dict", elem_type
822
- if isinstance(value, ast.Subscript):
823
- return self._subscript_value_type(value)
824
- if isinstance(value, ast.Constant):
825
- if isinstance(value.value, str):
826
- return "str", None
827
- if isinstance(value, ast.Name):
828
- if value.id in self.type_map:
829
- return self.type_map[value.id], self.type_map.get(f"{value.id}.__element__")
830
- if value.id in {"character", "combat", "ctx"}:
831
- return value.id, None
832
- if isinstance(value, ast.Attribute):
833
- attr_name = value.attr
834
- base_type = None
835
- base_elem = None
836
- if isinstance(value.value, ast.Name):
837
- base_type = self.type_map.get(value.value.id)
838
- base_elem = self.type_map.get(f"{value.value.id}.__element__")
839
- if base_type is None:
840
- base_type, base_elem = self._value_type(value.value)
841
- if base_type:
842
- meta = _type_meta(base_type)
843
- attr_meta = meta.attrs.get(attr_name)
844
- if attr_meta:
845
- if attr_meta.type_name:
846
- return attr_meta.type_name, attr_meta.element_type or None
847
- if attr_meta.element_type:
848
- return base_type, attr_meta.element_type
849
- if base_elem:
850
- return base_elem, None
851
- if attr_name in TYPE_MAP:
852
- return attr_name, None
853
- return None, None
854
- if isinstance(value, ast.IfExp):
855
- t_type, t_elem = self._value_type(value.body)
856
- e_type, e_elem = self._value_type(value.orelse)
857
- if t_type and e_type and t_type == e_type:
858
- merged_elem = t_elem or e_elem
859
- if t_elem and e_elem and t_elem != e_elem:
860
- merged_elem = None
861
- return t_type, merged_elem
862
- return t_type or e_type, t_elem or e_elem
863
- return None, None
864
-
865
- def _iterable_element_from_values(self, values: Iterable[ast.AST]) -> tuple[Optional[str], Optional[str]]:
866
- elem_type: Optional[str] = None
867
- nested_elem: Optional[str] = None
868
- for node in values:
869
- val_type, inner_elem = self._value_type(node)
870
- if not val_type:
871
- return None, None
872
- if elem_type is None:
873
- elem_type = val_type
874
- nested_elem = inner_elem
875
- elif elem_type != val_type:
876
- return None, None
877
- if inner_elem:
878
- if nested_elem is None:
879
- nested_elem = inner_elem
880
- elif nested_elem != inner_elem:
881
- nested_elem = None
882
- return elem_type, nested_elem
883
-
884
- def _literal_key(self, node: ast.AST | None) -> str | int | None:
885
- if isinstance(node, ast.Constant):
886
- if isinstance(node.value, (str, int)):
887
- return node.value
888
- if hasattr(ast, "Index") and isinstance(node, getattr(ast, "Index")):
889
- return self._literal_key(getattr(node, "value", None))
890
- return None
891
-
892
- def _subscript_type(
893
- self,
894
- base_expr: ast.AST,
895
- key_literal: str | int | None,
896
- base_type: Optional[str],
897
- base_elem: Optional[str],
898
- ) -> tuple[Optional[str], Optional[str]]:
899
- base_name = base_expr.id if isinstance(base_expr, ast.Name) else None
900
- if base_name and key_literal is not None:
901
- dict_key = f"{base_name}.{key_literal}"
902
- if dict_key in self.type_map:
903
- return self.type_map[dict_key], self.type_map.get(f"{dict_key}.__element__")
904
- elem_hint = base_elem
905
- if base_name and not elem_hint:
906
- elem_hint = self.type_map.get(f"{base_name}.__element__")
907
- if base_type:
908
- meta = _type_meta(base_type)
909
- if key_literal is not None and key_literal in meta.attrs:
910
- attr_meta = meta.attrs[key_literal]
911
- if attr_meta.type_name:
912
- return attr_meta.type_name, attr_meta.element_type or None
913
- if attr_meta.element_type:
914
- return base_type, attr_meta.element_type
915
- elem_hint = elem_hint or meta.element_type
916
- if elem_hint:
917
- return elem_hint, None
918
- return base_type, None
919
-
920
- def _subscript_value_type(self, node: ast.Subscript) -> tuple[Optional[str], Optional[str]]:
921
- base_type, base_elem = self._value_type(node.value)
922
- key_literal = self._literal_key(getattr(node, "slice", None))
923
- return self._subscript_type(node.value, key_literal, base_type, base_elem)
924
-
925
- def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
926
- if not isinstance(value, ast.Dict):
927
- return
928
- for key_node, val_node in zip(value.keys or [], value.values or []):
929
- key_literal = self._literal_key(key_node)
930
- if key_literal is None:
931
- continue
932
- val_type, elem_type = self._value_type(val_node)
933
- if val_type:
934
- self.type_map[f"{var_name}.{key_literal}"] = val_type
935
- if elem_type:
936
- self.type_map[f"{var_name}.{key_literal}.__element__"] = elem_type
937
-
938
-
939
- def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
940
- mapping = type_map or _infer_type_map(code)
941
- get_match = DICT_GET_RE.match(receiver)
942
- if get_match:
943
- base, _, key = get_match.groups()
944
- dict_key = f"{base}.{key}"
945
- if dict_key in mapping:
946
- return mapping[dict_key]
947
- bracket = receiver.rfind("[")
948
- if bracket != -1 and receiver.endswith("]"):
949
- base_expr = receiver[:bracket]
950
- elem_hint = mapping.get(f"{base_expr}.__element__")
951
- if elem_hint:
952
- return elem_hint
953
- base_type = _resolve_type_name(base_expr, code, mapping)
954
- if base_type:
955
- base_meta = _type_meta(base_type)
956
- if base_meta.element_type:
957
- return base_meta.element_type
958
- return base_type
959
- receiver = receiver.rstrip("()")
960
- if "." in receiver:
961
- base_expr, attr_name = receiver.rsplit(".", 1)
962
- base_type = _resolve_type_name(base_expr, code, mapping)
963
- if base_type:
964
- meta = _type_meta(base_type)
965
- attr_key = attr_name.split("[", 1)[0]
966
- attr_meta = meta.attrs.get(attr_key)
967
- if attr_meta:
968
- if attr_meta.element_type:
969
- return attr_meta.element_type
970
- if attr_meta.type_name:
971
- return attr_meta.type_name
972
-
973
- if receiver in mapping:
974
- return mapping[receiver]
975
- elem_key = f"{receiver}.__element__"
976
- if elem_key in mapping:
977
- return mapping[elem_key]
978
- if receiver in TYPE_MAP:
979
- return receiver
980
- tail = receiver.split(".")[-1].split("[", 1)[0]
981
- if tail in TYPE_MAP:
982
- return tail
983
- return receiver
984
-
985
-
986
- def _type_meta(type_name: str) -> TypeMeta:
987
- return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}, element_type=""))
988
-
989
-
990
- @lru_cache()
991
- def _type_meta_map() -> Dict[str, TypeMeta]:
992
- meta: dict[str, TypeMeta] = {}
993
- reverse_type_map: dict[type, str] = {}
994
- for key, cls in TYPE_MAP.items():
995
- reverse_type_map[cls] = key
996
-
997
- def _iter_element_for_type_name(type_name: str) -> str:
998
- cls = TYPE_MAP.get(type_name)
999
- if not cls:
1000
- return ""
1001
- return _element_type_from_iterable(cls, reverse_type_map)
1002
-
1003
- def _getitem_element_for_type_name(type_name: str) -> str:
1004
- cls = TYPE_MAP.get(type_name)
1005
- if not cls:
1006
- return ""
1007
- return _element_type_from_getitem(cls, reverse_type_map)
1008
-
1009
- for type_name, cls in TYPE_MAP.items():
1010
- attrs: dict[str, AttrMeta] = {}
1011
- methods: dict[str, MethodMeta] = {}
1012
- iterable_element = _iter_element_for_type_name(type_name)
1013
- getitem_element = _getitem_element_for_type_name(type_name)
1014
- element_hint = iterable_element or getitem_element
1015
- override_docs = {
1016
- **_ATTR_DOC_OVERRIDES.get(type_name, {}),
1017
- **_ATTR_DOC_OVERRIDES.get(cls.__name__, {}),
1018
- }
1019
-
1020
- for attr in getattr(cls, "ATTRS", []):
1021
- doc = ""
1022
- type_name_hint = ""
1023
- element_type_hint = ""
1024
- try:
1025
- attr_obj = getattr(cls, attr)
1026
- except Exception:
1027
- attr_obj = None
1028
- if isinstance(attr_obj, property) and attr_obj.fget:
1029
- doc = (attr_obj.fget.__doc__ or "").strip()
1030
- ann = _return_annotation(attr_obj.fget, cls)
1031
- type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
1032
- elif attr_obj is not None:
1033
- doc = (getattr(attr_obj, "__doc__", "") or "").strip()
1034
- if not type_name_hint and not element_type_hint:
1035
- ann = _class_annotation(cls, attr)
1036
- type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
1037
- if not type_name_hint and element_hint:
1038
- type_name_hint = element_hint
1039
- if type_name_hint and not element_type_hint:
1040
- element_type_hint = _iter_element_for_type_name(type_name_hint)
1041
- if not doc:
1042
- doc = override_docs.get(attr, doc)
1043
- attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
1044
-
1045
- for meth in getattr(cls, "METHODS", []):
1046
- doc = ""
1047
- sig_label = ""
1048
- try:
1049
- meth_obj = getattr(cls, meth)
1050
- except Exception:
1051
- meth_obj = None
1052
- if callable(meth_obj):
1053
- sig_label = _format_method_signature(meth, meth_obj)
1054
- doc = (meth_obj.__doc__ or "").strip()
1055
- methods[meth] = MethodMeta(signature=sig_label, doc=doc)
1056
-
1057
- meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=element_hint)
1058
- return meta
1059
-
1060
-
1061
- def _format_method_signature(name: str, obj: Any) -> str:
1062
- try:
1063
- sig = inspect.signature(obj)
1064
- except (TypeError, ValueError):
1065
- return f"{name}()"
1066
- params = list(sig.parameters.values())
1067
- if params and params[0].name in {"self", "cls"}:
1068
- params = params[1:]
1069
- sig = sig.replace(parameters=params)
1070
- return f"{name}{sig}"
1071
-
1072
-
1073
- def _return_annotation(func: Any, cls: type) -> Any:
1074
- try:
1075
- module = inspect.getmodule(func) or inspect.getmodule(cls)
1076
- globalns = module.__dict__ if module else None
1077
- hints = typing.get_type_hints(func, globalns=globalns, include_extras=False)
1078
- return hints.get("return")
1079
- except Exception:
1080
- return getattr(func, "__annotations__", {}).get("return")
1081
-
1082
-
1083
- def _class_annotation(cls: type, attr: str) -> Any:
1084
- try:
1085
- module = inspect.getmodule(cls)
1086
- globalns = module.__dict__ if module else None
1087
- hints = typing.get_type_hints(cls, globalns=globalns, include_extras=False)
1088
- if attr in hints:
1089
- return hints[attr]
1090
- except Exception:
1091
- pass
1092
- return getattr(getattr(cls, "__annotations__", {}), "get", lambda _k: None)(attr)
1093
-
1094
-
1095
- def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) -> tuple[str, str]:
1096
- if ann is None:
1097
- return "", ""
1098
- if isinstance(ann, str):
1099
- return "", ""
1100
- try:
1101
- origin = getattr(ann, "__origin__", None)
1102
- except Exception:
1103
- origin = None
1104
- args = getattr(ann, "__args__", ()) if origin else ()
1105
-
1106
- if ann in reverse_type_map:
1107
- return reverse_type_map[ann], ""
1108
-
1109
- # handle list/sequence typing to detect element type
1110
- iterable_origins = {list, List, Iterable, typing.Sequence, typing.Iterable}
1111
- try:
1112
- from collections.abc import Iterable as ABCIterable, Sequence as ABCSequence
1113
- iterable_origins.update({ABCIterable, ABCSequence})
1114
- except Exception:
1115
- pass
1116
- if origin in iterable_origins:
1117
- if args:
1118
- elem = args[0]
1119
- elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
1120
- container_name = reverse_type_map.get(origin) or "list"
1121
- return container_name, elem_name
1122
- return reverse_type_map.get(origin) or "list", ""
1123
-
1124
- if isinstance(ann, type) and ann in reverse_type_map:
1125
- return reverse_type_map[ann], ""
1126
- return "", ""
1127
-
1128
-
1129
- def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) -> str:
1130
- try:
1131
- hints = typing.get_type_hints(cls.__iter__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
1132
- ret_ann = hints.get("return")
1133
- _, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
1134
- return elem
1135
- except Exception:
1136
- return ""
1137
-
1138
-
1139
- def _element_type_from_getitem(cls: type, reverse_type_map: Dict[type, str]) -> str:
1140
- try:
1141
- hints = typing.get_type_hints(cls.__getitem__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
1142
- ret_ann = hints.get("return")
1143
- name, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
1144
- return name or elem
1145
- except Exception:
1146
- return ""
1147
-
1148
-
1149
- def _attribute_at_position(line_text: str, cursor: int) -> tuple[Optional[str], Optional[str]]:
1150
- cursor = max(0, min(cursor, len(line_text)))
1151
- for match in ATTR_AT_CURSOR_RE.finditer(line_text):
1152
- start, end = match.span(2)
1153
- if start <= cursor <= end:
1154
- return match.group(1), match.group(2)
1155
- return None, None
1156
-
1157
-
1158
- def _infer_constant_bindings(code: str, upto_line: int | None, ctx_data: ContextData) -> Dict[str, Any]:
1159
- try:
1160
- tree = ast.parse(code)
1161
- except SyntaxError:
1162
- return {}
1163
- bindings: dict[str, Any] = {}
1164
- limit = None if upto_line is None else upto_line + 1
1165
-
1166
- def _value_for(node: ast.AST) -> Any | None:
1167
- value = _literal_value(node)
1168
- if value is None:
1169
- value = _evaluated_value(node, ctx_data, bindings)
1170
- return value
1171
-
1172
- def _loop_binding(node: ast.AST) -> Any | None:
1173
- value = _value_for(node)
1174
- if value is None:
1175
- return _LoopVarBinding()
1176
- try:
1177
- iterator = iter(value)
1178
- except TypeError:
1179
- return _LoopVarBinding()
1180
- try:
1181
- return next(iterator)
1182
- except StopIteration:
1183
- return _LoopVarBinding()
1184
-
1185
- class Visitor(ast.NodeVisitor):
1186
- def visit_Assign(self, node: ast.Assign):
1187
- if limit is not None and node.lineno > limit:
1188
- return
1189
- value = _value_for(node.value)
1190
- if value is None:
1191
- self.generic_visit(node)
1192
- return
1193
- for name in _names_from_target(node.targets):
1194
- bindings[name] = value
1195
-
1196
- def visit_AnnAssign(self, node: ast.AnnAssign):
1197
- if limit is not None and node.lineno > limit:
1198
- return
1199
- if node.value is None:
1200
- return
1201
- value = _value_for(node.value)
1202
- if value is None:
1203
- self.generic_visit(node)
1204
- return
1205
- for name in _names_from_target([node.target]):
1206
- bindings[name] = value
1207
-
1208
- def visit_For(self, node: ast.For):
1209
- if limit is not None and node.lineno > limit:
1210
- return
1211
- loop_val = _loop_binding(node.iter)
1212
- for name in _names_from_target([node.target]):
1213
- bindings[name] = loop_val
1214
- self.generic_visit(node)
1215
-
1216
- def visit_AsyncFor(self, node: ast.AsyncFor):
1217
- if limit is not None and node.lineno > limit:
1218
- return
1219
- loop_val = _loop_binding(node.iter)
1220
- for name in _names_from_target([node.target]):
1221
- bindings[name] = loop_val
1222
- self.generic_visit(node)
1223
-
1224
- Visitor().visit(tree)
1225
- return bindings
1226
-
1227
-
1228
- def _names_from_target(targets: Iterable[ast.expr]) -> List[str]:
1229
- names: list[str] = []
1230
- for target in targets:
1231
- if isinstance(target, ast.Name):
1232
- names.append(target.id)
1233
- elif isinstance(target, (ast.Tuple, ast.List)):
1234
- names.extend(_names_from_target(target.elts))
1235
- return names
1236
-
1237
-
1238
- def _literal_value(node: ast.AST) -> Any | None:
1239
- if isinstance(node, ast.Constant):
1240
- return node.value
1241
- if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
1242
- val = _literal_value(node.operand)
1243
- if isinstance(val, (int, float, complex)):
1244
- return val if isinstance(node.op, ast.UAdd) else -val
1245
- return None
1246
- if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
1247
- items = []
1248
- for elt in node.elts:
1249
- val = _literal_value(elt)
1250
- if val is None:
1251
- return None
1252
- items.append(val)
1253
- if isinstance(node, ast.List):
1254
- return items
1255
- if isinstance(node, ast.Tuple):
1256
- return tuple(items)
1257
- return set(items)
1258
- if isinstance(node, ast.Dict):
1259
- keys = []
1260
- values = []
1261
- for k, v in zip(node.keys, node.values):
1262
- key_val = _literal_value(k) if k is not None else None
1263
- val_val = _literal_value(v)
1264
- if key_val is None or val_val is None:
1265
- return None
1266
- keys.append(key_val)
1267
- values.append(val_val)
1268
- return dict(zip(keys, values))
1269
- return None
1270
-
1271
-
1272
- def _evaluated_value(node: ast.AST, ctx_data: ContextData, bindings: Dict[str, Any] | None = None) -> Any | None:
1273
- bindings = bindings or {}
1274
- try:
1275
- return _eval_node(node, ctx_data, bindings)
1276
- except Exception:
1277
- return None
1278
-
1279
-
1280
- def _eval_node(node: ast.AST, ctx_data: ContextData, bindings: Dict[str, Any]) -> Any | None:
1281
- if isinstance(node, ast.Attribute):
1282
- base = _eval_node(node.value, ctx_data, bindings)
1283
- if base is None:
1284
- return None
1285
- return getattr(base, node.attr, None)
1286
- if isinstance(node, ast.Call):
1287
- if isinstance(node.func, ast.Name):
1288
- if node.func.id == "character":
1289
- return CharacterAPI(ctx_data.character)
1290
- if node.func.id == "combat":
1291
- return SimpleCombat(ctx_data.combat)
1292
- if node.func.id == "range":
1293
- args = []
1294
- for arg in node.args:
1295
- val = _literal_value(arg)
1296
- if val is None:
1297
- return None
1298
- args.append(val)
1299
- try:
1300
- return range(*args)
1301
- except Exception:
1302
- return None
1303
- if isinstance(node.func, ast.Attribute):
1304
- base = _eval_node(node.func.value, ctx_data, bindings)
1305
- if base is None:
1306
- return None
1307
- method_name = node.func.attr
1308
- if not _is_safe_call(base, method_name):
1309
- return None
1310
- args = []
1311
- for arg in node.args:
1312
- val = _literal_value(arg)
1313
- if val is None:
1314
- val = _eval_node(arg, ctx_data, bindings)
1315
- if val is None:
1316
- return None
1317
- args.append(val)
1318
- kwargs = {}
1319
- for kw in node.keywords:
1320
- if kw.arg is None:
1321
- return None
1322
- val = _literal_value(kw.value)
1323
- if val is None:
1324
- val = _eval_node(kw.value, ctx_data, bindings)
1325
- if val is None:
1326
- return None
1327
- kwargs[kw.arg] = val
1328
- callee = getattr(base, method_name, None)
1329
- if not callable(callee):
1330
- return None
1331
- try:
1332
- return callee(*args, **kwargs)
1333
- except Exception:
1334
- return None
1335
- if isinstance(node, ast.Name):
1336
- if node.id in bindings:
1337
- return bindings[node.id]
1338
- if node.id == "ctx":
1339
- return AliasContextAPI(ctx_data.ctx)
1340
- return None
1341
-
1342
-
1343
- def _is_safe_call(base: Any, method: str) -> bool:
1344
- for cls, allowed in SAFE_METHODS.items():
1345
- if isinstance(base, cls) and method in allowed:
1346
- return True
1347
- return False
1348
-
1349
-
1350
- def _format_binding_hover(name: str, value: Any, label: str) -> types.Hover:
1351
- type_name = _describe_type(value)
1352
- preview = _preview_value(value)
1353
- contents = f"**{label}** `{name}`\n\nType: `{type_name}`\nValue: `{preview}`"
1354
- return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
1355
-
1356
-
1357
- def _describe_type(value: Any) -> str:
1358
- # Provide light element-type hints for common iterables so hover shows list[Foo].
1359
- def _iterable_type(iterable: Iterable[Any], container: str) -> str:
1360
- try:
1361
- seen = {type(item).__name__ for item in iterable if item is not None}
1362
- except Exception:
1363
- return container
1364
- return f"{container}[{seen.pop()}]" if len(seen) == 1 else container
1365
-
1366
- try:
1367
- if isinstance(value, list):
1368
- return _iterable_type(value, "list")
1369
- if isinstance(value, tuple):
1370
- return _iterable_type(value, "tuple")
1371
- if isinstance(value, set):
1372
- return _iterable_type(value, "set")
1373
- except Exception:
1374
- pass
1375
- return type(value).__name__
1376
-
1377
-
1378
- def _preview_value(value: Any) -> str:
1379
- text = repr(value)
1380
- if len(text) > 120:
1381
- text = text[:117] + "..."
1382
- return text
1383
-
1384
-
1385
- class _LoopVarBinding:
1386
- def __repr__(self) -> str:
1387
- return "<loop item>"
1388
- SAFE_METHODS: dict[type, set[str]] = {
1389
- AliasLevels: {"get"},
1390
- dict: {"get"},
1391
- }