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.
@@ -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