avrae-ls 0.3.1__py3-none-any.whl → 0.4.1__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/alias_preview.py +175 -9
- avrae_ls/api.py +229 -229
- avrae_ls/argparser.py +16 -3
- avrae_ls/code_actions.py +282 -0
- avrae_ls/codes.py +3 -0
- avrae_ls/completions.py +489 -78
- avrae_ls/config.py +61 -2
- avrae_ls/context.py +62 -1
- avrae_ls/diagnostics.py +267 -5
- avrae_ls/parser.py +7 -2
- avrae_ls/runtime.py +94 -15
- avrae_ls/server.py +52 -6
- avrae_ls/signature_help.py +56 -5
- avrae_ls/symbols.py +149 -33
- avrae_ls-0.4.1.dist-info/METADATA +86 -0
- avrae_ls-0.4.1.dist-info/RECORD +34 -0
- avrae_ls-0.3.1.dist-info/METADATA +0 -47
- avrae_ls-0.3.1.dist-info/RECORD +0 -32
- {avrae_ls-0.3.1.dist-info → avrae_ls-0.4.1.dist-info}/WHEEL +0 -0
- {avrae_ls-0.3.1.dist-info → avrae_ls-0.4.1.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.3.1.dist-info → avrae_ls-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {avrae_ls-0.3.1.dist-info → avrae_ls-0.4.1.dist-info}/top_level.txt +0 -0
avrae_ls/completions.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import Any, ClassVar, Dict, Iterable, List, Optional
|
|
|
11
11
|
from lsprotocol import types
|
|
12
12
|
|
|
13
13
|
from .context import ContextData, GVarResolver
|
|
14
|
+
from .argparser import ParsedArguments
|
|
14
15
|
from .runtime import _default_builtins
|
|
15
16
|
from .api import (
|
|
16
17
|
AliasAction,
|
|
@@ -123,7 +124,9 @@ TYPE_MAP: Dict[str, object] = {
|
|
|
123
124
|
"attacks": AliasAttackList,
|
|
124
125
|
"attack": AliasAttack,
|
|
125
126
|
"skills": AliasSkills,
|
|
127
|
+
"AliasSkills": AliasSkills,
|
|
126
128
|
"skill": AliasSkill,
|
|
129
|
+
"AliasSkill": AliasSkill,
|
|
127
130
|
"saves": AliasSaves,
|
|
128
131
|
"resistances": AliasResistances,
|
|
129
132
|
"coinpurse": AliasCoinpurse,
|
|
@@ -147,6 +150,7 @@ TYPE_MAP: Dict[str, object] = {
|
|
|
147
150
|
"list": _BuiltinList,
|
|
148
151
|
"dict": _BuiltinDict,
|
|
149
152
|
"str": _BuiltinStr,
|
|
153
|
+
"ParsedArguments": ParsedArguments,
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
|
|
@@ -184,6 +188,226 @@ class TypeMeta:
|
|
|
184
188
|
element_type: str = ""
|
|
185
189
|
|
|
186
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
|
+
|
|
187
411
|
def gather_suggestions(
|
|
188
412
|
ctx_data: ContextData,
|
|
189
413
|
resolver: GVarResolver,
|
|
@@ -459,96 +683,257 @@ def _infer_type_map(code: str) -> Dict[str, str]:
|
|
|
459
683
|
tree = ast.parse(code)
|
|
460
684
|
except SyntaxError:
|
|
461
685
|
return {}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
self.
|
|
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)
|
|
476
774
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if elem_type and isinstance(node.target, ast.Name):
|
|
482
|
-
type_map[node.target.id] = elem_type
|
|
483
|
-
self.generic_visit(node)
|
|
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
|
|
484
779
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
type_map[node.target.id] = val_type
|
|
490
|
-
if elem_type:
|
|
491
|
-
type_map[f"{node.target.id}.__element__"] = elem_type
|
|
492
|
-
self._record_dict_key_types(node.target.id, node.value)
|
|
493
|
-
self.generic_visit(node)
|
|
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
|
|
494
784
|
|
|
495
|
-
|
|
496
|
-
|
|
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):
|
|
497
788
|
if value.func.id in {"character", "combat"}:
|
|
498
789
|
return value.func.id, None
|
|
499
790
|
if value.func.id == "vroll":
|
|
500
791
|
return "SimpleRollResult", None
|
|
792
|
+
if value.func.id == "argparse":
|
|
793
|
+
return "ParsedArguments", None
|
|
794
|
+
if value.func.id == "range":
|
|
795
|
+
return "range", "int"
|
|
501
796
|
if value.func.id in {"list", "dict", "str"}:
|
|
502
797
|
return value.func.id, None
|
|
503
|
-
if isinstance(value, ast.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if isinstance(value, ast.Name):
|
|
511
|
-
if value.id in type_map:
|
|
512
|
-
return type_map[value.id], type_map.get(f"{value.id}.__element__")
|
|
513
|
-
if value.id in {"character", "combat", "ctx"}:
|
|
514
|
-
return value.id, None
|
|
515
|
-
if isinstance(value, ast.Attribute):
|
|
516
|
-
attr_name = value.attr
|
|
517
|
-
base_type = None
|
|
518
|
-
base_elem = None
|
|
519
|
-
if isinstance(value.value, ast.Name):
|
|
520
|
-
base_type = type_map.get(value.value.id)
|
|
521
|
-
base_elem = type_map.get(f"{value.value.id}.__element__")
|
|
522
|
-
if base_type is None:
|
|
523
|
-
base_type, base_elem = self._value_type(value.value)
|
|
524
|
-
if base_type:
|
|
525
|
-
meta = _type_meta(base_type)
|
|
526
|
-
attr_meta = meta.attrs.get(attr_name)
|
|
527
|
-
if attr_meta:
|
|
528
|
-
if attr_meta.type_name:
|
|
529
|
-
return attr_meta.type_name, attr_meta.element_type or None
|
|
530
|
-
if attr_meta.element_type:
|
|
531
|
-
return base_type, attr_meta.element_type
|
|
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
|
|
532
805
|
if base_elem:
|
|
533
806
|
return base_elem, None
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
537
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
|
|
538
891
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
552
937
|
|
|
553
938
|
|
|
554
939
|
def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
|
|
@@ -615,10 +1000,22 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
|
615
1000
|
return ""
|
|
616
1001
|
return _element_type_from_iterable(cls, reverse_type_map)
|
|
617
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
|
+
|
|
618
1009
|
for type_name, cls in TYPE_MAP.items():
|
|
619
1010
|
attrs: dict[str, AttrMeta] = {}
|
|
620
1011
|
methods: dict[str, MethodMeta] = {}
|
|
621
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
|
+
}
|
|
622
1019
|
|
|
623
1020
|
for attr in getattr(cls, "ATTRS", []):
|
|
624
1021
|
doc = ""
|
|
@@ -637,8 +1034,12 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
|
637
1034
|
if not type_name_hint and not element_type_hint:
|
|
638
1035
|
ann = _class_annotation(cls, attr)
|
|
639
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
|
|
640
1039
|
if type_name_hint and not element_type_hint:
|
|
641
1040
|
element_type_hint = _iter_element_for_type_name(type_name_hint)
|
|
1041
|
+
if not doc:
|
|
1042
|
+
doc = override_docs.get(attr, doc)
|
|
642
1043
|
attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
|
|
643
1044
|
|
|
644
1045
|
for meth in getattr(cls, "METHODS", []):
|
|
@@ -653,7 +1054,7 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
|
653
1054
|
doc = (meth_obj.__doc__ or "").strip()
|
|
654
1055
|
methods[meth] = MethodMeta(signature=sig_label, doc=doc)
|
|
655
1056
|
|
|
656
|
-
meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=
|
|
1057
|
+
meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=element_hint)
|
|
657
1058
|
return meta
|
|
658
1059
|
|
|
659
1060
|
|
|
@@ -735,6 +1136,16 @@ def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) ->
|
|
|
735
1136
|
return ""
|
|
736
1137
|
|
|
737
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
|
+
|
|
738
1149
|
def _attribute_at_position(line_text: str, cursor: int) -> tuple[Optional[str], Optional[str]]:
|
|
739
1150
|
cursor = max(0, min(cursor, len(line_text)))
|
|
740
1151
|
for match in ATTR_AT_CURSOR_RE.finditer(line_text):
|