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