avrae-ls 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
avrae_ls/completions.py CHANGED
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ import inspect
4
5
  import re
6
+ import typing
5
7
  from dataclasses import dataclass
8
+ from functools import lru_cache
6
9
  from typing import Any, Dict, Iterable, List, Optional
7
10
 
8
11
  from lsprotocol import types
@@ -75,8 +78,9 @@ TYPE_MAP: Dict[str, object] = {
75
78
 
76
79
 
77
80
  IDENT_RE = re.compile(r"[A-Za-z_]\w*$")
78
- ATTR_RE = re.compile(r"([A-Za-z_][\w\.]*)\.([A-Za-z_]\w*)?\s*$")
79
- ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.]*?(?:\(\))?)\.([A-Za-z_]\w*)")
81
+ ATTR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.(?:([A-Za-z_]\w*)\s*)?$")
82
+ DICT_GET_RE = re.compile(r"^([A-Za-z_]\w*)\.get\(\s*(['\"])(.+?)\2")
83
+ ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.([A-Za-z_]\w*)")
80
84
 
81
85
 
82
86
  @dataclass
@@ -87,6 +91,25 @@ class Suggestion:
87
91
  documentation: str = ""
88
92
 
89
93
 
94
+ @dataclass
95
+ class AttrMeta:
96
+ doc: str = ""
97
+ type_name: str = ""
98
+ element_type: str = ""
99
+
100
+
101
+ @dataclass
102
+ class MethodMeta:
103
+ signature: str = ""
104
+ doc: str = ""
105
+
106
+
107
+ @dataclass
108
+ class TypeMeta:
109
+ attrs: Dict[str, AttrMeta]
110
+ methods: Dict[str, MethodMeta]
111
+
112
+
90
113
  def gather_suggestions(
91
114
  ctx_data: ContextData,
92
115
  resolver: GVarResolver,
@@ -130,15 +153,14 @@ def completion_items_for_position(
130
153
  character: int,
131
154
  suggestions: Iterable[Suggestion],
132
155
  ) -> List[types.CompletionItem]:
133
- line_text = _line_text_to_cursor(code, line, character)
134
- attr_match = ATTR_RE.search(line_text)
135
- if attr_match:
136
- receiver = attr_match.group(1)
137
- attr_prefix = attr_match.group(2) or ""
156
+ attr_ctx = _attribute_receiver_and_prefix(code, line, character)
157
+ if attr_ctx:
158
+ receiver, attr_prefix = attr_ctx
138
159
  sanitized = _sanitize_incomplete_line(code, line, character)
139
160
  type_map = _infer_type_map(sanitized)
140
161
  return _attribute_completions(receiver, attr_prefix, sanitized, type_map)
141
162
 
163
+ line_text = _line_text_to_cursor(code, line, character)
142
164
  prefix = _current_prefix(line_text)
143
165
  items: list[types.CompletionItem] = []
144
166
  for sugg in suggestions:
@@ -158,10 +180,10 @@ def completion_items_for_position(
158
180
  def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict[str, str] | None = None) -> List[types.CompletionItem]:
159
181
  items: list[types.CompletionItem] = []
160
182
  type_key = _resolve_type_name(receiver, code, type_map)
161
- attrs, methods = _type_meta(type_key)
183
+ meta = _type_meta(type_key)
162
184
  detail = f"{type_key}()"
163
185
 
164
- for name in attrs:
186
+ for name, attr_meta in meta.attrs.items():
165
187
  if prefix and not name.startswith(prefix):
166
188
  continue
167
189
  items.append(
@@ -169,16 +191,19 @@ def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict
169
191
  label=name,
170
192
  kind=types.CompletionItemKind.Field,
171
193
  detail=detail,
194
+ documentation=attr_meta.doc or None,
172
195
  )
173
196
  )
174
- for name in methods:
197
+ for name, method_meta in meta.methods.items():
175
198
  if prefix and not name.startswith(prefix):
176
199
  continue
200
+ method_detail = method_meta.signature or f"{name}()"
177
201
  items.append(
178
202
  types.CompletionItem(
179
203
  label=name,
180
204
  kind=types.CompletionItemKind.Method,
181
- detail=f"{detail} method",
205
+ detail=method_detail,
206
+ documentation=method_meta.doc or None,
182
207
  )
183
208
  )
184
209
  return items
@@ -195,29 +220,39 @@ def hover_for_position(
195
220
  line_text = _line_text(code, line)
196
221
  type_map = _infer_type_map(code)
197
222
  bindings = _infer_constant_bindings(code, line, ctx_data)
198
- receiver, attr_name = _attribute_at_position(line_text, character)
199
- if receiver and attr_name:
223
+ attr_ctx = _attribute_receiver_and_prefix(code, line, character, capture_full_token=True)
224
+ if attr_ctx:
225
+ receiver, attr_prefix = attr_ctx
200
226
  inferred = _resolve_type_name(receiver, code, type_map)
201
- attrs, methods = _type_meta(inferred)
202
- if attr_name in attrs:
203
- contents = f"`{inferred}().{attr_name}`"
227
+ meta = _type_meta(inferred)
228
+ if attr_prefix in meta.attrs:
229
+ doc = meta.attrs[attr_prefix].doc
230
+ contents = f"```avrae\n{inferred}().{attr_prefix}\n```"
231
+ if doc:
232
+ contents += f"\n\n{doc}"
204
233
  return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
205
- if attr_name in methods:
206
- contents = f"`{inferred}().{attr_name}()`"
234
+ if attr_prefix in meta.methods:
235
+ method_meta = meta.methods[attr_prefix]
236
+ signature = method_meta.signature or f"{attr_prefix}()"
237
+ doc = method_meta.doc
238
+ contents = f"```avrae\n{signature}\n```"
239
+ if doc:
240
+ contents += f"\n\n{doc}"
207
241
  return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
208
242
 
209
243
  word, _, _ = _word_at_position(line_text, character)
210
244
  if not word:
211
245
  return None
246
+ if word in bindings:
247
+ return _format_binding_hover(word, bindings[word], "local")
212
248
  if word in type_map:
213
- contents = f"`{word}` type: `{type_map[word]}()`"
249
+ type_label = _display_type_label(type_map[word])
250
+ contents = f"`{word}` type: `{type_label}`"
214
251
  return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
215
252
  if word in sigs:
216
253
  sig = sigs[word]
217
254
  contents = f"```avrae\n{sig.label}\n```\n\n{sig.doc}"
218
255
  return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
219
- if word in bindings:
220
- return _format_binding_hover(word, bindings[word], "local")
221
256
 
222
257
  vars_map = ctx_data.vars.to_initial_names()
223
258
  if word in vars_map:
@@ -256,6 +291,49 @@ def _line_text_to_cursor(code: str, line: int, character: int) -> str:
256
291
  return lines[line][:character]
257
292
 
258
293
 
294
+ def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture_full_token: bool = False) -> Optional[tuple[str, str]]:
295
+ lines = code.splitlines()
296
+ if line >= len(lines):
297
+ return None
298
+ line_text = lines[line]
299
+ end = character
300
+ if capture_full_token:
301
+ while end < len(line_text) and (line_text[end].isalnum() or line_text[end] == "_"):
302
+ end += 1
303
+ line_text = line_text[: end]
304
+ dot = line_text.rfind(".")
305
+ if dot == -1:
306
+ return None
307
+ prefix = line_text[dot + 1 :].strip()
308
+ placeholder = "__COMPLETE__"
309
+ new_line = f"{line_text[:dot]}.{placeholder}"
310
+ mod_lines = list(lines)
311
+ mod_lines[line] = new_line
312
+ mod_code = "\n".join(mod_lines)
313
+ try:
314
+ tree = ast.parse(mod_code)
315
+ except SyntaxError:
316
+ return None
317
+
318
+ receiver_src: Optional[str] = None
319
+
320
+ class Finder(ast.NodeVisitor):
321
+ def visit_Attribute(self, node: ast.Attribute):
322
+ nonlocal receiver_src
323
+ if isinstance(node.attr, str) and node.attr == placeholder:
324
+ try:
325
+ receiver_src = ast.unparse(node.value)
326
+ except Exception:
327
+ receiver_src = None
328
+ self.generic_visit(node)
329
+
330
+ Finder().visit(tree)
331
+ if receiver_src is None:
332
+ return None
333
+ cleaned = re.sub(r"\[[^\]]*\]", "", receiver_src)
334
+ return cleaned, prefix
335
+
336
+
259
337
  def _sanitize_incomplete_line(code: str, line: int, character: int) -> str:
260
338
  lines = code.splitlines()
261
339
  if 0 <= line < len(lines):
@@ -273,6 +351,12 @@ def _line_text(code: str, line: int) -> str:
273
351
  return lines[line]
274
352
 
275
353
 
354
+ def _display_type_label(type_key: str) -> str:
355
+ if type_key in TYPE_MAP:
356
+ return TYPE_MAP[type_key].__name__
357
+ return type_key
358
+
359
+
276
360
  def _infer_receiver_type(code: str, name: str) -> Optional[str]:
277
361
  return _infer_type_map(code).get(name)
278
362
 
@@ -287,16 +371,20 @@ def _infer_type_map(code: str) -> Dict[str, str]:
287
371
  class Visitor(ast.NodeVisitor):
288
372
  def visit_Assign(self, node: ast.Assign):
289
373
  val_type = self._value_type(node.value)
290
- if val_type:
291
- for target in node.targets:
292
- if isinstance(target, ast.Name):
293
- type_map[target.id] = val_type
374
+ for target in node.targets:
375
+ if not isinstance(target, ast.Name):
376
+ continue
377
+ if val_type:
378
+ type_map[target.id] = val_type
379
+ self._record_dict_key_types(target.id, node.value)
294
380
  self.generic_visit(node)
295
381
 
296
382
  def visit_AnnAssign(self, node: ast.AnnAssign):
297
383
  val_type = self._value_type(node.value) if node.value else None
298
- if val_type and isinstance(node.target, ast.Name):
299
- type_map[node.target.id] = val_type
384
+ if isinstance(node.target, ast.Name):
385
+ if val_type:
386
+ type_map[node.target.id] = val_type
387
+ self._record_dict_key_types(node.target.id, node.value)
300
388
  self.generic_visit(node)
301
389
 
302
390
  def _value_type(self, value: ast.AST | None) -> Optional[str]:
@@ -322,13 +410,40 @@ def _infer_type_map(code: str) -> Dict[str, str]:
322
410
  return None
323
411
  return None
324
412
 
413
+ def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
414
+ if not isinstance(value, ast.Dict):
415
+ return
416
+ for key_node, val_node in zip(value.keys or [], value.values or []):
417
+ if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
418
+ val_type = self._value_type(val_node)
419
+ if val_type:
420
+ type_map[f"{var_name}.{key_node.value}"] = val_type
421
+
325
422
  Visitor().visit(tree)
326
423
  return type_map
327
424
 
328
425
 
329
426
  def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
330
- receiver = receiver.rstrip("()")
331
427
  mapping = type_map or _infer_type_map(code)
428
+ get_match = DICT_GET_RE.match(receiver)
429
+ if get_match:
430
+ base, _, key = get_match.groups()
431
+ dict_key = f"{base}.{key}"
432
+ if dict_key in mapping:
433
+ return mapping[dict_key]
434
+ receiver = receiver.rstrip("()")
435
+ if "." in receiver:
436
+ base_expr, attr_name = receiver.rsplit(".", 1)
437
+ base_type = _resolve_type_name(base_expr, code, mapping)
438
+ if base_type:
439
+ meta = _type_meta(base_type)
440
+ attr_meta = meta.attrs.get(attr_name)
441
+ if attr_meta:
442
+ if attr_meta.element_type:
443
+ return attr_meta.element_type
444
+ if attr_meta.type_name:
445
+ return attr_meta.type_name
446
+
332
447
  if receiver in mapping:
333
448
  return mapping[receiver]
334
449
  if receiver in TYPE_MAP:
@@ -339,13 +454,139 @@ def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None
339
454
  return receiver
340
455
 
341
456
 
342
- def _type_meta(type_name: str) -> tuple[List[str], List[str]]:
343
- cls = TYPE_MAP.get(type_name)
344
- if cls is None:
345
- return [], []
346
- attrs = list(getattr(cls, "ATTRS", []))
347
- methods = list(getattr(cls, "METHODS", []))
348
- return sorted(set(attrs)), sorted(set(methods))
457
+ def _type_meta(type_name: str) -> TypeMeta:
458
+ return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}))
459
+
460
+
461
+ @lru_cache()
462
+ def _type_meta_map() -> Dict[str, TypeMeta]:
463
+ meta: dict[str, TypeMeta] = {}
464
+ reverse_type_map: dict[type, str] = {}
465
+ for key, cls in TYPE_MAP.items():
466
+ reverse_type_map[cls] = key
467
+
468
+ def _iter_element_for_type_name(type_name: str) -> str:
469
+ cls = TYPE_MAP.get(type_name)
470
+ if not cls:
471
+ return ""
472
+ return _element_type_from_iterable(cls, reverse_type_map)
473
+
474
+ for type_name, cls in TYPE_MAP.items():
475
+ attrs: dict[str, AttrMeta] = {}
476
+ methods: dict[str, MethodMeta] = {}
477
+
478
+ for attr in getattr(cls, "ATTRS", []):
479
+ doc = ""
480
+ type_name_hint = ""
481
+ element_type_hint = ""
482
+ try:
483
+ attr_obj = getattr(cls, attr)
484
+ except Exception:
485
+ attr_obj = None
486
+ if isinstance(attr_obj, property) and attr_obj.fget:
487
+ doc = (attr_obj.fget.__doc__ or "").strip()
488
+ ann = _return_annotation(attr_obj.fget, cls)
489
+ type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
490
+ elif attr_obj is not None:
491
+ doc = (getattr(attr_obj, "__doc__", "") or "").strip()
492
+ if not type_name_hint and not element_type_hint:
493
+ ann = _class_annotation(cls, attr)
494
+ type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
495
+ if type_name_hint and not element_type_hint:
496
+ element_type_hint = _iter_element_for_type_name(type_name_hint)
497
+ attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
498
+
499
+ for meth in getattr(cls, "METHODS", []):
500
+ doc = ""
501
+ sig_label = ""
502
+ try:
503
+ meth_obj = getattr(cls, meth)
504
+ except Exception:
505
+ meth_obj = None
506
+ if callable(meth_obj):
507
+ sig_label = _format_method_signature(meth, meth_obj)
508
+ doc = (meth_obj.__doc__ or "").strip()
509
+ methods[meth] = MethodMeta(signature=sig_label, doc=doc)
510
+
511
+ meta[type_name] = TypeMeta(attrs=attrs, methods=methods)
512
+ return meta
513
+
514
+
515
+ def _format_method_signature(name: str, obj: Any) -> str:
516
+ try:
517
+ sig = inspect.signature(obj)
518
+ except (TypeError, ValueError):
519
+ return f"{name}()"
520
+ params = list(sig.parameters.values())
521
+ if params and params[0].name in {"self", "cls"}:
522
+ params = params[1:]
523
+ sig = sig.replace(parameters=params)
524
+ return f"{name}{sig}"
525
+
526
+
527
+ def _return_annotation(func: Any, cls: type) -> Any:
528
+ try:
529
+ module = inspect.getmodule(func) or inspect.getmodule(cls)
530
+ globalns = module.__dict__ if module else None
531
+ hints = typing.get_type_hints(func, globalns=globalns, include_extras=False)
532
+ return hints.get("return")
533
+ except Exception:
534
+ return getattr(func, "__annotations__", {}).get("return")
535
+
536
+
537
+ def _class_annotation(cls: type, attr: str) -> Any:
538
+ try:
539
+ module = inspect.getmodule(cls)
540
+ globalns = module.__dict__ if module else None
541
+ hints = typing.get_type_hints(cls, globalns=globalns, include_extras=False)
542
+ if attr in hints:
543
+ return hints[attr]
544
+ except Exception:
545
+ pass
546
+ return getattr(getattr(cls, "__annotations__", {}), "get", lambda _k: None)(attr)
547
+
548
+
549
+ def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) -> tuple[str, str]:
550
+ if ann is None:
551
+ return "", ""
552
+ if isinstance(ann, str):
553
+ return "", ""
554
+ try:
555
+ origin = getattr(ann, "__origin__", None)
556
+ except Exception:
557
+ origin = None
558
+ args = getattr(ann, "__args__", ()) if origin else ()
559
+
560
+ if ann in reverse_type_map:
561
+ return reverse_type_map[ann], ""
562
+
563
+ # handle list/sequence typing to detect element type
564
+ iterable_origins = {list, List, Iterable, typing.Sequence, typing.Iterable}
565
+ try:
566
+ from collections.abc import Iterable as ABCIterable, Sequence as ABCSequence
567
+ iterable_origins.update({ABCIterable, ABCSequence})
568
+ except Exception:
569
+ pass
570
+ if origin in iterable_origins:
571
+ if args:
572
+ elem = args[0]
573
+ elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
574
+ return "", elem_name
575
+ return "", ""
576
+
577
+ if isinstance(ann, type) and ann in reverse_type_map:
578
+ return reverse_type_map[ann], ""
579
+ return "", ""
580
+
581
+
582
+ def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) -> str:
583
+ try:
584
+ hints = typing.get_type_hints(cls.__iter__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
585
+ ret_ann = hints.get("return")
586
+ _, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
587
+ return elem
588
+ except Exception:
589
+ return ""
349
590
 
350
591
 
351
592
  def _attribute_at_position(line_text: str, cursor: int) -> tuple[Optional[str], Optional[str]]:
@@ -550,12 +791,33 @@ def _is_safe_call(base: Any, method: str) -> bool:
550
791
 
551
792
 
552
793
  def _format_binding_hover(name: str, value: Any, label: str) -> types.Hover:
553
- type_name = type(value).__name__
794
+ type_name = _describe_type(value)
554
795
  preview = _preview_value(value)
555
796
  contents = f"**{label}** `{name}`\n\nType: `{type_name}`\nValue: `{preview}`"
556
797
  return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
557
798
 
558
799
 
800
+ def _describe_type(value: Any) -> str:
801
+ # Provide light element-type hints for common iterables so hover shows list[Foo].
802
+ def _iterable_type(iterable: Iterable[Any], container: str) -> str:
803
+ try:
804
+ seen = {type(item).__name__ for item in iterable if item is not None}
805
+ except Exception:
806
+ return container
807
+ return f"{container}[{seen.pop()}]" if len(seen) == 1 else container
808
+
809
+ try:
810
+ if isinstance(value, list):
811
+ return _iterable_type(value, "list")
812
+ if isinstance(value, tuple):
813
+ return _iterable_type(value, "tuple")
814
+ if isinstance(value, set):
815
+ return _iterable_type(value, "set")
816
+ except Exception:
817
+ pass
818
+ return type(value).__name__
819
+
820
+
559
821
  def _preview_value(value: Any) -> str:
560
822
  text = repr(value)
561
823
  if len(text) > 120:
avrae_ls/context.py CHANGED
@@ -93,31 +93,49 @@ class GVarResolver:
93
93
  async def ensure(self, key: str) -> bool:
94
94
  key = str(key)
95
95
  if key in self._cache:
96
+ log.debug("GVAR ensure cache hit for %s", key)
96
97
  return True
97
98
  if not self._config.enable_gvar_fetch:
99
+ log.warning("GVAR fetch disabled; skipping %s", key)
98
100
  return False
99
101
  if not self._config.service.token:
100
102
  log.debug("GVAR fetch skipped for %s: no token configured", key)
101
103
  return False
102
104
 
103
105
  base_url = self._config.service.base_url.rstrip("/")
104
- url = f"{base_url}/gvars/{key}"
105
- headers = {"Authorization": f"Bearer {self._config.service.token}"}
106
+ url = f"{base_url}/customizations/gvars/{key}"
107
+ # Avrae service expects the JWT directly in Authorization (no Bearer prefix).
108
+ headers = {"Authorization": str(self._config.service.token)}
106
109
  try:
110
+ log.debug("GVAR fetching %s from %s", key, url)
107
111
  async with httpx.AsyncClient(timeout=5) as client:
108
112
  resp = await client.get(url, headers=headers)
109
113
  except Exception as exc:
110
- log.debug("GVAR fetch failed for %s: %s", key, exc)
114
+ log.error("GVAR fetch failed for %s: %s", key, exc)
111
115
  return False
112
116
 
113
117
  if resp.status_code != 200:
114
- log.debug("GVAR fetch returned %s for %s", resp.status_code, key)
118
+ log.warning(
119
+ "GVAR fetch returned %s for %s (body: %s)",
120
+ resp.status_code,
121
+ key,
122
+ (resp.text or "").strip(),
123
+ )
115
124
  return False
116
125
 
117
- payload = resp.json()
118
- value = payload.get("value") or payload.get("content") or payload.get("body")
126
+ value: Any = None
127
+ try:
128
+ payload = resp.json()
129
+ except Exception:
130
+ payload = None
131
+
132
+ if isinstance(payload, dict) and "value" in payload:
133
+ value = payload["value"]
134
+
135
+ log.debug("GVAR fetch parsed value for %s (type=%s)", key, type(value).__name__)
136
+
119
137
  if value is None:
120
- log.debug("GVAR %s payload missing value", key)
138
+ log.error("GVAR %s payload missing value", key)
121
139
  return False
122
140
  self._cache[key] = value
123
141
  return True
avrae_ls/diagnostics.py CHANGED
@@ -149,6 +149,13 @@ class DiagnosticProvider:
149
149
  )
150
150
  )
151
151
 
152
+ def visit_Call(self, node: ast.Call):
153
+ if isinstance(node.func, ast.Name) and node.func.id == "using":
154
+ for kw in node.keywords:
155
+ if kw.arg:
156
+ self.tracker.add(str(kw.arg))
157
+ self.generic_visit(node)
158
+
152
159
  walker = Walker(known)
153
160
  for stmt in body:
154
161
  walker.visit(stmt)
@@ -200,24 +207,41 @@ async def _check_gvars(
200
207
  settings: DiagnosticSettings,
201
208
  ) -> List[types.Diagnostic]:
202
209
  diagnostics: list[types.Diagnostic] = []
210
+ seen: set[str] = set()
211
+
212
+ def _literal_value(node: ast.AST) -> str | None:
213
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
214
+ return node.value
215
+ if isinstance(node, ast.Str):
216
+ return node.s
217
+ return None
218
+
203
219
  for node in _iter_calls(body):
204
- if not isinstance(node.func, ast.Name) or node.func.id != "get_gvar":
220
+ if not isinstance(node.func, ast.Name):
205
221
  continue
206
- if not node.args:
207
- continue
208
- arg = node.args[0]
209
- if isinstance(arg, (ast.Str, ast.Constant)) and isinstance(getattr(arg, "s", None) or arg.value, str):
210
- gvar_id = arg.s if isinstance(arg, ast.Str) else arg.value
222
+
223
+ async def _validate_gvar(arg_node: ast.AST):
224
+ gvar_id = _literal_value(arg_node)
225
+ if gvar_id is None or gvar_id in seen:
226
+ return
227
+ seen.add(gvar_id)
211
228
  found_local = resolver.get_local(gvar_id)
212
229
  ensured = found_local is not None or await resolver.ensure(gvar_id)
213
230
  if not ensured:
214
231
  diagnostics.append(
215
232
  _make_diagnostic(
216
- arg,
233
+ arg_node,
217
234
  f"Unknown gvar '{gvar_id}'",
218
235
  settings.semantic_level,
219
236
  )
220
237
  )
238
+
239
+ if node.func.id == "get_gvar":
240
+ if node.args:
241
+ await _validate_gvar(node.args[0])
242
+ elif node.func.id == "using":
243
+ for kw in node.keywords:
244
+ await _validate_gvar(kw.value)
221
245
  return diagnostics
222
246
 
223
247
 
@@ -342,7 +366,7 @@ def _build_builtin_signatures() -> dict[str, inspect.Signature]:
342
366
  def get(name, default=None): ...
343
367
  def using(**imports): ...
344
368
  def signature(data=0): ...
345
- def verify_signature(sig=None): ...
369
+ def verify_signature(data): ...
346
370
  def print_fn(*args, sep=" ", end="\n"): ...
347
371
 
348
372
  helpers = {