ZedScripts 0.0.2__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.
- ZedScripts/__about__.py +1 -0
- ZedScripts/__init__.py +38 -0
- ZedScripts/enums/Diagnostic.py +165 -0
- ZedScripts/enums/SyntaxErrorType.py +7 -0
- ZedScripts/environment/__init__.py +33 -0
- ZedScripts/environment/config.py +82 -0
- ZedScripts/environment/document.py +374 -0
- ZedScripts/environment/mod.py +62 -0
- ZedScripts/environment/version.py +202 -0
- ZedScripts/environment/workspace.py +320 -0
- ZedScripts/locale/en.json +38 -0
- ZedScripts/main.py +7 -0
- ZedScripts/providers/actions.py +13 -0
- ZedScripts/providers/capabilities.py +107 -0
- ZedScripts/providers/configuration.py +26 -0
- ZedScripts/providers/debug.py +59 -0
- ZedScripts/providers/diagnostics.py +101 -0
- ZedScripts/providers/hover.py +37 -0
- ZedScripts/providers/http.py +49 -0
- ZedScripts/providers/locale.py +80 -0
- ZedScripts/providers/notifications.py +45 -0
- ZedScripts/providers/semantic_tokens.py +189 -0
- ZedScripts/scripts/__init__.py +160 -0
- ZedScripts/scripts/block.py +547 -0
- ZedScripts/scripts/dataset.py +355 -0
- ZedScripts/scripts/value.py +706 -0
- ZedScripts/scripts/version.py +75 -0
- ZedScripts/server.py +288 -0
- ZedScripts/structure/ast.py +143 -0
- ZedScripts/structure/lexer.py +482 -0
- ZedScripts/structure/parser.py +243 -0
- ZedScripts/utils.py +85 -0
- zedscripts-0.0.2.dist-info/METADATA +105 -0
- zedscripts-0.0.2.dist-info/RECORD +37 -0
- zedscripts-0.0.2.dist-info/WHEEL +5 -0
- zedscripts-0.0.2.dist-info/licenses/LICENSE +21 -0
- zedscripts-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from lsprotocol import types
|
|
5
|
+
|
|
6
|
+
from .. import SCRIPTSDOCS_LINK, IS_DEBUG
|
|
7
|
+
from ..utils import path_to_uri
|
|
8
|
+
from . import Element
|
|
9
|
+
from ..enums.Diagnostic import DiagnosticType
|
|
10
|
+
from ..structure.ast import ValueNode
|
|
11
|
+
from ..structure.lexer import TextPosition, TokenCollection
|
|
12
|
+
from ..providers.semantic_tokens import SemanticTokenType, SemanticTokenModifier
|
|
13
|
+
from ..providers.hover import make_hover_information, format_tree
|
|
14
|
+
from ..scripts import DeprecatedInfo, ScriptBlockParameter, ValueType
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .block import Block, ScriptBlock
|
|
18
|
+
from ..structure.lexer import TextRange
|
|
19
|
+
from ..scripts.dataset import Dataset
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_deprecated_info(deprecated_data: 'DeprecatedInfo') -> tuple[DiagnosticType, dict[str, Any]]:
|
|
23
|
+
description = deprecated_data.get('description', "")
|
|
24
|
+
replacement = deprecated_data.get('replacedBy')
|
|
25
|
+
version = deprecated_data.get('version')
|
|
26
|
+
|
|
27
|
+
# retrieve relevant arguments for the diagnostic
|
|
28
|
+
params: dict[str, Any] = {
|
|
29
|
+
"description": description,
|
|
30
|
+
}
|
|
31
|
+
if replacement is not None:
|
|
32
|
+
params['replacement'] = replacement
|
|
33
|
+
if version is not None:
|
|
34
|
+
params['version'] = version
|
|
35
|
+
|
|
36
|
+
# retrieve the relevant diagnostic type
|
|
37
|
+
diagnostic_type: DiagnosticType
|
|
38
|
+
if replacement is not None and version is not None:
|
|
39
|
+
diagnostic_type = DiagnosticType.VALUE_DEPRECATED_REPLACEMENT_VERSION
|
|
40
|
+
elif replacement is not None:
|
|
41
|
+
diagnostic_type = DiagnosticType.VALUE_DEPRECATED_REPLACEMENT
|
|
42
|
+
elif version is not None:
|
|
43
|
+
diagnostic_type = DiagnosticType.VALUE_DEPRECATED_VERSION
|
|
44
|
+
else:
|
|
45
|
+
diagnostic_type = DiagnosticType.VALUE_DEPRECATED
|
|
46
|
+
|
|
47
|
+
return diagnostic_type, params
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Value(Element["ValueNode"]):
|
|
51
|
+
def __init__(self, string: str, node: ValueNode, parent: 'Block', comment: str) -> None:
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.string: str = string
|
|
54
|
+
"""
|
|
55
|
+
Includes any whitespace preceding the value, including before any preceding comments.
|
|
56
|
+
This sucks, but it's how the game parses them, so potentially needed for 1:1 behaviour.
|
|
57
|
+
"""
|
|
58
|
+
self.node: ValueNode = node
|
|
59
|
+
self.parent: 'Block' = parent
|
|
60
|
+
self.comment: str = comment
|
|
61
|
+
|
|
62
|
+
self.refs: list['ScriptBlock'] = []
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return f"Value(string={self.string}, parent={self.parent}, comment={self.comment})"
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return self.string
|
|
69
|
+
|
|
70
|
+
def is_key_value(self) -> bool:
|
|
71
|
+
return "=" in self.string
|
|
72
|
+
|
|
73
|
+
def key(self) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Returns the stripped text before the first ``=`` in the value.
|
|
76
|
+
Not all values take the form of key-value pairs.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
str: The key part of the key-value pair, stripped of surrounding whitespace.
|
|
80
|
+
"""
|
|
81
|
+
assert self.is_key_value()
|
|
82
|
+
return self.string.split("=", 1)[0].strip()
|
|
83
|
+
|
|
84
|
+
def value(self) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Returns the stripped text after the first ``=`` in the value.
|
|
87
|
+
Not all values take the form of key-value pairs.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
str: The value part of the key-value pair, stripped of surrounding whitespace. If the value is not a key-value pair, returns the entire string stripped of surrounding whitespace.
|
|
91
|
+
"""
|
|
92
|
+
if self.is_key_value():
|
|
93
|
+
return self.string.split("=", 1)[1].strip()
|
|
94
|
+
return self.string.strip()
|
|
95
|
+
|
|
96
|
+
def get_values(self, dataset: 'Dataset', value: str, param_data: 'ScriptBlockParameter') -> list[str]:
|
|
97
|
+
value_type = dataset.get_parameter_type(value, param_data)
|
|
98
|
+
|
|
99
|
+
# handle array case
|
|
100
|
+
if value_type == ValueType.ARRAY:
|
|
101
|
+
# we'll want to split by the array data
|
|
102
|
+
|
|
103
|
+
# retrieve array related data first
|
|
104
|
+
type_data = param_data.get('type')
|
|
105
|
+
assert type_data
|
|
106
|
+
array_data = type_data.get('array')
|
|
107
|
+
assert array_data
|
|
108
|
+
|
|
109
|
+
# split by the separator
|
|
110
|
+
separator = array_data['separator']
|
|
111
|
+
return [v.strip() for v in self.value().split(separator)]
|
|
112
|
+
|
|
113
|
+
# handle object case
|
|
114
|
+
elif value_type == ValueType.OBJECT:
|
|
115
|
+
# we simply split by the object's pairs separator
|
|
116
|
+
|
|
117
|
+
# retrieve object related data first
|
|
118
|
+
type_data = param_data.get('type')
|
|
119
|
+
assert type_data
|
|
120
|
+
object_data = type_data.get('object')
|
|
121
|
+
assert object_data
|
|
122
|
+
|
|
123
|
+
pairs_separator = object_data['pairsSeparator']
|
|
124
|
+
return [v.strip() for v in self.value().split(pairs_separator)]
|
|
125
|
+
|
|
126
|
+
# else if we have a value, then it's a single value
|
|
127
|
+
elif value != "":
|
|
128
|
+
return [value]
|
|
129
|
+
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
def cleanup_references(self) -> None:
|
|
133
|
+
"""Remove itself from any list of references in the previous blocks it references."""
|
|
134
|
+
for ref in self.refs:
|
|
135
|
+
if self in ref.references:
|
|
136
|
+
ref.references.remove(self)
|
|
137
|
+
self.refs.clear()
|
|
138
|
+
|
|
139
|
+
## information
|
|
140
|
+
|
|
141
|
+
def get_type_info(self, dataset: 'Dataset') -> str:
|
|
142
|
+
# retrieve parameter information
|
|
143
|
+
parent_type = self.parent.type
|
|
144
|
+
key = self.key()
|
|
145
|
+
if not dataset.can_block_have_parameter(parent_type, key):
|
|
146
|
+
return ""
|
|
147
|
+
parameter_data = dataset.get_parameter_data(parent_type, key)
|
|
148
|
+
|
|
149
|
+
out: str = ""
|
|
150
|
+
|
|
151
|
+
# type information
|
|
152
|
+
type_data = parameter_data.get('type')
|
|
153
|
+
expected_type: str | None = None
|
|
154
|
+
if type_data is not None:
|
|
155
|
+
expected_type = type_data['main']
|
|
156
|
+
type_text = expected_type
|
|
157
|
+
|
|
158
|
+
# based on the expected type, we adjust the type information
|
|
159
|
+
match expected_type:
|
|
160
|
+
case ValueType.ARRAY:
|
|
161
|
+
array_data = type_data.get('array')
|
|
162
|
+
assert array_data is not None
|
|
163
|
+
|
|
164
|
+
array_type = array_data['type']
|
|
165
|
+
separator = array_data['separator']
|
|
166
|
+
type_text = f"{array_type}[] (separator: '{separator}')"
|
|
167
|
+
|
|
168
|
+
case ValueType.OBJECT:
|
|
169
|
+
object_data = type_data.get('object')
|
|
170
|
+
assert object_data is not None
|
|
171
|
+
|
|
172
|
+
pairsSeparator = object_data['pairsSeparator']
|
|
173
|
+
keyValueSeparator = object_data['keyValueSeparator']
|
|
174
|
+
keyType = object_data['keyType']
|
|
175
|
+
valueType = object_data['valueType']
|
|
176
|
+
type_text = f"<{keyType}{keyValueSeparator}{valueType}> (pairs separator: '{pairsSeparator}')"
|
|
177
|
+
|
|
178
|
+
case ValueType.BLOCK:
|
|
179
|
+
block_data = type_data.get('block')
|
|
180
|
+
assert block_data is not None
|
|
181
|
+
|
|
182
|
+
block_type = block_data['name']
|
|
183
|
+
fullType = block_data.get('fullType', False)
|
|
184
|
+
fullType_text = "full" if fullType is True else "type only"
|
|
185
|
+
type_text = f"block<{block_type}> ({fullType_text})"
|
|
186
|
+
|
|
187
|
+
case ValueType.CALLBACK:
|
|
188
|
+
callback_data = type_data.get('callback')
|
|
189
|
+
assert callback_data is not None
|
|
190
|
+
|
|
191
|
+
parameters = callback_data['parameters']
|
|
192
|
+
returns = callback_data.get('returns')
|
|
193
|
+
|
|
194
|
+
type_text = f"callback({', '.join([f'{p['name']}: {p['type']}' for p in parameters])})"
|
|
195
|
+
if returns is not None:
|
|
196
|
+
type_text += f" -> {returns}"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
out += f" : {type_text}"
|
|
200
|
+
|
|
201
|
+
# default value information
|
|
202
|
+
default_value = parameter_data.get('default')
|
|
203
|
+
if default_value is not None:
|
|
204
|
+
default_text = str(default_value)
|
|
205
|
+
# handle special cases based on the expected type if available
|
|
206
|
+
match expected_type:
|
|
207
|
+
case ValueType.ARRAY:
|
|
208
|
+
if isinstance(default_value, list):
|
|
209
|
+
assert type_data is not None
|
|
210
|
+
array_data = type_data.get('array')
|
|
211
|
+
assert array_data is not None
|
|
212
|
+
|
|
213
|
+
separator = array_data['separator']
|
|
214
|
+
default_text = separator.join(default_value)
|
|
215
|
+
case ValueType.BOOLEAN:
|
|
216
|
+
if isinstance(default_value, bool):
|
|
217
|
+
default_text = str(default_value).lower()
|
|
218
|
+
|
|
219
|
+
out += f" = {default_text}"
|
|
220
|
+
|
|
221
|
+
return out.strip()
|
|
222
|
+
|
|
223
|
+
def get_tree(self, dataset: 'Dataset') -> str:
|
|
224
|
+
block_tree = self.parent.get_tree()
|
|
225
|
+
type_info = self.get_type_info(dataset)
|
|
226
|
+
|
|
227
|
+
txt = f"{block_tree} # {self.key()} {type_info}"
|
|
228
|
+
|
|
229
|
+
if dataset.can_block_have_parameter(self.parent.type, self.key()):
|
|
230
|
+
param_data = dataset.get_parameter_data(self.parent.type, self.key())
|
|
231
|
+
deprecated = param_data.get('deprecated')
|
|
232
|
+
if deprecated is not None:
|
|
233
|
+
txt = f"(deprecated)\n{txt}"
|
|
234
|
+
|
|
235
|
+
return txt
|
|
236
|
+
|
|
237
|
+
def get_description(self, dataset: 'Dataset') -> str:
|
|
238
|
+
parent_type = self.parent.type
|
|
239
|
+
key = self.key()
|
|
240
|
+
if not dataset.can_block_have_parameter(parent_type, key):
|
|
241
|
+
return ''
|
|
242
|
+
param_data = dataset.get_parameter_data(parent_type, key)
|
|
243
|
+
return param_data.get('description', '') if param_data else ''
|
|
244
|
+
|
|
245
|
+
def get_scriptsdocs_url(self, tree: list[str]) -> str | None:
|
|
246
|
+
return self.parent.get_scriptsdocs_url(tree) + f"#scripts-{self.parent.type}-{self.key()}".replace(' ', '-').lower()
|
|
247
|
+
|
|
248
|
+
def get_key_value_hover_information(self, dataset: 'Dataset', text_position: TextPosition) -> types.Hover | None:
|
|
249
|
+
node = self.node
|
|
250
|
+
key_node = node.key()
|
|
251
|
+
value_node = node.value()
|
|
252
|
+
|
|
253
|
+
# hovering the parameter, we show information about that
|
|
254
|
+
if key_node.to_range() == text_position:
|
|
255
|
+
# show a tree hierarchy of the parameter and description
|
|
256
|
+
tree = format_tree(self.get_tree(dataset))
|
|
257
|
+
desc = self.get_description(dataset)
|
|
258
|
+
|
|
259
|
+
# show link to ScriptsDocs
|
|
260
|
+
key = self.key()
|
|
261
|
+
parent_type = self.parent.type
|
|
262
|
+
if dataset.is_script_block(parent_type) \
|
|
263
|
+
and dataset.can_block_have_parameter(parent_type, key):
|
|
264
|
+
variant_tree = dataset.get_block_tree(parent_type)
|
|
265
|
+
url = self.get_scriptsdocs_url(variant_tree)
|
|
266
|
+
tree += f"\n[Documentation]({url})"
|
|
267
|
+
|
|
268
|
+
# format the whole thing
|
|
269
|
+
if desc == "":
|
|
270
|
+
txt = tree
|
|
271
|
+
else:
|
|
272
|
+
txt = f"{tree}\n\n---\n\n{desc}"
|
|
273
|
+
|
|
274
|
+
return make_hover_information(txt, key_node.to_range())
|
|
275
|
+
|
|
276
|
+
# hovering the value, we could show information about the value itself
|
|
277
|
+
if value_node.to_range() == text_position:
|
|
278
|
+
# if its expected type is block, we can list referenced blocks
|
|
279
|
+
txt = ""
|
|
280
|
+
|
|
281
|
+
if IS_DEBUG:
|
|
282
|
+
txt += "(debug)\n\n"
|
|
283
|
+
txt += f"- Refs: {[str(ref) for ref in self.refs]}"
|
|
284
|
+
|
|
285
|
+
return make_hover_information(txt, value_node.to_range())
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_hover_information(self, dataset: 'Dataset', text_position: TextPosition) -> types.Hover | None:
|
|
292
|
+
# hover for key-value
|
|
293
|
+
if self.is_key_value():
|
|
294
|
+
return self.get_key_value_hover_information(dataset, text_position)
|
|
295
|
+
# TODO: hover for non key-value
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
## actions
|
|
300
|
+
|
|
301
|
+
def get_ref_id_range(self) -> 'TextRange':
|
|
302
|
+
return self.node.value().split()[-1].to_range()
|
|
303
|
+
|
|
304
|
+
def get_linked_editing_ranges(self, text_position: TextPosition) -> list['TextRange'] | None:
|
|
305
|
+
if len(self.refs) == 0:
|
|
306
|
+
return None
|
|
307
|
+
ranges: list['TextRange'] = []
|
|
308
|
+
|
|
309
|
+
# add the range of the value itself
|
|
310
|
+
ranges.append(self.get_ref_id_range())
|
|
311
|
+
|
|
312
|
+
# for each refs, gather their linked editing ranges
|
|
313
|
+
document = self.parent.document
|
|
314
|
+
for ref in self.refs:
|
|
315
|
+
# gather the IDs of blocks that are in the same document only
|
|
316
|
+
if ref.document == document:
|
|
317
|
+
ranges.append(ref.get_ref_id_range())
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
# we gather the referenced ranges from Values that are in the same document
|
|
321
|
+
ranges.extend(ref.get_referenced_to_ranges(self.parent.document))
|
|
322
|
+
|
|
323
|
+
return ranges
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
## definition
|
|
327
|
+
|
|
328
|
+
def get_definition(self, text_position: TextPosition) -> list[types.LocationLink] | None:
|
|
329
|
+
if len(self.refs) == 0:
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
result: list[types.LocationLink] = []
|
|
333
|
+
for ref in self.refs:
|
|
334
|
+
ref_id_range = ref.get_ref_id_range().to_lsp()
|
|
335
|
+
|
|
336
|
+
location = types.LocationLink(
|
|
337
|
+
target_uri=path_to_uri(ref.document.path),
|
|
338
|
+
target_range=ref_id_range,
|
|
339
|
+
target_selection_range=ref_id_range,
|
|
340
|
+
)
|
|
341
|
+
result.append(location)
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
## semantic tokens
|
|
347
|
+
|
|
348
|
+
def add_value_type_semantic_tokens(self, actual_type: ValueType, param_data: 'ScriptBlockParameter', value_node: 'TokenCollection') -> None:
|
|
349
|
+
match actual_type:
|
|
350
|
+
case ValueType.FLOAT:
|
|
351
|
+
self.parent.add_semantic_token(
|
|
352
|
+
type=SemanticTokenType.NUMBER,
|
|
353
|
+
location=value_node.to_range()
|
|
354
|
+
)
|
|
355
|
+
case ValueType.INTEGER:
|
|
356
|
+
self.parent.add_semantic_token(
|
|
357
|
+
type=SemanticTokenType.NUMBER,
|
|
358
|
+
location=value_node.to_range()
|
|
359
|
+
)
|
|
360
|
+
case ValueType.BOOLEAN:
|
|
361
|
+
self.parent.add_semantic_token(
|
|
362
|
+
type=SemanticTokenType.VARIABLE,
|
|
363
|
+
location=value_node.to_range()
|
|
364
|
+
)
|
|
365
|
+
case ValueType.BLOCK:
|
|
366
|
+
self.parent.add_semantic_token(
|
|
367
|
+
type=SemanticTokenType.CLASS,
|
|
368
|
+
location=value_node.to_range(),
|
|
369
|
+
modifiers=[SemanticTokenModifier.DEFINITION]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# the rest is either already handled via the texmate
|
|
373
|
+
# grammar or shouldn't get specific semantic tokens
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
## diagnostics
|
|
377
|
+
|
|
378
|
+
def validate(self, dataset: 'Dataset') -> bool:
|
|
379
|
+
if self.is_key_value():
|
|
380
|
+
return self.validate_key_value(dataset)
|
|
381
|
+
|
|
382
|
+
# TODO: implement validation for non-key-value strings
|
|
383
|
+
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
def validate_key_value(self, dataset: 'Dataset') -> bool:
|
|
387
|
+
parent_type = self.parent.type
|
|
388
|
+
|
|
389
|
+
key = self.key()
|
|
390
|
+
value = self.value()
|
|
391
|
+
|
|
392
|
+
node = self.node
|
|
393
|
+
key_node = node.key()
|
|
394
|
+
value_node = node.value()
|
|
395
|
+
|
|
396
|
+
self.parent.add_semantic_token(
|
|
397
|
+
type=SemanticTokenType.PARAMETER,
|
|
398
|
+
location=key_node.to_range()
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if not dataset.can_block_have_parameter(parent_type, key):
|
|
402
|
+
# add a diagnostic for the invalid parameter
|
|
403
|
+
self.parent.add_diagnostic(
|
|
404
|
+
type=DiagnosticType.VALUE_UNKNOWN_PARAMETER,
|
|
405
|
+
location=key_node.to_range(),
|
|
406
|
+
args={"type": parent_type, "key": key}
|
|
407
|
+
)
|
|
408
|
+
# technically custom parameters are allowed by the game
|
|
409
|
+
# and used so we could validate the other stuff too
|
|
410
|
+
# but since other validations depend on parameter data
|
|
411
|
+
# there's nothing else to validate for this parameter
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
param_data = dataset.get_parameter_data(parent_type, key)
|
|
415
|
+
|
|
416
|
+
# do the type highlight before anything else
|
|
417
|
+
# so we highlight by type even if the value is otherwise invalid
|
|
418
|
+
type_data = param_data.get('type')
|
|
419
|
+
value_type = dataset.get_parameter_type(value, param_data)
|
|
420
|
+
self.add_value_type_semantic_tokens(value_type, param_data, value_node)
|
|
421
|
+
|
|
422
|
+
# check if value has a newline, which is usually not normal
|
|
423
|
+
if "\n" in value:
|
|
424
|
+
self.parent.add_diagnostic(
|
|
425
|
+
type=DiagnosticType.VALUE_WITH_NEWLINE,
|
|
426
|
+
location=value_node.strip().to_range(),
|
|
427
|
+
args={"type": parent_type, "key": key, "value": value}
|
|
428
|
+
)
|
|
429
|
+
# no return, bcs technically still valid for the game
|
|
430
|
+
# so this may be normal
|
|
431
|
+
|
|
432
|
+
# verify whenever the parameter is deprecated
|
|
433
|
+
deprecated_data = param_data.get('deprecated', None)
|
|
434
|
+
if deprecated_data is not None:
|
|
435
|
+
depr_type, param = _get_deprecated_info(deprecated_data)
|
|
436
|
+
self.parent.add_diagnostic(
|
|
437
|
+
type=depr_type,
|
|
438
|
+
location=key_node.strip().to_range(),
|
|
439
|
+
args=param
|
|
440
|
+
)
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
# check for duplicates
|
|
444
|
+
can_be_duplicate = param_data.get('allowedDuplicate', False)
|
|
445
|
+
if not can_be_duplicate and self.parent.check_for_duplicates(key):
|
|
446
|
+
self.parent.add_diagnostic(
|
|
447
|
+
type=DiagnosticType.VALUE_DUPLICATE,
|
|
448
|
+
location=key_node.to_range(),
|
|
449
|
+
args={"type": parent_type, "key": key}
|
|
450
|
+
)
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
# check if the value is missing
|
|
454
|
+
can_be_empty = param_data.get('canBeEmpty', False)
|
|
455
|
+
if not can_be_empty and value == "":
|
|
456
|
+
self.parent.add_diagnostic(
|
|
457
|
+
type=DiagnosticType.VALUE_MISSING,
|
|
458
|
+
location=node.tokens.strip().to_range(),
|
|
459
|
+
args={"type": parent_type, "key": key}
|
|
460
|
+
)
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
# we retrieve the list of values
|
|
464
|
+
values = self.get_values(dataset, value, param_data)
|
|
465
|
+
|
|
466
|
+
# check forbidden values
|
|
467
|
+
# that is values that are not allowed for this parameter
|
|
468
|
+
accepted_values = param_data.get('values', None)
|
|
469
|
+
if accepted_values is not None:
|
|
470
|
+
# forbidden values are the ones not present in the accepted values list
|
|
471
|
+
forbidden_values = [v for v in values if v not in accepted_values]
|
|
472
|
+
|
|
473
|
+
# if the list is not empty, then we have forbidden values for this parameter
|
|
474
|
+
if forbidden_values:
|
|
475
|
+
self.parent.add_diagnostic(
|
|
476
|
+
type=DiagnosticType.VALUE_FORBIDDEN,
|
|
477
|
+
location=value_node.to_range(),
|
|
478
|
+
args={"type": parent_type, "key": key,
|
|
479
|
+
"forbidden_values": forbidden_values,
|
|
480
|
+
"accepted_values": accepted_values}
|
|
481
|
+
)
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
# init key_value_separator
|
|
485
|
+
# if it is equal to "=", we need to ignore the diagnostic of extra = signs
|
|
486
|
+
# possibly marking a wrongly formatted key-value pair
|
|
487
|
+
key_value_separator = None
|
|
488
|
+
|
|
489
|
+
# verify the type
|
|
490
|
+
if value_type is not None and type_data is not None:
|
|
491
|
+
# first we verify that the expected type and the actual
|
|
492
|
+
# type of the value correspond
|
|
493
|
+
expected_type = type_data['main']
|
|
494
|
+
if value_type != expected_type:
|
|
495
|
+
self.parent.add_diagnostic(
|
|
496
|
+
type=DiagnosticType.VALUE_INVALID_TYPE,
|
|
497
|
+
location=value_node.strip().to_range(),
|
|
498
|
+
args={"key": key, "value": value, "expected_type": expected_type, "actual_type": value_type}
|
|
499
|
+
)
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
# verify based on type
|
|
503
|
+
match expected_type:
|
|
504
|
+
# if it's a translation type, then we need to verify it
|
|
505
|
+
case ValueType.TRANSLATION:
|
|
506
|
+
# TODO: needs to implement
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
# if it's an object, then we verify the object composition
|
|
510
|
+
case ValueType.OBJECT:
|
|
511
|
+
# retrieve the object data
|
|
512
|
+
object_data = type_data.get('object')
|
|
513
|
+
assert object_data is not None
|
|
514
|
+
|
|
515
|
+
# first make sure that each pairs contain the key-value separator
|
|
516
|
+
key_value_separator = object_data['keyValueSeparator']
|
|
517
|
+
for v in values:
|
|
518
|
+
if key_value_separator not in v:
|
|
519
|
+
self.parent.add_diagnostic(
|
|
520
|
+
type=DiagnosticType.VALUE_INVALID_OBJECT_FORMAT,
|
|
521
|
+
location=value_node.to_range(),
|
|
522
|
+
args={"type": parent_type, "key": key, "key_value_separator": key_value_separator}
|
|
523
|
+
)
|
|
524
|
+
return False
|
|
525
|
+
|
|
526
|
+
# then check if the key and values have the right format
|
|
527
|
+
obj_key_type = object_data['keyType']
|
|
528
|
+
obj_value_type = object_data['valueType']
|
|
529
|
+
failed = False
|
|
530
|
+
for v in values:
|
|
531
|
+
# we test each key and value for a type
|
|
532
|
+
obj_key, obj_value = v.split(key_value_separator, 1)
|
|
533
|
+
key_type_result = dataset.test_for_type(obj_key_type, obj_key)
|
|
534
|
+
value_type_result = dataset.test_for_type(obj_value_type, obj_value)
|
|
535
|
+
|
|
536
|
+
# if the expected type and actual type doesn't correspond, then something
|
|
537
|
+
# is wrong and we need to report a diagnostic
|
|
538
|
+
if key_type_result != obj_key_type:
|
|
539
|
+
self.parent.add_diagnostic(
|
|
540
|
+
type=DiagnosticType.VALUE_INVALID_OBJECT_KEY_TYPE,
|
|
541
|
+
location=value_node.to_range(),
|
|
542
|
+
args={"obj_key": obj_key, "key": key,
|
|
543
|
+
"expected_type": obj_key_type,
|
|
544
|
+
"actual_type": key_type_result}
|
|
545
|
+
)
|
|
546
|
+
failed = True
|
|
547
|
+
if value_type_result != obj_value_type:
|
|
548
|
+
self.parent.add_diagnostic(
|
|
549
|
+
type=DiagnosticType.VALUE_INVALID_OBJECT_VALUE_TYPE,
|
|
550
|
+
location=value_node.to_range(),
|
|
551
|
+
args={"obj_value": obj_value, "key": key,
|
|
552
|
+
"expected_type": obj_value_type,
|
|
553
|
+
"actual_type": value_type_result}
|
|
554
|
+
)
|
|
555
|
+
failed = True
|
|
556
|
+
|
|
557
|
+
# verify by
|
|
558
|
+
|
|
559
|
+
# if at least one failed, then the entire object is considered invalid
|
|
560
|
+
if failed:
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
# if it's a block type, then it needs to reference another block
|
|
564
|
+
case ValueType.BLOCK:
|
|
565
|
+
passed = self.validate_block_ref(value, param_data, value_node)
|
|
566
|
+
if not passed:
|
|
567
|
+
return False
|
|
568
|
+
|
|
569
|
+
# diagnostic possibly wrongly formatted key-value pair
|
|
570
|
+
if key_value_separator != "=":
|
|
571
|
+
if "=" in value:
|
|
572
|
+
self.parent.add_diagnostic(
|
|
573
|
+
type=DiagnosticType.VALUE_WRONGLY_FORMATTED_KEY_VALUE_PAIR,
|
|
574
|
+
location=node.tokens.strip().to_range(),
|
|
575
|
+
args={"type": parent_type, "key": key, "value": value}
|
|
576
|
+
)
|
|
577
|
+
return False
|
|
578
|
+
|
|
579
|
+
# TODO: need to validate dependent parameters (needs)
|
|
580
|
+
|
|
581
|
+
return True
|
|
582
|
+
|
|
583
|
+
# TODO: need to implement this function usage
|
|
584
|
+
# def validate_type(self, dataset: Dataset, value: str, expected_type: ValueType, node: TokenCollection, block_type_data: BlockType | None = None) -> bool:
|
|
585
|
+
# # test for type the value
|
|
586
|
+
# value_type = dataset.test_for_type(expected_type, value)
|
|
587
|
+
# if value_type != expected_type:
|
|
588
|
+
# return False
|
|
589
|
+
|
|
590
|
+
# # if it expects a block reference, then validate that
|
|
591
|
+
# if (expected_type == ValueType.BLOCK
|
|
592
|
+
# and block_type_data is not None
|
|
593
|
+
# and not self.validate_block_ref(value, block_type_data, node)):
|
|
594
|
+
# return False
|
|
595
|
+
|
|
596
|
+
# return True
|
|
597
|
+
|
|
598
|
+
def block_ref_to_fulltype(self, value: str) -> tuple[str | None, str] | None:
|
|
599
|
+
module = None
|
|
600
|
+
block = None
|
|
601
|
+
|
|
602
|
+
parts = value.split(".")
|
|
603
|
+
match len(parts):
|
|
604
|
+
case 2:
|
|
605
|
+
module, block = parts
|
|
606
|
+
return module, block
|
|
607
|
+
case 1:
|
|
608
|
+
block = parts[0]
|
|
609
|
+
return None, block
|
|
610
|
+
case _:
|
|
611
|
+
return None # invalid format
|
|
612
|
+
|
|
613
|
+
def validate_block_ref(self, value: str, param_data: ScriptBlockParameter, node: TokenCollection) -> bool:
|
|
614
|
+
type_data = param_data.get('type')
|
|
615
|
+
assert type_data is not None, "Type data must be provided in the parameter data"
|
|
616
|
+
block_type_data = type_data.get('block')
|
|
617
|
+
assert block_type_data is not None, "Block type data must be provided in the type data"
|
|
618
|
+
|
|
619
|
+
can_be_empty = block_type_data.get('canBeEmpty', False)
|
|
620
|
+
|
|
621
|
+
# retrieve the full type elements
|
|
622
|
+
fulltype = self.block_ref_to_fulltype(value)
|
|
623
|
+
if fulltype is None:
|
|
624
|
+
self.parent.add_diagnostic(
|
|
625
|
+
type=DiagnosticType.VALUE_INVALID_BLOCK_REFERENCE,
|
|
626
|
+
location=node.strip().to_range(),
|
|
627
|
+
args={"value": value}
|
|
628
|
+
)
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
module = fulltype[0]
|
|
632
|
+
block = fulltype[1]
|
|
633
|
+
|
|
634
|
+
# check that a module can be provided
|
|
635
|
+
can_full_type = block_type_data.get('fullType', False)
|
|
636
|
+
if not can_full_type and module is not None:
|
|
637
|
+
self.parent.add_diagnostic(
|
|
638
|
+
type=DiagnosticType.VALUE_CANNOT_PROVIDE_MODULE,
|
|
639
|
+
location=node.strip().to_range(),
|
|
640
|
+
args={"value": value, "module": module}
|
|
641
|
+
)
|
|
642
|
+
return False
|
|
643
|
+
|
|
644
|
+
# check whenever the block can be empty
|
|
645
|
+
if block == "":
|
|
646
|
+
if can_be_empty:
|
|
647
|
+
return True
|
|
648
|
+
self.parent.add_diagnostic(
|
|
649
|
+
type=DiagnosticType.VALUE_BLOCK_REF_CANNOT_BE_EMPTY,
|
|
650
|
+
location=node.strip().to_range(),
|
|
651
|
+
args={"value": value}
|
|
652
|
+
)
|
|
653
|
+
return False
|
|
654
|
+
|
|
655
|
+
# TODO: look for the source block
|
|
656
|
+
|
|
657
|
+
root = self.parent.get_root()
|
|
658
|
+
|
|
659
|
+
# retrieve imports
|
|
660
|
+
searchable_modules: set[str] = {"Base"}
|
|
661
|
+
if module is not None:
|
|
662
|
+
# searchable_modules = set()
|
|
663
|
+
# else:
|
|
664
|
+
searchable_modules.update(root.get_imports())
|
|
665
|
+
|
|
666
|
+
# the used module is searched into too
|
|
667
|
+
searchable_modules.add(module)
|
|
668
|
+
|
|
669
|
+
# if allowed, auto import the parent module block
|
|
670
|
+
if not block_type_data.get('noAutoImport', False):
|
|
671
|
+
module_block = root.get_module()
|
|
672
|
+
if (module_block is not None
|
|
673
|
+
and module_block.id is not None
|
|
674
|
+
and module_block.id not in searchable_modules):
|
|
675
|
+
searchable_modules.add(module_block.id)
|
|
676
|
+
|
|
677
|
+
# search the block reference in the searchable modules
|
|
678
|
+
ref_type = block_type_data['name']
|
|
679
|
+
# FIXME: this is calling a static function as an instance method, not sure that's correct to do that
|
|
680
|
+
# but it had to be done due to circular dependency issues
|
|
681
|
+
self.cleanup_references()
|
|
682
|
+
refs = self.parent.document.workspace.search_for_block_references(root.document.version, searchable_modules, block, ref_type)
|
|
683
|
+
self.refs = refs # cache result since that is used for other providers
|
|
684
|
+
|
|
685
|
+
# cache itself inside refs
|
|
686
|
+
for ref in refs:
|
|
687
|
+
ref.references.append(self)
|
|
688
|
+
|
|
689
|
+
# diagnostics handling
|
|
690
|
+
refs_len = len(refs)
|
|
691
|
+
if refs_len == 0:
|
|
692
|
+
self.parent.add_diagnostic(
|
|
693
|
+
type=DiagnosticType.VALUE_UNMATCHED_BLOCK_REF,
|
|
694
|
+
location=node.strip().to_range(),
|
|
695
|
+
args={"value": value, "parameter": self.key(), "expected_block": ref_type}
|
|
696
|
+
)
|
|
697
|
+
return False
|
|
698
|
+
if refs_len > 1:
|
|
699
|
+
self.parent.add_diagnostic(
|
|
700
|
+
type=DiagnosticType.VALUE_MULTIPLE_BLOCK_REFS,
|
|
701
|
+
location=node.strip().to_range(),
|
|
702
|
+
args={"value": value, "parameter": self.key(), "expected_block": ref_type}
|
|
703
|
+
)
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
return True
|