avrae-ls 0.6.3__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.
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import re
5
+ from typing import Dict, Iterable, Optional
6
+
7
+ from .type_system import resolve_type_key, type_meta
8
+
9
+
10
+ def annotation_label(node: ast.AST | None) -> str:
11
+ base, elem = _annotation_types(node)
12
+ if not base:
13
+ return ""
14
+ if elem:
15
+ return f"{base}[{elem}]"
16
+ return base
17
+
18
+
19
+ def infer_type_map(code: str, line: int | None = None) -> Dict[str, str]:
20
+ try:
21
+ tree = ast.parse(code, type_comments=True)
22
+ except SyntaxError:
23
+ return {}
24
+ visitor = _TypeInferencer(code)
25
+ visitor.visit(tree)
26
+ return visitor.export_types(line)
27
+
28
+
29
+ def resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
30
+ mapping = type_map or infer_type_map(code)
31
+ get_match = _DICT_GET_RE.match(receiver)
32
+ if get_match:
33
+ base, _, key = get_match.groups()
34
+ dict_key = f"{base}.{key}"
35
+ if dict_key in mapping:
36
+ return mapping[dict_key]
37
+ bracket = receiver.rfind("[")
38
+ if bracket != -1 and receiver.endswith("]"):
39
+ base_expr = receiver[:bracket]
40
+ elem_hint = mapping.get(f"{base_expr}.__element__")
41
+ if elem_hint:
42
+ return elem_hint
43
+ base_type = resolve_type_name(base_expr, code, mapping)
44
+ if base_type:
45
+ base_meta = type_meta(base_type)
46
+ if base_meta.element_type:
47
+ return base_meta.element_type
48
+ return base_type
49
+ receiver = receiver.rstrip("()")
50
+ if "." in receiver:
51
+ base_expr, attr_name = receiver.rsplit(".", 1)
52
+ base_type = resolve_type_name(base_expr, code, mapping)
53
+ if base_type:
54
+ meta = type_meta(base_type)
55
+ attr_key = attr_name.split("[", 1)[0]
56
+ attr_meta = meta.attrs.get(attr_key)
57
+ if attr_meta:
58
+ if attr_meta.element_type:
59
+ return attr_meta.element_type
60
+ if attr_meta.type_name:
61
+ return attr_meta.type_name
62
+
63
+ if receiver in mapping:
64
+ return mapping[receiver]
65
+ elem_key = f"{receiver}.__element__"
66
+ if elem_key in mapping:
67
+ return mapping[elem_key]
68
+ resolved_receiver = resolve_type_key(receiver)
69
+ if resolved_receiver:
70
+ return resolved_receiver
71
+ tail = receiver.split(".")[-1].split("[", 1)[0]
72
+ resolved_tail = resolve_type_key(tail)
73
+ if resolved_tail:
74
+ return resolved_tail
75
+ return receiver
76
+
77
+
78
+ _DICT_GET_RE = re.compile(r"^([A-Za-z_]\w*)\.get\(\s*(['\"])(.+?)\2")
79
+
80
+
81
+ def _split_annotation_string(text: str) -> tuple[Optional[str], Optional[str]]:
82
+ stripped = text.strip().strip("'\"")
83
+ if not stripped:
84
+ return None, None
85
+ match = re.match(
86
+ r"^([A-Za-z_][\w]*)\s*(?:\[\s*([A-Za-z_][\w]*)(?:\s*,\s*([A-Za-z_][\w]*))?\s*\])?$",
87
+ stripped,
88
+ )
89
+ if not match:
90
+ return stripped, None
91
+ base = match.group(1)
92
+ elem = match.group(3) or match.group(2)
93
+ base_norm = base.lower() if base.lower() in {"list", "dict", "set", "tuple"} else base
94
+ return base_norm, elem
95
+
96
+
97
+ def _annotation_types(node: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
98
+ if node is None:
99
+ return None, None
100
+ if isinstance(node, str):
101
+ return _split_annotation_string(node)
102
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
103
+ return _split_annotation_string(node.value)
104
+ if isinstance(node, ast.Str):
105
+ return _split_annotation_string(node.s)
106
+ if isinstance(node, ast.Name):
107
+ return node.id, None
108
+ if isinstance(node, ast.Attribute):
109
+ return node.attr, None
110
+ try:
111
+ text = ast.unparse(node)
112
+ except Exception:
113
+ text = ""
114
+ if text:
115
+ return _split_annotation_string(text)
116
+ return None, None
117
+
118
+
119
+ class _TypeInferencer(ast.NodeVisitor):
120
+ def __init__(self, code: str) -> None:
121
+ self.code = code
122
+ self._scopes: list[dict[str, str]] = [dict()]
123
+ self._scoped_maps: list[tuple[tuple[int, int], dict[str, str]]] = []
124
+
125
+ def _current_scope(self) -> dict[str, str]:
126
+ return self._scopes[-1]
127
+
128
+ def _push_scope(self) -> None:
129
+ self._scopes.append(dict())
130
+
131
+ def _pop_scope(self) -> None:
132
+ self._scopes.pop()
133
+
134
+ def export_types(self, line: int | None = None) -> dict[str, str]:
135
+ if line is not None:
136
+ for (start, end), scope in reversed(self._scoped_maps):
137
+ if start <= line <= end:
138
+ combined = dict(self._scopes[0])
139
+ combined.update(scope)
140
+ return combined
141
+ return dict(self._scopes[0])
142
+
143
+ def _set_type(self, key: str, value: Optional[str]) -> None:
144
+ if value:
145
+ self._current_scope()[key] = value
146
+
147
+ def _get_type(self, key: str) -> Optional[str]:
148
+ for scope in reversed(self._scopes):
149
+ if key in scope:
150
+ return scope[key]
151
+ return None
152
+
153
+ def visit_Assign(self, node: ast.Assign):
154
+ val_type, elem_type = self._value_type(node.value)
155
+ if getattr(node, "type_comment", None):
156
+ ann_type, ann_elem = _annotation_types(node.type_comment)
157
+ val_type = ann_type or val_type
158
+ elem_type = ann_elem or elem_type
159
+ for target in node.targets:
160
+ self._bind_target(target, val_type, elem_type, node.value)
161
+ self.generic_visit(node)
162
+
163
+ def visit_AnnAssign(self, node: ast.AnnAssign):
164
+ val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
165
+ ann_type, ann_elem = _annotation_types(getattr(node, "annotation", None))
166
+ val_type = val_type or ann_type
167
+ elem_type = elem_type or ann_elem
168
+ if getattr(node, "type_comment", None):
169
+ c_type, c_elem = _annotation_types(node.type_comment)
170
+ val_type = val_type or c_type
171
+ elem_type = elem_type or c_elem
172
+ self._bind_target(node.target, val_type, elem_type, node.value)
173
+ self.generic_visit(node)
174
+
175
+ def visit_AugAssign(self, node: ast.AugAssign):
176
+ val_type, elem_type = self._value_type(node.value)
177
+ self._bind_target(
178
+ node.target,
179
+ val_type or self._existing_type(node.target),
180
+ elem_type or self._existing_element(node.target),
181
+ None,
182
+ )
183
+ self.generic_visit(node)
184
+
185
+ def visit_For(self, node: ast.For):
186
+ _, elem_type = self._value_type(node.iter)
187
+ if not elem_type and isinstance(node.iter, ast.Name):
188
+ elem_type = self._get_type(f"{node.iter.id}.__element__")
189
+ self._bind_target(node.target, elem_type, None, None)
190
+ self.generic_visit(node)
191
+
192
+ def visit_AsyncFor(self, node: ast.AsyncFor):
193
+ _, elem_type = self._value_type(node.iter)
194
+ if not elem_type and isinstance(node.iter, ast.Name):
195
+ elem_type = self._get_type(f"{node.iter.id}.__element__")
196
+ self._bind_target(node.target, elem_type, None, None)
197
+ self.generic_visit(node)
198
+
199
+ def visit_FunctionDef(self, node: ast.FunctionDef):
200
+ self._push_scope()
201
+ try:
202
+ self._bind_function_args(node.args)
203
+ for stmt in node.body:
204
+ self.visit(stmt)
205
+ self._record_scope(node)
206
+ finally:
207
+ self._pop_scope()
208
+
209
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
210
+ self._push_scope()
211
+ try:
212
+ self._bind_function_args(node.args)
213
+ for stmt in node.body:
214
+ self.visit(stmt)
215
+ self._record_scope(node)
216
+ finally:
217
+ self._pop_scope()
218
+
219
+ def visit_ClassDef(self, node: ast.ClassDef):
220
+ self._push_scope()
221
+ try:
222
+ for stmt in node.body:
223
+ self.visit(stmt)
224
+ self._record_scope(node)
225
+ finally:
226
+ self._pop_scope()
227
+
228
+ def visit_If(self, node: ast.If):
229
+ self.visit(node.test)
230
+ base_map = self._current_scope().copy()
231
+ body_map = self._visit_block(node.body, base_map.copy())
232
+ orelse_seed = base_map.copy()
233
+ orelse_map = self._visit_block(node.orelse, orelse_seed) if node.orelse else orelse_seed
234
+ self._current_scope().update(self._merge_branch_types(base_map, body_map, orelse_map))
235
+
236
+ def _visit_block(self, nodes: Iterable[ast.stmt], seed: dict[str, str]) -> dict[str, str]:
237
+ walker = _TypeInferencer(self.code)
238
+ walker._scopes = [seed.copy()]
239
+ for stmt in nodes:
240
+ walker.visit(stmt)
241
+ self._scoped_maps.extend(walker._scoped_maps)
242
+ return walker._current_scope()
243
+
244
+ def _merge_branch_types(self, base: dict[str, str], left: dict[str, str], right: dict[str, str]) -> dict[str, str]:
245
+ merged = base.copy()
246
+ for key in set(left) | set(right):
247
+ l_val = left.get(key)
248
+ r_val = right.get(key)
249
+ if l_val and r_val and l_val == r_val:
250
+ merged[key] = l_val
251
+ elif key in base:
252
+ merged[key] = base[key]
253
+ elif l_val and not r_val:
254
+ merged[key] = l_val
255
+ elif r_val and not l_val:
256
+ merged[key] = r_val
257
+ elif key in merged:
258
+ merged.pop(key, None)
259
+ return merged
260
+
261
+ def _bind_target(self, target: ast.AST, val_type: Optional[str], elem_type: Optional[str], source: ast.AST | None):
262
+ if isinstance(target, ast.Name):
263
+ self._set_type(target.id, val_type)
264
+ self._set_type(f"{target.id}.__element__", elem_type)
265
+ if source is not None:
266
+ self._record_dict_key_types(target.id, source)
267
+ elif isinstance(target, (ast.Tuple, ast.List)):
268
+ if isinstance(source, (ast.Tuple, ast.List)) and len(source.elts or []) == len(target.elts):
269
+ for elt, val_node in zip(target.elts, source.elts):
270
+ elt_type, elt_elem = self._value_type(val_node)
271
+ self._bind_target(elt, elt_type, elt_elem, val_node)
272
+ else:
273
+ for elt in target.elts:
274
+ self._bind_target(elt, val_type, elem_type, source)
275
+
276
+ def _bind_function_args(self, args: ast.arguments) -> None:
277
+ for arg in getattr(args, "posonlyargs", []):
278
+ self._bind_arg_annotation(arg)
279
+ for arg in args.args:
280
+ self._bind_arg_annotation(arg)
281
+ if args.vararg:
282
+ self._bind_arg_annotation(args.vararg)
283
+ for arg in args.kwonlyargs:
284
+ self._bind_arg_annotation(arg)
285
+ if args.kwarg:
286
+ self._bind_arg_annotation(args.kwarg)
287
+
288
+ def _bind_arg_annotation(self, arg: ast.arg) -> None:
289
+ ann_type, elem_type = _annotation_types(getattr(arg, "annotation", None))
290
+ if ann_type:
291
+ self._set_type(arg.arg, ann_type)
292
+ if elem_type:
293
+ self._set_type(f"{arg.arg}.__element__", elem_type)
294
+
295
+ def _existing_type(self, target: ast.AST) -> Optional[str]:
296
+ if isinstance(target, ast.Name):
297
+ return self._get_type(target.id)
298
+ return None
299
+
300
+ def _existing_element(self, target: ast.AST) -> Optional[str]:
301
+ if isinstance(target, ast.Name):
302
+ return self._get_type(f"{target.id}.__element__")
303
+ return None
304
+
305
+ def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
306
+ if isinstance(value, ast.Call):
307
+ if isinstance(value.func, ast.Name):
308
+ if value.func.id in {"character", "combat"}:
309
+ return value.func.id, None
310
+ if value.func.id == "vroll":
311
+ return "SimpleRollResult", None
312
+ if value.func.id == "argparse":
313
+ return "ParsedArguments", None
314
+ if value.func.id == "range":
315
+ return "range", "int"
316
+ if value.func.id in {"list", "dict", "str", "int", "float"}:
317
+ return value.func.id, None
318
+ if isinstance(value.func, ast.Attribute):
319
+ base_type, base_elem = self._value_type(value.func.value)
320
+ if value.func.attr == "get" and value.args:
321
+ key_literal = self._literal_key(value.args[0])
322
+ val_type, elem_type = self._subscript_type(value.func.value, key_literal, base_type, base_elem)
323
+ if val_type:
324
+ return val_type, elem_type
325
+ if base_elem:
326
+ return base_elem, None
327
+ if isinstance(value, ast.Compare):
328
+ return "bool", None
329
+ if isinstance(value, ast.List):
330
+ elem_type, _ = self._iterable_element_from_values(value.elts)
331
+ return "list", elem_type
332
+ if isinstance(value, ast.Tuple):
333
+ elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
334
+ return "tuple", elem_type
335
+ if isinstance(value, ast.Set):
336
+ elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
337
+ return "set", elem_type
338
+ if isinstance(value, ast.ListComp):
339
+ comp_type, comp_elem = self._value_type(value.elt)
340
+ return "list", comp_type or comp_elem
341
+ if isinstance(value, ast.Dict):
342
+ elem_type, _ = self._iterable_element_from_values(value.values or [])
343
+ return "dict", elem_type
344
+ if isinstance(value, ast.Subscript):
345
+ return self._subscript_value_type(value)
346
+ if isinstance(value, ast.Constant):
347
+ if isinstance(value.value, str):
348
+ return "str", None
349
+ if isinstance(value, ast.Name):
350
+ existing = self._get_type(value.id)
351
+ if existing:
352
+ return existing, self._get_type(f"{value.id}.__element__")
353
+ if value.id in {"character", "combat", "ctx"}:
354
+ return value.id, None
355
+ if isinstance(value, ast.Attribute):
356
+ attr_name = value.attr
357
+ base_type = None
358
+ base_elem = None
359
+ if isinstance(value.value, ast.Name):
360
+ base_type = self._get_type(value.value.id)
361
+ base_elem = self._get_type(f"{value.value.id}.__element__")
362
+ if base_type is None:
363
+ base_type, base_elem = self._value_type(value.value)
364
+ if base_type:
365
+ meta = type_meta(base_type)
366
+ attr_meta = meta.attrs.get(attr_name)
367
+ if attr_meta:
368
+ if attr_meta.type_name:
369
+ return attr_meta.type_name, attr_meta.element_type or None
370
+ if attr_meta.element_type:
371
+ return base_type, attr_meta.element_type
372
+ if base_elem:
373
+ return base_elem, None
374
+ resolved_attr_type = resolve_type_key(attr_name, base_type)
375
+ if resolved_attr_type:
376
+ return resolved_attr_type, None
377
+ return None, None
378
+ if isinstance(value, ast.IfExp):
379
+ t_type, t_elem = self._value_type(value.body)
380
+ e_type, e_elem = self._value_type(value.orelse)
381
+ if t_type and e_type and t_type == e_type:
382
+ merged_elem = t_elem or e_elem
383
+ if t_elem and e_elem and t_elem != e_elem:
384
+ merged_elem = None
385
+ return t_type, merged_elem
386
+ return t_type or e_type, t_elem or e_elem
387
+ return None, None
388
+
389
+ def _iterable_element_from_values(self, values: Iterable[ast.AST]) -> tuple[Optional[str], Optional[str]]:
390
+ elem_type: Optional[str] = None
391
+ nested_elem: Optional[str] = None
392
+ for node in values:
393
+ val_type, inner_elem = self._value_type(node)
394
+ if not val_type:
395
+ return None, None
396
+ if elem_type is None:
397
+ elem_type = val_type
398
+ nested_elem = inner_elem
399
+ elif elem_type != val_type:
400
+ return None, None
401
+ if inner_elem:
402
+ if nested_elem is None:
403
+ nested_elem = inner_elem
404
+ elif nested_elem != inner_elem:
405
+ nested_elem = None
406
+ return elem_type, nested_elem
407
+
408
+ def _literal_key(self, node: ast.AST | None) -> str | int | None:
409
+ if isinstance(node, ast.Constant):
410
+ if isinstance(node.value, (str, int)):
411
+ return node.value
412
+ if hasattr(ast, "Index") and isinstance(node, getattr(ast, "Index")):
413
+ return self._literal_key(getattr(node, "value", None))
414
+ return None
415
+
416
+ def _subscript_type(
417
+ self,
418
+ base_expr: ast.AST,
419
+ key_literal: str | int | None,
420
+ base_type: Optional[str],
421
+ base_elem: Optional[str],
422
+ ) -> tuple[Optional[str], Optional[str]]:
423
+ base_name = base_expr.id if isinstance(base_expr, ast.Name) else None
424
+ if base_name and key_literal is not None:
425
+ dict_key = f"{base_name}.{key_literal}"
426
+ dict_type = self._get_type(dict_key)
427
+ if dict_type:
428
+ return dict_type, self._get_type(f"{dict_key}.__element__")
429
+ elem_hint = base_elem
430
+ if base_name and not elem_hint:
431
+ elem_hint = self._get_type(f"{base_name}.__element__")
432
+ if base_type:
433
+ meta = type_meta(base_type)
434
+ if key_literal is not None and key_literal in meta.attrs:
435
+ attr_meta = meta.attrs[key_literal]
436
+ if attr_meta.type_name:
437
+ return attr_meta.type_name, attr_meta.element_type or None
438
+ if attr_meta.element_type:
439
+ return base_type, attr_meta.element_type
440
+ elem_hint = elem_hint or meta.element_type
441
+ if elem_hint:
442
+ return elem_hint, None
443
+ return base_type, None
444
+
445
+ def _subscript_value_type(self, node: ast.Subscript) -> tuple[Optional[str], Optional[str]]:
446
+ base_type, base_elem = self._value_type(node.value)
447
+ key_literal = self._literal_key(getattr(node, "slice", None))
448
+ return self._subscript_type(node.value, key_literal, base_type, base_elem)
449
+
450
+ def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
451
+ if not isinstance(value, ast.Dict):
452
+ return
453
+ for key_node, val_node in zip(value.keys or [], value.values or []):
454
+ key_literal = self._literal_key(key_node)
455
+ if key_literal is None:
456
+ continue
457
+ val_type, elem_type = self._value_type(val_node)
458
+ if val_type:
459
+ self._set_type(f"{var_name}.{key_literal}", val_type)
460
+ if elem_type:
461
+ self._set_type(f"{var_name}.{key_literal}.__element__", elem_type)
462
+
463
+ def _record_scope(self, node: ast.AST) -> None:
464
+ start = getattr(node, "lineno", None)
465
+ end = getattr(node, "end_lineno", start)
466
+ if start is None or end is None:
467
+ return
468
+ start = max(start - 1, 0)
469
+ end = max(end - 1, start)
470
+ self._scoped_maps.append(((start, end), self._current_scope().copy()))