avrae-ls 0.4.1__py3-none-any.whl → 0.5.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/diagnostics.py DELETED
@@ -1,751 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ast
4
- import inspect
5
- import logging
6
- from typing import Any, Dict, Iterable, List, Sequence, Set
7
-
8
- import draconic
9
- from lsprotocol import types
10
-
11
- from .alias_preview import simulate_command
12
- from .codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
13
- from .argument_parsing import apply_argument_parsing
14
- from .completions import _infer_type_map, _resolve_type_name, _type_meta
15
- from .config import DiagnosticSettings
16
- from .context import ContextData, GVarResolver
17
- from .parser import find_draconic_blocks
18
- from .runtime import MockExecutor, _default_builtins
19
-
20
- log = logging.getLogger(__name__)
21
-
22
- SEVERITY = {
23
- "error": types.DiagnosticSeverity.Error,
24
- "warning": types.DiagnosticSeverity.Warning,
25
- "info": types.DiagnosticSeverity.Information,
26
- }
27
-
28
-
29
- class DiagnosticProvider:
30
- def __init__(self, executor: MockExecutor, settings: DiagnosticSettings):
31
- self._executor = executor
32
- self._settings = settings
33
- self._builtin_signatures = _build_builtin_signatures()
34
-
35
- async def analyze(
36
- self,
37
- source: str,
38
- ctx_data: ContextData,
39
- gvar_resolver: GVarResolver,
40
- ) -> List[types.Diagnostic]:
41
- diagnostics: list[types.Diagnostic] = []
42
-
43
- source = apply_argument_parsing(source)
44
- blocks = find_draconic_blocks(source)
45
- if not blocks:
46
- plain = _plain_command_diagnostics(source)
47
- if plain is not None:
48
- diagnostics.extend(plain)
49
- return diagnostics
50
- diagnostics.extend(await self._analyze_code(source, ctx_data, gvar_resolver))
51
- return diagnostics
52
-
53
- for block in blocks:
54
- block_diags = await self._analyze_code(block.code, ctx_data, gvar_resolver)
55
- diagnostics.extend(_shift_diagnostics(block_diags, block.line_offset, block.char_offset))
56
- return diagnostics
57
-
58
- async def _analyze_code(
59
- self,
60
- code: str,
61
- ctx_data: ContextData,
62
- gvar_resolver: GVarResolver,
63
- ) -> List[types.Diagnostic]:
64
- diagnostics: list[types.Diagnostic] = []
65
- parser = draconic.DraconicInterpreter()
66
- line_shift = 0
67
- try:
68
- body = parser.parse(code)
69
- except draconic.DraconicSyntaxError as exc:
70
- wrapped, added = _wrap_draconic(code)
71
- try:
72
- body = parser.parse(wrapped)
73
- line_shift = -added
74
- except draconic.DraconicSyntaxError:
75
- diagnostics.append(_syntax_diagnostic(exc))
76
- return diagnostics
77
- except SyntaxError as exc:
78
- diagnostics.append(_syntax_from_std(exc))
79
- return diagnostics
80
-
81
- diagnostics.extend(
82
- self._check_unknown_names(body, ctx_data, self._settings.semantic_level)
83
- )
84
- diagnostics.extend(await _check_gvars(body, gvar_resolver, self._settings))
85
- diagnostics.extend(_check_imports(body, self._settings.semantic_level))
86
- diagnostics.extend(_check_call_args(body, self._builtin_signatures, self._settings.semantic_level))
87
- diagnostics.extend(_check_private_method_calls(body))
88
- diagnostics.extend(
89
- _check_api_misuse(body, code, ctx_data, self._settings.semantic_level)
90
- )
91
- if line_shift:
92
- diagnostics = _shift_diagnostics(diagnostics, line_shift, 0)
93
- return diagnostics
94
-
95
- def _check_unknown_names(
96
- self,
97
- body: Sequence[ast.AST],
98
- ctx_data: ContextData,
99
- severity_level: str,
100
- ) -> List[types.Diagnostic]:
101
- known: Set[str] = set(self._executor.available_names(ctx_data))
102
- diagnostics: list[types.Diagnostic] = []
103
-
104
- class Walker(ast.NodeVisitor):
105
- def __init__(self, tracker: Set[str]):
106
- self.tracker = tracker
107
-
108
- def visit_Assign(self, node: ast.Assign):
109
- self.visit(node.value)
110
- for target in node.targets:
111
- self.tracker.update(_names_in_target(target))
112
-
113
- def visit_AnnAssign(self, node: ast.AnnAssign):
114
- if node.value:
115
- self.visit(node.value)
116
- self.tracker.update(_names_in_target(node.target))
117
-
118
- def visit_AugAssign(self, node: ast.AugAssign):
119
- self.visit(node.value)
120
- self.tracker.update(_names_in_target(node.target))
121
-
122
- def visit_FunctionDef(self, node: ast.FunctionDef):
123
- self.tracker.add(node.name)
124
- for arg in node.args.args:
125
- self.tracker.add(arg.arg)
126
- for stmt in node.body:
127
- self.visit(stmt)
128
-
129
- def visit_ClassDef(self, node: ast.ClassDef):
130
- self.tracker.add(node.name)
131
- for stmt in node.body:
132
- self.visit(stmt)
133
-
134
- def visit_For(self, node: ast.For):
135
- # Loop targets become defined for the loop body and after the loop
136
- self.tracker.update(_names_in_target(node.target))
137
- self.visit(node.iter)
138
- for stmt in node.body:
139
- self.visit(stmt)
140
- for stmt in node.orelse:
141
- self.visit(stmt)
142
-
143
- def visit_AsyncFor(self, node: ast.AsyncFor):
144
- # Async loop targets follow the same scoping rules as regular loops
145
- self.tracker.update(_names_in_target(node.target))
146
- self.visit(node.iter)
147
- for stmt in node.body:
148
- self.visit(stmt)
149
- for stmt in node.orelse:
150
- self.visit(stmt)
151
-
152
- def visit_Name(self, node: ast.Name):
153
- if isinstance(node.ctx, ast.Load) and node.id not in self.tracker:
154
- diagnostics.append(
155
- _make_diagnostic(
156
- node,
157
- f"'{node.id}' may be undefined in this scope",
158
- severity_level,
159
- code=UNDEFINED_NAME_CODE,
160
- data={"name": node.id},
161
- )
162
- )
163
-
164
- def visit_Call(self, node: ast.Call):
165
- if isinstance(node.func, ast.Name) and node.func.id == "using":
166
- for kw in node.keywords:
167
- if kw.arg:
168
- self.tracker.add(str(kw.arg))
169
- self.generic_visit(node)
170
-
171
- walker = Walker(known)
172
- for stmt in body:
173
- walker.visit(stmt)
174
- return diagnostics
175
-
176
-
177
- def _syntax_diagnostic(exc: draconic.DraconicSyntaxError) -> types.Diagnostic:
178
- rng = _range_from_positions(
179
- exc.lineno,
180
- exc.offset,
181
- exc.end_lineno,
182
- exc.end_offset,
183
- )
184
- return types.Diagnostic(
185
- message=exc.msg,
186
- range=rng,
187
- severity=types.DiagnosticSeverity.Error,
188
- source="avrae-ls",
189
- )
190
-
191
-
192
- def _syntax_from_std(exc: SyntaxError) -> types.Diagnostic:
193
- lineno, offset = exc.lineno, exc.offset
194
- rng = _range_from_positions(lineno, offset, getattr(exc, "end_lineno", None), getattr(exc, "end_offset", None))
195
- return types.Diagnostic(
196
- message=exc.msg,
197
- range=rng,
198
- severity=types.DiagnosticSeverity.Error,
199
- source="avrae-ls",
200
- )
201
-
202
-
203
- def _names_in_target(target: ast.AST) -> Set[str]:
204
- names: set[str] = set()
205
- if isinstance(target, ast.Name):
206
- names.add(target.id)
207
- elif isinstance(target, ast.Tuple):
208
- for elt in target.elts:
209
- names.update(_names_in_target(elt))
210
- elif isinstance(target, ast.List):
211
- for elt in target.elts:
212
- names.update(_names_in_target(elt))
213
- return names
214
-
215
-
216
- async def _check_gvars(
217
- body: Sequence[ast.AST],
218
- resolver: GVarResolver,
219
- settings: DiagnosticSettings,
220
- ) -> List[types.Diagnostic]:
221
- diagnostics: list[types.Diagnostic] = []
222
- seen: set[str] = set()
223
-
224
- def _literal_value(node: ast.AST) -> str | None:
225
- if isinstance(node, ast.Constant) and isinstance(node.value, str):
226
- return node.value
227
- if isinstance(node, ast.Str):
228
- return node.s
229
- return None
230
-
231
- for node in _iter_calls(body):
232
- if not isinstance(node.func, ast.Name):
233
- continue
234
-
235
- async def _validate_gvar(arg_node: ast.AST):
236
- gvar_id = _literal_value(arg_node)
237
- if gvar_id is None or gvar_id in seen:
238
- return
239
- seen.add(gvar_id)
240
- found_local = resolver.get_local(gvar_id)
241
- ensured = found_local is not None or await resolver.ensure(gvar_id)
242
- if not ensured:
243
- diagnostics.append(
244
- _make_diagnostic(
245
- arg_node,
246
- f"Unknown gvar '{gvar_id}'",
247
- settings.semantic_level,
248
- code=MISSING_GVAR_CODE,
249
- data={"gvar": gvar_id},
250
- )
251
- )
252
-
253
- if node.func.id == "get_gvar":
254
- if node.args:
255
- await _validate_gvar(node.args[0])
256
- elif node.func.id == "using":
257
- for kw in node.keywords:
258
- await _validate_gvar(kw.value)
259
- return diagnostics
260
-
261
-
262
- def _iter_calls(body: Sequence[ast.AST]) -> Iterable[ast.Call]:
263
- class Finder(ast.NodeVisitor):
264
- def __init__(self):
265
- self.calls: list[ast.Call] = []
266
-
267
- def visit_Call(self, node: ast.Call):
268
- self.calls.append(node)
269
- self.generic_visit(node)
270
-
271
- finder = Finder()
272
- for stmt in body:
273
- finder.visit(stmt)
274
- return finder.calls
275
-
276
-
277
- def _check_private_method_calls(body: Sequence[ast.AST]) -> List[types.Diagnostic]:
278
- diagnostics: list[types.Diagnostic] = []
279
-
280
- class Finder(ast.NodeVisitor):
281
- def visit_Call(self, node: ast.Call):
282
- func = node.func
283
- if isinstance(func, ast.Attribute) and func.attr.startswith("_"):
284
- diagnostics.append(
285
- _make_diagnostic(
286
- func,
287
- "Calling private methods (starting with '_') is not allowed",
288
- "error",
289
- )
290
- )
291
- self.generic_visit(node)
292
-
293
- finder = Finder()
294
- for stmt in body:
295
- finder.visit(stmt)
296
- return diagnostics
297
-
298
-
299
- def _check_api_misuse(
300
- body: Sequence[ast.AST],
301
- code: str,
302
- ctx_data: ContextData,
303
- severity_level: str,
304
- ) -> List[types.Diagnostic]:
305
- """Heuristics for common API mistakes (list vs scalar, missing context, property calls)."""
306
- diagnostics: list[types.Diagnostic] = []
307
- module = ast.Module(body=list(body), type_ignores=[])
308
- parent_map = _build_parent_map(module)
309
- assigned_names = _collect_assigned_names(module)
310
- type_map = _diagnostic_type_map(code)
311
- context_seen: set[str] = set()
312
-
313
- for node in ast.walk(module):
314
- if isinstance(node, ast.Call):
315
- diagnostics.extend(_context_call_diagnostics(node, ctx_data, severity_level, context_seen))
316
- diagnostics.extend(_property_call_diagnostics(node, type_map, code, severity_level))
317
- if isinstance(node, ast.Attribute):
318
- diagnostics.extend(_uncalled_context_attr_diagnostics(node, assigned_names, severity_level))
319
- diagnostics.extend(_iterable_attr_diagnostics(node, parent_map, type_map, code, severity_level))
320
- return diagnostics
321
-
322
-
323
- def _context_call_diagnostics(
324
- node: ast.Call,
325
- ctx_data: ContextData,
326
- severity_level: str,
327
- seen: set[str],
328
- ) -> List[types.Diagnostic]:
329
- diagnostics: list[types.Diagnostic] = []
330
- if isinstance(node.func, ast.Name):
331
- if node.func.id == "character" and not ctx_data.character and "character" not in seen:
332
- seen.add("character")
333
- diagnostics.append(
334
- _make_diagnostic(
335
- node.func,
336
- "No character context configured; character() will raise at runtime.",
337
- severity_level,
338
- )
339
- )
340
- elif node.func.id == "combat" and not ctx_data.combat and "combat" not in seen:
341
- seen.add("combat")
342
- diagnostics.append(
343
- _make_diagnostic(
344
- node.func,
345
- "No combat context configured; combat() will return None.",
346
- severity_level,
347
- )
348
- )
349
- return diagnostics
350
-
351
-
352
- def _property_call_diagnostics(
353
- node: ast.Call,
354
- type_map: Dict[str, str],
355
- code: str,
356
- severity_level: str,
357
- ) -> List[types.Diagnostic]:
358
- if not isinstance(node.func, ast.Attribute):
359
- return []
360
- base_type = _resolve_expr_type(node.func.value, type_map, code)
361
- if not base_type:
362
- return []
363
- meta = _type_meta(base_type)
364
- attr = node.func.attr
365
- if attr in meta.methods or attr not in meta.attrs:
366
- return []
367
- receiver = _expr_to_str(node.func.value) or base_type
368
- return [
369
- _make_diagnostic(
370
- node.func,
371
- f"'{attr}' on {receiver} is a property; drop the parentheses.",
372
- severity_level,
373
- )
374
- ]
375
-
376
-
377
- def _uncalled_context_attr_diagnostics(
378
- node: ast.Attribute,
379
- assigned_names: Set[str],
380
- severity_level: str,
381
- ) -> List[types.Diagnostic]:
382
- if isinstance(node.value, ast.Name) and node.value.id in {"character", "combat"} and node.value.id not in assigned_names:
383
- call_hint = f"{node.value.id}()"
384
- return [
385
- _make_diagnostic(
386
- node.value,
387
- f"Call {call_hint} before accessing '{node.attr}'.",
388
- severity_level,
389
- )
390
- ]
391
- return []
392
-
393
-
394
- def _iterable_attr_diagnostics(
395
- node: ast.Attribute,
396
- parent_map: Dict[ast.AST, ast.AST],
397
- type_map: Dict[str, str],
398
- code: str,
399
- severity_level: str,
400
- ) -> List[types.Diagnostic]:
401
- parent = parent_map.get(node)
402
- if parent is None:
403
- return []
404
- if isinstance(parent, ast.Subscript) and parent.value is node:
405
- return []
406
-
407
- base_type = _resolve_expr_type(node.value, type_map, code)
408
- if not base_type:
409
- return []
410
- meta = _type_meta(base_type)
411
- attr_meta = meta.attrs.get(node.attr)
412
- if not attr_meta:
413
- return []
414
-
415
- is_collection = bool(attr_meta.element_type) or attr_meta.type_name in {"list", "dict"}
416
- if not is_collection:
417
- return []
418
-
419
- expr_label = _expr_to_str(node) or node.attr
420
- element_label = attr_meta.element_type or "items"
421
- container_label = attr_meta.type_name or "collection"
422
-
423
- if isinstance(parent, ast.Attribute) and parent.value is node:
424
- next_attr = parent.attr
425
- message = f"'{expr_label}' is a {container_label} of {element_label}; index or iterate before accessing '{next_attr}'."
426
- return [_make_diagnostic(node, message, severity_level)]
427
-
428
- if isinstance(parent, ast.Call) and parent.func is node:
429
- message = f"'{expr_label}' is a {container_label} of {element_label}; index or iterate before calling it."
430
- return [_make_diagnostic(node, message, severity_level)]
431
-
432
- return []
433
-
434
-
435
- def _diagnostic_type_map(code: str) -> Dict[str, str]:
436
- mapping = _infer_type_map(code)
437
- if mapping:
438
- return mapping
439
- wrapped, _ = _wrap_draconic(code)
440
- return _infer_type_map(wrapped)
441
-
442
-
443
- def _resolve_expr_type(expr: ast.AST, type_map: Dict[str, str], code: str) -> str:
444
- expr_text = _expr_to_str(expr)
445
- if not expr_text:
446
- return ""
447
- return _resolve_type_name(expr_text, code, type_map)
448
-
449
-
450
- def _expr_to_str(expr: ast.AST) -> str:
451
- try:
452
- return ast.unparse(expr)
453
- except Exception:
454
- return ""
455
-
456
-
457
- def _collect_assigned_names(module: ast.Module) -> Set[str]:
458
- assigned: set[str] = set()
459
-
460
- class Collector(ast.NodeVisitor):
461
- def visit_Assign(self, node: ast.Assign):
462
- for target in node.targets:
463
- assigned.update(_names_in_target(target))
464
- self.generic_visit(node)
465
-
466
- def visit_AnnAssign(self, node: ast.AnnAssign):
467
- assigned.update(_names_in_target(node.target))
468
- self.generic_visit(node)
469
-
470
- def visit_For(self, node: ast.For):
471
- assigned.update(_names_in_target(node.target))
472
- self.generic_visit(node)
473
-
474
- def visit_AsyncFor(self, node: ast.AsyncFor):
475
- assigned.update(_names_in_target(node.target))
476
- self.generic_visit(node)
477
-
478
- def visit_FunctionDef(self, node: ast.FunctionDef):
479
- assigned.add(node.name)
480
- for arg in node.args.args:
481
- assigned.add(arg.arg)
482
- self.generic_visit(node)
483
-
484
- def visit_ClassDef(self, node: ast.ClassDef):
485
- assigned.add(node.name)
486
- self.generic_visit(node)
487
-
488
- Collector().visit(module)
489
- return assigned
490
-
491
-
492
- def _build_parent_map(root: ast.AST) -> Dict[ast.AST, ast.AST]:
493
- parents: dict[ast.AST, ast.AST] = {}
494
- for parent in ast.walk(root):
495
- for child in ast.iter_child_nodes(parent):
496
- parents[child] = parent
497
- return parents
498
-
499
-
500
- def _make_diagnostic(
501
- node: ast.AST,
502
- message: str,
503
- level: str,
504
- *,
505
- code: str | None = None,
506
- data: Dict[str, Any] | None = None,
507
- ) -> types.Diagnostic:
508
- severity = SEVERITY.get(level, types.DiagnosticSeverity.Warning)
509
- if hasattr(node, "lineno"):
510
- rng = _range_from_positions(
511
- getattr(node, "lineno", 1),
512
- getattr(node, "col_offset", 0) + 1,
513
- getattr(node, "end_lineno", None),
514
- getattr(node, "end_col_offset", None),
515
- )
516
- else:
517
- rng = types.Range(
518
- start=types.Position(line=0, character=0),
519
- end=types.Position(line=0, character=1),
520
- )
521
- return types.Diagnostic(
522
- message=message,
523
- range=rng,
524
- severity=severity,
525
- source="avrae-ls",
526
- code=code,
527
- data=data,
528
- )
529
-
530
-
531
- def _shift_diagnostics(diags: List[types.Diagnostic], line_offset: int, char_offset: int) -> List[types.Diagnostic]:
532
- shifted: list[types.Diagnostic] = []
533
- for diag in diags:
534
- shifted.append(
535
- types.Diagnostic(
536
- message=diag.message,
537
- range=_shift_range(diag.range, line_offset, char_offset),
538
- severity=diag.severity,
539
- source=diag.source,
540
- code=diag.code,
541
- code_description=diag.code_description,
542
- tags=diag.tags,
543
- related_information=diag.related_information,
544
- data=diag.data,
545
- )
546
- )
547
- return shifted
548
-
549
-
550
- def _shift_range(rng: types.Range, line_offset: int, char_offset: int) -> types.Range:
551
- def _shift_pos(pos: types.Position) -> types.Position:
552
- return types.Position(
553
- line=max(pos.line + line_offset, 0),
554
- character=max(pos.character + (char_offset if pos.line == 0 else 0), 0),
555
- )
556
-
557
- return types.Range(start=_shift_pos(rng.start), end=_shift_pos(rng.end))
558
-
559
-
560
- def _wrap_draconic(code: str) -> tuple[str, int]:
561
- indented = "\n".join(f" {line}" for line in code.splitlines())
562
- wrapped = f"def __alias_main__():\n{indented}\n__alias_main__()"
563
- return wrapped, 1
564
-
565
-
566
- def _build_builtin_signatures() -> dict[str, inspect.Signature]:
567
- sigs: dict[str, inspect.Signature] = {}
568
- builtins = _default_builtins()
569
-
570
- def try_add(name: str, obj):
571
- try:
572
- sigs[name] = inspect.signature(obj)
573
- except (TypeError, ValueError):
574
- pass
575
-
576
- for name, obj in builtins.items():
577
- try_add(name, obj)
578
-
579
- # runtime helpers we expose
580
- def get_gvar(key): ...
581
- def get_svar(name, default=None): ...
582
- def get_cvar(name, default=None): ...
583
- def get_uvar(name, default=None): ...
584
- def get_uvars(): ...
585
- def set_uvar(name, value): ...
586
- def set_uvar_nx(name, value): ...
587
- def delete_uvar(name): ...
588
- def uvar_exists(name): ...
589
- def exists(name): ...
590
- def get(name, default=None): ...
591
- def using(**imports): ...
592
- def signature(data=0): ...
593
- def verify_signature(data): ...
594
- def print_fn(*args, sep=" ", end="\n"): ...
595
-
596
- helpers = {
597
- "get_gvar": get_gvar,
598
- "get_svar": get_svar,
599
- "get_cvar": get_cvar,
600
- "get_uvar": get_uvar,
601
- "get_uvars": get_uvars,
602
- "set_uvar": set_uvar,
603
- "set_uvar_nx": set_uvar_nx,
604
- "delete_uvar": delete_uvar,
605
- "uvar_exists": uvar_exists,
606
- "exists": exists,
607
- "get": get,
608
- "using": using,
609
- "signature": signature,
610
- "verify_signature": verify_signature,
611
- "print": print_fn,
612
- }
613
- for name, obj in helpers.items():
614
- try_add(name, obj)
615
- return sigs
616
-
617
-
618
- def _check_call_args(
619
- body: Sequence[ast.AST],
620
- signatures: dict[str, inspect.Signature],
621
- severity_level: str,
622
- ) -> List[types.Diagnostic]:
623
- diagnostics: list[types.Diagnostic] = []
624
-
625
- class Visitor(ast.NodeVisitor):
626
- def visit_Call(self, node: ast.Call):
627
- if isinstance(node.func, ast.Name):
628
- fn = node.func.id
629
- if fn in signatures:
630
- sig = signatures[fn]
631
- if not _call_args_match(sig, node):
632
- diagnostics.append(
633
- _make_diagnostic(
634
- node.func,
635
- f"Call to '{fn}' may have invalid arguments",
636
- severity_level,
637
- )
638
- )
639
- self.generic_visit(node)
640
-
641
- visitor = Visitor()
642
- for stmt in body:
643
- visitor.visit(stmt)
644
- return diagnostics
645
-
646
-
647
- def _call_args_match(sig: inspect.Signature, call: ast.Call) -> bool:
648
- params = list(sig.parameters.values())
649
- required = [
650
- p
651
- for p in params
652
- if p.default is inspect._empty
653
- and p.kind
654
- in (
655
- inspect.Parameter.POSITIONAL_ONLY,
656
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
657
- )
658
- ]
659
- max_args = None
660
- if any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params):
661
- max_args = None
662
- else:
663
- max_args = len(
664
- [
665
- p
666
- for p in params
667
- if p.kind
668
- in (
669
- inspect.Parameter.POSITIONAL_ONLY,
670
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
671
- )
672
- ]
673
- )
674
-
675
- arg_count = len(call.args)
676
- if arg_count < len(required):
677
- return False
678
- if max_args is not None and arg_count > max_args:
679
- return False
680
- return True
681
-
682
-
683
- def _check_imports(body: Sequence[ast.AST], severity_level: str) -> List[types.Diagnostic]:
684
- diagnostics: list[types.Diagnostic] = []
685
-
686
- class Visitor(ast.NodeVisitor):
687
- def visit_Import(self, node: ast.Import):
688
- module = node.names[0].name if node.names else None
689
- diagnostics.append(
690
- _make_diagnostic(
691
- node,
692
- "Imports are not supported in draconic aliases",
693
- severity_level,
694
- code=UNSUPPORTED_IMPORT_CODE,
695
- data={"module": module} if module else None,
696
- )
697
- )
698
-
699
- def visit_ImportFrom(self, node: ast.ImportFrom):
700
- diagnostics.append(
701
- _make_diagnostic(
702
- node,
703
- "Imports are not supported in draconic aliases",
704
- severity_level,
705
- code=UNSUPPORTED_IMPORT_CODE,
706
- data={"module": node.module},
707
- )
708
- )
709
-
710
- visitor = Visitor()
711
- for stmt in body:
712
- visitor.visit(stmt)
713
- return diagnostics
714
-
715
-
716
- def _range_from_positions(
717
- lineno: int | None,
718
- col_offset: int | None,
719
- end_lineno: int | None,
720
- end_col_offset: int | None,
721
- ) -> types.Range:
722
- start = types.Position(
723
- line=max((lineno or 1) - 1, 0),
724
- character=max((col_offset or 1) - 1, 0),
725
- )
726
- end = types.Position(
727
- line=max(((end_lineno or lineno or 1) - 1), 0),
728
- character=max(((end_col_offset or col_offset or 1) - 1), 0),
729
- )
730
- return types.Range(start=start, end=end)
731
-
732
-
733
- def _plain_command_diagnostics(source: str) -> list[types.Diagnostic] | None:
734
- """Handle simple commands (embed/echo) without draconic blocks."""
735
- simulated = simulate_command(source)
736
- if not simulated.command_name:
737
- return None
738
- if simulated.command_name == "embed":
739
- if simulated.validation_error:
740
- return [
741
- types.Diagnostic(
742
- message=simulated.validation_error,
743
- range=_range_from_positions(1, 1, 1, 1),
744
- severity=SEVERITY["warning"],
745
- source="avrae-ls",
746
- )
747
- ]
748
- return []
749
- if simulated.command_name == "echo":
750
- return []
751
- return None