avrae-ls 0.6.4__py3-none-any.whl → 0.7.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/__main__.py +42 -5
- avrae_ls/alias_preview.py +38 -6
- avrae_ls/alias_tests.py +2 -0
- avrae_ls/ast_utils.py +14 -0
- avrae_ls/code_actions.py +6 -2
- avrae_ls/completions.py +191 -1156
- avrae_ls/config.py +1 -0
- avrae_ls/context.py +62 -32
- avrae_ls/diagnostics.py +33 -60
- avrae_ls/lsp_utils.py +41 -0
- avrae_ls/parser.py +30 -3
- avrae_ls/server.py +85 -47
- avrae_ls/source_context.py +30 -0
- avrae_ls/symbols.py +27 -60
- avrae_ls/type_inference.py +470 -0
- avrae_ls/type_system.py +729 -0
- {avrae_ls-0.6.4.dist-info → avrae_ls-0.7.0.dist-info}/METADATA +6 -1
- avrae_ls-0.7.0.dist-info/RECORD +39 -0
- avrae_ls-0.6.4.dist-info/RECORD +0 -34
- {avrae_ls-0.6.4.dist-info → avrae_ls-0.7.0.dist-info}/WHEEL +0 -0
- {avrae_ls-0.6.4.dist-info → avrae_ls-0.7.0.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.6.4.dist-info → avrae_ls-0.7.0.dist-info}/licenses/LICENSE +0 -0
avrae_ls/completions.py
CHANGED
|
@@ -1,223 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
-
import inspect
|
|
5
4
|
import re
|
|
6
|
-
import typing
|
|
7
5
|
from dataclasses import dataclass
|
|
8
|
-
from
|
|
9
|
-
from html import unescape
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any, Callable, ClassVar, Dict, Iterable, List, Optional
|
|
6
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
12
7
|
|
|
13
8
|
from lsprotocol import types
|
|
14
9
|
|
|
15
10
|
from .context import ContextData, GVarResolver
|
|
16
|
-
from .argparser import ParsedArguments
|
|
17
11
|
from .runtime import _default_builtins
|
|
18
|
-
from .api import
|
|
19
|
-
AliasAction,
|
|
20
|
-
AliasBaseStats,
|
|
21
|
-
AliasCoinpurse,
|
|
22
|
-
AliasContextAPI,
|
|
23
|
-
AliasCustomCounter,
|
|
24
|
-
AliasDeathSaves,
|
|
25
|
-
AliasResistances,
|
|
26
|
-
AliasSaves,
|
|
27
|
-
AliasSkill,
|
|
28
|
-
AliasSkills,
|
|
29
|
-
AliasSpellbook,
|
|
30
|
-
AliasSpellbookSpell,
|
|
31
|
-
AliasAttack,
|
|
32
|
-
AliasAttackList,
|
|
33
|
-
AliasLevels,
|
|
34
|
-
CategoryAPI,
|
|
35
|
-
ChannelAPI,
|
|
36
|
-
CharacterAPI,
|
|
37
|
-
SimpleCombat,
|
|
38
|
-
SimpleCombatant,
|
|
39
|
-
GuildAPI,
|
|
40
|
-
RoleAPI,
|
|
41
|
-
AuthorAPI,
|
|
42
|
-
SimpleEffect,
|
|
43
|
-
SimpleGroup,
|
|
44
|
-
SimpleRollResult,
|
|
45
|
-
)
|
|
12
|
+
from .api import AliasContextAPI, CharacterAPI, SimpleCombat
|
|
46
13
|
from .signature_help import FunctionSig
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ATTRS: ClassVar[list[str]] = []
|
|
51
|
-
METHODS: ClassVar[list[str]] = ["append", "extend", "insert", "remove", "pop", "clear", "index", "count", "sort", "reverse", "copy"]
|
|
52
|
-
|
|
53
|
-
def __iter__(self) -> Iterable[Any]:
|
|
54
|
-
return iter([])
|
|
55
|
-
|
|
56
|
-
def append(self, value: Any) -> None: ...
|
|
57
|
-
def extend(self, iterable: Iterable[Any]) -> None: ...
|
|
58
|
-
def insert(self, index: int, value: Any) -> None: ...
|
|
59
|
-
def remove(self, value: Any) -> None: ...
|
|
60
|
-
def pop(self, index: int = -1) -> Any: ...
|
|
61
|
-
def clear(self) -> None: ...
|
|
62
|
-
def index(self, value: Any, start: int = 0, stop: int | None = None) -> int: ...
|
|
63
|
-
def count(self, value: Any) -> int: ...
|
|
64
|
-
def sort(self, *, key=None, reverse: bool = False) -> None: ...
|
|
65
|
-
def reverse(self) -> None: ...
|
|
66
|
-
def copy(self) -> list[Any]: ...
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class _BuiltinDict:
|
|
70
|
-
ATTRS: ClassVar[list[str]] = []
|
|
71
|
-
METHODS: ClassVar[list[str]] = ["get", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "clear", "copy"]
|
|
72
|
-
|
|
73
|
-
def __iter__(self) -> Iterable[Any]:
|
|
74
|
-
return iter({})
|
|
75
|
-
|
|
76
|
-
def get(self, key: Any, default: Any = None) -> Any: ...
|
|
77
|
-
def keys(self) -> Any: ...
|
|
78
|
-
def values(self) -> Any: ...
|
|
79
|
-
def items(self) -> Any: ...
|
|
80
|
-
def pop(self, key: Any, default: Any = None) -> Any: ...
|
|
81
|
-
def popitem(self) -> tuple[Any, Any]: ...
|
|
82
|
-
def update(self, *args, **kwargs) -> None: ...
|
|
83
|
-
def setdefault(self, key: Any, default: Any = None) -> Any: ...
|
|
84
|
-
def clear(self) -> None: ...
|
|
85
|
-
def copy(self) -> dict[Any, Any]: ...
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class _BuiltinStr:
|
|
89
|
-
ATTRS: ClassVar[list[str]] = []
|
|
90
|
-
METHODS: ClassVar[list[str]] = [
|
|
91
|
-
"lower",
|
|
92
|
-
"upper",
|
|
93
|
-
"title",
|
|
94
|
-
"split",
|
|
95
|
-
"join",
|
|
96
|
-
"replace",
|
|
97
|
-
"strip",
|
|
98
|
-
"startswith",
|
|
99
|
-
"endswith",
|
|
100
|
-
"format",
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
def __iter__(self) -> Iterable[str]:
|
|
104
|
-
return iter("")
|
|
105
|
-
|
|
106
|
-
def lower(self) -> str: ...
|
|
107
|
-
def upper(self) -> str: ...
|
|
108
|
-
def title(self) -> str: ...
|
|
109
|
-
def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...
|
|
110
|
-
def join(self, iterable: Iterable[str]) -> str: ...
|
|
111
|
-
def replace(self, old: str, new: str, count: int = -1) -> str: ...
|
|
112
|
-
def strip(self, chars: str | None = None) -> str: ...
|
|
113
|
-
def startswith(self, prefix, start: int = 0, end: int | None = None) -> bool: ...
|
|
114
|
-
def endswith(self, suffix, start: int = 0, end: int | None = None) -> bool: ...
|
|
115
|
-
def format(self, *args, **kwargs) -> str: ...
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
TypeResolver = Callable[[str | None], str | None]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
@dataclass(frozen=True)
|
|
122
|
-
class TypeEntry:
|
|
123
|
-
cls: type
|
|
124
|
-
resolver: TypeResolver | None = None
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@dataclass(frozen=True)
|
|
128
|
-
class TypeSpec:
|
|
129
|
-
name: str
|
|
130
|
-
cls: type
|
|
131
|
-
parents: tuple[str, ...] = ()
|
|
132
|
-
safe_methods: tuple[str, ...] = ()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _allow_from(type_key: str, *receiver_types: str) -> TypeResolver:
|
|
136
|
-
allowed = set(receiver_types)
|
|
137
|
-
|
|
138
|
-
def _resolver(receiver_type: str | None) -> str | None:
|
|
139
|
-
if receiver_type in allowed:
|
|
140
|
-
return type_key
|
|
141
|
-
return None
|
|
142
|
-
|
|
143
|
-
return _resolver
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
TYPE_SPECS: list[TypeSpec] = [
|
|
147
|
-
TypeSpec("character", CharacterAPI, safe_methods=("get_cvar", "get_cc")),
|
|
148
|
-
TypeSpec("combat", SimpleCombat, safe_methods=("get_combatant", "get_group", "get_metadata")),
|
|
149
|
-
TypeSpec("SimpleCombat", SimpleCombat, safe_methods=("get_combatant", "get_group", "get_metadata")),
|
|
150
|
-
TypeSpec("ctx", AliasContextAPI),
|
|
151
|
-
TypeSpec("SimpleRollResult", SimpleRollResult),
|
|
152
|
-
TypeSpec("stats", AliasBaseStats),
|
|
153
|
-
TypeSpec("levels", AliasLevels, parents=("character",), safe_methods=("get",)),
|
|
154
|
-
TypeSpec("attacks", AliasAttackList, parents=("character",)),
|
|
155
|
-
TypeSpec("attack", AliasAttack, parents=("attacks", "actions")),
|
|
156
|
-
TypeSpec("skills", AliasSkills, parents=("character",)),
|
|
157
|
-
TypeSpec("AliasSkills", AliasSkills, parents=("character",)),
|
|
158
|
-
TypeSpec("skill", AliasSkill, parents=("skills",)),
|
|
159
|
-
TypeSpec("AliasSkill", AliasSkill, parents=("skills",)),
|
|
160
|
-
TypeSpec("saves", AliasSaves, parents=("character",), safe_methods=("get",)),
|
|
161
|
-
TypeSpec("resistances", AliasResistances, parents=("character",), safe_methods=("is_resistant", "is_immune", "is_vulnerable", "is_neutral")),
|
|
162
|
-
TypeSpec("coinpurse", AliasCoinpurse, parents=("character",), safe_methods=("get_coins",)),
|
|
163
|
-
TypeSpec("custom_counter", AliasCustomCounter, parents=("character",)),
|
|
164
|
-
TypeSpec("consumable", AliasCustomCounter, parents=("character",)),
|
|
165
|
-
TypeSpec("death_saves", AliasDeathSaves, parents=("character",), safe_methods=("is_stable", "is_dead")),
|
|
166
|
-
TypeSpec("action", AliasAction, parents=("actions", "character")),
|
|
167
|
-
TypeSpec("spellbook", AliasSpellbook, parents=("character",), safe_methods=("find", "get_slots", "get_max_slots", "remaining_casts_of", "can_cast")),
|
|
168
|
-
TypeSpec("spell", AliasSpellbookSpell, parents=("spellbook",)),
|
|
169
|
-
TypeSpec("guild", GuildAPI, parents=("ctx",)),
|
|
170
|
-
TypeSpec("channel", ChannelAPI, parents=("ctx",)),
|
|
171
|
-
TypeSpec("category", CategoryAPI, parents=("channel",)),
|
|
172
|
-
TypeSpec("author", AuthorAPI, parents=("ctx",)),
|
|
173
|
-
TypeSpec("role", RoleAPI, parents=("author",)),
|
|
174
|
-
TypeSpec("combatant", SimpleCombatant, parents=("combat", "SimpleCombat", "group", "SimpleGroup"), safe_methods=("get_effect",)),
|
|
175
|
-
TypeSpec("SimpleCombatant", SimpleCombatant, parents=("combat", "SimpleCombat", "group", "SimpleGroup"), safe_methods=("get_effect",)),
|
|
176
|
-
TypeSpec("group", SimpleGroup, parents=("combat", "SimpleCombat"), safe_methods=("get_combatant",)),
|
|
177
|
-
TypeSpec("SimpleGroup", SimpleGroup, parents=("combat", "SimpleCombat"), safe_methods=("get_combatant",)),
|
|
178
|
-
TypeSpec("effect", SimpleEffect, parents=("combatant", "SimpleCombatant")),
|
|
179
|
-
TypeSpec("SimpleEffect", SimpleEffect, parents=("combatant", "SimpleCombatant")),
|
|
180
|
-
TypeSpec("list", _BuiltinList),
|
|
181
|
-
TypeSpec("int", int),
|
|
182
|
-
TypeSpec("float", float),
|
|
183
|
-
TypeSpec("dict", _BuiltinDict, safe_methods=("get",)),
|
|
184
|
-
TypeSpec("str", _BuiltinStr),
|
|
185
|
-
TypeSpec("ParsedArguments", ParsedArguments),
|
|
186
|
-
]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def _build_type_maps(specs: list[TypeSpec]) -> tuple[Dict[str, TypeEntry], dict[type, set[str]]]:
|
|
190
|
-
type_map: dict[str, TypeEntry] = {}
|
|
191
|
-
safe_methods: dict[type, set[str]] = {}
|
|
192
|
-
for spec in specs:
|
|
193
|
-
resolver = _allow_from(spec.name, *spec.parents) if spec.parents else None
|
|
194
|
-
type_map[spec.name] = TypeEntry(spec.cls, resolver=resolver)
|
|
195
|
-
if spec.safe_methods:
|
|
196
|
-
safe_methods.setdefault(spec.cls, set()).update(spec.safe_methods)
|
|
197
|
-
return type_map, safe_methods
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
TYPE_MAP, SAFE_METHODS = _build_type_maps(TYPE_SPECS)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def _resolve_type_key(type_key: str, receiver_type: str | None = None) -> str | None:
|
|
204
|
-
entry = TYPE_MAP.get(type_key)
|
|
205
|
-
if not entry:
|
|
206
|
-
return None
|
|
207
|
-
return entry.resolver(receiver_type) if entry.resolver else type_key
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _type_cls(type_key: str) -> type | None:
|
|
211
|
-
entry = TYPE_MAP.get(type_key)
|
|
212
|
-
if not entry:
|
|
213
|
-
return None
|
|
214
|
-
return entry.cls
|
|
14
|
+
from .type_inference import annotation_label, infer_type_map, resolve_type_name
|
|
15
|
+
from .type_system import display_type_label, is_safe_call, type_meta
|
|
16
|
+
from .ast_utils import collect_target_names
|
|
215
17
|
|
|
216
18
|
|
|
217
19
|
IDENT_RE = re.compile(r"[A-Za-z_]\w*$")
|
|
218
|
-
ATTR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.(?:([A-Za-z_]\w*)\s*)?$")
|
|
219
|
-
DICT_GET_RE = re.compile(r"^([A-Za-z_]\w*)\.get\(\s*(['\"])(.+?)\2")
|
|
220
|
-
ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.([A-Za-z_]\w*)")
|
|
221
20
|
|
|
222
21
|
|
|
223
22
|
@dataclass
|
|
@@ -228,297 +27,11 @@ class Suggestion:
|
|
|
228
27
|
documentation: str = ""
|
|
229
28
|
|
|
230
29
|
|
|
231
|
-
@dataclass
|
|
232
|
-
class
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
@dataclass
|
|
239
|
-
class MethodMeta:
|
|
240
|
-
signature: str = ""
|
|
241
|
-
doc: str = ""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
@dataclass
|
|
245
|
-
class TypeMeta:
|
|
246
|
-
attrs: Dict[str, AttrMeta]
|
|
247
|
-
methods: Dict[str, MethodMeta]
|
|
248
|
-
element_type: str = ""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
_SKILL_DOCS: dict[str, str] = {
|
|
252
|
-
"acrobatics": "Acrobatics skill bonus.",
|
|
253
|
-
"animalHandling": "Animal Handling skill bonus.",
|
|
254
|
-
"arcana": "Arcana skill bonus.",
|
|
255
|
-
"athletics": "Athletics skill bonus.",
|
|
256
|
-
"deception": "Deception skill bonus.",
|
|
257
|
-
"history": "History skill bonus.",
|
|
258
|
-
"initiative": "Initiative modifier.",
|
|
259
|
-
"insight": "Insight skill bonus.",
|
|
260
|
-
"intimidation": "Intimidation skill bonus.",
|
|
261
|
-
"investigation": "Investigation skill bonus.",
|
|
262
|
-
"medicine": "Medicine skill bonus.",
|
|
263
|
-
"nature": "Nature skill bonus.",
|
|
264
|
-
"perception": "Perception skill bonus.",
|
|
265
|
-
"performance": "Performance skill bonus.",
|
|
266
|
-
"persuasion": "Persuasion skill bonus.",
|
|
267
|
-
"religion": "Religion skill bonus.",
|
|
268
|
-
"sleightOfHand": "Sleight of Hand skill bonus.",
|
|
269
|
-
"stealth": "Stealth skill bonus.",
|
|
270
|
-
"survival": "Survival skill bonus.",
|
|
271
|
-
"strength": "Strength ability score for this skill block.",
|
|
272
|
-
"dexterity": "Dexterity ability score for this skill block.",
|
|
273
|
-
"constitution": "Constitution ability score for this skill block.",
|
|
274
|
-
"intelligence": "Intelligence ability score for this skill block.",
|
|
275
|
-
"wisdom": "Wisdom ability score for this skill block.",
|
|
276
|
-
"charisma": "Charisma ability score for this skill block.",
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
_COUNTER_DOCS: dict[str, str] = {
|
|
280
|
-
"name": "Internal name of the counter.",
|
|
281
|
-
"title": "Display title for the counter.",
|
|
282
|
-
"desc": "Description text for the counter.",
|
|
283
|
-
"value": "Current counter value.",
|
|
284
|
-
"max": "Maximum value for the counter.",
|
|
285
|
-
"min": "Minimum value for the counter.",
|
|
286
|
-
"reset_on": "Reset cadence for the counter (e.g., long/short rest).",
|
|
287
|
-
"display_type": "Display style for the counter.",
|
|
288
|
-
"reset_to": "Value to reset the counter to.",
|
|
289
|
-
"reset_by": "Increment applied when the counter resets.",
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
_EFFECT_DOCS: dict[str, str] = {
|
|
293
|
-
"name": "Effect name.",
|
|
294
|
-
"duration": "Configured duration for the effect.",
|
|
295
|
-
"remaining": "Remaining duration for the effect.",
|
|
296
|
-
"effect": "Raw effect payload.",
|
|
297
|
-
"attacks": "Attack data attached to the effect, if any.",
|
|
298
|
-
"buttons": "Buttons provided by the effect.",
|
|
299
|
-
"conc": "Whether the effect requires concentration.",
|
|
300
|
-
"desc": "Effect description text.",
|
|
301
|
-
"ticks_on_end": "Whether the effect ticks when it ends.",
|
|
302
|
-
"combatant_name": "Name of the owning combatant.",
|
|
303
|
-
"parent": "Parent effect, if nested.",
|
|
304
|
-
"children": "Child effects nested under this effect.",
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
_ATTR_DOC_OVERRIDES: dict[str, dict[str, str]] = {
|
|
308
|
-
"SimpleRollResult": {
|
|
309
|
-
"dice": "Markdown representation of the dice that were rolled.",
|
|
310
|
-
"total": "Numeric total of the resolved roll.",
|
|
311
|
-
"full": "Rendered roll result string.",
|
|
312
|
-
"result": "Underlying d20 RollResult object.",
|
|
313
|
-
"raw": "Original d20 expression for the roll.",
|
|
314
|
-
},
|
|
315
|
-
"stats": {
|
|
316
|
-
"prof_bonus": "Proficiency bonus for the character.",
|
|
317
|
-
"strength": "Strength ability score.",
|
|
318
|
-
"dexterity": "Dexterity ability score.",
|
|
319
|
-
"constitution": "Constitution ability score.",
|
|
320
|
-
"intelligence": "Intelligence ability score.",
|
|
321
|
-
"wisdom": "Wisdom ability score.",
|
|
322
|
-
"charisma": "Charisma ability score.",
|
|
323
|
-
},
|
|
324
|
-
"AliasBaseStats": {
|
|
325
|
-
"prof_bonus": "Proficiency bonus for the character.",
|
|
326
|
-
"strength": "Strength ability score.",
|
|
327
|
-
"dexterity": "Dexterity ability score.",
|
|
328
|
-
"constitution": "Constitution ability score.",
|
|
329
|
-
"intelligence": "Intelligence ability score.",
|
|
330
|
-
"wisdom": "Wisdom ability score.",
|
|
331
|
-
"charisma": "Charisma ability score.",
|
|
332
|
-
},
|
|
333
|
-
"levels": {
|
|
334
|
-
"total_level": "Sum of all class levels.",
|
|
335
|
-
},
|
|
336
|
-
"AliasLevels": {
|
|
337
|
-
"total_level": "Sum of all class levels.",
|
|
338
|
-
},
|
|
339
|
-
"attack": {
|
|
340
|
-
"name": "Attack name.",
|
|
341
|
-
"verb": "Attack verb or action phrase.",
|
|
342
|
-
"proper": "Whether the attack name is treated as proper.",
|
|
343
|
-
"activation_type": "Activation type identifier for this attack.",
|
|
344
|
-
"raw": "Raw attack payload from the statblock.",
|
|
345
|
-
},
|
|
346
|
-
"AliasAttack": {
|
|
347
|
-
"name": "Attack name.",
|
|
348
|
-
"verb": "Attack verb or action phrase.",
|
|
349
|
-
"proper": "Whether the attack name is treated as proper.",
|
|
350
|
-
"activation_type": "Activation type identifier for this attack.",
|
|
351
|
-
"raw": "Raw attack payload from the statblock.",
|
|
352
|
-
},
|
|
353
|
-
"skills": _SKILL_DOCS,
|
|
354
|
-
"AliasSkills": _SKILL_DOCS,
|
|
355
|
-
"skill": {
|
|
356
|
-
"value": "Total modifier for the skill.",
|
|
357
|
-
"prof": "Proficiency value applied to the skill.",
|
|
358
|
-
"bonus": "Base bonus before rolling.",
|
|
359
|
-
"adv": "Advantage state for the skill roll (True/False/None).",
|
|
360
|
-
},
|
|
361
|
-
"AliasSkill": {
|
|
362
|
-
"value": "Total modifier for the skill.",
|
|
363
|
-
"prof": "Proficiency value applied to the skill.",
|
|
364
|
-
"bonus": "Base bonus before rolling.",
|
|
365
|
-
"adv": "Advantage state for the skill roll (True/False/None).",
|
|
366
|
-
},
|
|
367
|
-
"resistances": {
|
|
368
|
-
"resist": "Damage types resisted.",
|
|
369
|
-
"vuln": "Damage types this target is vulnerable to.",
|
|
370
|
-
"immune": "Damage types the target is immune to.",
|
|
371
|
-
"neutral": "Damage types with no modifiers.",
|
|
372
|
-
},
|
|
373
|
-
"AliasResistances": {
|
|
374
|
-
"resist": "Damage types resisted.",
|
|
375
|
-
"vuln": "Damage types this target is vulnerable to.",
|
|
376
|
-
"immune": "Damage types the target is immune to.",
|
|
377
|
-
"neutral": "Damage types with no modifiers.",
|
|
378
|
-
},
|
|
379
|
-
"coinpurse": {
|
|
380
|
-
"pp": "Platinum pieces carried.",
|
|
381
|
-
"gp": "Gold pieces carried.",
|
|
382
|
-
"ep": "Electrum pieces carried.",
|
|
383
|
-
"sp": "Silver pieces carried.",
|
|
384
|
-
"cp": "Copper pieces carried.",
|
|
385
|
-
"total": "Total value of all coins.",
|
|
386
|
-
},
|
|
387
|
-
"AliasCoinpurse": {
|
|
388
|
-
"pp": "Platinum pieces carried.",
|
|
389
|
-
"gp": "Gold pieces carried.",
|
|
390
|
-
"ep": "Electrum pieces carried.",
|
|
391
|
-
"sp": "Silver pieces carried.",
|
|
392
|
-
"cp": "Copper pieces carried.",
|
|
393
|
-
"total": "Total value of all coins.",
|
|
394
|
-
},
|
|
395
|
-
"custom_counter": _COUNTER_DOCS,
|
|
396
|
-
"consumable": _COUNTER_DOCS,
|
|
397
|
-
"AliasCustomCounter": _COUNTER_DOCS,
|
|
398
|
-
"death_saves": {
|
|
399
|
-
"successes": "Number of successful death saves.",
|
|
400
|
-
"fails": "Number of failed death saves.",
|
|
401
|
-
},
|
|
402
|
-
"AliasDeathSaves": {
|
|
403
|
-
"successes": "Number of successful death saves.",
|
|
404
|
-
"fails": "Number of failed death saves.",
|
|
405
|
-
},
|
|
406
|
-
"spellbook": {
|
|
407
|
-
"dc": "Save DC for spells in this spellbook.",
|
|
408
|
-
"sab": "Spell attack bonus for this spellbook.",
|
|
409
|
-
"caster_level": "Caster level used for the spellbook.",
|
|
410
|
-
"spell_mod": "Spellcasting ability modifier.",
|
|
411
|
-
"spells": "Spells grouped by level.",
|
|
412
|
-
"pact_slot_level": "Level of pact slots, if any.",
|
|
413
|
-
"num_pact_slots": "Number of pact slots available.",
|
|
414
|
-
"max_pact_slots": "Maximum pact slots available.",
|
|
415
|
-
},
|
|
416
|
-
"AliasSpellbook": {
|
|
417
|
-
"dc": "Save DC for spells in this spellbook.",
|
|
418
|
-
"sab": "Spell attack bonus for this spellbook.",
|
|
419
|
-
"caster_level": "Caster level used for the spellbook.",
|
|
420
|
-
"spell_mod": "Spellcasting ability modifier.",
|
|
421
|
-
"spells": "Spells grouped by level.",
|
|
422
|
-
"pact_slot_level": "Level of pact slots, if any.",
|
|
423
|
-
"num_pact_slots": "Number of pact slots available.",
|
|
424
|
-
"max_pact_slots": "Maximum pact slots available.",
|
|
425
|
-
},
|
|
426
|
-
"spell": {
|
|
427
|
-
"name": "Spell name.",
|
|
428
|
-
"dc": "Save DC for this spell.",
|
|
429
|
-
"sab": "Spell attack bonus for this spell.",
|
|
430
|
-
"mod": "Spellcasting modifier applied to the spell.",
|
|
431
|
-
"prepared": "Whether the spell is prepared/known.",
|
|
432
|
-
},
|
|
433
|
-
"AliasSpellbookSpell": {
|
|
434
|
-
"name": "Spell name.",
|
|
435
|
-
"dc": "Save DC for this spell.",
|
|
436
|
-
"sab": "Spell attack bonus for this spell.",
|
|
437
|
-
"mod": "Spellcasting modifier applied to the spell.",
|
|
438
|
-
"prepared": "Whether the spell is prepared/known.",
|
|
439
|
-
},
|
|
440
|
-
"guild": {
|
|
441
|
-
"name": "Guild (server) name.",
|
|
442
|
-
"id": "Guild (server) id.",
|
|
443
|
-
},
|
|
444
|
-
"channel": {
|
|
445
|
-
"name": "Channel name.",
|
|
446
|
-
"id": "Channel id.",
|
|
447
|
-
"topic": "Channel topic, if set.",
|
|
448
|
-
"category": "Parent category for the channel.",
|
|
449
|
-
"parent": "Parent channel, if present.",
|
|
450
|
-
},
|
|
451
|
-
"category": {
|
|
452
|
-
"name": "Category name.",
|
|
453
|
-
"id": "Category id.",
|
|
454
|
-
},
|
|
455
|
-
"author": {
|
|
456
|
-
"name": "User name for the invoking author.",
|
|
457
|
-
"id": "User id for the invoking author.",
|
|
458
|
-
"discriminator": "User discriminator/tag.",
|
|
459
|
-
"display_name": "Display name for the author.",
|
|
460
|
-
"roles": "Roles held by the author.",
|
|
461
|
-
},
|
|
462
|
-
"role": {
|
|
463
|
-
"name": "Role name.",
|
|
464
|
-
"id": "Role id.",
|
|
465
|
-
},
|
|
466
|
-
"effect": _EFFECT_DOCS,
|
|
467
|
-
"SimpleEffect": _EFFECT_DOCS,
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
_METHOD_DOC_OVERRIDES: dict[str, dict[str, str]] = {
|
|
471
|
-
"ParsedArguments": {
|
|
472
|
-
"get": "returns all values for the arg cast to the given type.",
|
|
473
|
-
"last": "returns the most recent value cast to the given type.",
|
|
474
|
-
"adv": "returns -1/0/1/2 indicator for dis/normal/adv/elven accuracy.",
|
|
475
|
-
"join": "joins all argument values with a separator into a string.",
|
|
476
|
-
"ignore": "removes argument values so later reads skip them.",
|
|
477
|
-
"update": "replaces values for an argument.",
|
|
478
|
-
"update_nx": "sets values only if the argument is missing.",
|
|
479
|
-
"set_context": "associates a context bucket for nested parsing.",
|
|
480
|
-
"add_context": "appends a context bucket for nested parsing.",
|
|
481
|
-
},
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def _load_method_docs_from_html(path: Path | str = "tmp_avrae_api.html") -> dict[str, dict[str, str]]:
|
|
486
|
-
docs: dict[str, dict[str, str]] = {}
|
|
487
|
-
try:
|
|
488
|
-
html = Path(path).read_text(encoding="utf-8")
|
|
489
|
-
except Exception:
|
|
490
|
-
return docs
|
|
491
|
-
pattern = re.compile(
|
|
492
|
-
r'<dt class="sig[^"]*" id="aliasing\.api\.[^\.]+\.(?P<class>\w+)\.(?P<method>\w+)">.*?</dt>\s*(?P<body><dd.*?</dd>)',
|
|
493
|
-
re.DOTALL,
|
|
494
|
-
)
|
|
495
|
-
tag_re = re.compile(r"<[^>]+>")
|
|
496
|
-
for match in pattern.finditer(html):
|
|
497
|
-
cls = match.group("class")
|
|
498
|
-
method = match.group("method")
|
|
499
|
-
body = match.group("body")
|
|
500
|
-
raw_text = unescape(tag_re.sub("", body)).strip()
|
|
501
|
-
text = _strip_signature_prefix(raw_text)
|
|
502
|
-
if not text:
|
|
503
|
-
continue
|
|
504
|
-
docs.setdefault(cls, {})[method] = text
|
|
505
|
-
return docs
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
def _strip_signature_prefix(text: str) -> str:
|
|
509
|
-
cleaned = re.sub(r"^[A-Za-z_][\w]*\s*\([^)]*\)\s*(?:->|→)?\s*", "", text)
|
|
510
|
-
if cleaned != text:
|
|
511
|
-
return cleaned.strip()
|
|
512
|
-
# Fallback: split on common dash separators after a signature-like prefix.
|
|
513
|
-
for sep in ("–", "—", "-"):
|
|
514
|
-
parts = text.split(sep, 1)
|
|
515
|
-
if len(parts) == 2 and "(" in parts[0] and ")" in parts[0]:
|
|
516
|
-
return parts[1].strip()
|
|
517
|
-
return text.strip()
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
# Enrich method docs from the bundled API HTML when available.
|
|
521
|
-
_METHOD_DOC_OVERRIDES.update(_load_method_docs_from_html())
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class FunctionContext:
|
|
32
|
+
params: list[str]
|
|
33
|
+
param_types: dict[str, str]
|
|
34
|
+
locals: list[str]
|
|
522
35
|
|
|
523
36
|
|
|
524
37
|
def gather_suggestions(
|
|
@@ -542,6 +55,9 @@ def gather_suggestions(
|
|
|
542
55
|
for name in vars_map:
|
|
543
56
|
suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Variable, detail="var"))
|
|
544
57
|
|
|
58
|
+
for name in ctx_data.vars.svars:
|
|
59
|
+
suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Variable, detail="svar"))
|
|
60
|
+
|
|
545
61
|
gvars = resolver.snapshot()
|
|
546
62
|
for name in gvars:
|
|
547
63
|
suggestions.append(Suggestion(name=name, kind=types.CompletionItemKind.Variable, detail="gvar"))
|
|
@@ -568,13 +84,50 @@ def completion_items_for_position(
|
|
|
568
84
|
if attr_ctx:
|
|
569
85
|
receiver, attr_prefix = attr_ctx
|
|
570
86
|
sanitized = _sanitize_incomplete_line(code, line, character)
|
|
571
|
-
type_map =
|
|
87
|
+
type_map = infer_type_map(sanitized, line)
|
|
572
88
|
return _attribute_completions(receiver, attr_prefix, sanitized, type_map)
|
|
573
89
|
|
|
574
90
|
line_text = _line_text_to_cursor(code, line, character)
|
|
575
91
|
prefix = _current_prefix(line_text)
|
|
576
92
|
items: list[types.CompletionItem] = []
|
|
93
|
+
seen: set[str] = set()
|
|
94
|
+
func_ctx = _function_context_at_line(code, line)
|
|
95
|
+
type_map = infer_type_map(code, line) if func_ctx else {}
|
|
96
|
+
for name in func_ctx.params if func_ctx else []:
|
|
97
|
+
if prefix and not name.startswith(prefix):
|
|
98
|
+
continue
|
|
99
|
+
detail = "param"
|
|
100
|
+
param_type = func_ctx.param_types.get(name) if func_ctx else None
|
|
101
|
+
if param_type:
|
|
102
|
+
detail = f"param: {param_type}"
|
|
103
|
+
items.append(
|
|
104
|
+
types.CompletionItem(
|
|
105
|
+
label=name,
|
|
106
|
+
kind=types.CompletionItemKind.Variable,
|
|
107
|
+
detail=detail,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
seen.add(name)
|
|
111
|
+
for name in func_ctx.locals if func_ctx else []:
|
|
112
|
+
if name in seen:
|
|
113
|
+
continue
|
|
114
|
+
if prefix and not name.startswith(prefix):
|
|
115
|
+
continue
|
|
116
|
+
detail = "local"
|
|
117
|
+
local_type = type_map.get(name)
|
|
118
|
+
if local_type:
|
|
119
|
+
detail = f"local: {display_type_label(local_type)}"
|
|
120
|
+
items.append(
|
|
121
|
+
types.CompletionItem(
|
|
122
|
+
label=name,
|
|
123
|
+
kind=types.CompletionItemKind.Variable,
|
|
124
|
+
detail=detail,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
seen.add(name)
|
|
577
128
|
for sugg in suggestions:
|
|
129
|
+
if sugg.name in seen:
|
|
130
|
+
continue
|
|
578
131
|
if prefix and not sugg.name.startswith(prefix):
|
|
579
132
|
continue
|
|
580
133
|
items.append(
|
|
@@ -590,11 +143,11 @@ def completion_items_for_position(
|
|
|
590
143
|
|
|
591
144
|
def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict[str, str] | None = None) -> List[types.CompletionItem]:
|
|
592
145
|
items: list[types.CompletionItem] = []
|
|
593
|
-
type_key =
|
|
146
|
+
type_key = resolve_type_name(receiver, code, type_map)
|
|
594
147
|
if IDENT_RE.fullmatch(receiver) and (not type_map or receiver not in type_map) and type_key == receiver:
|
|
595
148
|
# Avoid treating arbitrary variable names as known API types unless they were inferred.
|
|
596
149
|
return items
|
|
597
|
-
meta =
|
|
150
|
+
meta = type_meta(type_key)
|
|
598
151
|
detail = f"{type_key}()"
|
|
599
152
|
|
|
600
153
|
for name, attr_meta in meta.attrs.items():
|
|
@@ -632,13 +185,13 @@ def hover_for_position(
|
|
|
632
185
|
resolver: GVarResolver,
|
|
633
186
|
) -> Optional[types.Hover]:
|
|
634
187
|
line_text = _line_text(code, line)
|
|
635
|
-
type_map =
|
|
188
|
+
type_map = infer_type_map(code, line)
|
|
636
189
|
bindings = _infer_constant_bindings(code, line, ctx_data)
|
|
637
190
|
attr_ctx = _attribute_receiver_and_prefix(code, line, character, capture_full_token=True)
|
|
638
191
|
if attr_ctx:
|
|
639
192
|
receiver, attr_prefix = attr_ctx
|
|
640
|
-
inferred =
|
|
641
|
-
meta =
|
|
193
|
+
inferred = resolve_type_name(receiver, code, type_map)
|
|
194
|
+
meta = type_meta(inferred)
|
|
642
195
|
if attr_prefix in meta.attrs:
|
|
643
196
|
doc = meta.attrs[attr_prefix].doc
|
|
644
197
|
contents = f"```avrae\n{inferred}().{attr_prefix}\n```"
|
|
@@ -660,7 +213,7 @@ def hover_for_position(
|
|
|
660
213
|
if word in bindings:
|
|
661
214
|
return _format_binding_hover(word, bindings[word], "local")
|
|
662
215
|
if word in type_map:
|
|
663
|
-
type_label =
|
|
216
|
+
type_label = display_type_label(type_map[word])
|
|
664
217
|
contents = f"`{word}` type: `{type_label}`"
|
|
665
218
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
666
219
|
if word in sigs:
|
|
@@ -705,6 +258,135 @@ def _line_text_to_cursor(code: str, line: int, character: int) -> str:
|
|
|
705
258
|
return lines[line][:character]
|
|
706
259
|
|
|
707
260
|
|
|
261
|
+
def _function_context_at_line(code: str, line: int) -> FunctionContext | None:
|
|
262
|
+
try:
|
|
263
|
+
tree = ast.parse(code)
|
|
264
|
+
except SyntaxError:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
target: ast.FunctionDef | ast.AsyncFunctionDef | None = None
|
|
268
|
+
best_start = -1
|
|
269
|
+
for node in ast.walk(tree):
|
|
270
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
271
|
+
start = max(getattr(node, "lineno", 1) - 1, 0)
|
|
272
|
+
end = max(getattr(node, "end_lineno", node.lineno) - 1, 0)
|
|
273
|
+
if start <= line <= end and start >= best_start:
|
|
274
|
+
target = node
|
|
275
|
+
best_start = start
|
|
276
|
+
|
|
277
|
+
if target is None:
|
|
278
|
+
return None
|
|
279
|
+
params: list[str] = []
|
|
280
|
+
param_types: dict[str, str] = {}
|
|
281
|
+
|
|
282
|
+
def _add_arg(arg: ast.arg) -> None:
|
|
283
|
+
params.append(arg.arg)
|
|
284
|
+
label = annotation_label(getattr(arg, "annotation", None))
|
|
285
|
+
if label:
|
|
286
|
+
param_types[arg.arg] = label
|
|
287
|
+
|
|
288
|
+
args = target.args
|
|
289
|
+
for arg in getattr(args, "posonlyargs", []):
|
|
290
|
+
_add_arg(arg)
|
|
291
|
+
for arg in args.args:
|
|
292
|
+
_add_arg(arg)
|
|
293
|
+
if args.vararg:
|
|
294
|
+
_add_arg(args.vararg)
|
|
295
|
+
for arg in args.kwonlyargs:
|
|
296
|
+
_add_arg(arg)
|
|
297
|
+
if args.kwarg:
|
|
298
|
+
_add_arg(args.kwarg)
|
|
299
|
+
|
|
300
|
+
locals_list = _function_locals_at_node(target, line + 1, params)
|
|
301
|
+
return FunctionContext(params=params, param_types=param_types, locals=locals_list)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _function_locals_at_node(
|
|
305
|
+
target: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
306
|
+
line_limit: int,
|
|
307
|
+
params: list[str],
|
|
308
|
+
) -> list[str]:
|
|
309
|
+
seen = set(params)
|
|
310
|
+
names: list[str] = []
|
|
311
|
+
|
|
312
|
+
def _add_name(name: str) -> None:
|
|
313
|
+
if name in seen:
|
|
314
|
+
return
|
|
315
|
+
seen.add(name)
|
|
316
|
+
names.append(name)
|
|
317
|
+
|
|
318
|
+
class LocalCollector(ast.NodeVisitor):
|
|
319
|
+
def __init__(self) -> None:
|
|
320
|
+
super().__init__()
|
|
321
|
+
|
|
322
|
+
def visit(self, node: ast.AST): # type: ignore[override]
|
|
323
|
+
lineno = getattr(node, "lineno", None)
|
|
324
|
+
if lineno is not None and lineno > line_limit:
|
|
325
|
+
return
|
|
326
|
+
return super().visit(node)
|
|
327
|
+
|
|
328
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
329
|
+
if node is target:
|
|
330
|
+
for stmt in node.body:
|
|
331
|
+
self.visit(stmt)
|
|
332
|
+
return
|
|
333
|
+
_add_name(node.name)
|
|
334
|
+
|
|
335
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
336
|
+
if node is target:
|
|
337
|
+
for stmt in node.body:
|
|
338
|
+
self.visit(stmt)
|
|
339
|
+
return
|
|
340
|
+
_add_name(node.name)
|
|
341
|
+
|
|
342
|
+
def visit_ClassDef(self, node: ast.ClassDef):
|
|
343
|
+
_add_name(node.name)
|
|
344
|
+
|
|
345
|
+
def visit_Assign(self, node: ast.Assign):
|
|
346
|
+
for name in collect_target_names(node.targets):
|
|
347
|
+
_add_name(name)
|
|
348
|
+
|
|
349
|
+
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
350
|
+
for name in collect_target_names([node.target]):
|
|
351
|
+
_add_name(name)
|
|
352
|
+
|
|
353
|
+
def visit_AugAssign(self, node: ast.AugAssign):
|
|
354
|
+
for name in collect_target_names([node.target]):
|
|
355
|
+
_add_name(name)
|
|
356
|
+
|
|
357
|
+
def visit_For(self, node: ast.For):
|
|
358
|
+
for name in collect_target_names([node.target]):
|
|
359
|
+
_add_name(name)
|
|
360
|
+
self.generic_visit(node)
|
|
361
|
+
|
|
362
|
+
def visit_AsyncFor(self, node: ast.AsyncFor):
|
|
363
|
+
for name in collect_target_names([node.target]):
|
|
364
|
+
_add_name(name)
|
|
365
|
+
self.generic_visit(node)
|
|
366
|
+
|
|
367
|
+
def visit_With(self, node: ast.With):
|
|
368
|
+
for item in node.items:
|
|
369
|
+
if item.optional_vars:
|
|
370
|
+
for name in collect_target_names([item.optional_vars]):
|
|
371
|
+
_add_name(name)
|
|
372
|
+
self.generic_visit(node)
|
|
373
|
+
|
|
374
|
+
def visit_AsyncWith(self, node: ast.AsyncWith):
|
|
375
|
+
for item in node.items:
|
|
376
|
+
if item.optional_vars:
|
|
377
|
+
for name in collect_target_names([item.optional_vars]):
|
|
378
|
+
_add_name(name)
|
|
379
|
+
self.generic_visit(node)
|
|
380
|
+
|
|
381
|
+
def visit_ExceptHandler(self, node: ast.ExceptHandler):
|
|
382
|
+
if isinstance(getattr(node, "name", None), str):
|
|
383
|
+
_add_name(node.name)
|
|
384
|
+
self.generic_visit(node)
|
|
385
|
+
|
|
386
|
+
LocalCollector().visit(target)
|
|
387
|
+
return names
|
|
388
|
+
|
|
389
|
+
|
|
708
390
|
def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture_full_token: bool = False) -> Optional[tuple[str, str]]:
|
|
709
391
|
lines = code.splitlines()
|
|
710
392
|
if line >= len(lines):
|
|
@@ -815,636 +497,6 @@ def _line_text(code: str, line: int) -> str:
|
|
|
815
497
|
return lines[line]
|
|
816
498
|
|
|
817
499
|
|
|
818
|
-
def _display_type_label(type_key: str) -> str:
|
|
819
|
-
cls = _type_cls(type_key)
|
|
820
|
-
if cls is None:
|
|
821
|
-
return type_key
|
|
822
|
-
name = cls.__name__
|
|
823
|
-
if name.startswith("_Builtin"):
|
|
824
|
-
return type_key
|
|
825
|
-
return name
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
def _split_annotation_string(text: str) -> tuple[Optional[str], Optional[str]]:
|
|
829
|
-
stripped = text.strip().strip("'\"")
|
|
830
|
-
if not stripped:
|
|
831
|
-
return None, None
|
|
832
|
-
match = re.match(r"^([A-Za-z_][\w]*)\s*(?:\[\s*([A-Za-z_][\w]*)(?:\s*,\s*([A-Za-z_][\w]*))?\s*\])?$", stripped)
|
|
833
|
-
if not match:
|
|
834
|
-
return stripped, None
|
|
835
|
-
base = match.group(1)
|
|
836
|
-
elem = match.group(3) or match.group(2)
|
|
837
|
-
base_norm = base.lower() if base.lower() in {"list", "dict", "set", "tuple"} else base
|
|
838
|
-
return base_norm, elem
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
def _annotation_types(node: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
|
|
842
|
-
if node is None:
|
|
843
|
-
return None, None
|
|
844
|
-
if isinstance(node, str):
|
|
845
|
-
return _split_annotation_string(node)
|
|
846
|
-
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
847
|
-
return _split_annotation_string(node.value)
|
|
848
|
-
if isinstance(node, ast.Str):
|
|
849
|
-
return _split_annotation_string(node.s)
|
|
850
|
-
if isinstance(node, ast.Name):
|
|
851
|
-
return node.id, None
|
|
852
|
-
if isinstance(node, ast.Attribute):
|
|
853
|
-
return node.attr, None
|
|
854
|
-
try:
|
|
855
|
-
text = ast.unparse(node)
|
|
856
|
-
except Exception:
|
|
857
|
-
text = ""
|
|
858
|
-
if text:
|
|
859
|
-
return _split_annotation_string(text)
|
|
860
|
-
return None, None
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
def _infer_receiver_type(code: str, name: str, line: int | None = None) -> Optional[str]:
|
|
864
|
-
return _infer_type_map(code, line).get(name)
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
def _infer_type_map(code: str, line: int | None = None) -> Dict[str, str]:
|
|
868
|
-
try:
|
|
869
|
-
tree = ast.parse(code, type_comments=True)
|
|
870
|
-
except SyntaxError:
|
|
871
|
-
return {}
|
|
872
|
-
visitor = _TypeInferencer(code)
|
|
873
|
-
visitor.visit(tree)
|
|
874
|
-
return visitor.export_types(line)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
class _TypeInferencer(ast.NodeVisitor):
|
|
878
|
-
def __init__(self, code: str) -> None:
|
|
879
|
-
self.code = code
|
|
880
|
-
self._scopes: list[dict[str, str]] = [dict()]
|
|
881
|
-
self._scoped_maps: list[tuple[tuple[int, int], dict[str, str]]] = []
|
|
882
|
-
|
|
883
|
-
def _current_scope(self) -> dict[str, str]:
|
|
884
|
-
return self._scopes[-1]
|
|
885
|
-
|
|
886
|
-
def _push_scope(self) -> None:
|
|
887
|
-
self._scopes.append(dict())
|
|
888
|
-
|
|
889
|
-
def _pop_scope(self) -> None:
|
|
890
|
-
self._scopes.pop()
|
|
891
|
-
|
|
892
|
-
def export_types(self, line: int | None = None) -> dict[str, str]:
|
|
893
|
-
if line is not None:
|
|
894
|
-
for (start, end), scope in reversed(self._scoped_maps):
|
|
895
|
-
if start <= line <= end:
|
|
896
|
-
combined = dict(self._scopes[0])
|
|
897
|
-
combined.update(scope)
|
|
898
|
-
return combined
|
|
899
|
-
return dict(self._scopes[0])
|
|
900
|
-
|
|
901
|
-
def _set_type(self, key: str, value: Optional[str]) -> None:
|
|
902
|
-
if value:
|
|
903
|
-
self._current_scope()[key] = value
|
|
904
|
-
|
|
905
|
-
def _get_type(self, key: str) -> Optional[str]:
|
|
906
|
-
for scope in reversed(self._scopes):
|
|
907
|
-
if key in scope:
|
|
908
|
-
return scope[key]
|
|
909
|
-
return None
|
|
910
|
-
|
|
911
|
-
def visit_Assign(self, node: ast.Assign):
|
|
912
|
-
val_type, elem_type = self._value_type(node.value)
|
|
913
|
-
if getattr(node, "type_comment", None):
|
|
914
|
-
ann_type, ann_elem = _annotation_types(node.type_comment)
|
|
915
|
-
val_type = ann_type or val_type
|
|
916
|
-
elem_type = ann_elem or elem_type
|
|
917
|
-
for target in node.targets:
|
|
918
|
-
self._bind_target(target, val_type, elem_type, node.value)
|
|
919
|
-
self.generic_visit(node)
|
|
920
|
-
|
|
921
|
-
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
922
|
-
val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
|
|
923
|
-
ann_type, ann_elem = _annotation_types(getattr(node, "annotation", None))
|
|
924
|
-
val_type = val_type or ann_type
|
|
925
|
-
elem_type = elem_type or ann_elem
|
|
926
|
-
if getattr(node, "type_comment", None):
|
|
927
|
-
c_type, c_elem = _annotation_types(node.type_comment)
|
|
928
|
-
val_type = val_type or c_type
|
|
929
|
-
elem_type = elem_type or c_elem
|
|
930
|
-
self._bind_target(node.target, val_type, elem_type, node.value)
|
|
931
|
-
self.generic_visit(node)
|
|
932
|
-
|
|
933
|
-
def visit_AugAssign(self, node: ast.AugAssign):
|
|
934
|
-
val_type, elem_type = self._value_type(node.value)
|
|
935
|
-
self._bind_target(
|
|
936
|
-
node.target,
|
|
937
|
-
val_type or self._existing_type(node.target),
|
|
938
|
-
elem_type or self._existing_element(node.target),
|
|
939
|
-
None,
|
|
940
|
-
)
|
|
941
|
-
self.generic_visit(node)
|
|
942
|
-
|
|
943
|
-
def visit_For(self, node: ast.For):
|
|
944
|
-
_, elem_type = self._value_type(node.iter)
|
|
945
|
-
if not elem_type and isinstance(node.iter, ast.Name):
|
|
946
|
-
elem_type = self._get_type(f"{node.iter.id}.__element__")
|
|
947
|
-
self._bind_target(node.target, elem_type, None, None)
|
|
948
|
-
self.generic_visit(node)
|
|
949
|
-
|
|
950
|
-
def visit_AsyncFor(self, node: ast.AsyncFor):
|
|
951
|
-
_, elem_type = self._value_type(node.iter)
|
|
952
|
-
if not elem_type and isinstance(node.iter, ast.Name):
|
|
953
|
-
elem_type = self._get_type(f"{node.iter.id}.__element__")
|
|
954
|
-
self._bind_target(node.target, elem_type, None, None)
|
|
955
|
-
self.generic_visit(node)
|
|
956
|
-
|
|
957
|
-
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
958
|
-
self._push_scope()
|
|
959
|
-
try:
|
|
960
|
-
self._bind_function_args(node.args)
|
|
961
|
-
for stmt in node.body:
|
|
962
|
-
self.visit(stmt)
|
|
963
|
-
self._record_scope(node)
|
|
964
|
-
finally:
|
|
965
|
-
self._pop_scope()
|
|
966
|
-
|
|
967
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
968
|
-
self._push_scope()
|
|
969
|
-
try:
|
|
970
|
-
self._bind_function_args(node.args)
|
|
971
|
-
for stmt in node.body:
|
|
972
|
-
self.visit(stmt)
|
|
973
|
-
self._record_scope(node)
|
|
974
|
-
finally:
|
|
975
|
-
self._pop_scope()
|
|
976
|
-
|
|
977
|
-
def visit_ClassDef(self, node: ast.ClassDef):
|
|
978
|
-
self._push_scope()
|
|
979
|
-
try:
|
|
980
|
-
for stmt in node.body:
|
|
981
|
-
self.visit(stmt)
|
|
982
|
-
self._record_scope(node)
|
|
983
|
-
finally:
|
|
984
|
-
self._pop_scope()
|
|
985
|
-
|
|
986
|
-
def visit_If(self, node: ast.If):
|
|
987
|
-
self.visit(node.test)
|
|
988
|
-
base_map = self._current_scope().copy()
|
|
989
|
-
body_map = self._visit_block(node.body, base_map.copy())
|
|
990
|
-
orelse_seed = base_map.copy()
|
|
991
|
-
orelse_map = self._visit_block(node.orelse, orelse_seed) if node.orelse else orelse_seed
|
|
992
|
-
self._current_scope().update(self._merge_branch_types(base_map, body_map, orelse_map))
|
|
993
|
-
|
|
994
|
-
def _visit_block(self, nodes: Iterable[ast.stmt], seed: dict[str, str]) -> dict[str, str]:
|
|
995
|
-
walker = _TypeInferencer(self.code)
|
|
996
|
-
walker._scopes = [seed.copy()]
|
|
997
|
-
for stmt in nodes:
|
|
998
|
-
walker.visit(stmt)
|
|
999
|
-
self._scoped_maps.extend(walker._scoped_maps)
|
|
1000
|
-
return walker._current_scope()
|
|
1001
|
-
|
|
1002
|
-
def _merge_branch_types(self, base: dict[str, str], left: dict[str, str], right: dict[str, str]) -> dict[str, str]:
|
|
1003
|
-
merged = base.copy()
|
|
1004
|
-
for key in set(left) | set(right):
|
|
1005
|
-
l_val = left.get(key)
|
|
1006
|
-
r_val = right.get(key)
|
|
1007
|
-
if l_val and r_val and l_val == r_val:
|
|
1008
|
-
merged[key] = l_val
|
|
1009
|
-
elif key in base:
|
|
1010
|
-
merged[key] = base[key]
|
|
1011
|
-
elif l_val and not r_val:
|
|
1012
|
-
merged[key] = l_val
|
|
1013
|
-
elif r_val and not l_val:
|
|
1014
|
-
merged[key] = r_val
|
|
1015
|
-
elif key in merged:
|
|
1016
|
-
merged.pop(key, None)
|
|
1017
|
-
return merged
|
|
1018
|
-
|
|
1019
|
-
def _bind_target(self, target: ast.AST, val_type: Optional[str], elem_type: Optional[str], source: ast.AST | None):
|
|
1020
|
-
if isinstance(target, ast.Name):
|
|
1021
|
-
self._set_type(target.id, val_type)
|
|
1022
|
-
self._set_type(f"{target.id}.__element__", elem_type)
|
|
1023
|
-
if source is not None:
|
|
1024
|
-
self._record_dict_key_types(target.id, source)
|
|
1025
|
-
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
1026
|
-
if isinstance(source, (ast.Tuple, ast.List)) and len(source.elts or []) == len(target.elts):
|
|
1027
|
-
for elt, val_node in zip(target.elts, source.elts):
|
|
1028
|
-
elt_type, elt_elem = self._value_type(val_node)
|
|
1029
|
-
self._bind_target(elt, elt_type, elt_elem, val_node)
|
|
1030
|
-
else:
|
|
1031
|
-
for elt in target.elts:
|
|
1032
|
-
self._bind_target(elt, val_type, elem_type, source)
|
|
1033
|
-
|
|
1034
|
-
def _bind_function_args(self, args: ast.arguments) -> None:
|
|
1035
|
-
for arg in getattr(args, "posonlyargs", []):
|
|
1036
|
-
self._bind_arg_annotation(arg)
|
|
1037
|
-
for arg in args.args:
|
|
1038
|
-
self._bind_arg_annotation(arg)
|
|
1039
|
-
if args.vararg:
|
|
1040
|
-
self._bind_arg_annotation(args.vararg)
|
|
1041
|
-
for arg in args.kwonlyargs:
|
|
1042
|
-
self._bind_arg_annotation(arg)
|
|
1043
|
-
if args.kwarg:
|
|
1044
|
-
self._bind_arg_annotation(args.kwarg)
|
|
1045
|
-
|
|
1046
|
-
def _bind_arg_annotation(self, arg: ast.arg) -> None:
|
|
1047
|
-
ann_type, elem_type = _annotation_types(getattr(arg, "annotation", None))
|
|
1048
|
-
if ann_type:
|
|
1049
|
-
self._set_type(arg.arg, ann_type)
|
|
1050
|
-
if elem_type:
|
|
1051
|
-
self._set_type(f"{arg.arg}.__element__", elem_type)
|
|
1052
|
-
|
|
1053
|
-
def _existing_type(self, target: ast.AST) -> Optional[str]:
|
|
1054
|
-
if isinstance(target, ast.Name):
|
|
1055
|
-
return self._get_type(target.id)
|
|
1056
|
-
return None
|
|
1057
|
-
|
|
1058
|
-
def _existing_element(self, target: ast.AST) -> Optional[str]:
|
|
1059
|
-
if isinstance(target, ast.Name):
|
|
1060
|
-
return self._get_type(f"{target.id}.__element__")
|
|
1061
|
-
return None
|
|
1062
|
-
|
|
1063
|
-
def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
|
|
1064
|
-
if isinstance(value, ast.Call):
|
|
1065
|
-
if isinstance(value.func, ast.Name):
|
|
1066
|
-
if value.func.id in {"character", "combat"}:
|
|
1067
|
-
return value.func.id, None
|
|
1068
|
-
if value.func.id == "vroll":
|
|
1069
|
-
return "SimpleRollResult", None
|
|
1070
|
-
if value.func.id == "argparse":
|
|
1071
|
-
return "ParsedArguments", None
|
|
1072
|
-
if value.func.id == "range":
|
|
1073
|
-
return "range", "int"
|
|
1074
|
-
if value.func.id in {"list", "dict", "str", "int", "float"}:
|
|
1075
|
-
return value.func.id, None
|
|
1076
|
-
if isinstance(value.func, ast.Attribute):
|
|
1077
|
-
base_type, base_elem = self._value_type(value.func.value)
|
|
1078
|
-
if value.func.attr == "get" and value.args:
|
|
1079
|
-
key_literal = self._literal_key(value.args[0])
|
|
1080
|
-
val_type, elem_type = self._subscript_type(value.func.value, key_literal, base_type, base_elem)
|
|
1081
|
-
if val_type:
|
|
1082
|
-
return val_type, elem_type
|
|
1083
|
-
if base_elem:
|
|
1084
|
-
return base_elem, None
|
|
1085
|
-
if isinstance(value, ast.Compare):
|
|
1086
|
-
return "bool", None
|
|
1087
|
-
if isinstance(value, ast.List):
|
|
1088
|
-
elem_type, _ = self._iterable_element_from_values(value.elts)
|
|
1089
|
-
return "list", elem_type
|
|
1090
|
-
if isinstance(value, ast.Tuple):
|
|
1091
|
-
elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
|
|
1092
|
-
return "tuple", elem_type
|
|
1093
|
-
if isinstance(value, ast.Set):
|
|
1094
|
-
elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
|
|
1095
|
-
return "set", elem_type
|
|
1096
|
-
if isinstance(value, ast.ListComp):
|
|
1097
|
-
comp_type, comp_elem = self._value_type(value.elt)
|
|
1098
|
-
return "list", comp_type or comp_elem
|
|
1099
|
-
if isinstance(value, ast.Dict):
|
|
1100
|
-
elem_type, _ = self._iterable_element_from_values(value.values or [])
|
|
1101
|
-
return "dict", elem_type
|
|
1102
|
-
if isinstance(value, ast.Subscript):
|
|
1103
|
-
return self._subscript_value_type(value)
|
|
1104
|
-
if isinstance(value, ast.Constant):
|
|
1105
|
-
if isinstance(value.value, str):
|
|
1106
|
-
return "str", None
|
|
1107
|
-
if isinstance(value, ast.Name):
|
|
1108
|
-
existing = self._get_type(value.id)
|
|
1109
|
-
if existing:
|
|
1110
|
-
return existing, self._get_type(f"{value.id}.__element__")
|
|
1111
|
-
if value.id in {"character", "combat", "ctx"}:
|
|
1112
|
-
return value.id, None
|
|
1113
|
-
if isinstance(value, ast.Attribute):
|
|
1114
|
-
attr_name = value.attr
|
|
1115
|
-
base_type = None
|
|
1116
|
-
base_elem = None
|
|
1117
|
-
if isinstance(value.value, ast.Name):
|
|
1118
|
-
base_type = self._get_type(value.value.id)
|
|
1119
|
-
base_elem = self._get_type(f"{value.value.id}.__element__")
|
|
1120
|
-
if base_type is None:
|
|
1121
|
-
base_type, base_elem = self._value_type(value.value)
|
|
1122
|
-
if base_type:
|
|
1123
|
-
meta = _type_meta(base_type)
|
|
1124
|
-
attr_meta = meta.attrs.get(attr_name)
|
|
1125
|
-
if attr_meta:
|
|
1126
|
-
if attr_meta.type_name:
|
|
1127
|
-
return attr_meta.type_name, attr_meta.element_type or None
|
|
1128
|
-
if attr_meta.element_type:
|
|
1129
|
-
return base_type, attr_meta.element_type
|
|
1130
|
-
if base_elem:
|
|
1131
|
-
return base_elem, None
|
|
1132
|
-
resolved_attr_type = _resolve_type_key(attr_name, base_type)
|
|
1133
|
-
if resolved_attr_type:
|
|
1134
|
-
return resolved_attr_type, None
|
|
1135
|
-
return None, None
|
|
1136
|
-
if isinstance(value, ast.IfExp):
|
|
1137
|
-
t_type, t_elem = self._value_type(value.body)
|
|
1138
|
-
e_type, e_elem = self._value_type(value.orelse)
|
|
1139
|
-
if t_type and e_type and t_type == e_type:
|
|
1140
|
-
merged_elem = t_elem or e_elem
|
|
1141
|
-
if t_elem and e_elem and t_elem != e_elem:
|
|
1142
|
-
merged_elem = None
|
|
1143
|
-
return t_type, merged_elem
|
|
1144
|
-
return t_type or e_type, t_elem or e_elem
|
|
1145
|
-
return None, None
|
|
1146
|
-
|
|
1147
|
-
def _iterable_element_from_values(self, values: Iterable[ast.AST]) -> tuple[Optional[str], Optional[str]]:
|
|
1148
|
-
elem_type: Optional[str] = None
|
|
1149
|
-
nested_elem: Optional[str] = None
|
|
1150
|
-
for node in values:
|
|
1151
|
-
val_type, inner_elem = self._value_type(node)
|
|
1152
|
-
if not val_type:
|
|
1153
|
-
return None, None
|
|
1154
|
-
if elem_type is None:
|
|
1155
|
-
elem_type = val_type
|
|
1156
|
-
nested_elem = inner_elem
|
|
1157
|
-
elif elem_type != val_type:
|
|
1158
|
-
return None, None
|
|
1159
|
-
if inner_elem:
|
|
1160
|
-
if nested_elem is None:
|
|
1161
|
-
nested_elem = inner_elem
|
|
1162
|
-
elif nested_elem != inner_elem:
|
|
1163
|
-
nested_elem = None
|
|
1164
|
-
return elem_type, nested_elem
|
|
1165
|
-
|
|
1166
|
-
def _literal_key(self, node: ast.AST | None) -> str | int | None:
|
|
1167
|
-
if isinstance(node, ast.Constant):
|
|
1168
|
-
if isinstance(node.value, (str, int)):
|
|
1169
|
-
return node.value
|
|
1170
|
-
if hasattr(ast, "Index") and isinstance(node, getattr(ast, "Index")):
|
|
1171
|
-
return self._literal_key(getattr(node, "value", None))
|
|
1172
|
-
return None
|
|
1173
|
-
|
|
1174
|
-
def _subscript_type(
|
|
1175
|
-
self,
|
|
1176
|
-
base_expr: ast.AST,
|
|
1177
|
-
key_literal: str | int | None,
|
|
1178
|
-
base_type: Optional[str],
|
|
1179
|
-
base_elem: Optional[str],
|
|
1180
|
-
) -> tuple[Optional[str], Optional[str]]:
|
|
1181
|
-
base_name = base_expr.id if isinstance(base_expr, ast.Name) else None
|
|
1182
|
-
if base_name and key_literal is not None:
|
|
1183
|
-
dict_key = f"{base_name}.{key_literal}"
|
|
1184
|
-
dict_type = self._get_type(dict_key)
|
|
1185
|
-
if dict_type:
|
|
1186
|
-
return dict_type, self._get_type(f"{dict_key}.__element__")
|
|
1187
|
-
elem_hint = base_elem
|
|
1188
|
-
if base_name and not elem_hint:
|
|
1189
|
-
elem_hint = self._get_type(f"{base_name}.__element__")
|
|
1190
|
-
if base_type:
|
|
1191
|
-
meta = _type_meta(base_type)
|
|
1192
|
-
if key_literal is not None and key_literal in meta.attrs:
|
|
1193
|
-
attr_meta = meta.attrs[key_literal]
|
|
1194
|
-
if attr_meta.type_name:
|
|
1195
|
-
return attr_meta.type_name, attr_meta.element_type or None
|
|
1196
|
-
if attr_meta.element_type:
|
|
1197
|
-
return base_type, attr_meta.element_type
|
|
1198
|
-
elem_hint = elem_hint or meta.element_type
|
|
1199
|
-
if elem_hint:
|
|
1200
|
-
return elem_hint, None
|
|
1201
|
-
return base_type, None
|
|
1202
|
-
|
|
1203
|
-
def _subscript_value_type(self, node: ast.Subscript) -> tuple[Optional[str], Optional[str]]:
|
|
1204
|
-
base_type, base_elem = self._value_type(node.value)
|
|
1205
|
-
key_literal = self._literal_key(getattr(node, "slice", None))
|
|
1206
|
-
return self._subscript_type(node.value, key_literal, base_type, base_elem)
|
|
1207
|
-
|
|
1208
|
-
def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
|
|
1209
|
-
if not isinstance(value, ast.Dict):
|
|
1210
|
-
return
|
|
1211
|
-
for key_node, val_node in zip(value.keys or [], value.values or []):
|
|
1212
|
-
key_literal = self._literal_key(key_node)
|
|
1213
|
-
if key_literal is None:
|
|
1214
|
-
continue
|
|
1215
|
-
val_type, elem_type = self._value_type(val_node)
|
|
1216
|
-
if val_type:
|
|
1217
|
-
self._set_type(f"{var_name}.{key_literal}", val_type)
|
|
1218
|
-
if elem_type:
|
|
1219
|
-
self._set_type(f"{var_name}.{key_literal}.__element__", elem_type)
|
|
1220
|
-
|
|
1221
|
-
def _record_scope(self, node: ast.AST) -> None:
|
|
1222
|
-
start = getattr(node, "lineno", None)
|
|
1223
|
-
end = getattr(node, "end_lineno", start)
|
|
1224
|
-
if start is None or end is None:
|
|
1225
|
-
return
|
|
1226
|
-
start = max(start - 1, 0)
|
|
1227
|
-
end = max(end - 1, start)
|
|
1228
|
-
self._scoped_maps.append(((start, end), self._current_scope().copy()))
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
|
|
1232
|
-
mapping = type_map or _infer_type_map(code)
|
|
1233
|
-
get_match = DICT_GET_RE.match(receiver)
|
|
1234
|
-
if get_match:
|
|
1235
|
-
base, _, key = get_match.groups()
|
|
1236
|
-
dict_key = f"{base}.{key}"
|
|
1237
|
-
if dict_key in mapping:
|
|
1238
|
-
return mapping[dict_key]
|
|
1239
|
-
bracket = receiver.rfind("[")
|
|
1240
|
-
if bracket != -1 and receiver.endswith("]"):
|
|
1241
|
-
base_expr = receiver[:bracket]
|
|
1242
|
-
elem_hint = mapping.get(f"{base_expr}.__element__")
|
|
1243
|
-
if elem_hint:
|
|
1244
|
-
return elem_hint
|
|
1245
|
-
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
1246
|
-
if base_type:
|
|
1247
|
-
base_meta = _type_meta(base_type)
|
|
1248
|
-
if base_meta.element_type:
|
|
1249
|
-
return base_meta.element_type
|
|
1250
|
-
return base_type
|
|
1251
|
-
receiver = receiver.rstrip("()")
|
|
1252
|
-
if "." in receiver:
|
|
1253
|
-
base_expr, attr_name = receiver.rsplit(".", 1)
|
|
1254
|
-
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
1255
|
-
if base_type:
|
|
1256
|
-
meta = _type_meta(base_type)
|
|
1257
|
-
attr_key = attr_name.split("[", 1)[0]
|
|
1258
|
-
attr_meta = meta.attrs.get(attr_key)
|
|
1259
|
-
if attr_meta:
|
|
1260
|
-
if attr_meta.element_type:
|
|
1261
|
-
return attr_meta.element_type
|
|
1262
|
-
if attr_meta.type_name:
|
|
1263
|
-
return attr_meta.type_name
|
|
1264
|
-
|
|
1265
|
-
if receiver in mapping:
|
|
1266
|
-
return mapping[receiver]
|
|
1267
|
-
elem_key = f"{receiver}.__element__"
|
|
1268
|
-
if elem_key in mapping:
|
|
1269
|
-
return mapping[elem_key]
|
|
1270
|
-
resolved_receiver = _resolve_type_key(receiver)
|
|
1271
|
-
if resolved_receiver:
|
|
1272
|
-
return resolved_receiver
|
|
1273
|
-
tail = receiver.split(".")[-1].split("[", 1)[0]
|
|
1274
|
-
resolved_tail = _resolve_type_key(tail)
|
|
1275
|
-
if resolved_tail:
|
|
1276
|
-
return resolved_tail
|
|
1277
|
-
return receiver
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
def _type_meta(type_name: str) -> TypeMeta:
|
|
1281
|
-
return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}, element_type=""))
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
@lru_cache()
|
|
1285
|
-
def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
1286
|
-
meta: dict[str, TypeMeta] = {}
|
|
1287
|
-
reverse_type_map: dict[type, str] = {entry.cls: key for key, entry in TYPE_MAP.items()}
|
|
1288
|
-
|
|
1289
|
-
def _iter_element_for_type_name(type_name: str) -> str:
|
|
1290
|
-
cls = _type_cls(type_name)
|
|
1291
|
-
if not cls:
|
|
1292
|
-
return ""
|
|
1293
|
-
return _element_type_from_iterable(cls, reverse_type_map)
|
|
1294
|
-
|
|
1295
|
-
def _getitem_element_for_type_name(type_name: str) -> str:
|
|
1296
|
-
cls = _type_cls(type_name)
|
|
1297
|
-
if not cls:
|
|
1298
|
-
return ""
|
|
1299
|
-
return _element_type_from_getitem(cls, reverse_type_map)
|
|
1300
|
-
|
|
1301
|
-
for type_name, entry in TYPE_MAP.items():
|
|
1302
|
-
cls = entry.cls
|
|
1303
|
-
attrs: dict[str, AttrMeta] = {}
|
|
1304
|
-
methods: dict[str, MethodMeta] = {}
|
|
1305
|
-
iterable_element = _iter_element_for_type_name(type_name)
|
|
1306
|
-
getitem_element = _getitem_element_for_type_name(type_name)
|
|
1307
|
-
element_hint = iterable_element or getitem_element
|
|
1308
|
-
override_docs = {
|
|
1309
|
-
**_ATTR_DOC_OVERRIDES.get(type_name, {}),
|
|
1310
|
-
**_ATTR_DOC_OVERRIDES.get(cls.__name__, {}),
|
|
1311
|
-
}
|
|
1312
|
-
method_override_docs = {
|
|
1313
|
-
**_METHOD_DOC_OVERRIDES.get(type_name, {}),
|
|
1314
|
-
**_METHOD_DOC_OVERRIDES.get(cls.__name__, {}),
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
for attr in getattr(cls, "ATTRS", []):
|
|
1318
|
-
doc = ""
|
|
1319
|
-
type_name_hint = ""
|
|
1320
|
-
element_type_hint = ""
|
|
1321
|
-
try:
|
|
1322
|
-
attr_obj = getattr(cls, attr)
|
|
1323
|
-
except Exception:
|
|
1324
|
-
attr_obj = None
|
|
1325
|
-
if isinstance(attr_obj, property) and attr_obj.fget:
|
|
1326
|
-
doc = (attr_obj.fget.__doc__ or "").strip()
|
|
1327
|
-
ann = _return_annotation(attr_obj.fget, cls)
|
|
1328
|
-
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
1329
|
-
elif attr_obj is not None:
|
|
1330
|
-
doc = (getattr(attr_obj, "__doc__", "") or "").strip()
|
|
1331
|
-
if not type_name_hint and not element_type_hint:
|
|
1332
|
-
ann = _class_annotation(cls, attr)
|
|
1333
|
-
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
1334
|
-
if not type_name_hint and element_hint:
|
|
1335
|
-
type_name_hint = element_hint
|
|
1336
|
-
if type_name_hint and not element_type_hint:
|
|
1337
|
-
element_type_hint = _iter_element_for_type_name(type_name_hint)
|
|
1338
|
-
if not doc:
|
|
1339
|
-
doc = override_docs.get(attr, doc)
|
|
1340
|
-
attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
|
|
1341
|
-
|
|
1342
|
-
for meth in getattr(cls, "METHODS", []):
|
|
1343
|
-
doc = ""
|
|
1344
|
-
sig_label = ""
|
|
1345
|
-
try:
|
|
1346
|
-
meth_obj = getattr(cls, meth)
|
|
1347
|
-
except Exception:
|
|
1348
|
-
meth_obj = None
|
|
1349
|
-
if callable(meth_obj):
|
|
1350
|
-
sig_label = _format_method_signature(meth, meth_obj)
|
|
1351
|
-
doc = (meth_obj.__doc__ or "").strip()
|
|
1352
|
-
if not doc:
|
|
1353
|
-
doc = method_override_docs.get(meth, doc)
|
|
1354
|
-
methods[meth] = MethodMeta(signature=sig_label, doc=doc)
|
|
1355
|
-
|
|
1356
|
-
meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=element_hint)
|
|
1357
|
-
return meta
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
def _format_method_signature(name: str, obj: Any) -> str:
|
|
1361
|
-
try:
|
|
1362
|
-
sig = inspect.signature(obj)
|
|
1363
|
-
except (TypeError, ValueError):
|
|
1364
|
-
return f"{name}()"
|
|
1365
|
-
params = list(sig.parameters.values())
|
|
1366
|
-
if params and params[0].name in {"self", "cls"}:
|
|
1367
|
-
params = params[1:]
|
|
1368
|
-
sig = sig.replace(parameters=params)
|
|
1369
|
-
return f"{name}{sig}"
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
def _return_annotation(func: Any, cls: type) -> Any:
|
|
1373
|
-
try:
|
|
1374
|
-
module = inspect.getmodule(func) or inspect.getmodule(cls)
|
|
1375
|
-
globalns = module.__dict__ if module else None
|
|
1376
|
-
hints = typing.get_type_hints(func, globalns=globalns, include_extras=False)
|
|
1377
|
-
return hints.get("return")
|
|
1378
|
-
except Exception:
|
|
1379
|
-
return getattr(func, "__annotations__", {}).get("return")
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
def _class_annotation(cls: type, attr: str) -> Any:
|
|
1383
|
-
try:
|
|
1384
|
-
module = inspect.getmodule(cls)
|
|
1385
|
-
globalns = module.__dict__ if module else None
|
|
1386
|
-
hints = typing.get_type_hints(cls, globalns=globalns, include_extras=False)
|
|
1387
|
-
if attr in hints:
|
|
1388
|
-
return hints[attr]
|
|
1389
|
-
except Exception:
|
|
1390
|
-
pass
|
|
1391
|
-
return getattr(getattr(cls, "__annotations__", {}), "get", lambda _k: None)(attr)
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) -> tuple[str, str]:
|
|
1395
|
-
if ann is None:
|
|
1396
|
-
return "", ""
|
|
1397
|
-
if isinstance(ann, str):
|
|
1398
|
-
return "", ""
|
|
1399
|
-
try:
|
|
1400
|
-
origin = getattr(ann, "__origin__", None)
|
|
1401
|
-
except Exception:
|
|
1402
|
-
origin = None
|
|
1403
|
-
args = getattr(ann, "__args__", ()) if origin else ()
|
|
1404
|
-
|
|
1405
|
-
if ann in reverse_type_map:
|
|
1406
|
-
return reverse_type_map[ann], ""
|
|
1407
|
-
|
|
1408
|
-
# handle list/sequence typing to detect element type
|
|
1409
|
-
iterable_origins = {list, List, Iterable, typing.Sequence, typing.Iterable}
|
|
1410
|
-
try:
|
|
1411
|
-
from collections.abc import Iterable as ABCIterable, Sequence as ABCSequence
|
|
1412
|
-
iterable_origins.update({ABCIterable, ABCSequence})
|
|
1413
|
-
except Exception:
|
|
1414
|
-
pass
|
|
1415
|
-
if origin in iterable_origins:
|
|
1416
|
-
if args:
|
|
1417
|
-
elem = args[0]
|
|
1418
|
-
elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
|
|
1419
|
-
container_name = reverse_type_map.get(origin) or "list"
|
|
1420
|
-
return container_name, elem_name
|
|
1421
|
-
return reverse_type_map.get(origin) or "list", ""
|
|
1422
|
-
|
|
1423
|
-
if isinstance(ann, type) and ann in reverse_type_map:
|
|
1424
|
-
return reverse_type_map[ann], ""
|
|
1425
|
-
return "", ""
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) -> str:
|
|
1429
|
-
try:
|
|
1430
|
-
hints = typing.get_type_hints(cls.__iter__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
|
|
1431
|
-
ret_ann = hints.get("return")
|
|
1432
|
-
_, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
|
|
1433
|
-
return elem
|
|
1434
|
-
except Exception:
|
|
1435
|
-
return ""
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
def _element_type_from_getitem(cls: type, reverse_type_map: Dict[type, str]) -> str:
|
|
1439
|
-
try:
|
|
1440
|
-
hints = typing.get_type_hints(cls.__getitem__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
|
|
1441
|
-
ret_ann = hints.get("return")
|
|
1442
|
-
name, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
|
|
1443
|
-
return name or elem
|
|
1444
|
-
except Exception:
|
|
1445
|
-
return ""
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
500
|
def _infer_constant_bindings(code: str, upto_line: int | None, ctx_data: ContextData) -> Dict[str, Any]:
|
|
1449
501
|
try:
|
|
1450
502
|
tree = ast.parse(code)
|
|
@@ -1480,7 +532,7 @@ def _infer_constant_bindings(code: str, upto_line: int | None, ctx_data: Context
|
|
|
1480
532
|
if value is None:
|
|
1481
533
|
self.generic_visit(node)
|
|
1482
534
|
return
|
|
1483
|
-
for name in
|
|
535
|
+
for name in collect_target_names(node.targets):
|
|
1484
536
|
bindings[name] = value
|
|
1485
537
|
|
|
1486
538
|
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
@@ -1492,14 +544,14 @@ def _infer_constant_bindings(code: str, upto_line: int | None, ctx_data: Context
|
|
|
1492
544
|
if value is None:
|
|
1493
545
|
self.generic_visit(node)
|
|
1494
546
|
return
|
|
1495
|
-
for name in
|
|
547
|
+
for name in collect_target_names([node.target]):
|
|
1496
548
|
bindings[name] = value
|
|
1497
549
|
|
|
1498
550
|
def visit_For(self, node: ast.For):
|
|
1499
551
|
if limit is not None and node.lineno > limit:
|
|
1500
552
|
return
|
|
1501
553
|
loop_val = _loop_binding(node.iter)
|
|
1502
|
-
for name in
|
|
554
|
+
for name in collect_target_names([node.target]):
|
|
1503
555
|
bindings[name] = loop_val
|
|
1504
556
|
self.generic_visit(node)
|
|
1505
557
|
|
|
@@ -1507,7 +559,7 @@ def _infer_constant_bindings(code: str, upto_line: int | None, ctx_data: Context
|
|
|
1507
559
|
if limit is not None and node.lineno > limit:
|
|
1508
560
|
return
|
|
1509
561
|
loop_val = _loop_binding(node.iter)
|
|
1510
|
-
for name in
|
|
562
|
+
for name in collect_target_names([node.target]):
|
|
1511
563
|
bindings[name] = loop_val
|
|
1512
564
|
self.generic_visit(node)
|
|
1513
565
|
|
|
@@ -1515,16 +567,6 @@ def _infer_constant_bindings(code: str, upto_line: int | None, ctx_data: Context
|
|
|
1515
567
|
return bindings
|
|
1516
568
|
|
|
1517
569
|
|
|
1518
|
-
def _names_from_target(targets: Iterable[ast.expr]) -> List[str]:
|
|
1519
|
-
names: list[str] = []
|
|
1520
|
-
for target in targets:
|
|
1521
|
-
if isinstance(target, ast.Name):
|
|
1522
|
-
names.append(target.id)
|
|
1523
|
-
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
1524
|
-
names.extend(_names_from_target(target.elts))
|
|
1525
|
-
return names
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
570
|
def _literal_value(node: ast.AST) -> Any | None:
|
|
1529
571
|
if isinstance(node, ast.Constant):
|
|
1530
572
|
return node.value
|
|
@@ -1595,7 +637,7 @@ def _eval_node(node: ast.AST, ctx_data: ContextData, bindings: Dict[str, Any]) -
|
|
|
1595
637
|
if base is None:
|
|
1596
638
|
return None
|
|
1597
639
|
method_name = node.func.attr
|
|
1598
|
-
if not
|
|
640
|
+
if not is_safe_call(base, method_name):
|
|
1599
641
|
return None
|
|
1600
642
|
args = []
|
|
1601
643
|
for arg in node.args:
|
|
@@ -1630,13 +672,6 @@ def _eval_node(node: ast.AST, ctx_data: ContextData, bindings: Dict[str, Any]) -
|
|
|
1630
672
|
return None
|
|
1631
673
|
|
|
1632
674
|
|
|
1633
|
-
def _is_safe_call(base: Any, method: str) -> bool:
|
|
1634
|
-
for cls, allowed in SAFE_METHODS.items():
|
|
1635
|
-
if isinstance(base, cls) and method in allowed:
|
|
1636
|
-
return True
|
|
1637
|
-
return False
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
675
|
def _format_binding_hover(name: str, value: Any, label: str) -> types.Hover:
|
|
1641
676
|
type_name = _describe_type(value)
|
|
1642
677
|
preview = _preview_value(value)
|