avrae-ls 0.6.4__py3-none-any.whl → 0.7.1__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 +66 -6
- avrae_ls/alias_tests.py +13 -1
- avrae_ls/ast_utils.py +14 -0
- avrae_ls/code_actions.py +6 -2
- avrae_ls/completions.py +191 -1156
- avrae_ls/config.py +1 -0
- avrae_ls/context.py +62 -32
- 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.4.dist-info → avrae_ls-0.7.1.dist-info}/METADATA +6 -1
- avrae_ls-0.7.1.dist-info/RECORD +39 -0
- avrae_ls-0.6.4.dist-info/RECORD +0 -34
- {avrae_ls-0.6.4.dist-info → avrae_ls-0.7.1.dist-info}/WHEEL +0 -0
- {avrae_ls-0.6.4.dist-info → avrae_ls-0.7.1.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.6.4.dist-info → avrae_ls-0.7.1.dist-info}/licenses/LICENSE +0 -0
avrae_ls/__main__.py
CHANGED
|
@@ -42,6 +42,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
42
42
|
const=".",
|
|
43
43
|
help="Run alias tests in PATH (defaults to current directory)",
|
|
44
44
|
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--silent-gvar-fetch",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Silently ignore gvar fetch failures and treat them as None",
|
|
49
|
+
)
|
|
45
50
|
parser.add_argument("--token", help="Avrae API token (overrides config)")
|
|
46
51
|
parser.add_argument("--base-url", help="Avrae API base URL (overrides config)")
|
|
47
52
|
parser.add_argument("--version", action="store_true", help="Print version and exit")
|
|
@@ -58,12 +63,26 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
58
63
|
parser.error("--run-tests cannot be combined with --tcp")
|
|
59
64
|
if args.analyze:
|
|
60
65
|
parser.error("--run-tests cannot be combined with --analyze")
|
|
61
|
-
sys.exit(
|
|
66
|
+
sys.exit(
|
|
67
|
+
_run_alias_tests(
|
|
68
|
+
Path(args.run_tests),
|
|
69
|
+
token_override=args.token,
|
|
70
|
+
base_url_override=args.base_url,
|
|
71
|
+
silent_gvar_fetch=args.silent_gvar_fetch,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
62
74
|
|
|
63
75
|
if args.analyze:
|
|
64
76
|
if args.tcp:
|
|
65
77
|
parser.error("--analyze cannot be combined with --tcp")
|
|
66
|
-
sys.exit(
|
|
78
|
+
sys.exit(
|
|
79
|
+
_run_analysis(
|
|
80
|
+
Path(args.analyze),
|
|
81
|
+
token_override=args.token,
|
|
82
|
+
base_url_override=args.base_url,
|
|
83
|
+
silent_gvar_fetch=args.silent_gvar_fetch,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
67
86
|
|
|
68
87
|
server = create_server()
|
|
69
88
|
if args.tcp:
|
|
@@ -82,7 +101,13 @@ def _configure_logging(level: str) -> None:
|
|
|
82
101
|
)
|
|
83
102
|
|
|
84
103
|
|
|
85
|
-
def _run_analysis(
|
|
104
|
+
def _run_analysis(
|
|
105
|
+
path: Path,
|
|
106
|
+
*,
|
|
107
|
+
token_override: str | None = None,
|
|
108
|
+
base_url_override: str | None = None,
|
|
109
|
+
silent_gvar_fetch: bool = False,
|
|
110
|
+
) -> int:
|
|
86
111
|
if not path.exists():
|
|
87
112
|
print(f"File not found: {path}", file=sys.stderr)
|
|
88
113
|
return 2
|
|
@@ -96,6 +121,8 @@ def _run_analysis(path: Path, *, token_override: str | None = None, base_url_ove
|
|
|
96
121
|
config.service.token = token_override
|
|
97
122
|
if base_url_override:
|
|
98
123
|
config.service.base_url = base_url_override
|
|
124
|
+
if silent_gvar_fetch:
|
|
125
|
+
config.silent_gvar_fetch = True
|
|
99
126
|
for warning in warnings:
|
|
100
127
|
log.warning(warning)
|
|
101
128
|
|
|
@@ -111,7 +138,11 @@ def _run_analysis(path: Path, *, token_override: str | None = None, base_url_ove
|
|
|
111
138
|
|
|
112
139
|
|
|
113
140
|
def _run_alias_tests(
|
|
114
|
-
target: Path,
|
|
141
|
+
target: Path,
|
|
142
|
+
*,
|
|
143
|
+
token_override: str | None = None,
|
|
144
|
+
base_url_override: str | None = None,
|
|
145
|
+
silent_gvar_fetch: bool = False,
|
|
115
146
|
) -> int:
|
|
116
147
|
if not target.exists():
|
|
117
148
|
print(f"Test path not found: {target}", file=sys.stderr)
|
|
@@ -126,6 +157,8 @@ def _run_alias_tests(
|
|
|
126
157
|
config.service.token = token_override
|
|
127
158
|
if base_url_override:
|
|
128
159
|
config.service.base_url = base_url_override
|
|
160
|
+
if silent_gvar_fetch:
|
|
161
|
+
config.silent_gvar_fetch = True
|
|
129
162
|
for warning in warnings:
|
|
130
163
|
log.warning(warning)
|
|
131
164
|
|
|
@@ -169,7 +202,17 @@ def _print_test_results(results: Iterable[AliasTestResult], workspace_root: Path
|
|
|
169
202
|
passed += 1
|
|
170
203
|
continue
|
|
171
204
|
if res.error:
|
|
172
|
-
|
|
205
|
+
if res.error_line is not None and res.error_col is not None:
|
|
206
|
+
label = f" Execution Error (line {res.error_line} col {res.error_col})"
|
|
207
|
+
elif res.error_line is not None:
|
|
208
|
+
label = f" Execution Error (line {res.error_line})"
|
|
209
|
+
else:
|
|
210
|
+
label = " Execution Error"
|
|
211
|
+
print(_colorize_error_line(label))
|
|
212
|
+
print(f" {res.error}")
|
|
213
|
+
if res.stdout:
|
|
214
|
+
print(f" Stdout: {res.stdout.strip()}")
|
|
215
|
+
continue
|
|
173
216
|
if res.details:
|
|
174
217
|
print(f" {res.details}")
|
|
175
218
|
expected_val, actual_val = _summarize_mismatch(res.case.expected, res.actual)
|
|
@@ -225,6 +268,12 @@ def _colorize_diff_line(line: str) -> str:
|
|
|
225
268
|
return line
|
|
226
269
|
|
|
227
270
|
|
|
271
|
+
def _colorize_error_line(line: str) -> str:
|
|
272
|
+
if not sys.stdout.isatty():
|
|
273
|
+
return line
|
|
274
|
+
return f"\x1b[31m{line}\x1b[0m"
|
|
275
|
+
|
|
276
|
+
|
|
228
277
|
def _print_labeled_value(label: str, value: str) -> None:
|
|
229
278
|
lines = value.splitlines() or [""]
|
|
230
279
|
if len(lines) == 1:
|
avrae_ls/alias_preview.py
CHANGED
|
@@ -17,6 +17,8 @@ class RenderedAlias:
|
|
|
17
17
|
stdout: str
|
|
18
18
|
error: Optional[BaseException]
|
|
19
19
|
last_value: Any | None = None
|
|
20
|
+
error_line: int | None = None
|
|
21
|
+
error_col: int | None = None
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
@dataclass
|
|
@@ -58,7 +60,7 @@ class SimulatedCommand:
|
|
|
58
60
|
embed: EmbedPreview | None = None
|
|
59
61
|
|
|
60
62
|
|
|
61
|
-
def
|
|
63
|
+
def _strip_alias_header_with_offset(text: str) -> tuple[str, int]:
|
|
62
64
|
lines = text.splitlines()
|
|
63
65
|
if lines and lines[0].lstrip().startswith("!alias"):
|
|
64
66
|
first = lines[0].lstrip()
|
|
@@ -66,9 +68,57 @@ def _strip_alias_header(text: str) -> str:
|
|
|
66
68
|
remainder = parts[2] if len(parts) > 2 else ""
|
|
67
69
|
body = "\n".join(lines[1:])
|
|
68
70
|
if remainder:
|
|
69
|
-
return remainder + ("\n" + body if body else "")
|
|
70
|
-
return body
|
|
71
|
-
return text
|
|
71
|
+
return remainder + ("\n" + body if body else ""), 0
|
|
72
|
+
return body, 1
|
|
73
|
+
return text, 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _strip_alias_header(text: str) -> str:
|
|
77
|
+
body, _ = _strip_alias_header_with_offset(text)
|
|
78
|
+
return body
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _line_index_for_offset(text: str, offset: int) -> int:
|
|
82
|
+
return text.count("\n", 0, offset)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _error_position_for_match(
|
|
86
|
+
body: str, match: re.Match[str], error: BaseException, line_offset: int
|
|
87
|
+
) -> tuple[int | None, int | None]:
|
|
88
|
+
base_line = _line_index_for_offset(body, match.start(1))
|
|
89
|
+
base_line_start = body.rfind("\n", 0, match.start(1))
|
|
90
|
+
base_col = match.start(1) - (base_line_start + 1 if base_line_start != -1 else 0)
|
|
91
|
+
line_in_code: int | None = None
|
|
92
|
+
col_in_code: int | None = None
|
|
93
|
+
node = getattr(error, "node", None)
|
|
94
|
+
if node is not None:
|
|
95
|
+
node_line = getattr(node, "lineno", None)
|
|
96
|
+
if isinstance(node_line, int) and node_line > 0:
|
|
97
|
+
line_in_code = node_line
|
|
98
|
+
node_col = getattr(node, "col_offset", None)
|
|
99
|
+
if isinstance(node_col, int) and node_col >= 0:
|
|
100
|
+
col_in_code = node_col
|
|
101
|
+
if line_in_code is None:
|
|
102
|
+
lineno = getattr(error, "lineno", None)
|
|
103
|
+
if isinstance(lineno, int) and lineno > 0:
|
|
104
|
+
line_in_code = lineno
|
|
105
|
+
offset = getattr(error, "offset", None)
|
|
106
|
+
if isinstance(offset, int) and offset > 0:
|
|
107
|
+
col_in_code = offset - 1
|
|
108
|
+
if line_in_code is not None:
|
|
109
|
+
line_index = base_line + (line_in_code - 1)
|
|
110
|
+
else:
|
|
111
|
+
code = match.group(1)
|
|
112
|
+
leading_newlines = len(code) - len(code.lstrip("\n"))
|
|
113
|
+
line_index = base_line + leading_newlines
|
|
114
|
+
col_in_code = 0 if col_in_code is None else col_in_code
|
|
115
|
+
if col_in_code is None:
|
|
116
|
+
col_in_code = 0
|
|
117
|
+
if line_in_code is None or line_in_code == 1:
|
|
118
|
+
col_index = base_col + col_in_code
|
|
119
|
+
else:
|
|
120
|
+
col_index = col_in_code
|
|
121
|
+
return line_index + line_offset + 1, col_index + 1
|
|
72
122
|
|
|
73
123
|
|
|
74
124
|
async def render_alias_command(
|
|
@@ -79,12 +129,14 @@ async def render_alias_command(
|
|
|
79
129
|
args: list[str] | None = None,
|
|
80
130
|
) -> RenderedAlias:
|
|
81
131
|
"""Replace <drac2> blocks with their evaluated values and return final command."""
|
|
82
|
-
body =
|
|
132
|
+
body, line_offset = _strip_alias_header_with_offset(text)
|
|
83
133
|
body = apply_argument_parsing(body, args)
|
|
84
134
|
stdout_parts: list[str] = []
|
|
85
135
|
parts: list[str] = []
|
|
86
136
|
last_value = None
|
|
87
137
|
error: BaseException | None = None
|
|
138
|
+
error_line: int | None = None
|
|
139
|
+
error_col: int | None = None
|
|
88
140
|
|
|
89
141
|
pos = 0
|
|
90
142
|
matches: list[tuple[str, re.Match[str]]] = []
|
|
@@ -109,6 +161,7 @@ async def render_alias_command(
|
|
|
109
161
|
stdout_parts.append(result.stdout)
|
|
110
162
|
if result.error:
|
|
111
163
|
error = result.error
|
|
164
|
+
error_line, error_col = _error_position_for_match(body, match, result.error, line_offset)
|
|
112
165
|
break
|
|
113
166
|
last_value = result.value
|
|
114
167
|
if result.value is not None:
|
|
@@ -124,7 +177,14 @@ async def render_alias_command(
|
|
|
124
177
|
parts.append(body[pos:])
|
|
125
178
|
|
|
126
179
|
final_command = "".join(parts)
|
|
127
|
-
return RenderedAlias(
|
|
180
|
+
return RenderedAlias(
|
|
181
|
+
command=final_command,
|
|
182
|
+
stdout="".join(stdout_parts),
|
|
183
|
+
error=error,
|
|
184
|
+
last_value=last_value,
|
|
185
|
+
error_line=error_line,
|
|
186
|
+
error_col=error_col,
|
|
187
|
+
)
|
|
128
188
|
|
|
129
189
|
|
|
130
190
|
def validate_embed_payload(payload: str) -> Tuple[bool, str | None]:
|
avrae_ls/alias_tests.py
CHANGED
|
@@ -42,6 +42,8 @@ class AliasTestResult:
|
|
|
42
42
|
embed: dict[str, Any] | None = None
|
|
43
43
|
error: str | None = None
|
|
44
44
|
details: str | None = None
|
|
45
|
+
error_line: int | None = None
|
|
46
|
+
error_col: int | None = None
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
def discover_test_files(
|
|
@@ -159,6 +161,8 @@ async def run_alias_test(case: AliasTestCase, builder: ContextBuilder, executor:
|
|
|
159
161
|
actual=None,
|
|
160
162
|
stdout=rendered.stdout,
|
|
161
163
|
error=str(rendered.error),
|
|
164
|
+
error_line=rendered.error_line,
|
|
165
|
+
error_col=rendered.error_col,
|
|
162
166
|
)
|
|
163
167
|
|
|
164
168
|
preview = simulate_command(rendered.command)
|
|
@@ -171,7 +175,15 @@ async def run_alias_test(case: AliasTestCase, builder: ContextBuilder, executor:
|
|
|
171
175
|
error=preview.validation_error,
|
|
172
176
|
)
|
|
173
177
|
|
|
174
|
-
|
|
178
|
+
if preview.preview is not None:
|
|
179
|
+
actual = preview.preview
|
|
180
|
+
else:
|
|
181
|
+
if rendered.command.strip() == "" and rendered.last_value is None:
|
|
182
|
+
actual = None
|
|
183
|
+
elif rendered.last_value is not None and rendered.command.strip() == str(rendered.last_value):
|
|
184
|
+
actual = rendered.last_value
|
|
185
|
+
else:
|
|
186
|
+
actual = rendered.command
|
|
175
187
|
embed_dict = preview.embed.to_dict() if preview.embed else None
|
|
176
188
|
|
|
177
189
|
if embed_dict is not None and isinstance(case.expected, dict):
|
avrae_ls/ast_utils.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import Iterable, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def collect_target_names(targets: Iterable[ast.AST]) -> List[str]:
|
|
8
|
+
names: list[str] = []
|
|
9
|
+
for target in targets:
|
|
10
|
+
if isinstance(target, ast.Name):
|
|
11
|
+
names.append(target.id)
|
|
12
|
+
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
13
|
+
names.extend(collect_target_names(target.elts))
|
|
14
|
+
return names
|
avrae_ls/code_actions.py
CHANGED
|
@@ -10,7 +10,8 @@ from typing import Iterable, List, Sequence
|
|
|
10
10
|
from lsprotocol import types
|
|
11
11
|
|
|
12
12
|
from .codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
|
|
13
|
-
from .parser import DraconicBlock
|
|
13
|
+
from .parser import DraconicBlock
|
|
14
|
+
from .source_context import build_source_context
|
|
14
15
|
|
|
15
16
|
log = logging.getLogger(__name__)
|
|
16
17
|
|
|
@@ -42,10 +43,13 @@ def code_actions_for_document(
|
|
|
42
43
|
source: str,
|
|
43
44
|
params: types.CodeActionParams,
|
|
44
45
|
workspace_root: Path,
|
|
46
|
+
*,
|
|
47
|
+
treat_as_module: bool = False,
|
|
45
48
|
) -> List[types.CodeAction]:
|
|
46
49
|
"""Collect code actions for a document without requiring a running server."""
|
|
47
50
|
actions: list[types.CodeAction] = []
|
|
48
|
-
|
|
51
|
+
source_ctx = build_source_context(source, treat_as_module, apply_args=False)
|
|
52
|
+
blocks = source_ctx.blocks
|
|
49
53
|
snippets = _load_snippets(workspace_root)
|
|
50
54
|
only_kinds = list(params.context.only or [])
|
|
51
55
|
|