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.
- avrae_ls/__main__.py +54 -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 +2 -5
- avrae_ls/context.py +65 -34
- 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.3.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.3.dist-info/RECORD +0 -34
- {avrae_ls-0.6.3.dist-info → avrae_ls-0.7.0.dist-info}/WHEEL +0 -0
- {avrae_ls-0.6.3.dist-info → avrae_ls-0.7.0.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.6.3.dist-info → avrae_ls-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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()))
|