robotcode-robot 0.68.2__py3-none-any.whl → 0.68.5__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,2165 @@
1
+ import ast
2
+ import enum
3
+ import itertools
4
+ import re
5
+ import time
6
+ import weakref
7
+ from collections import OrderedDict, defaultdict
8
+ from concurrent.futures import CancelledError
9
+ from itertools import chain
10
+ from pathlib import Path
11
+ from typing import (
12
+ Any,
13
+ Dict,
14
+ Iterable,
15
+ Iterator,
16
+ List,
17
+ NamedTuple,
18
+ Optional,
19
+ Sequence,
20
+ Set,
21
+ Tuple,
22
+ Union,
23
+ )
24
+
25
+ from robot.errors import VariableError
26
+ from robot.libraries import STDLIBS
27
+ from robot.parsing.lexer.tokens import Token
28
+ from robot.parsing.model.blocks import (
29
+ Keyword,
30
+ SettingSection,
31
+ TestCase,
32
+ VariableSection,
33
+ )
34
+ from robot.parsing.model.statements import Arguments, Statement
35
+ from robot.parsing.model.statements import LibraryImport as RobotLibraryImport
36
+ from robot.parsing.model.statements import ResourceImport as RobotResourceImport
37
+ from robot.parsing.model.statements import (
38
+ VariablesImport as RobotVariablesImport,
39
+ )
40
+ from robot.variables.search import (
41
+ is_scalar_assign,
42
+ is_variable,
43
+ search_variable,
44
+ )
45
+ from robotcode.core.concurrent import RLock
46
+ from robotcode.core.event import event
47
+ from robotcode.core.lsp.types import (
48
+ CodeDescription,
49
+ Diagnostic,
50
+ DiagnosticRelatedInformation,
51
+ DiagnosticSeverity,
52
+ DiagnosticTag,
53
+ DocumentUri,
54
+ Location,
55
+ Position,
56
+ Range,
57
+ )
58
+ from robotcode.core.text_document import TextDocument
59
+ from robotcode.core.uri import Uri
60
+ from robotcode.core.utils.logging import LoggingDescriptor
61
+
62
+ from ..utils import get_robot_version
63
+ from ..utils.ast import (
64
+ range_from_node,
65
+ range_from_token,
66
+ strip_variable_token,
67
+ tokenize_variables,
68
+ )
69
+ from ..utils.match import eq_namespace
70
+ from ..utils.stubs import Languages
71
+ from ..utils.variables import BUILTIN_VARIABLES
72
+ from ..utils.visitor import Visitor
73
+ from .entities import (
74
+ ArgumentDefinition,
75
+ BuiltInVariableDefinition,
76
+ CommandLineVariableDefinition,
77
+ EnvironmentVariableDefinition,
78
+ Import,
79
+ InvalidVariableError,
80
+ LibraryEntry,
81
+ LibraryImport,
82
+ LocalVariableDefinition,
83
+ ResourceEntry,
84
+ ResourceImport,
85
+ VariableDefinition,
86
+ VariableMatcher,
87
+ VariablesEntry,
88
+ VariablesImport,
89
+ )
90
+ from .errors import DIAGNOSTICS_SOURCE_NAME, Error
91
+ from .imports_manager import ImportsManager
92
+ from .library_doc import (
93
+ BUILTIN_LIBRARY_NAME,
94
+ DEFAULT_LIBRARIES,
95
+ KeywordDoc,
96
+ KeywordError,
97
+ KeywordMatcher,
98
+ LibraryDoc,
99
+ )
100
+
101
+ EXTRACT_COMMENT_PATTERN = re.compile(r".*(?:^ *|\t+| {2,})#(?P<comment>.*)$")
102
+ ROBOTCODE_PATTERN = re.compile(r"(?P<marker>\brobotcode\b)\s*:\s*(?P<rule>\b\w+\b)")
103
+
104
+
105
+ class DiagnosticsError(Exception):
106
+ pass
107
+
108
+
109
+ class DiagnosticsWarningError(DiagnosticsError):
110
+ pass
111
+
112
+
113
+ class ImportError(DiagnosticsError):
114
+ pass
115
+
116
+
117
+ class NameSpaceError(Exception):
118
+ pass
119
+
120
+
121
+ class VariablesVisitor(Visitor):
122
+ def get(self, source: str, model: ast.AST) -> List[VariableDefinition]:
123
+ self._results: List[VariableDefinition] = []
124
+ self.source = source
125
+ self.visit(model)
126
+ return self._results
127
+
128
+ def visit_Section(self, node: ast.AST) -> None: # noqa: N802
129
+ if isinstance(node, VariableSection):
130
+ self.generic_visit(node)
131
+
132
+ def visit_Variable(self, node: Statement) -> None: # noqa: N802
133
+ name_token = node.get_token(Token.VARIABLE)
134
+ if name_token is None:
135
+ return
136
+
137
+ name = name_token.value
138
+
139
+ if name is not None:
140
+ match = search_variable(name, ignore_errors=True)
141
+ if not match.is_assign(allow_assign_mark=True):
142
+ return
143
+
144
+ if name.endswith("="):
145
+ name = name[:-1].rstrip()
146
+
147
+ values = node.get_values(Token.ARGUMENT)
148
+ has_value = bool(values)
149
+ value = tuple(
150
+ s.replace(
151
+ "${CURDIR}",
152
+ str(Path(self.source).parent).replace("\\", "\\\\"),
153
+ )
154
+ for s in values
155
+ )
156
+
157
+ self._results.append(
158
+ VariableDefinition(
159
+ name=name,
160
+ name_token=strip_variable_token(
161
+ Token(
162
+ name_token.type,
163
+ name,
164
+ name_token.lineno,
165
+ name_token.col_offset,
166
+ name_token.error,
167
+ )
168
+ ),
169
+ line_no=node.lineno,
170
+ col_offset=node.col_offset,
171
+ end_line_no=node.lineno,
172
+ end_col_offset=node.end_col_offset,
173
+ source=self.source,
174
+ has_value=has_value,
175
+ resolvable=True,
176
+ value=value,
177
+ )
178
+ )
179
+
180
+
181
+ class BlockVariableVisitor(Visitor):
182
+ def __init__(
183
+ self,
184
+ library_doc: LibraryDoc,
185
+ source: str,
186
+ position: Optional[Position] = None,
187
+ in_args: bool = True,
188
+ ) -> None:
189
+ super().__init__()
190
+ self.library_doc = library_doc
191
+ self.source = source
192
+ self.position = position
193
+ self.in_args = in_args
194
+
195
+ self._results: Dict[str, VariableDefinition] = {}
196
+ self.current_kw_doc: Optional[KeywordDoc] = None
197
+
198
+ def get(self, model: ast.AST) -> List[VariableDefinition]:
199
+ self._results = {}
200
+
201
+ self.visit(model)
202
+
203
+ return list(self._results.values())
204
+
205
+ def visit(self, node: ast.AST) -> None:
206
+ if self.position is None or self.position >= range_from_node(node).start:
207
+ super().visit(node)
208
+
209
+ def visit_Keyword(self, node: ast.AST) -> None: # noqa: N802
210
+ try:
211
+ self.generic_visit(node)
212
+ finally:
213
+ self.current_kw_doc = None
214
+
215
+ def visit_KeywordName(self, node: Statement) -> None: # noqa: N802
216
+ from .model_helper import ModelHelper
217
+
218
+ name_token = node.get_token(Token.KEYWORD_NAME)
219
+
220
+ if name_token is not None and name_token.value:
221
+ keyword = ModelHelper.get_keyword_definition_at_token(self.library_doc, name_token)
222
+ self.current_kw_doc = keyword
223
+
224
+ for variable_token in filter(
225
+ lambda e: e.type == Token.VARIABLE,
226
+ tokenize_variables(name_token, identifiers="$", ignore_errors=True),
227
+ ):
228
+ if variable_token.value:
229
+ match = search_variable(variable_token.value, "$", ignore_errors=True)
230
+ if match.base is None:
231
+ continue
232
+ name = match.base.split(":", 1)[0]
233
+ full_name = f"{match.identifier}{{{name}}}"
234
+ var_token = strip_variable_token(variable_token)
235
+ var_token.value = name
236
+ self._results[full_name] = ArgumentDefinition(
237
+ name=full_name,
238
+ name_token=var_token,
239
+ line_no=variable_token.lineno,
240
+ col_offset=variable_token.col_offset,
241
+ end_line_no=variable_token.lineno,
242
+ end_col_offset=variable_token.end_col_offset,
243
+ source=self.source,
244
+ keyword_doc=self.current_kw_doc,
245
+ )
246
+
247
+ def get_variable_token(self, token: Token) -> Optional[Token]:
248
+ return next(
249
+ (
250
+ v
251
+ for v in itertools.dropwhile(
252
+ lambda t: t.type in Token.NON_DATA_TOKENS,
253
+ tokenize_variables(token, ignore_errors=True, extra_types={Token.VARIABLE}),
254
+ )
255
+ if v.type == Token.VARIABLE
256
+ ),
257
+ None,
258
+ )
259
+
260
+ def visit_Arguments(self, node: Statement) -> None: # noqa: N802
261
+ args: List[str] = []
262
+
263
+ arguments = node.get_tokens(Token.ARGUMENT)
264
+
265
+ for argument_token in arguments:
266
+ try:
267
+ argument = self.get_variable_token(argument_token)
268
+
269
+ if argument is not None and argument.value != "@{}":
270
+ if (
271
+ self.in_args
272
+ and self.position is not None
273
+ and self.position in range_from_token(argument_token)
274
+ and self.position > range_from_token(argument).end
275
+ ):
276
+ break
277
+
278
+ if argument.value not in args:
279
+ args.append(argument.value)
280
+ arg_def = ArgumentDefinition(
281
+ name=argument.value,
282
+ name_token=strip_variable_token(argument),
283
+ line_no=argument.lineno,
284
+ col_offset=argument.col_offset,
285
+ end_line_no=argument.lineno,
286
+ end_col_offset=argument.end_col_offset,
287
+ source=self.source,
288
+ keyword_doc=self.current_kw_doc,
289
+ )
290
+ self._results[argument.value] = arg_def
291
+
292
+ except VariableError:
293
+ pass
294
+
295
+ def visit_ExceptHeader(self, node: Statement) -> None: # noqa: N802
296
+ variables = node.get_tokens(Token.VARIABLE)[:1]
297
+ if variables and is_scalar_assign(variables[0].value):
298
+ try:
299
+ variable = self.get_variable_token(variables[0])
300
+
301
+ if variable is not None:
302
+ self._results[variable.value] = LocalVariableDefinition(
303
+ name=variable.value,
304
+ name_token=strip_variable_token(variable),
305
+ line_no=variable.lineno,
306
+ col_offset=variable.col_offset,
307
+ end_line_no=variable.lineno,
308
+ end_col_offset=variable.end_col_offset,
309
+ source=self.source,
310
+ )
311
+
312
+ except VariableError:
313
+ pass
314
+
315
+ def visit_KeywordCall(self, node: Statement) -> None: # noqa: N802
316
+ # TODO analyze "Set Local/Global/Suite Variable"
317
+
318
+ for assign_token in node.get_tokens(Token.ASSIGN):
319
+ variable_token = self.get_variable_token(assign_token)
320
+
321
+ try:
322
+ if variable_token is not None:
323
+ if (
324
+ self.position is not None
325
+ and self.position in range_from_node(node)
326
+ and self.position > range_from_token(variable_token).end
327
+ ):
328
+ continue
329
+
330
+ if variable_token.value not in self._results:
331
+ self._results[variable_token.value] = LocalVariableDefinition(
332
+ name=variable_token.value,
333
+ name_token=strip_variable_token(variable_token),
334
+ line_no=variable_token.lineno,
335
+ col_offset=variable_token.col_offset,
336
+ end_line_no=variable_token.lineno,
337
+ end_col_offset=variable_token.end_col_offset,
338
+ source=self.source,
339
+ )
340
+
341
+ except VariableError:
342
+ pass
343
+
344
+ def visit_InlineIfHeader(self, node: Statement) -> None: # noqa: N802
345
+ for assign_token in node.get_tokens(Token.ASSIGN):
346
+ variable_token = self.get_variable_token(assign_token)
347
+
348
+ try:
349
+ if variable_token is not None:
350
+ if (
351
+ self.position is not None
352
+ and self.position in range_from_node(node)
353
+ and self.position > range_from_token(variable_token).end
354
+ ):
355
+ continue
356
+
357
+ if variable_token.value not in self._results:
358
+ self._results[variable_token.value] = LocalVariableDefinition(
359
+ name=variable_token.value,
360
+ name_token=strip_variable_token(variable_token),
361
+ line_no=variable_token.lineno,
362
+ col_offset=variable_token.col_offset,
363
+ end_line_no=variable_token.lineno,
364
+ end_col_offset=variable_token.end_col_offset,
365
+ source=self.source,
366
+ )
367
+
368
+ except VariableError:
369
+ pass
370
+
371
+ def visit_ForHeader(self, node: Statement) -> None: # noqa: N802
372
+ variables = node.get_tokens(Token.VARIABLE)
373
+ for variable in variables:
374
+ variable_token = self.get_variable_token(variable)
375
+ if variable_token is not None and variable_token.value and variable_token.value not in self._results:
376
+ self._results[variable_token.value] = LocalVariableDefinition(
377
+ name=variable_token.value,
378
+ name_token=strip_variable_token(variable_token),
379
+ line_no=variable_token.lineno,
380
+ col_offset=variable_token.col_offset,
381
+ end_line_no=variable_token.lineno,
382
+ end_col_offset=variable_token.end_col_offset,
383
+ source=self.source,
384
+ )
385
+
386
+ def visit_Var(self, node: Statement) -> None: # noqa: N802
387
+ variable = node.get_token(Token.VARIABLE)
388
+ if variable is None:
389
+ return
390
+ try:
391
+ if not is_variable(variable.value):
392
+ return
393
+
394
+ self._results[variable.value] = LocalVariableDefinition(
395
+ name=variable.value,
396
+ name_token=strip_variable_token(variable),
397
+ line_no=variable.lineno,
398
+ col_offset=variable.col_offset,
399
+ end_line_no=variable.lineno,
400
+ end_col_offset=variable.end_col_offset,
401
+ source=self.source,
402
+ )
403
+
404
+ except VariableError:
405
+ pass
406
+
407
+
408
+ class ImportVisitor(Visitor):
409
+ def get(self, source: str, model: ast.AST) -> List[Import]:
410
+ self._results: List[Import] = []
411
+ self.source = source
412
+ self.visit(model)
413
+ return self._results
414
+
415
+ def visit_Section(self, node: ast.AST) -> None: # noqa: N802
416
+ if isinstance(node, SettingSection):
417
+ self.generic_visit(node)
418
+
419
+ def visit_LibraryImport(self, node: RobotLibraryImport) -> None: # noqa: N802
420
+ name = node.get_token(Token.NAME)
421
+
422
+ separator = node.get_token(Token.WITH_NAME)
423
+ alias_token = node.get_tokens(Token.NAME)[-1] if separator else None
424
+
425
+ last_data_token = next(v for v in reversed(node.tokens) if v.type not in Token.NON_DATA_TOKENS)
426
+ if node.name:
427
+ self._results.append(
428
+ LibraryImport(
429
+ name=node.name,
430
+ name_token=name if name is not None else None,
431
+ args=node.args,
432
+ alias=node.alias,
433
+ alias_token=alias_token,
434
+ line_no=node.lineno,
435
+ col_offset=node.col_offset,
436
+ end_line_no=last_data_token.lineno
437
+ if last_data_token is not None
438
+ else node.end_lineno
439
+ if node.end_lineno is not None
440
+ else -1,
441
+ end_col_offset=last_data_token.end_col_offset
442
+ if last_data_token is not None
443
+ else node.end_col_offset
444
+ if node.end_col_offset is not None
445
+ else -1,
446
+ source=self.source,
447
+ )
448
+ )
449
+
450
+ def visit_ResourceImport(self, node: RobotResourceImport) -> None: # noqa: N802
451
+ name = node.get_token(Token.NAME)
452
+
453
+ last_data_token = next(v for v in reversed(node.tokens) if v.type not in Token.NON_DATA_TOKENS)
454
+ if node.name:
455
+ self._results.append(
456
+ ResourceImport(
457
+ name=node.name,
458
+ name_token=name if name is not None else None,
459
+ line_no=node.lineno,
460
+ col_offset=node.col_offset,
461
+ end_line_no=last_data_token.lineno
462
+ if last_data_token is not None
463
+ else node.end_lineno
464
+ if node.end_lineno is not None
465
+ else -1,
466
+ end_col_offset=last_data_token.end_col_offset
467
+ if last_data_token is not None
468
+ else node.end_col_offset
469
+ if node.end_col_offset is not None
470
+ else -1,
471
+ source=self.source,
472
+ )
473
+ )
474
+
475
+ def visit_VariablesImport(self, node: RobotVariablesImport) -> None: # noqa: N802
476
+ name = node.get_token(Token.NAME)
477
+
478
+ last_data_token = next(v for v in reversed(node.tokens) if v.type not in Token.NON_DATA_TOKENS)
479
+ if node.name:
480
+ self._results.append(
481
+ VariablesImport(
482
+ name=node.name,
483
+ name_token=name if name is not None else None,
484
+ args=node.args,
485
+ line_no=node.lineno,
486
+ col_offset=node.col_offset,
487
+ end_line_no=last_data_token.lineno
488
+ if last_data_token is not None
489
+ else node.end_lineno
490
+ if node.end_lineno is not None
491
+ else -1,
492
+ end_col_offset=last_data_token.end_col_offset
493
+ if last_data_token is not None
494
+ else node.end_col_offset
495
+ if node.end_col_offset is not None
496
+ else -1,
497
+ source=self.source,
498
+ )
499
+ )
500
+
501
+
502
+ class DocumentType(enum.Enum):
503
+ UNKNOWN = "unknown"
504
+ GENERAL = "robot"
505
+ RESOURCE = "resource"
506
+ INIT = "init"
507
+
508
+
509
+ class Namespace:
510
+ _logger = LoggingDescriptor()
511
+
512
+ @_logger.call
513
+ def __init__(
514
+ self,
515
+ imports_manager: ImportsManager,
516
+ model: ast.AST,
517
+ source: str,
518
+ document: Optional[TextDocument] = None,
519
+ document_type: Optional[DocumentType] = None,
520
+ languages: Optional[Languages] = None,
521
+ workspace_languages: Optional[Languages] = None,
522
+ ) -> None:
523
+ super().__init__()
524
+
525
+ self.imports_manager = imports_manager
526
+
527
+ self.model = model
528
+ self.source = source
529
+ self._document = weakref.ref(document) if document is not None else None
530
+ self.document_type: Optional[DocumentType] = document_type
531
+ self.languages = languages
532
+ self.workspace_languages = workspace_languages
533
+
534
+ self._libraries: Dict[str, LibraryEntry] = OrderedDict()
535
+ self._namespaces: Optional[Dict[KeywordMatcher, List[LibraryEntry]]] = None
536
+ self._libraries_matchers: Optional[Dict[KeywordMatcher, LibraryEntry]] = None
537
+ self._resources: Dict[str, ResourceEntry] = OrderedDict()
538
+ self._resources_matchers: Optional[Dict[KeywordMatcher, ResourceEntry]] = None
539
+ self._variables: Dict[str, VariablesEntry] = OrderedDict()
540
+ self._initialized = False
541
+ self._invalid = False
542
+ self._initialize_lock = RLock(default_timeout=120, name="Namespace.initialize")
543
+ self._analyzed = False
544
+ self._analyze_lock = RLock(default_timeout=120, name="Namespace.analyze")
545
+ self._library_doc: Optional[LibraryDoc] = None
546
+ self._library_doc_lock = RLock(default_timeout=120, name="Namespace.library_doc")
547
+ self._imports: Optional[List[Import]] = None
548
+ self._import_entries: Dict[Import, LibraryEntry] = OrderedDict()
549
+ self._own_variables: Optional[List[VariableDefinition]] = None
550
+ self._own_variables_lock = RLock(default_timeout=120, name="Namespace.own_variables")
551
+ self._global_variables: Optional[List[VariableDefinition]] = None
552
+ self._global_variables_lock = RLock(default_timeout=120, name="Namespace.global_variables")
553
+
554
+ self._diagnostics: List[Diagnostic] = []
555
+ self._keyword_references: Dict[KeywordDoc, Set[Location]] = {}
556
+ self._variable_references: Dict[VariableDefinition, Set[Location]] = {}
557
+ self._local_variable_assignments: Dict[VariableDefinition, Set[Range]] = {}
558
+ self._namespace_references: Dict[LibraryEntry, Set[Location]] = {}
559
+
560
+ self._imported_keywords: Optional[List[KeywordDoc]] = None
561
+ self._imported_keywords_lock = RLock(default_timeout=120, name="Namespace.imported_keywords")
562
+ self._keywords: Optional[List[KeywordDoc]] = None
563
+ self._keywords_lock = RLock(default_timeout=120, name="Namespace.keywords")
564
+
565
+ # TODO: how to get the search order from model
566
+ self.search_order: Tuple[str, ...] = ()
567
+
568
+ self._finder: Optional[KeywordFinder] = None
569
+
570
+ self.imports_manager.imports_changed.add(self.imports_changed)
571
+ self.imports_manager.libraries_changed.add(self.libraries_changed)
572
+ self.imports_manager.resources_changed.add(self.resources_changed)
573
+ self.imports_manager.variables_changed.add(self.variables_changed)
574
+
575
+ self._in_initialize = False
576
+
577
+ self._ignored_lines: Optional[List[int]] = None
578
+
579
+ @event
580
+ def has_invalidated(sender) -> None:
581
+ ...
582
+
583
+ @event
584
+ def has_initialized(sender) -> None:
585
+ ...
586
+
587
+ @event
588
+ def has_imports_changed(sender) -> None:
589
+ ...
590
+
591
+ @event
592
+ def has_analysed(sender) -> None:
593
+ ...
594
+
595
+ @property
596
+ def document(self) -> Optional[TextDocument]:
597
+ return self._document() if self._document is not None else None
598
+
599
+ def imports_changed(self, sender: Any, uri: DocumentUri) -> None:
600
+ # TODO: optimise this by checking our imports
601
+ if self.document is not None:
602
+ self.document.set_data(Namespace.DataEntry, None)
603
+
604
+ self.invalidate()
605
+
606
+ @_logger.call
607
+ def libraries_changed(self, sender: Any, libraries: List[LibraryDoc]) -> None:
608
+ if not self.initialized or self.invalid:
609
+ return
610
+
611
+ invalidate = False
612
+
613
+ for p in libraries:
614
+ if any(e for e in self._libraries.values() if e.library_doc == p):
615
+ invalidate = True
616
+ break
617
+
618
+ if invalidate:
619
+ if self.document is not None:
620
+ self.document.set_data(Namespace.DataEntry, None)
621
+
622
+ self.invalidate()
623
+
624
+ @_logger.call
625
+ def resources_changed(self, sender: Any, resources: List[LibraryDoc]) -> None:
626
+ if not self.initialized or self.invalid:
627
+ return
628
+
629
+ invalidate = False
630
+
631
+ for p in resources:
632
+ if any(e for e in self._resources.values() if e.library_doc.source == p.source):
633
+ invalidate = True
634
+ break
635
+
636
+ if invalidate:
637
+ if self.document is not None:
638
+ self.document.set_data(Namespace.DataEntry, None)
639
+
640
+ self.invalidate()
641
+
642
+ @_logger.call
643
+ def variables_changed(self, sender: Any, variables: List[LibraryDoc]) -> None:
644
+ if not self.initialized or self.invalid:
645
+ return
646
+
647
+ invalidate = False
648
+
649
+ for p in variables:
650
+ if any(e for e in self._variables.values() if e.library_doc.source == p.source):
651
+ invalidate = True
652
+ break
653
+
654
+ if invalidate:
655
+ if self.document is not None:
656
+ self.document.set_data(Namespace.DataEntry, None)
657
+
658
+ self.invalidate()
659
+
660
+ def is_initialized(self) -> bool:
661
+ with self._initialize_lock:
662
+ return self._initialized
663
+
664
+ def _invalidate(self) -> None:
665
+ self._invalid = True
666
+
667
+ @_logger.call
668
+ def invalidate(self) -> None:
669
+ with self._initialize_lock:
670
+ self._invalidate()
671
+ self.has_invalidated(self)
672
+
673
+ @_logger.call
674
+ def get_diagnostics(self) -> List[Diagnostic]:
675
+ self.ensure_initialized()
676
+
677
+ self.analyze()
678
+
679
+ return self._diagnostics
680
+
681
+ @_logger.call
682
+ def get_keyword_references(self) -> Dict[KeywordDoc, Set[Location]]:
683
+ self.ensure_initialized()
684
+
685
+ self.analyze()
686
+
687
+ return self._keyword_references
688
+
689
+ def get_variable_references(
690
+ self,
691
+ ) -> Dict[VariableDefinition, Set[Location]]:
692
+ self.ensure_initialized()
693
+
694
+ self.analyze()
695
+
696
+ return self._variable_references
697
+
698
+ def get_local_variable_assignments(
699
+ self,
700
+ ) -> Dict[VariableDefinition, Set[Range]]:
701
+ self.ensure_initialized()
702
+
703
+ self.analyze()
704
+
705
+ return self._local_variable_assignments
706
+
707
+ def get_namespace_references(self) -> Dict[LibraryEntry, Set[Location]]:
708
+ self.ensure_initialized()
709
+
710
+ self.analyze()
711
+
712
+ return self._namespace_references
713
+
714
+ def get_import_entries(self) -> Dict[Import, LibraryEntry]:
715
+ self.ensure_initialized()
716
+
717
+ return self._import_entries
718
+
719
+ def get_libraries(self) -> Dict[str, LibraryEntry]:
720
+ self.ensure_initialized()
721
+
722
+ return self._libraries
723
+
724
+ def get_namespaces(self) -> Dict[KeywordMatcher, List[LibraryEntry]]:
725
+ self.ensure_initialized()
726
+
727
+ if self._namespaces is None:
728
+ self._namespaces = defaultdict(list)
729
+
730
+ for v in (self.get_libraries()).values():
731
+ self._namespaces[KeywordMatcher(v.alias or v.name or v.import_name, is_namespace=True)].append(v)
732
+ for v in (self.get_resources()).values():
733
+ self._namespaces[KeywordMatcher(v.alias or v.name or v.import_name, is_namespace=True)].append(v)
734
+ return self._namespaces
735
+
736
+ def get_resources(self) -> Dict[str, ResourceEntry]:
737
+ self.ensure_initialized()
738
+
739
+ return self._resources
740
+
741
+ def get_imported_variables(self) -> Dict[str, VariablesEntry]:
742
+ self.ensure_initialized()
743
+
744
+ return self._variables
745
+
746
+ @_logger.call
747
+ def get_library_doc(self) -> LibraryDoc:
748
+ with self._library_doc_lock:
749
+ if self._library_doc is None:
750
+ self._library_doc = self.imports_manager.get_libdoc_from_model(
751
+ self.model,
752
+ self.source,
753
+ model_type="RESOURCE",
754
+ append_model_errors=self.document_type is not None and self.document_type == DocumentType.RESOURCE,
755
+ )
756
+
757
+ return self._library_doc
758
+
759
+ class DataEntry(NamedTuple):
760
+ libraries: Dict[str, LibraryEntry] = OrderedDict()
761
+ resources: Dict[str, ResourceEntry] = OrderedDict()
762
+ variables: Dict[str, VariablesEntry] = OrderedDict()
763
+ diagnostics: List[Diagnostic] = []
764
+ import_entries: Dict[Import, LibraryEntry] = OrderedDict()
765
+ imported_keywords: Optional[List[KeywordDoc]] = None
766
+
767
+ @_logger.call(condition=lambda self: not self._initialized)
768
+ def ensure_initialized(self) -> bool:
769
+ run_initialize = False
770
+ imports_changed = False
771
+
772
+ with self._initialize_lock:
773
+ if not self._initialized:
774
+ if self._in_initialize:
775
+ self._logger.critical(lambda: f"already initialized {self.document}")
776
+
777
+ self._in_initialize = True
778
+
779
+ try:
780
+ self._logger.debug(lambda: f"ensure_initialized -> initialize {self.document}")
781
+
782
+ imports = self.get_imports()
783
+
784
+ data_entry: Optional[Namespace.DataEntry] = None
785
+ if self.document is not None:
786
+ # check or save several data in documents data cache,
787
+ # if imports are different, then the data is invalid
788
+ old_imports: Optional[List[Import]] = self.document.get_data(Namespace)
789
+ if old_imports is None:
790
+ self.document.set_data(Namespace, imports)
791
+ elif old_imports != imports:
792
+ imports_changed = True
793
+
794
+ self.document.set_data(Namespace, imports)
795
+ self.document.set_data(Namespace.DataEntry, None)
796
+ else:
797
+ data_entry = self.document.get_data(Namespace.DataEntry)
798
+
799
+ if data_entry is not None:
800
+ self._libraries = data_entry.libraries.copy()
801
+ self._resources = data_entry.resources.copy()
802
+ self._variables = data_entry.variables.copy()
803
+ self._diagnostics = data_entry.diagnostics.copy()
804
+ self._import_entries = data_entry.import_entries.copy()
805
+ self._imported_keywords = (
806
+ data_entry.imported_keywords.copy() if data_entry.imported_keywords else None
807
+ )
808
+ else:
809
+ variables = self.get_resolvable_variables()
810
+
811
+ self._import_default_libraries(variables)
812
+ self._import_imports(
813
+ imports,
814
+ str(Path(self.source).parent),
815
+ top_level=True,
816
+ variables=variables,
817
+ )
818
+
819
+ if self.document is not None:
820
+ self.document.set_data(
821
+ Namespace.DataEntry,
822
+ Namespace.DataEntry(
823
+ self._libraries.copy(),
824
+ self._resources.copy(),
825
+ self._variables.copy(),
826
+ self._diagnostics.copy(),
827
+ self._import_entries.copy(),
828
+ self._imported_keywords.copy() if self._imported_keywords else None,
829
+ ),
830
+ )
831
+
832
+ self._reset_global_variables()
833
+
834
+ self._initialized = True
835
+ run_initialize = True
836
+
837
+ except BaseException:
838
+ if self.document is not None:
839
+ self.document.remove_data(Namespace)
840
+ self.document.remove_data(Namespace.DataEntry)
841
+
842
+ self._invalidate()
843
+ raise
844
+ finally:
845
+ self._in_initialize = False
846
+
847
+ if run_initialize:
848
+ self.has_initialized(self)
849
+
850
+ if imports_changed:
851
+ self.has_imports_changed(self)
852
+
853
+ return self._initialized
854
+
855
+ @property
856
+ def initialized(self) -> bool:
857
+ return self._initialized
858
+
859
+ @property
860
+ def invalid(self) -> bool:
861
+ return self._invalid
862
+
863
+ @_logger.call
864
+ def get_imports(self) -> List[Import]:
865
+ if self._imports is None:
866
+ self._imports = ImportVisitor().get(self.source, self.model)
867
+
868
+ return self._imports
869
+
870
+ @_logger.call
871
+ def get_own_variables(self) -> List[VariableDefinition]:
872
+ with self._own_variables_lock:
873
+ if self._own_variables is None:
874
+ self._own_variables = VariablesVisitor().get(self.source, self.model)
875
+
876
+ return self._own_variables
877
+
878
+ _builtin_variables: Optional[List[BuiltInVariableDefinition]] = None
879
+
880
+ @classmethod
881
+ def get_builtin_variables(cls) -> List[BuiltInVariableDefinition]:
882
+ if cls._builtin_variables is None:
883
+ cls._builtin_variables = [BuiltInVariableDefinition(0, 0, 0, 0, "", n, None) for n in BUILTIN_VARIABLES]
884
+
885
+ return cls._builtin_variables
886
+
887
+ @_logger.call
888
+ def get_command_line_variables(self) -> List[VariableDefinition]:
889
+ return self.imports_manager.get_command_line_variables()
890
+
891
+ def _reset_global_variables(self) -> None:
892
+ with self._global_variables_lock:
893
+ self._global_variables = None
894
+
895
+ def get_global_variables(self) -> List[VariableDefinition]:
896
+ with self._global_variables_lock:
897
+ if self._global_variables is None:
898
+ self._global_variables = list(
899
+ itertools.chain(
900
+ self.get_command_line_variables(),
901
+ self.get_own_variables(),
902
+ *(e.variables for e in self._resources.values()),
903
+ *(e.variables for e in self._variables.values()),
904
+ self.get_builtin_variables(),
905
+ )
906
+ )
907
+
908
+ return self._global_variables
909
+
910
+ def yield_variables(
911
+ self,
912
+ nodes: Optional[List[ast.AST]] = None,
913
+ position: Optional[Position] = None,
914
+ skip_commandline_variables: bool = False,
915
+ ) -> Iterator[Tuple[VariableMatcher, VariableDefinition]]:
916
+ yielded: Dict[VariableMatcher, VariableDefinition] = {}
917
+
918
+ test_or_keyword_nodes = list(
919
+ itertools.dropwhile(
920
+ lambda v: not isinstance(v, (TestCase, Keyword)),
921
+ nodes if nodes else [],
922
+ )
923
+ )
924
+ test_or_keyword = test_or_keyword_nodes[0] if test_or_keyword_nodes else None
925
+
926
+ for var in chain(
927
+ *[
928
+ (
929
+ BlockVariableVisitor(
930
+ self.get_library_doc(),
931
+ self.source,
932
+ position,
933
+ isinstance(test_or_keyword_nodes[-1], Arguments) if nodes else False,
934
+ ).get(test_or_keyword)
935
+ )
936
+ if test_or_keyword is not None
937
+ else []
938
+ ],
939
+ self.get_global_variables(),
940
+ ):
941
+ if var.matcher not in yielded:
942
+ if skip_commandline_variables and isinstance(var, CommandLineVariableDefinition):
943
+ continue
944
+
945
+ yielded[var.matcher] = var
946
+
947
+ yield var.matcher, var
948
+
949
+ def get_resolvable_variables(
950
+ self,
951
+ nodes: Optional[List[ast.AST]] = None,
952
+ position: Optional[Position] = None,
953
+ ) -> Dict[str, Any]:
954
+ return {
955
+ v.name: v.value
956
+ for k, v in self.yield_variables(nodes, position, skip_commandline_variables=True)
957
+ if v.has_value
958
+ }
959
+
960
+ def get_variable_matchers(
961
+ self,
962
+ nodes: Optional[List[ast.AST]] = None,
963
+ position: Optional[Position] = None,
964
+ ) -> Dict[VariableMatcher, VariableDefinition]:
965
+ self.ensure_initialized()
966
+
967
+ return {m: v for m, v in self.yield_variables(nodes, position)}
968
+
969
+ @_logger.call
970
+ def find_variable(
971
+ self,
972
+ name: str,
973
+ nodes: Optional[List[ast.AST]] = None,
974
+ position: Optional[Position] = None,
975
+ skip_commandline_variables: bool = False,
976
+ ignore_error: bool = False,
977
+ ) -> Optional[VariableDefinition]:
978
+ self.ensure_initialized()
979
+
980
+ if name[:2] == "%{" and name[-1] == "}":
981
+ var_name, _, default_value = name[2:-1].partition("=")
982
+ return EnvironmentVariableDefinition(
983
+ 0,
984
+ 0,
985
+ 0,
986
+ 0,
987
+ "",
988
+ f"%{{{var_name}}}",
989
+ None,
990
+ default_value=default_value or None,
991
+ )
992
+
993
+ try:
994
+ matcher = VariableMatcher(name)
995
+
996
+ for m, v in self.yield_variables(
997
+ nodes,
998
+ position,
999
+ skip_commandline_variables=skip_commandline_variables,
1000
+ ):
1001
+ if matcher == m:
1002
+ return v
1003
+ except InvalidVariableError:
1004
+ if not ignore_error:
1005
+ raise
1006
+
1007
+ return None
1008
+
1009
+ def _import_imports(
1010
+ self,
1011
+ imports: Iterable[Import],
1012
+ base_dir: str,
1013
+ *,
1014
+ top_level: bool = False,
1015
+ variables: Optional[Dict[str, Any]] = None,
1016
+ source: Optional[str] = None,
1017
+ parent_import: Optional[Import] = None,
1018
+ ) -> None:
1019
+ def _import(
1020
+ value: Import, variables: Optional[Dict[str, Any]] = None
1021
+ ) -> Tuple[Optional[LibraryEntry], Optional[Dict[str, Any]]]:
1022
+ result: Optional[LibraryEntry] = None
1023
+ try:
1024
+ if isinstance(value, LibraryImport):
1025
+ if value.name is None:
1026
+ raise NameSpaceError("Library setting requires value.")
1027
+
1028
+ result = self._get_library_entry(
1029
+ value.name,
1030
+ value.args,
1031
+ value.alias,
1032
+ base_dir,
1033
+ sentinel=value,
1034
+ variables=variables,
1035
+ )
1036
+ result.import_range = value.range
1037
+ result.import_source = value.source
1038
+ result.alias_range = value.alias_range
1039
+
1040
+ self._import_entries[value] = result
1041
+
1042
+ if (
1043
+ top_level
1044
+ and result.library_doc.errors is None
1045
+ and (len(result.library_doc.keywords) == 0 and not bool(result.library_doc.has_listener))
1046
+ ):
1047
+ self.append_diagnostics(
1048
+ range=value.range,
1049
+ message=f"Imported library '{value.name}' contains no keywords.",
1050
+ severity=DiagnosticSeverity.WARNING,
1051
+ source=DIAGNOSTICS_SOURCE_NAME,
1052
+ code=Error.LIBRARY_CONTAINS_NO_KEYWORDS,
1053
+ )
1054
+ elif isinstance(value, ResourceImport):
1055
+ if value.name is None:
1056
+ raise NameSpaceError("Resource setting requires value.")
1057
+
1058
+ source = self.imports_manager.find_resource(value.name, base_dir, variables=variables)
1059
+
1060
+ if self.source == source:
1061
+ if parent_import:
1062
+ self.append_diagnostics(
1063
+ range=parent_import.range,
1064
+ message="Possible circular import.",
1065
+ severity=DiagnosticSeverity.INFORMATION,
1066
+ source=DIAGNOSTICS_SOURCE_NAME,
1067
+ related_information=[
1068
+ DiagnosticRelatedInformation(
1069
+ location=Location(
1070
+ str(Uri.from_path(value.source)),
1071
+ value.range,
1072
+ ),
1073
+ message=f"'{Path(self.source).name}' is also imported here.",
1074
+ )
1075
+ ]
1076
+ if value.source
1077
+ else None,
1078
+ code=Error.POSSIBLE_CIRCULAR_IMPORT,
1079
+ )
1080
+ else:
1081
+ result = self._get_resource_entry(
1082
+ value.name,
1083
+ base_dir,
1084
+ sentinel=value,
1085
+ variables=variables,
1086
+ )
1087
+ result.import_range = value.range
1088
+ result.import_source = value.source
1089
+
1090
+ self._import_entries[value] = result
1091
+ if result.variables:
1092
+ variables = None
1093
+
1094
+ if top_level and (
1095
+ not result.library_doc.errors
1096
+ and top_level
1097
+ and not result.imports
1098
+ and not result.variables
1099
+ and not result.library_doc.keywords
1100
+ ):
1101
+ self.append_diagnostics(
1102
+ range=value.range,
1103
+ message=f"Imported resource file '{value.name}' is empty.",
1104
+ severity=DiagnosticSeverity.WARNING,
1105
+ source=DIAGNOSTICS_SOURCE_NAME,
1106
+ code=Error.RESOURCE_EMPTY,
1107
+ )
1108
+
1109
+ elif isinstance(value, VariablesImport):
1110
+ if value.name is None:
1111
+ raise NameSpaceError("Variables setting requires value.")
1112
+
1113
+ result = self._get_variables_entry(
1114
+ value.name,
1115
+ value.args,
1116
+ base_dir,
1117
+ sentinel=value,
1118
+ variables=variables,
1119
+ )
1120
+
1121
+ result.import_range = value.range
1122
+ result.import_source = value.source
1123
+
1124
+ self._import_entries[value] = result
1125
+ variables = None
1126
+ else:
1127
+ raise DiagnosticsError("Unknown import type.")
1128
+
1129
+ if top_level and result is not None:
1130
+ if result.library_doc.source is not None and result.library_doc.errors:
1131
+ if any(err.source and Path(err.source).is_absolute() for err in result.library_doc.errors):
1132
+ self.append_diagnostics(
1133
+ range=value.range,
1134
+ message="Import definition contains errors.",
1135
+ severity=DiagnosticSeverity.ERROR,
1136
+ source=DIAGNOSTICS_SOURCE_NAME,
1137
+ related_information=[
1138
+ DiagnosticRelatedInformation(
1139
+ location=Location(
1140
+ uri=str(Uri.from_path(err.source)),
1141
+ range=Range(
1142
+ start=Position(
1143
+ line=err.line_no - 1
1144
+ if err.line_no is not None
1145
+ else max(
1146
+ result.library_doc.line_no,
1147
+ 0,
1148
+ ),
1149
+ character=0,
1150
+ ),
1151
+ end=Position(
1152
+ line=err.line_no - 1
1153
+ if err.line_no is not None
1154
+ else max(
1155
+ result.library_doc.line_no,
1156
+ 0,
1157
+ ),
1158
+ character=0,
1159
+ ),
1160
+ ),
1161
+ ),
1162
+ message=err.message,
1163
+ )
1164
+ for err in result.library_doc.errors
1165
+ if err.source is not None
1166
+ ],
1167
+ code=Error.IMPORT_CONTAINS_ERRORS,
1168
+ )
1169
+ for err in filter(
1170
+ lambda e: e.source is None or not Path(e.source).is_absolute(),
1171
+ result.library_doc.errors,
1172
+ ):
1173
+ self.append_diagnostics(
1174
+ range=value.range,
1175
+ message=err.message,
1176
+ severity=DiagnosticSeverity.ERROR,
1177
+ source=DIAGNOSTICS_SOURCE_NAME,
1178
+ code=err.type_name,
1179
+ )
1180
+ elif result.library_doc.errors is not None:
1181
+ for err in result.library_doc.errors:
1182
+ self.append_diagnostics(
1183
+ range=value.range,
1184
+ message=err.message,
1185
+ severity=DiagnosticSeverity.ERROR,
1186
+ source=DIAGNOSTICS_SOURCE_NAME,
1187
+ code=err.type_name,
1188
+ )
1189
+
1190
+ except (SystemExit, KeyboardInterrupt):
1191
+ raise
1192
+ except BaseException as e:
1193
+ if top_level:
1194
+ self.append_diagnostics(
1195
+ range=value.range,
1196
+ message=str(e),
1197
+ severity=DiagnosticSeverity.ERROR,
1198
+ source=DIAGNOSTICS_SOURCE_NAME,
1199
+ code=type(e).__qualname__,
1200
+ )
1201
+ finally:
1202
+ self._reset_global_variables()
1203
+
1204
+ return result, variables
1205
+
1206
+ current_time = time.monotonic()
1207
+ self._logger.debug(lambda: f"start imports for {self.document if top_level else source}")
1208
+ try:
1209
+ for imp in imports:
1210
+ if variables is None:
1211
+ variables = self.get_resolvable_variables()
1212
+
1213
+ entry, variables = _import(imp, variables=variables)
1214
+
1215
+ if entry is not None:
1216
+ if isinstance(entry, ResourceEntry):
1217
+ assert entry.library_doc.source is not None
1218
+ already_imported_resources = next(
1219
+ (e for e in self._resources.values() if e.library_doc.source == entry.library_doc.source),
1220
+ None,
1221
+ )
1222
+
1223
+ if already_imported_resources is None and entry.library_doc.source != self.source:
1224
+ self._resources[entry.import_name] = entry
1225
+ try:
1226
+ self._import_imports(
1227
+ entry.imports,
1228
+ str(Path(entry.library_doc.source).parent),
1229
+ top_level=False,
1230
+ variables=variables,
1231
+ source=entry.library_doc.source,
1232
+ parent_import=imp if top_level else parent_import,
1233
+ )
1234
+ except (SystemExit, KeyboardInterrupt):
1235
+ raise
1236
+ except BaseException as e:
1237
+ if top_level:
1238
+ self.append_diagnostics(
1239
+ range=entry.import_range,
1240
+ message=str(e) or type(entry).__name__,
1241
+ severity=DiagnosticSeverity.ERROR,
1242
+ source=DIAGNOSTICS_SOURCE_NAME,
1243
+ code=type(e).__qualname__,
1244
+ )
1245
+ else:
1246
+ if top_level:
1247
+ if entry.library_doc.source == self.source:
1248
+ self.append_diagnostics(
1249
+ range=entry.import_range,
1250
+ message="Recursive resource import.",
1251
+ severity=DiagnosticSeverity.INFORMATION,
1252
+ source=DIAGNOSTICS_SOURCE_NAME,
1253
+ code=Error.RECURSIVE_IMPORT,
1254
+ )
1255
+ elif (
1256
+ already_imported_resources is not None
1257
+ and already_imported_resources.library_doc.source
1258
+ ):
1259
+ self.append_diagnostics(
1260
+ range=entry.import_range,
1261
+ message=f"Resource {entry} already imported.",
1262
+ severity=DiagnosticSeverity.INFORMATION,
1263
+ source=DIAGNOSTICS_SOURCE_NAME,
1264
+ related_information=[
1265
+ DiagnosticRelatedInformation(
1266
+ location=Location(
1267
+ uri=str(Uri.from_path(already_imported_resources.import_source)),
1268
+ range=already_imported_resources.import_range,
1269
+ ),
1270
+ message="",
1271
+ )
1272
+ ]
1273
+ if already_imported_resources.import_source
1274
+ else None,
1275
+ code=Error.RESOURCE_ALREADY_IMPORTED,
1276
+ )
1277
+
1278
+ elif isinstance(entry, VariablesEntry):
1279
+ already_imported_variables = [
1280
+ e
1281
+ for e in self._variables.values()
1282
+ if e.library_doc.source == entry.library_doc.source
1283
+ and e.alias == entry.alias
1284
+ and e.args == entry.args
1285
+ ]
1286
+ if (
1287
+ top_level
1288
+ and already_imported_variables
1289
+ and already_imported_variables[0].library_doc.source
1290
+ ):
1291
+ self.append_diagnostics(
1292
+ range=entry.import_range,
1293
+ message=f'Variables "{entry}" already imported.',
1294
+ severity=DiagnosticSeverity.INFORMATION,
1295
+ source=DIAGNOSTICS_SOURCE_NAME,
1296
+ related_information=[
1297
+ DiagnosticRelatedInformation(
1298
+ location=Location(
1299
+ uri=str(Uri.from_path(already_imported_variables[0].import_source)),
1300
+ range=already_imported_variables[0].import_range,
1301
+ ),
1302
+ message="",
1303
+ )
1304
+ ]
1305
+ if already_imported_variables[0].import_source
1306
+ else None,
1307
+ code=Error.VARIABLES_ALREADY_IMPORTED,
1308
+ )
1309
+
1310
+ if (entry.alias or entry.name or entry.import_name) not in self._variables:
1311
+ self._variables[entry.alias or entry.name or entry.import_name] = entry
1312
+
1313
+ elif isinstance(entry, LibraryEntry):
1314
+ if top_level and entry.name == BUILTIN_LIBRARY_NAME and entry.alias is None:
1315
+ self.append_diagnostics(
1316
+ range=entry.import_range,
1317
+ message=f'Library "{entry}" is not imported,'
1318
+ ' because it would override the "BuiltIn" library.',
1319
+ severity=DiagnosticSeverity.INFORMATION,
1320
+ source=DIAGNOSTICS_SOURCE_NAME,
1321
+ related_information=[
1322
+ DiagnosticRelatedInformation(
1323
+ location=Location(
1324
+ uri=str(Uri.from_path(entry.import_source)),
1325
+ range=entry.import_range,
1326
+ ),
1327
+ message="",
1328
+ )
1329
+ ]
1330
+ if entry.import_source
1331
+ else None,
1332
+ code=Error.LIBRARY_OVERRIDES_BUILTIN,
1333
+ )
1334
+ continue
1335
+
1336
+ already_imported_library = [
1337
+ e
1338
+ for e in self._libraries.values()
1339
+ if e.library_doc.source == entry.library_doc.source
1340
+ and e.library_doc.member_name == entry.library_doc.member_name
1341
+ and e.alias == entry.alias
1342
+ and e.args == entry.args
1343
+ ]
1344
+ if top_level and already_imported_library and already_imported_library[0].library_doc.source:
1345
+ self.append_diagnostics(
1346
+ range=entry.import_range,
1347
+ message=f'Library "{entry}" already imported.',
1348
+ severity=DiagnosticSeverity.INFORMATION,
1349
+ source=DIAGNOSTICS_SOURCE_NAME,
1350
+ related_information=[
1351
+ DiagnosticRelatedInformation(
1352
+ location=Location(
1353
+ uri=str(Uri.from_path(already_imported_library[0].import_source)),
1354
+ range=already_imported_library[0].import_range,
1355
+ ),
1356
+ message="",
1357
+ )
1358
+ ]
1359
+ if already_imported_library[0].import_source
1360
+ else None,
1361
+ code=Error.LIBRARY_ALREADY_IMPORTED,
1362
+ )
1363
+
1364
+ if (entry.alias or entry.name or entry.import_name) not in self._libraries:
1365
+ self._libraries[entry.alias or entry.name or entry.import_name] = entry
1366
+ finally:
1367
+ self._logger.debug(
1368
+ lambda: "end import imports for "
1369
+ f"{self.document if top_level else source} in {time.monotonic() - current_time}s"
1370
+ )
1371
+
1372
+ def _import_default_libraries(self, variables: Optional[Dict[str, Any]] = None) -> None:
1373
+ def _import_lib(library: str, variables: Optional[Dict[str, Any]] = None) -> Optional[LibraryEntry]:
1374
+ try:
1375
+ return self._get_library_entry(
1376
+ library,
1377
+ (),
1378
+ None,
1379
+ str(Path(self.source).parent),
1380
+ is_default_library=True,
1381
+ variables=variables,
1382
+ )
1383
+ except (SystemExit, KeyboardInterrupt):
1384
+ raise
1385
+ except BaseException as e:
1386
+ self.append_diagnostics(
1387
+ range=Range.zero(),
1388
+ message=f"Can't import default library '{library}': {str(e) or type(e).__name__}",
1389
+ severity=DiagnosticSeverity.ERROR,
1390
+ source="Robot",
1391
+ code=type(e).__qualname__,
1392
+ )
1393
+ return None
1394
+
1395
+ self._logger.debug(lambda: f"start import default libraries for document {self.document}")
1396
+ try:
1397
+ for library in DEFAULT_LIBRARIES:
1398
+ e = _import_lib(library, variables or self.get_resolvable_variables())
1399
+ if e is not None:
1400
+ self._libraries[e.alias or e.name or e.import_name] = e
1401
+ finally:
1402
+ self._logger.debug(lambda: f"end import default libraries for document {self.document}")
1403
+
1404
+ @_logger.call
1405
+ def _get_library_entry(
1406
+ self,
1407
+ name: str,
1408
+ args: Tuple[Any, ...],
1409
+ alias: Optional[str],
1410
+ base_dir: str,
1411
+ *,
1412
+ is_default_library: bool = False,
1413
+ sentinel: Any = None,
1414
+ variables: Optional[Dict[str, Any]] = None,
1415
+ ) -> LibraryEntry:
1416
+ library_doc = self.imports_manager.get_libdoc_for_library_import(
1417
+ name,
1418
+ args,
1419
+ base_dir=base_dir,
1420
+ sentinel=None if is_default_library else sentinel,
1421
+ variables=variables or self.get_resolvable_variables(),
1422
+ )
1423
+
1424
+ return LibraryEntry(
1425
+ name=library_doc.name,
1426
+ import_name=name,
1427
+ library_doc=library_doc,
1428
+ args=args,
1429
+ alias=alias,
1430
+ )
1431
+
1432
+ @_logger.call
1433
+ def get_imported_library_libdoc(
1434
+ self, name: str, args: Tuple[str, ...] = (), alias: Optional[str] = None
1435
+ ) -> Optional[LibraryDoc]:
1436
+ self.ensure_initialized()
1437
+
1438
+ return next(
1439
+ (
1440
+ v.library_doc
1441
+ for e, v in self._import_entries.items()
1442
+ if isinstance(e, LibraryImport) and v.import_name == name and v.args == args and v.alias == alias
1443
+ ),
1444
+ None,
1445
+ )
1446
+
1447
+ @_logger.call
1448
+ def _get_resource_entry(
1449
+ self,
1450
+ name: str,
1451
+ base_dir: str,
1452
+ *,
1453
+ sentinel: Any = None,
1454
+ variables: Optional[Dict[str, Any]] = None,
1455
+ ) -> ResourceEntry:
1456
+ (
1457
+ namespace,
1458
+ library_doc,
1459
+ ) = self.imports_manager.get_namespace_and_libdoc_for_resource_import(
1460
+ name,
1461
+ base_dir,
1462
+ sentinel=sentinel,
1463
+ variables=variables or self.get_resolvable_variables(),
1464
+ )
1465
+
1466
+ return ResourceEntry(
1467
+ name=library_doc.name,
1468
+ import_name=name,
1469
+ library_doc=library_doc,
1470
+ imports=namespace.get_imports(),
1471
+ variables=namespace.get_own_variables(),
1472
+ )
1473
+
1474
+ @_logger.call
1475
+ def get_imported_resource_libdoc(self, name: str) -> Optional[LibraryDoc]:
1476
+ self.ensure_initialized()
1477
+
1478
+ return next(
1479
+ (
1480
+ v.library_doc
1481
+ for e, v in self._import_entries.items()
1482
+ if isinstance(e, ResourceImport) and v.import_name == name
1483
+ ),
1484
+ None,
1485
+ )
1486
+
1487
+ @_logger.call
1488
+ def _get_variables_entry(
1489
+ self,
1490
+ name: str,
1491
+ args: Tuple[Any, ...],
1492
+ base_dir: str,
1493
+ *,
1494
+ sentinel: Any = None,
1495
+ variables: Optional[Dict[str, Any]] = None,
1496
+ ) -> VariablesEntry:
1497
+ library_doc = self.imports_manager.get_libdoc_for_variables_import(
1498
+ name,
1499
+ args,
1500
+ base_dir=base_dir,
1501
+ sentinel=sentinel,
1502
+ variables=variables or self.get_resolvable_variables(),
1503
+ )
1504
+
1505
+ return VariablesEntry(
1506
+ name=library_doc.name,
1507
+ import_name=name,
1508
+ library_doc=library_doc,
1509
+ args=args,
1510
+ variables=library_doc.variables,
1511
+ )
1512
+
1513
+ @_logger.call
1514
+ def get_imported_variables_libdoc(self, name: str, args: Tuple[str, ...] = ()) -> Optional[LibraryDoc]:
1515
+ self.ensure_initialized()
1516
+
1517
+ return next(
1518
+ (
1519
+ v.library_doc
1520
+ for e, v in self._import_entries.items()
1521
+ if isinstance(e, VariablesImport) and v.import_name == name and v.args == args
1522
+ ),
1523
+ None,
1524
+ )
1525
+
1526
+ def get_imported_keywords(self) -> List[KeywordDoc]:
1527
+ with self._imported_keywords_lock:
1528
+ if self._imported_keywords is None:
1529
+ self._imported_keywords = list(
1530
+ itertools.chain(
1531
+ *(e.library_doc.keywords for e in self._libraries.values()),
1532
+ *(e.library_doc.keywords for e in self._resources.values()),
1533
+ )
1534
+ )
1535
+
1536
+ return self._imported_keywords
1537
+
1538
+ @_logger.call
1539
+ def iter_all_keywords(self) -> Iterator[KeywordDoc]:
1540
+ import itertools
1541
+
1542
+ libdoc = self.get_library_doc()
1543
+
1544
+ for doc in itertools.chain(
1545
+ self.get_imported_keywords(),
1546
+ libdoc.keywords if libdoc is not None else [],
1547
+ ):
1548
+ yield doc
1549
+
1550
+ @_logger.call
1551
+ def get_keywords(self) -> List[KeywordDoc]:
1552
+ with self._keywords_lock:
1553
+ if self._keywords is None:
1554
+ current_time = time.monotonic()
1555
+ self._logger.debug("start collecting keywords")
1556
+ try:
1557
+ i = 0
1558
+
1559
+ self.ensure_initialized()
1560
+
1561
+ result: Dict[KeywordMatcher, KeywordDoc] = {}
1562
+
1563
+ for doc in self.iter_all_keywords():
1564
+ i += 1
1565
+ result[doc.matcher] = doc
1566
+
1567
+ self._keywords = list(result.values())
1568
+ except BaseException:
1569
+ self._logger.debug("Canceled collecting keywords ")
1570
+ raise
1571
+ else:
1572
+ self._logger.debug(
1573
+ lambda: f"end collecting {len(self._keywords) if self._keywords else 0}"
1574
+ f" keywords in {time.monotonic() - current_time}s analyze {i} keywords"
1575
+ )
1576
+
1577
+ return self._keywords
1578
+
1579
+ def append_diagnostics(
1580
+ self,
1581
+ range: Range,
1582
+ message: str,
1583
+ severity: Optional[DiagnosticSeverity] = None,
1584
+ code: Union[int, str, None] = None,
1585
+ code_description: Optional[CodeDescription] = None,
1586
+ source: Optional[str] = None,
1587
+ tags: Optional[List[DiagnosticTag]] = None,
1588
+ related_information: Optional[List[DiagnosticRelatedInformation]] = None,
1589
+ data: Optional[Any] = None,
1590
+ ) -> None:
1591
+ if self._should_ignore(range):
1592
+ return
1593
+
1594
+ self._diagnostics.append(
1595
+ Diagnostic(
1596
+ range,
1597
+ message,
1598
+ severity,
1599
+ code,
1600
+ code_description,
1601
+ source,
1602
+ tags,
1603
+ related_information,
1604
+ data,
1605
+ )
1606
+ )
1607
+
1608
+ @_logger.call(condition=lambda self: not self._analyzed)
1609
+ def analyze(self) -> None:
1610
+ import time
1611
+
1612
+ from .namespace_analyzer import NamespaceAnalyzer
1613
+
1614
+ with self._analyze_lock:
1615
+ if not self._analyzed:
1616
+ canceled = False
1617
+
1618
+ self._logger.debug(lambda: f"start analyze {self.document}")
1619
+ start_time = time.monotonic()
1620
+
1621
+ try:
1622
+ result = NamespaceAnalyzer(
1623
+ self.model,
1624
+ self,
1625
+ self.create_finder(),
1626
+ self.get_ignored_lines(self.document) if self.document is not None else [],
1627
+ ).run()
1628
+
1629
+ self._diagnostics += result.diagnostics
1630
+ self._keyword_references = result.keyword_references
1631
+ self._variable_references = result.variable_references
1632
+ self._local_variable_assignments = result.local_variable_assignments
1633
+ self._namespace_references = result.namespace_references
1634
+
1635
+ lib_doc = self.get_library_doc()
1636
+
1637
+ if lib_doc.errors is not None:
1638
+ for err in lib_doc.errors:
1639
+ self.append_diagnostics(
1640
+ range=Range(
1641
+ start=Position(
1642
+ line=((err.line_no - 1) if err.line_no is not None else 0),
1643
+ character=0,
1644
+ ),
1645
+ end=Position(
1646
+ line=((err.line_no - 1) if err.line_no is not None else 0),
1647
+ character=0,
1648
+ ),
1649
+ ),
1650
+ message=err.message,
1651
+ severity=DiagnosticSeverity.ERROR,
1652
+ source=DIAGNOSTICS_SOURCE_NAME,
1653
+ code=err.type_name,
1654
+ )
1655
+ # TODO: implement CancelationToken
1656
+ except CancelledError:
1657
+ canceled = True
1658
+ self._logger.debug("analyzing canceled")
1659
+ raise
1660
+ finally:
1661
+ self._analyzed = not canceled
1662
+
1663
+ self._logger.debug(
1664
+ lambda: f"end analyzed {self.document} succeed in {time.monotonic() - start_time}s"
1665
+ if self._analyzed
1666
+ else f"end analyzed {self.document} failed in {time.monotonic() - start_time}s"
1667
+ )
1668
+
1669
+ self.has_analysed(self)
1670
+
1671
+ def get_finder(self) -> "KeywordFinder":
1672
+ if self._finder is None:
1673
+ self._finder = self.create_finder()
1674
+ return self._finder
1675
+
1676
+ def create_finder(self) -> "KeywordFinder":
1677
+ self.ensure_initialized()
1678
+ return KeywordFinder(self, self.get_library_doc())
1679
+
1680
+ @_logger.call(condition=lambda self, name, **kwargs: self._finder is not None and name not in self._finder._cache)
1681
+ def find_keyword(
1682
+ self,
1683
+ name: Optional[str],
1684
+ *,
1685
+ raise_keyword_error: bool = True,
1686
+ handle_bdd_style: bool = True,
1687
+ ) -> Optional[KeywordDoc]:
1688
+ finder = self._finder if self._finder is not None else self.get_finder()
1689
+
1690
+ return finder.find_keyword(
1691
+ name,
1692
+ raise_keyword_error=raise_keyword_error,
1693
+ handle_bdd_style=handle_bdd_style,
1694
+ )
1695
+
1696
+ @classmethod
1697
+ def get_ignored_lines(cls, document: TextDocument) -> List[int]:
1698
+ return document.get_cache(cls.__get_ignored_lines)
1699
+
1700
+ @staticmethod
1701
+ def __get_ignored_lines(document: TextDocument) -> List[int]:
1702
+ result = []
1703
+ lines = document.get_lines()
1704
+ for line_no, line in enumerate(lines):
1705
+ comment = EXTRACT_COMMENT_PATTERN.match(line)
1706
+ if comment and comment.group("comment"):
1707
+ for match in ROBOTCODE_PATTERN.finditer(comment.group("comment")):
1708
+ if match.group("rule") == "ignore":
1709
+ result.append(line_no)
1710
+
1711
+ return result
1712
+
1713
+ @classmethod
1714
+ def should_ignore(cls, document: Optional[TextDocument], range: Range) -> bool:
1715
+ return cls.__should_ignore(
1716
+ cls.get_ignored_lines(document) if document is not None else [],
1717
+ range,
1718
+ )
1719
+
1720
+ def _should_ignore(self, range: Range) -> bool:
1721
+ if self._ignored_lines is None:
1722
+ self._ignored_lines = self.get_ignored_lines(self.document) if self.document is not None else []
1723
+
1724
+ return self.__should_ignore(self._ignored_lines, range)
1725
+
1726
+ @staticmethod
1727
+ def __should_ignore(lines: List[int], range: Range) -> bool:
1728
+ import builtins
1729
+
1730
+ return any(line_no in lines for line_no in builtins.range(range.start.line, range.end.line + 1))
1731
+
1732
+
1733
+ class DiagnosticsEntry(NamedTuple):
1734
+ message: str
1735
+ severity: DiagnosticSeverity
1736
+ code: Optional[str] = None
1737
+
1738
+
1739
+ class CancelSearchError(Exception):
1740
+ pass
1741
+
1742
+
1743
+ DEFAULT_BDD_PREFIXES = {"Given ", "When ", "Then ", "And ", "But "}
1744
+
1745
+
1746
+ class KeywordFinder:
1747
+ def __init__(self, namespace: Namespace, library_doc: LibraryDoc) -> None:
1748
+ self.namespace = namespace
1749
+ self.self_library_doc = library_doc
1750
+
1751
+ self.diagnostics: List[DiagnosticsEntry] = []
1752
+ self.multiple_keywords_result: Optional[List[KeywordDoc]] = None
1753
+ self._cache: Dict[
1754
+ Tuple[Optional[str], bool],
1755
+ Tuple[
1756
+ Optional[KeywordDoc],
1757
+ List[DiagnosticsEntry],
1758
+ Optional[List[KeywordDoc]],
1759
+ ],
1760
+ ] = {}
1761
+ self.handle_bdd_style = True
1762
+ self._all_keywords: Optional[List[LibraryEntry]] = None
1763
+ self._resource_keywords: Optional[List[ResourceEntry]] = None
1764
+ self._library_keywords: Optional[List[LibraryEntry]] = None
1765
+
1766
+ def reset_diagnostics(self) -> None:
1767
+ self.diagnostics = []
1768
+ self.multiple_keywords_result = None
1769
+
1770
+ def find_keyword(
1771
+ self,
1772
+ name: Optional[str],
1773
+ *,
1774
+ raise_keyword_error: bool = False,
1775
+ handle_bdd_style: bool = True,
1776
+ ) -> Optional[KeywordDoc]:
1777
+ try:
1778
+ self.reset_diagnostics()
1779
+
1780
+ self.handle_bdd_style = handle_bdd_style
1781
+
1782
+ cached = self._cache.get((name, self.handle_bdd_style), None)
1783
+
1784
+ if cached is not None:
1785
+ self.diagnostics = cached[1]
1786
+ self.multiple_keywords_result = cached[2]
1787
+ return cached[0]
1788
+
1789
+ try:
1790
+ result = self._find_keyword(name)
1791
+ if result is None:
1792
+ self.diagnostics.append(
1793
+ DiagnosticsEntry(
1794
+ f"No keyword with name '{name}' found.",
1795
+ DiagnosticSeverity.ERROR,
1796
+ Error.KEYWORD_NOT_FOUND,
1797
+ )
1798
+ )
1799
+ except KeywordError as e:
1800
+ if e.multiple_keywords:
1801
+ self._add_to_multiple_keywords_result(e.multiple_keywords)
1802
+
1803
+ if raise_keyword_error:
1804
+ raise
1805
+
1806
+ result = None
1807
+ self.diagnostics.append(DiagnosticsEntry(str(e), DiagnosticSeverity.ERROR, Error.KEYWORD_ERROR))
1808
+
1809
+ self._cache[(name, self.handle_bdd_style)] = (
1810
+ result,
1811
+ self.diagnostics,
1812
+ self.multiple_keywords_result,
1813
+ )
1814
+
1815
+ return result
1816
+ except CancelSearchError:
1817
+ return None
1818
+
1819
+ def _find_keyword(self, name: Optional[str]) -> Optional[KeywordDoc]:
1820
+ if not name:
1821
+ self.diagnostics.append(
1822
+ DiagnosticsEntry(
1823
+ "Keyword name cannot be empty.",
1824
+ DiagnosticSeverity.ERROR,
1825
+ Error.KEYWORD_ERROR,
1826
+ )
1827
+ )
1828
+ raise CancelSearchError
1829
+ if not isinstance(name, str):
1830
+ self.diagnostics.append( # type: ignore
1831
+ DiagnosticsEntry(
1832
+ "Keyword name must be a string.",
1833
+ DiagnosticSeverity.ERROR,
1834
+ Error.KEYWORD_ERROR,
1835
+ )
1836
+ )
1837
+ raise CancelSearchError
1838
+
1839
+ result = self._get_keyword_from_self(name)
1840
+ if not result and "." in name:
1841
+ result = self._get_explicit_keyword(name)
1842
+
1843
+ if not result:
1844
+ result = self._get_implicit_keyword(name)
1845
+
1846
+ if not result and self.handle_bdd_style:
1847
+ return self._get_bdd_style_keyword(name)
1848
+
1849
+ return result
1850
+
1851
+ def _get_keyword_from_self(self, name: str) -> Optional[KeywordDoc]:
1852
+ if get_robot_version() >= (6, 0):
1853
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = [
1854
+ (None, v) for v in self.self_library_doc.keywords.get_all(name)
1855
+ ]
1856
+ if len(found) > 1:
1857
+ found = self._select_best_matches(found)
1858
+ if len(found) > 1:
1859
+ self.diagnostics.append(
1860
+ DiagnosticsEntry(
1861
+ self._create_multiple_keywords_found_message(name, found, implicit=False),
1862
+ DiagnosticSeverity.ERROR,
1863
+ Error.KEYWORD_ERROR,
1864
+ )
1865
+ )
1866
+ raise CancelSearchError
1867
+
1868
+ if len(found) == 1:
1869
+ # TODO warning if keyword found is defined in resource and suite
1870
+ return found[0][1]
1871
+
1872
+ return None
1873
+
1874
+ try:
1875
+ return self.self_library_doc.keywords.get(name, None)
1876
+ except KeywordError as e:
1877
+ self.diagnostics.append(DiagnosticsEntry(str(e), DiagnosticSeverity.ERROR, Error.KEYWORD_ERROR))
1878
+ raise CancelSearchError from e
1879
+
1880
+ def _yield_owner_and_kw_names(self, full_name: str) -> Iterator[Tuple[str, ...]]:
1881
+ tokens = full_name.split(".")
1882
+ for i in range(1, len(tokens)):
1883
+ yield ".".join(tokens[:i]), ".".join(tokens[i:])
1884
+
1885
+ def _get_explicit_keyword(self, name: str) -> Optional[KeywordDoc]:
1886
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
1887
+ for owner_name, kw_name in self._yield_owner_and_kw_names(name):
1888
+ found.extend(self.find_keywords(owner_name, kw_name))
1889
+
1890
+ if get_robot_version() >= (6, 0) and len(found) > 1:
1891
+ found = self._select_best_matches(found)
1892
+
1893
+ if len(found) > 1:
1894
+ self.diagnostics.append(
1895
+ DiagnosticsEntry(
1896
+ self._create_multiple_keywords_found_message(name, found, implicit=False),
1897
+ DiagnosticSeverity.ERROR,
1898
+ Error.KEYWORD_ERROR,
1899
+ )
1900
+ )
1901
+ raise CancelSearchError
1902
+
1903
+ return found[0][1] if found else None
1904
+
1905
+ def find_keywords(self, owner_name: str, name: str) -> List[Tuple[LibraryEntry, KeywordDoc]]:
1906
+ if self._all_keywords is None:
1907
+ self._all_keywords = list(
1908
+ chain(
1909
+ self.namespace._libraries.values(),
1910
+ self.namespace._resources.values(),
1911
+ )
1912
+ )
1913
+
1914
+ if get_robot_version() >= (6, 0):
1915
+ result: List[Tuple[LibraryEntry, KeywordDoc]] = []
1916
+ for v in self._all_keywords:
1917
+ if eq_namespace(v.alias or v.name, owner_name):
1918
+ result.extend((v, kw) for kw in v.library_doc.keywords.get_all(name))
1919
+ return result
1920
+
1921
+ result = []
1922
+ for v in self._all_keywords:
1923
+ if eq_namespace(v.alias or v.name, owner_name):
1924
+ kw = v.library_doc.keywords.get(name, None)
1925
+ if kw is not None:
1926
+ result.append((v, kw))
1927
+ return result
1928
+
1929
+ def _add_to_multiple_keywords_result(self, kw: Iterable[KeywordDoc]) -> None:
1930
+ if self.multiple_keywords_result is None:
1931
+ self.multiple_keywords_result = list(kw)
1932
+ else:
1933
+ self.multiple_keywords_result.extend(kw)
1934
+
1935
+ def _create_multiple_keywords_found_message(
1936
+ self,
1937
+ name: str,
1938
+ found: Sequence[Tuple[Optional[LibraryEntry], KeywordDoc]],
1939
+ implicit: bool = True,
1940
+ ) -> str:
1941
+ self._add_to_multiple_keywords_result([k for _, k in found])
1942
+
1943
+ if any(e[1].is_embedded for e in found):
1944
+ error = f"Multiple keywords matching name '{name}' found"
1945
+ else:
1946
+ error = f"Multiple keywords with name '{name}' found"
1947
+
1948
+ if implicit:
1949
+ error += ". Give the full name of the keyword you want to use"
1950
+
1951
+ names = sorted(f"{e[1].name if e[0] is None else f'{e[0].alias or e[0].name}.{e[1].name}'}" for e in found)
1952
+ return "\n ".join([f"{error}:", *names])
1953
+
1954
+ def _get_implicit_keyword(self, name: str) -> Optional[KeywordDoc]:
1955
+ result = self._get_keyword_from_resource_files(name)
1956
+ if not result:
1957
+ return self._get_keyword_from_libraries(name)
1958
+ return result
1959
+
1960
+ def _prioritize_same_file_or_public(
1961
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
1962
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
1963
+ matches = [h for h in entries if h[1].source == self.namespace.source]
1964
+ if matches:
1965
+ return matches
1966
+
1967
+ matches = [handler for handler in entries if not handler[1].is_private()]
1968
+
1969
+ return matches or entries
1970
+
1971
+ def _select_best_matches(
1972
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
1973
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
1974
+ normal = [hand for hand in entries if not hand[1].is_embedded]
1975
+ if normal:
1976
+ return normal
1977
+
1978
+ matches = [hand for hand in entries if not self._is_worse_match_than_others(hand, entries)]
1979
+ return matches or entries
1980
+
1981
+ def _is_worse_match_than_others(
1982
+ self,
1983
+ candidate: Tuple[Optional[LibraryEntry], KeywordDoc],
1984
+ alternatives: List[Tuple[Optional[LibraryEntry], KeywordDoc]],
1985
+ ) -> bool:
1986
+ for other in alternatives:
1987
+ if (
1988
+ candidate[1] is not other[1]
1989
+ and self._is_better_match(other, candidate)
1990
+ and not self._is_better_match(candidate, other)
1991
+ ):
1992
+ return True
1993
+ return False
1994
+
1995
+ def _is_better_match(
1996
+ self,
1997
+ candidate: Tuple[Optional[LibraryEntry], KeywordDoc],
1998
+ other: Tuple[Optional[LibraryEntry], KeywordDoc],
1999
+ ) -> bool:
2000
+ return (
2001
+ other[1].matcher.embedded_arguments.match(candidate[1].name) is not None
2002
+ and candidate[1].matcher.embedded_arguments.match(other[1].name) is None
2003
+ )
2004
+
2005
+ def _get_keyword_from_resource_files(self, name: str) -> Optional[KeywordDoc]:
2006
+ if self._resource_keywords is None:
2007
+ self._resource_keywords = list(chain(self.namespace._resources.values()))
2008
+
2009
+ if get_robot_version() >= (6, 0):
2010
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
2011
+ for v in self._resource_keywords:
2012
+ r = v.library_doc.keywords.get_all(name)
2013
+ if r:
2014
+ found.extend([(v, k) for k in r])
2015
+ else:
2016
+ found = []
2017
+ for k in self._resource_keywords:
2018
+ s = k.library_doc.keywords.get(name, None)
2019
+ if s is not None:
2020
+ found.append((k, s))
2021
+
2022
+ if not found:
2023
+ return None
2024
+
2025
+ if get_robot_version() >= (6, 0):
2026
+ if len(found) > 1:
2027
+ found = self._prioritize_same_file_or_public(found)
2028
+
2029
+ if len(found) > 1:
2030
+ found = self._select_best_matches(found)
2031
+
2032
+ if len(found) > 1:
2033
+ found = self._get_keyword_based_on_search_order(found)
2034
+
2035
+ else:
2036
+ if len(found) > 1:
2037
+ found = self._get_keyword_based_on_search_order(found)
2038
+
2039
+ if len(found) == 1:
2040
+ return found[0][1]
2041
+
2042
+ self.diagnostics.append(
2043
+ DiagnosticsEntry(
2044
+ self._create_multiple_keywords_found_message(name, found),
2045
+ DiagnosticSeverity.ERROR,
2046
+ Error.KEYWORD_ERROR,
2047
+ )
2048
+ )
2049
+ raise CancelSearchError
2050
+
2051
+ def _get_keyword_based_on_search_order(
2052
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
2053
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
2054
+ for libname in self.namespace.search_order:
2055
+ for e in entries:
2056
+ if e[0] is not None and eq_namespace(libname, e[0].alias or e[0].name):
2057
+ return [e]
2058
+
2059
+ return entries
2060
+
2061
+ def _get_keyword_from_libraries(self, name: str) -> Optional[KeywordDoc]:
2062
+ if self._library_keywords is None:
2063
+ self._library_keywords = list(chain(self.namespace._libraries.values()))
2064
+
2065
+ if get_robot_version() >= (6, 0):
2066
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
2067
+ for v in self._library_keywords:
2068
+ r = v.library_doc.keywords.get_all(name)
2069
+ if r:
2070
+ found.extend([(v, k) for k in r])
2071
+ else:
2072
+ found = []
2073
+
2074
+ for k in self._library_keywords:
2075
+ s = k.library_doc.keywords.get(name, None)
2076
+ if s is not None:
2077
+ found.append((k, s))
2078
+
2079
+ if not found:
2080
+ return None
2081
+
2082
+ if get_robot_version() >= (6, 0):
2083
+ if len(found) > 1:
2084
+ found = self._select_best_matches(found)
2085
+ if len(found) > 1:
2086
+ found = self._get_keyword_based_on_search_order(found)
2087
+ else:
2088
+ if len(found) > 1:
2089
+ found = self._get_keyword_based_on_search_order(found)
2090
+ if len(found) == 2:
2091
+ found = self._filter_stdlib_runner(*found)
2092
+
2093
+ if len(found) == 1:
2094
+ return found[0][1]
2095
+
2096
+ self.diagnostics.append(
2097
+ DiagnosticsEntry(
2098
+ self._create_multiple_keywords_found_message(name, found),
2099
+ DiagnosticSeverity.ERROR,
2100
+ Error.KEYWORD_ERROR,
2101
+ )
2102
+ )
2103
+ raise CancelSearchError
2104
+
2105
+ def _filter_stdlib_runner(
2106
+ self,
2107
+ entry1: Tuple[Optional[LibraryEntry], KeywordDoc],
2108
+ entry2: Tuple[Optional[LibraryEntry], KeywordDoc],
2109
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
2110
+ stdlibs_without_remote = STDLIBS - {"Remote"}
2111
+ if entry1[0] is not None and entry1[0].name in stdlibs_without_remote:
2112
+ standard, custom = entry1, entry2
2113
+ elif entry2[0] is not None and entry2[0].name in stdlibs_without_remote:
2114
+ standard, custom = entry2, entry1
2115
+ else:
2116
+ return [entry1, entry2]
2117
+
2118
+ self.diagnostics.append(
2119
+ DiagnosticsEntry(
2120
+ self._create_custom_and_standard_keyword_conflict_warning_message(custom, standard),
2121
+ DiagnosticSeverity.WARNING,
2122
+ Error.KEYWORD_ERROR,
2123
+ )
2124
+ )
2125
+
2126
+ return [custom]
2127
+
2128
+ def _create_custom_and_standard_keyword_conflict_warning_message(
2129
+ self,
2130
+ custom: Tuple[Optional[LibraryEntry], KeywordDoc],
2131
+ standard: Tuple[Optional[LibraryEntry], KeywordDoc],
2132
+ ) -> str:
2133
+ custom_with_name = standard_with_name = ""
2134
+ if custom[0] is not None and custom[0].alias is not None:
2135
+ custom_with_name = " imported as '%s'" % custom[0].alias
2136
+ if standard[0] is not None and standard[0].alias is not None:
2137
+ standard_with_name = " imported as '%s'" % standard[0].alias
2138
+ return (
2139
+ f"Keyword '{standard[1].name}' found both from a custom test library "
2140
+ f"'{'' if custom[0] is None else custom[0].name}'{custom_with_name} "
2141
+ f"and a standard library '{standard[1].name}'{standard_with_name}. "
2142
+ f"The custom keyword is used. To select explicitly, and to get "
2143
+ f"rid of this warning, use either "
2144
+ f"'{'' if custom[0] is None else custom[0].alias or custom[0].name}.{custom[1].name}' "
2145
+ f"or '{'' if standard[0] is None else standard[0].alias or standard[0].name}.{standard[1].name}'."
2146
+ )
2147
+
2148
+ def _get_bdd_style_keyword(self, name: str) -> Optional[KeywordDoc]:
2149
+ if get_robot_version() < (6, 0):
2150
+ lower = name.lower()
2151
+ for prefix in ["given ", "when ", "then ", "and ", "but "]:
2152
+ if lower.startswith(prefix):
2153
+ return self._find_keyword(name[len(prefix) :])
2154
+ return None
2155
+
2156
+ parts = name.split()
2157
+ if len(parts) < 2:
2158
+ return None
2159
+ for index in range(1, len(parts)):
2160
+ prefix = " ".join(parts[:index]).title()
2161
+ if prefix.title() in (
2162
+ self.namespace.languages.bdd_prefixes if self.namespace.languages is not None else DEFAULT_BDD_PREFIXES
2163
+ ):
2164
+ return self._find_keyword(" ".join(parts[index:]))
2165
+ return None