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,547 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Iterable, Iterator
|
|
3
|
+
|
|
4
|
+
from lsprotocol import types
|
|
5
|
+
|
|
6
|
+
from .. import SCRIPTSDOCS_LINK
|
|
7
|
+
from ..utils import path_to_uri
|
|
8
|
+
from . import Element
|
|
9
|
+
from ..enums.Diagnostic import DiagnosticType
|
|
10
|
+
from ..structure.ast import BlockNode, Chunk
|
|
11
|
+
from ..structure.lexer import TextRange, TextPosition
|
|
12
|
+
from ..providers.semantic_tokens import SemanticTokenType, SemanticTokenModifier
|
|
13
|
+
from ..providers.hover import make_hover_information, format_tree
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .value import Value
|
|
17
|
+
from .dataset import Dataset
|
|
18
|
+
from ..environment.document import Document
|
|
19
|
+
|
|
20
|
+
class Block:
|
|
21
|
+
def __init__(self, document: 'Document', type: str):
|
|
22
|
+
self.document: 'Document' = document
|
|
23
|
+
self.type: str = type
|
|
24
|
+
"""
|
|
25
|
+
Type of the block.
|
|
26
|
+
All blocks must have an explicit type, except the root block of a file, which is given an empty string.
|
|
27
|
+
If the block was created from AST, the type may be empty for non-root blocks, but this is ill-formed.
|
|
28
|
+
"""
|
|
29
|
+
self.children: list['ScriptBlock'] = []
|
|
30
|
+
self.values: list['Value'] = []
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"Block(type={self.type}, children={len(self.children)}, values={len(self.values)})"
|
|
34
|
+
|
|
35
|
+
def __iter__(self) -> Iterator['ScriptBlock']:
|
|
36
|
+
return iter(self.children)
|
|
37
|
+
|
|
38
|
+
def get_tree(self) -> str:
|
|
39
|
+
type = self.type
|
|
40
|
+
|
|
41
|
+
parents: list[str] = [type]
|
|
42
|
+
current = self
|
|
43
|
+
while isinstance(current, ScriptBlock) and current.parent is not None:
|
|
44
|
+
parents.insert(0, current.parent.type)
|
|
45
|
+
current = current.parent
|
|
46
|
+
|
|
47
|
+
return " → ".join(parents)
|
|
48
|
+
|
|
49
|
+
def get_root(self) -> 'Root':
|
|
50
|
+
"""
|
|
51
|
+
Find the root block of the whole block hierarchy.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Root: The root block of the whole tree.
|
|
55
|
+
"""
|
|
56
|
+
return NotImplemented
|
|
57
|
+
|
|
58
|
+
def get_description(self, dataset: 'Dataset') -> str:
|
|
59
|
+
return NotImplemented
|
|
60
|
+
|
|
61
|
+
def get_scriptsdocs_url(self, tree: list[str]) -> str:
|
|
62
|
+
return SCRIPTSDOCS_LINK + "/".join(tree).replace(" ", "-").lower()
|
|
63
|
+
|
|
64
|
+
def get_element_at(self, text_position: TextPosition) -> 'ScriptBlock | Value | None':
|
|
65
|
+
# first check the values
|
|
66
|
+
for value in self.values:
|
|
67
|
+
node = value.node
|
|
68
|
+
if node.contains_position(text_position):
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
# then check the children blocks
|
|
72
|
+
for child in self.children:
|
|
73
|
+
element = child.get_element_at(text_position)
|
|
74
|
+
if element is not None:
|
|
75
|
+
return element
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_hover_information(self, dataset: 'Dataset', text_position: TextPosition) -> types.Hover | None:
|
|
81
|
+
element = self.get_element_at(text_position)
|
|
82
|
+
if element is not None and element is not self:
|
|
83
|
+
return element.get_hover_information(dataset, text_position)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def validate(self, dataset: 'Dataset') -> bool: ...
|
|
87
|
+
def validate_block(self, dataset: 'Dataset') -> bool: ...
|
|
88
|
+
def validate_children(self, dataset: 'Dataset') -> None: ...
|
|
89
|
+
|
|
90
|
+
def get_keys(self) -> list[str]:
|
|
91
|
+
"""
|
|
92
|
+
Get all the keys from the block's key-value pairs.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
list[str]: A list of all keys in the block's key-value pairs, converted to lowercase.
|
|
96
|
+
"""
|
|
97
|
+
keys = []
|
|
98
|
+
for value in self.values:
|
|
99
|
+
if value.is_key_value():
|
|
100
|
+
keys.append(value.key().lower())
|
|
101
|
+
return keys
|
|
102
|
+
|
|
103
|
+
def check_for_duplicates(self, key: str) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Check if the given key has duplicates within the block's key-value pairs.
|
|
106
|
+
A key is considered a duplicate if it appears more than once in the block's key-value pairs.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
key (str): The key to check for duplicates.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
bool: True if the key has duplicates, False otherwise.
|
|
113
|
+
"""
|
|
114
|
+
keys = self.get_keys()
|
|
115
|
+
duplicate_count = len([k for k in keys if k == key.lower()])
|
|
116
|
+
return duplicate_count > 1 # > 1 bcs there's itself in the list
|
|
117
|
+
|
|
118
|
+
def add_diagnostic(self, type: DiagnosticType, location: TextRange, args: dict[str, Any] = {}) -> None:
|
|
119
|
+
self.document.diagnostics.add(type=type, location=location, args=args)
|
|
120
|
+
|
|
121
|
+
def add_semantic_token(self, type: SemanticTokenType, location: TextRange, modifiers: list[SemanticTokenModifier] = []) -> None:
|
|
122
|
+
self.document.semantic_tokens.add(type=type, location=location, modifiers=modifiers)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ScriptBlock(Block, Element["BlockNode"]):
|
|
127
|
+
def __init__(self, document: 'Document',
|
|
128
|
+
type: str, id: str | None,
|
|
129
|
+
node: BlockNode, parent: Block,
|
|
130
|
+
comment: str) -> None:
|
|
131
|
+
super().__init__(document=document, type=type)
|
|
132
|
+
self.id: str | None = id
|
|
133
|
+
"""
|
|
134
|
+
ID of the block.
|
|
135
|
+
Blocks are not required to have an ID: this is represented by None.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
self.node: BlockNode = node
|
|
139
|
+
self.comment: str = comment
|
|
140
|
+
|
|
141
|
+
self.parent: Block = parent
|
|
142
|
+
|
|
143
|
+
self.references: list[Value] = []
|
|
144
|
+
"""List of values that reference this block."""
|
|
145
|
+
|
|
146
|
+
def __repr__(self) -> str:
|
|
147
|
+
return f"ScriptBlock(type={self.type}, id={self.id})"
|
|
148
|
+
|
|
149
|
+
def __str__(self) -> str:
|
|
150
|
+
if self.id is None:
|
|
151
|
+
return f"{self.type}"
|
|
152
|
+
return f"{self.type}:{self.id}"
|
|
153
|
+
|
|
154
|
+
def get_element_at(self, text_position: TextPosition) -> 'ScriptBlock | Value | None':
|
|
155
|
+
result = super().get_element_at(text_position)
|
|
156
|
+
if result is not None:
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
# check if it's within itself
|
|
160
|
+
node = self.node
|
|
161
|
+
if node.contains_position(text_position):
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## information
|
|
168
|
+
|
|
169
|
+
def get_root(self) -> 'Root':
|
|
170
|
+
current: Block = self
|
|
171
|
+
i = 0
|
|
172
|
+
while isinstance(current, ScriptBlock) and i < 1000:
|
|
173
|
+
current = current.parent
|
|
174
|
+
i += 1
|
|
175
|
+
assert isinstance(current, Root), "reached iteration limit without finding root"
|
|
176
|
+
return current
|
|
177
|
+
|
|
178
|
+
def get_description(self, dataset: 'Dataset') -> str:
|
|
179
|
+
if not dataset.is_script_block(self.type):
|
|
180
|
+
return ''
|
|
181
|
+
block_data = dataset.get_script_block_data(self.type)
|
|
182
|
+
return block_data.get('description', '')
|
|
183
|
+
|
|
184
|
+
def get_hover_information(self, dataset: 'Dataset', text_position: TextPosition) -> types.Hover | None:
|
|
185
|
+
node = self.node
|
|
186
|
+
type_node = node.type
|
|
187
|
+
if type_node is not None and type_node.to_range() == text_position:
|
|
188
|
+
# show a tree hierarchy of the parameter
|
|
189
|
+
tree = format_tree(self.get_tree())
|
|
190
|
+
desc = self.get_description(dataset)
|
|
191
|
+
|
|
192
|
+
# show link to ScriptsDocs
|
|
193
|
+
if dataset.is_script_block(self.type):
|
|
194
|
+
variant_tree = dataset.get_block_tree(self.type)
|
|
195
|
+
url = self.get_scriptsdocs_url(variant_tree)
|
|
196
|
+
tree += f"\n[Documentation]({url})"
|
|
197
|
+
|
|
198
|
+
# format the whole thing
|
|
199
|
+
if desc == "":
|
|
200
|
+
txt = tree
|
|
201
|
+
else:
|
|
202
|
+
txt = f"{tree}\n\n---\n\n{desc}"
|
|
203
|
+
|
|
204
|
+
return make_hover_information(
|
|
205
|
+
txt,
|
|
206
|
+
range=self.node.type.to_range() if self.node.type is not None else None,
|
|
207
|
+
)
|
|
208
|
+
# TODO: check ID hover info
|
|
209
|
+
return super().get_hover_information(dataset, text_position)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## actions
|
|
213
|
+
|
|
214
|
+
def get_ref_id_range(self) -> 'TextRange':
|
|
215
|
+
assert self.node.id is not None, "Block must have an ID"
|
|
216
|
+
return self.node.id.to_range()
|
|
217
|
+
|
|
218
|
+
def get_referenced_to_ranges(self, source_document: 'Document | None' = None) -> list['TextRange']:
|
|
219
|
+
ranges: list['TextRange'] = []
|
|
220
|
+
if source_document is None:
|
|
221
|
+
source_document = self.document
|
|
222
|
+
|
|
223
|
+
# add ranges for all references to this block
|
|
224
|
+
for ref in self.references:
|
|
225
|
+
# only apply editing ranges for values that are in the same document
|
|
226
|
+
if ref.parent.document != source_document:
|
|
227
|
+
continue
|
|
228
|
+
ranges.append(ref.get_ref_id_range())
|
|
229
|
+
|
|
230
|
+
return ranges
|
|
231
|
+
|
|
232
|
+
def gather_linked_editing_ranges(self) -> list[TextRange]:
|
|
233
|
+
assert self.node.id is not None, "Block must have an ID"
|
|
234
|
+
ranges: list[TextRange] = []
|
|
235
|
+
|
|
236
|
+
ranges.append(self.get_ref_id_range())
|
|
237
|
+
ranges.extend(self.get_referenced_to_ranges())
|
|
238
|
+
|
|
239
|
+
return ranges
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_linked_editing_ranges(self, text_position: TextPosition) -> list[TextRange] | None:
|
|
244
|
+
# TODO: if it's the ID of a module block
|
|
245
|
+
# we need to change specifically the module of the references only
|
|
246
|
+
|
|
247
|
+
# if we're editing its ID, then edit all occurrences of the ID in references to this block
|
|
248
|
+
node = self.node
|
|
249
|
+
id_node = node.id
|
|
250
|
+
if id_node is not None and id_node.to_range() == text_position:
|
|
251
|
+
# find all occurrences of this ID in references to this block
|
|
252
|
+
return self.gather_linked_editing_ranges()
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## definition
|
|
257
|
+
|
|
258
|
+
def gather_id_definitions(self) -> list[types.LocationLink]:
|
|
259
|
+
# assert self.node.id is not None, "Block must have an ID"
|
|
260
|
+
# id_node = self.node.id
|
|
261
|
+
|
|
262
|
+
result: list[types.LocationLink] = []
|
|
263
|
+
|
|
264
|
+
for ref in self.references:
|
|
265
|
+
ref_id_range = ref.get_ref_id_range().to_lsp()
|
|
266
|
+
|
|
267
|
+
location = types.LocationLink(
|
|
268
|
+
target_uri=path_to_uri(ref.parent.document.path),
|
|
269
|
+
target_range=ref_id_range,
|
|
270
|
+
target_selection_range=ref_id_range,
|
|
271
|
+
)
|
|
272
|
+
result.append(location)
|
|
273
|
+
|
|
274
|
+
return result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_definition(self, text_position: 'TextPosition') -> list[types.LocationLink] | None:
|
|
280
|
+
# check if we're on the ID of the block
|
|
281
|
+
node = self.node
|
|
282
|
+
id_node = node.id
|
|
283
|
+
if id_node is not None and id_node.to_range() == text_position:
|
|
284
|
+
return self.gather_id_definitions()
|
|
285
|
+
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
## validation
|
|
290
|
+
|
|
291
|
+
def validate(self, dataset: 'Dataset') -> bool:
|
|
292
|
+
# validate self
|
|
293
|
+
if not self.validate_block(dataset):
|
|
294
|
+
# don't validate the rest since they are dependent
|
|
295
|
+
# on the data of this block
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# validate key-values
|
|
299
|
+
for value in self.values:
|
|
300
|
+
value.validate(dataset)
|
|
301
|
+
|
|
302
|
+
# validate children
|
|
303
|
+
self.validate_children(dataset)
|
|
304
|
+
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
def validate_block(self, dataset: 'Dataset') -> bool:
|
|
308
|
+
"""
|
|
309
|
+
Validate the block itself, without considering its children or key-values.
|
|
310
|
+
This includes:
|
|
311
|
+
- validating the block type
|
|
312
|
+
- validating its ID
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
dataset (Dataset): The dataset used for validation.
|
|
316
|
+
diagnostics (DiagnosticCollection): The collection to which any validation diagnostics should be added.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
bool: True if the block is valid, False otherwise.
|
|
320
|
+
"""
|
|
321
|
+
type = self.type
|
|
322
|
+
node = self.node
|
|
323
|
+
if node.type is None or type == "":
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
self.add_semantic_token(
|
|
327
|
+
SemanticTokenType.KEYWORD,
|
|
328
|
+
node.type.to_range()
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# validate the type of the block
|
|
332
|
+
if not dataset.is_script_block(type):
|
|
333
|
+
self.add_diagnostic(
|
|
334
|
+
DiagnosticType.BLOCK_UNKNOWN_BLOCK,
|
|
335
|
+
node.type.to_range(),
|
|
336
|
+
{"type": type}
|
|
337
|
+
)
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
# validate the ID of the block
|
|
341
|
+
if not self.validate_id(dataset):
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
def validate_id(self, dataset: 'Dataset') -> bool:
|
|
347
|
+
type = self.type
|
|
348
|
+
node = self.node
|
|
349
|
+
if node.type is None or type == "":
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
# get ID data
|
|
353
|
+
block_data = dataset.get_script_block_data(self.type)
|
|
354
|
+
ID_data = block_data.get('ID')
|
|
355
|
+
|
|
356
|
+
# retrieve ID info
|
|
357
|
+
id = self.id
|
|
358
|
+
has_ID = id is not None
|
|
359
|
+
|
|
360
|
+
# no ID data means there shouldn't be any ID
|
|
361
|
+
if has_ID:
|
|
362
|
+
node_id = node.id
|
|
363
|
+
assert node_id is not None, "ID node should not be None when ID is present"
|
|
364
|
+
|
|
365
|
+
# add semantic token for the ID
|
|
366
|
+
# that the ID is valid or not
|
|
367
|
+
self.add_semantic_token(
|
|
368
|
+
SemanticTokenType.CLASS,
|
|
369
|
+
node_id.to_range(),
|
|
370
|
+
[SemanticTokenModifier.DECLARATION]
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# there shouldn't be an ID
|
|
374
|
+
if ID_data is None:
|
|
375
|
+
self.add_diagnostic(
|
|
376
|
+
DiagnosticType.BLOCK_UNEXPECTED_ID,
|
|
377
|
+
node_id.to_range(),
|
|
378
|
+
{"type": type, "id": id}
|
|
379
|
+
)
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
# it doesn't have ID needs
|
|
383
|
+
if ID_data is None:
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
# check if ID is optional for this block with a specific parent type
|
|
387
|
+
optional_blocks = ID_data.get('optional')
|
|
388
|
+
if optional_blocks is not None:
|
|
389
|
+
parent_type = self.parent.type
|
|
390
|
+
if parent_type.lower() in [block.lower() for block in optional_blocks]:
|
|
391
|
+
return True # it's optional, so we skip any other checks
|
|
392
|
+
|
|
393
|
+
# used to check if the parent block requires an ID for this sub-block
|
|
394
|
+
parents_without = ID_data.get('parentsWithout')
|
|
395
|
+
should_have_id_from_parent = True
|
|
396
|
+
if parents_without is not None:
|
|
397
|
+
parent_type = self.parent.type
|
|
398
|
+
if parent_type.lower() in [block.lower() for block in parents_without]:
|
|
399
|
+
should_have_id_from_parent = False
|
|
400
|
+
|
|
401
|
+
# there should be an ID but there isn't one
|
|
402
|
+
if not has_ID:
|
|
403
|
+
if should_have_id_from_parent:
|
|
404
|
+
self.add_diagnostic(
|
|
405
|
+
DiagnosticType.BLOCK_MISSING_ID,
|
|
406
|
+
node.type.to_range(),
|
|
407
|
+
{"id": ID_data}
|
|
408
|
+
)
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
# there is an ID, so validate it
|
|
412
|
+
else:
|
|
413
|
+
if not should_have_id_from_parent:
|
|
414
|
+
assert parents_without is not None, "parents_without should not be None when checking for unexpected ID with specific parent"
|
|
415
|
+
self.add_diagnostic(
|
|
416
|
+
DiagnosticType.BLOCK_HAS_ID_IN_PARENT,
|
|
417
|
+
node.type.to_range(),
|
|
418
|
+
{"type": type, "parent_type": self.parent.type, "invalid_blocks": parents_without}
|
|
419
|
+
)
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
# verify that the ID can have spaces
|
|
423
|
+
can_have_spaces = ID_data.get('canHaveSpace', False)
|
|
424
|
+
if not can_have_spaces and " " in id:
|
|
425
|
+
self.add_diagnostic(
|
|
426
|
+
DiagnosticType.BLOCK_ID_CANNOT_CONTAIN_SPACES,
|
|
427
|
+
node.type.to_range(),
|
|
428
|
+
{"type": type, "id": id}
|
|
429
|
+
)
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
# check if the ID is an accepted value
|
|
433
|
+
valid_IDs = ID_data.get('values')
|
|
434
|
+
if valid_IDs is not None:
|
|
435
|
+
if id not in valid_IDs:
|
|
436
|
+
self.add_diagnostic(
|
|
437
|
+
DiagnosticType.BLOCK_INVALID_ID,
|
|
438
|
+
node.type.to_range(),
|
|
439
|
+
{"type": type, "id": id, "validIDs": valid_IDs}
|
|
440
|
+
)
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
# TODO: this needs to be modified to support a wider range of condition for variants
|
|
444
|
+
# for example specific parameters or parents defining variants
|
|
445
|
+
|
|
446
|
+
# whenever we need to consider it as its own type based on the provided ID
|
|
447
|
+
if ID_data.get('asType', False):
|
|
448
|
+
self.type = self.type + " " + id
|
|
449
|
+
self.id = None # set the ID to None since it is now part of the type
|
|
450
|
+
|
|
451
|
+
# forbidden IDs
|
|
452
|
+
forbidden_IDs = ID_data.get('forbidden')
|
|
453
|
+
if forbidden_IDs is not None:
|
|
454
|
+
if id in forbidden_IDs:
|
|
455
|
+
self.add_diagnostic(
|
|
456
|
+
DiagnosticType.BLOCK_FORBIDDEN_ID,
|
|
457
|
+
node.type.to_range(),
|
|
458
|
+
{"type": type, "id": id, "forbiddenIDs": forbidden_IDs}
|
|
459
|
+
)
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
# TODO: implement ID translation diagnostics
|
|
463
|
+
# requires workspace handling setup
|
|
464
|
+
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def validate_children(self, dataset: 'Dataset') -> None:
|
|
469
|
+
# validate all children blocks
|
|
470
|
+
for child in self.children:
|
|
471
|
+
child.validate(dataset)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class Root(Block, Element["Chunk"]):
|
|
475
|
+
def __init__(self, document: 'Document', type: str, node: Chunk, comment: str) -> None:
|
|
476
|
+
super().__init__(document, type)
|
|
477
|
+
self.type = type
|
|
478
|
+
self.node = node
|
|
479
|
+
self.comment = comment
|
|
480
|
+
|
|
481
|
+
def get_root(self) -> 'Root':
|
|
482
|
+
return self
|
|
483
|
+
|
|
484
|
+
def validate(self, dataset: 'Dataset') -> bool:
|
|
485
|
+
# validate key-values
|
|
486
|
+
for value in self.values:
|
|
487
|
+
value.validate(dataset)
|
|
488
|
+
|
|
489
|
+
# validate children
|
|
490
|
+
self.validate_children(dataset)
|
|
491
|
+
return True
|
|
492
|
+
|
|
493
|
+
def validate_children(self, dataset: 'Dataset') -> None:
|
|
494
|
+
# validate all children blocks
|
|
495
|
+
for child in self.children:
|
|
496
|
+
child.validate(dataset)
|
|
497
|
+
|
|
498
|
+
# TODO: this only supports one module block rn
|
|
499
|
+
# but can multiple ones be used by the game ?
|
|
500
|
+
def get_module(self) -> 'ScriptBlock | None':
|
|
501
|
+
"""
|
|
502
|
+
Retrieves the module block within the current root block, if it exists.
|
|
503
|
+
"""
|
|
504
|
+
for child in self:
|
|
505
|
+
if child.type == "module":
|
|
506
|
+
return child
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
def get_imports(self) -> set[str]:
|
|
510
|
+
"""
|
|
511
|
+
Retrieves all the imports of the current module block.
|
|
512
|
+
"""
|
|
513
|
+
imports: set[str] = set()
|
|
514
|
+
|
|
515
|
+
# find the module block
|
|
516
|
+
module = self.get_module()
|
|
517
|
+
if module is None:
|
|
518
|
+
return set(imports)
|
|
519
|
+
|
|
520
|
+
# search for import statements within the module block
|
|
521
|
+
for child in module:
|
|
522
|
+
if child.type != "import":
|
|
523
|
+
continue
|
|
524
|
+
# retrieve all the values
|
|
525
|
+
for value in child.values:
|
|
526
|
+
if not value.is_key_value():
|
|
527
|
+
imports.add(value.value())
|
|
528
|
+
return imports
|
|
529
|
+
|
|
530
|
+
def find_references(self, modules: Iterable[str], id: str, block_type: str) -> list['ScriptBlock']:
|
|
531
|
+
result: set['ScriptBlock'] = set()
|
|
532
|
+
|
|
533
|
+
# find the corresponding module block within the current root block
|
|
534
|
+
module: ScriptBlock | None = self.get_module()
|
|
535
|
+
if module is None:
|
|
536
|
+
return list(result)
|
|
537
|
+
module_id = module.id
|
|
538
|
+
if module_id is None or module_id not in modules:
|
|
539
|
+
return list(result)
|
|
540
|
+
|
|
541
|
+
# iterate over all children of the module block
|
|
542
|
+
# and find our block
|
|
543
|
+
for child in module:
|
|
544
|
+
if child.type == block_type and child.id == id:
|
|
545
|
+
result.add(child)
|
|
546
|
+
|
|
547
|
+
return list(result)
|