robotcode-robot 0.93.1__py3-none-any.whl → 0.94.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,458 @@
1
+ from itertools import chain
2
+ from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple
3
+
4
+ from robot.libraries import STDLIBS
5
+ from robotcode.core.lsp.types import (
6
+ DiagnosticSeverity,
7
+ )
8
+
9
+ from ..utils import get_robot_version
10
+ from ..utils.match import eq_namespace
11
+ from .entities import (
12
+ LibraryEntry,
13
+ ResourceEntry,
14
+ )
15
+ from .errors import Error
16
+ from .library_doc import (
17
+ KeywordDoc,
18
+ KeywordError,
19
+ LibraryDoc,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from .namespace import Namespace
24
+
25
+
26
+ class DiagnosticsEntry(NamedTuple):
27
+ message: str
28
+ severity: DiagnosticSeverity
29
+ code: Optional[str] = None
30
+
31
+
32
+ class CancelSearchError(Exception):
33
+ pass
34
+
35
+
36
+ DEFAULT_BDD_PREFIXES = {"Given ", "When ", "Then ", "And ", "But "}
37
+
38
+
39
+ class KeywordFinder:
40
+ def __init__(self, namespace: "Namespace", library_doc: LibraryDoc) -> None:
41
+ self.namespace = namespace
42
+ self.self_library_doc = library_doc
43
+
44
+ self.diagnostics: List[DiagnosticsEntry] = []
45
+ self.multiple_keywords_result: Optional[List[KeywordDoc]] = None
46
+ self._cache: Dict[
47
+ Tuple[Optional[str], bool],
48
+ Tuple[
49
+ Optional[KeywordDoc],
50
+ List[DiagnosticsEntry],
51
+ Optional[List[KeywordDoc]],
52
+ ],
53
+ ] = {}
54
+ self.handle_bdd_style = True
55
+ self._all_keywords: Optional[List[LibraryEntry]] = None
56
+ self._resource_keywords: Optional[List[ResourceEntry]] = None
57
+ self._library_keywords: Optional[List[LibraryEntry]] = None
58
+
59
+ def reset_diagnostics(self) -> None:
60
+ self.diagnostics = []
61
+ self.multiple_keywords_result = None
62
+
63
+ def find_keyword(
64
+ self,
65
+ name: Optional[str],
66
+ *,
67
+ raise_keyword_error: bool = False,
68
+ handle_bdd_style: bool = True,
69
+ ) -> Optional[KeywordDoc]:
70
+ try:
71
+ self.reset_diagnostics()
72
+
73
+ self.handle_bdd_style = handle_bdd_style
74
+
75
+ cached = self._cache.get((name, self.handle_bdd_style), None)
76
+
77
+ if cached is not None:
78
+ self.diagnostics = cached[1]
79
+ self.multiple_keywords_result = cached[2]
80
+ return cached[0]
81
+
82
+ try:
83
+ result = self._find_keyword(name)
84
+ if result is None:
85
+ self.diagnostics.append(
86
+ DiagnosticsEntry(
87
+ f"No keyword with name '{name}' found.",
88
+ DiagnosticSeverity.ERROR,
89
+ Error.KEYWORD_NOT_FOUND,
90
+ )
91
+ )
92
+ except KeywordError as e:
93
+ if e.multiple_keywords:
94
+ self._add_to_multiple_keywords_result(e.multiple_keywords)
95
+
96
+ if raise_keyword_error:
97
+ raise
98
+
99
+ result = None
100
+ self.diagnostics.append(DiagnosticsEntry(str(e), DiagnosticSeverity.ERROR, Error.KEYWORD_ERROR))
101
+
102
+ self._cache[(name, self.handle_bdd_style)] = (
103
+ result,
104
+ self.diagnostics,
105
+ self.multiple_keywords_result,
106
+ )
107
+
108
+ return result
109
+ except CancelSearchError:
110
+ return None
111
+
112
+ def _find_keyword(self, name: Optional[str]) -> Optional[KeywordDoc]:
113
+ if not name:
114
+ self.diagnostics.append(
115
+ DiagnosticsEntry(
116
+ "Keyword name cannot be empty.",
117
+ DiagnosticSeverity.ERROR,
118
+ Error.KEYWORD_ERROR,
119
+ )
120
+ )
121
+ raise CancelSearchError
122
+ if not isinstance(name, str):
123
+ self.diagnostics.append( # type: ignore
124
+ DiagnosticsEntry(
125
+ "Keyword name must be a string.",
126
+ DiagnosticSeverity.ERROR,
127
+ Error.KEYWORD_ERROR,
128
+ )
129
+ )
130
+ raise CancelSearchError
131
+
132
+ result = self._get_keyword_from_self(name)
133
+ if not result and "." in name:
134
+ result = self._get_explicit_keyword(name)
135
+
136
+ if not result:
137
+ result = self._get_implicit_keyword(name)
138
+
139
+ if not result and self.handle_bdd_style:
140
+ return self._get_bdd_style_keyword(name)
141
+
142
+ return result
143
+
144
+ def _get_keyword_from_self(self, name: str) -> Optional[KeywordDoc]:
145
+ if get_robot_version() >= (6, 0):
146
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = [
147
+ (None, v) for v in self.self_library_doc.keywords.iter_all(name)
148
+ ]
149
+ if len(found) > 1:
150
+ found = self._select_best_matches(found)
151
+ if len(found) > 1:
152
+ self.diagnostics.append(
153
+ DiagnosticsEntry(
154
+ self._create_multiple_keywords_found_message(name, found, implicit=False),
155
+ DiagnosticSeverity.ERROR,
156
+ Error.MULTIPLE_KEYWORDS,
157
+ )
158
+ )
159
+ raise CancelSearchError
160
+
161
+ if len(found) == 1:
162
+ # TODO warning if keyword found is defined in resource and suite
163
+ return found[0][1]
164
+
165
+ return None
166
+
167
+ try:
168
+ return self.self_library_doc.keywords.get(name, None)
169
+ except KeywordError as e:
170
+ self.diagnostics.append(DiagnosticsEntry(str(e), DiagnosticSeverity.ERROR, Error.KEYWORD_ERROR))
171
+ raise CancelSearchError from e
172
+
173
+ def _yield_owner_and_kw_names(self, full_name: str) -> Iterator[Tuple[str, ...]]:
174
+ tokens = full_name.split(".")
175
+ for i in range(1, len(tokens)):
176
+ yield ".".join(tokens[:i]), ".".join(tokens[i:])
177
+
178
+ def _get_explicit_keyword(self, name: str) -> Optional[KeywordDoc]:
179
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
180
+ for owner_name, kw_name in self._yield_owner_and_kw_names(name):
181
+ found.extend(self.find_keywords(owner_name, kw_name))
182
+
183
+ if get_robot_version() >= (6, 0) and len(found) > 1:
184
+ found = self._select_best_matches(found)
185
+
186
+ if len(found) > 1:
187
+ self.diagnostics.append(
188
+ DiagnosticsEntry(
189
+ self._create_multiple_keywords_found_message(name, found, implicit=False),
190
+ DiagnosticSeverity.ERROR,
191
+ Error.MULTIPLE_KEYWORDS,
192
+ )
193
+ )
194
+ raise CancelSearchError
195
+
196
+ return found[0][1] if found else None
197
+
198
+ def find_keywords(self, owner_name: str, name: str) -> List[Tuple[LibraryEntry, KeywordDoc]]:
199
+ if self._all_keywords is None:
200
+ self._all_keywords = list(
201
+ chain(
202
+ self.namespace._libraries.values(),
203
+ self.namespace._resources.values(),
204
+ )
205
+ )
206
+
207
+ if get_robot_version() >= (6, 0):
208
+ result: List[Tuple[LibraryEntry, KeywordDoc]] = []
209
+ for v in self._all_keywords:
210
+ if eq_namespace(v.alias or v.name, owner_name):
211
+ result.extend((v, kw) for kw in v.library_doc.keywords.iter_all(name))
212
+ return result
213
+
214
+ result = []
215
+ for v in self._all_keywords:
216
+ if eq_namespace(v.alias or v.name, owner_name):
217
+ kw = v.library_doc.keywords.get(name, None)
218
+ if kw is not None:
219
+ result.append((v, kw))
220
+ return result
221
+
222
+ def _add_to_multiple_keywords_result(self, kw: Iterable[KeywordDoc]) -> None:
223
+ if self.multiple_keywords_result is None:
224
+ self.multiple_keywords_result = list(kw)
225
+ else:
226
+ self.multiple_keywords_result.extend(kw)
227
+
228
+ def _create_multiple_keywords_found_message(
229
+ self,
230
+ name: str,
231
+ found: Sequence[Tuple[Optional[LibraryEntry], KeywordDoc]],
232
+ implicit: bool = True,
233
+ ) -> str:
234
+ self._add_to_multiple_keywords_result([k for _, k in found])
235
+
236
+ if any(e[1].is_embedded for e in found):
237
+ error = f"Multiple keywords matching name '{name}' found"
238
+ else:
239
+ error = f"Multiple keywords with name '{name}' found"
240
+
241
+ if implicit:
242
+ error += ". Give the full name of the keyword you want to use"
243
+
244
+ 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)
245
+ return "\n ".join([f"{error}:", *names])
246
+
247
+ def _get_implicit_keyword(self, name: str) -> Optional[KeywordDoc]:
248
+ result = self._get_keyword_from_resource_files(name)
249
+ if not result:
250
+ return self._get_keyword_from_libraries(name)
251
+ return result
252
+
253
+ def _prioritize_same_file_or_public(
254
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
255
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
256
+ matches = [h for h in entries if h[1].source == self.namespace.source]
257
+ if matches:
258
+ return matches
259
+
260
+ matches = [handler for handler in entries if not handler[1].is_private()]
261
+
262
+ return matches or entries
263
+
264
+ def _select_best_matches(
265
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
266
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
267
+ normal = [hand for hand in entries if not hand[1].is_embedded]
268
+ if normal:
269
+ return normal
270
+
271
+ matches = [hand for hand in entries if not self._is_worse_match_than_others(hand, entries)]
272
+ return matches or entries
273
+
274
+ def _is_worse_match_than_others(
275
+ self,
276
+ candidate: Tuple[Optional[LibraryEntry], KeywordDoc],
277
+ alternatives: List[Tuple[Optional[LibraryEntry], KeywordDoc]],
278
+ ) -> bool:
279
+ for other in alternatives:
280
+ if (
281
+ candidate[1] is not other[1]
282
+ and self._is_better_match(other, candidate)
283
+ and not self._is_better_match(candidate, other)
284
+ ):
285
+ return True
286
+ return False
287
+
288
+ def _is_better_match(
289
+ self,
290
+ candidate: Tuple[Optional[LibraryEntry], KeywordDoc],
291
+ other: Tuple[Optional[LibraryEntry], KeywordDoc],
292
+ ) -> bool:
293
+ return (
294
+ other[1].matcher.embedded_arguments.match(candidate[1].name) is not None
295
+ and candidate[1].matcher.embedded_arguments.match(other[1].name) is None
296
+ )
297
+
298
+ def _get_keyword_from_resource_files(self, name: str) -> Optional[KeywordDoc]:
299
+ if self._resource_keywords is None:
300
+ self._resource_keywords = list(chain(self.namespace._resources.values()))
301
+
302
+ if get_robot_version() >= (6, 0):
303
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
304
+ for v in self._resource_keywords:
305
+ r = v.library_doc.keywords.get_all(name)
306
+ if r:
307
+ found.extend([(v, k) for k in r])
308
+ else:
309
+ found = []
310
+ for k in self._resource_keywords:
311
+ s = k.library_doc.keywords.get(name, None)
312
+ if s is not None:
313
+ found.append((k, s))
314
+
315
+ if not found:
316
+ return None
317
+
318
+ if get_robot_version() >= (6, 0):
319
+ if len(found) > 1:
320
+ found = self._prioritize_same_file_or_public(found)
321
+
322
+ if len(found) > 1:
323
+ found = self._select_best_matches(found)
324
+
325
+ if len(found) > 1:
326
+ found = self._get_keyword_based_on_search_order(found)
327
+
328
+ else:
329
+ if len(found) > 1:
330
+ found = self._get_keyword_based_on_search_order(found)
331
+
332
+ if len(found) == 1:
333
+ return found[0][1]
334
+
335
+ self.diagnostics.append(
336
+ DiagnosticsEntry(
337
+ self._create_multiple_keywords_found_message(name, found),
338
+ DiagnosticSeverity.ERROR,
339
+ Error.MULTIPLE_KEYWORDS,
340
+ )
341
+ )
342
+ raise CancelSearchError
343
+
344
+ def _get_keyword_based_on_search_order(
345
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
346
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
347
+ for libname in self.namespace.search_order:
348
+ for e in entries:
349
+ if e[0] is not None and eq_namespace(libname, e[0].alias or e[0].name):
350
+ return [e]
351
+
352
+ return entries
353
+
354
+ def _get_keyword_from_libraries(self, name: str) -> Optional[KeywordDoc]:
355
+ if self._library_keywords is None:
356
+ self._library_keywords = list(chain(self.namespace._libraries.values()))
357
+
358
+ if get_robot_version() >= (6, 0):
359
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
360
+ for v in self._library_keywords:
361
+ r = v.library_doc.keywords.get_all(name)
362
+ if r:
363
+ found.extend([(v, k) for k in r])
364
+ else:
365
+ found = []
366
+
367
+ for k in self._library_keywords:
368
+ s = k.library_doc.keywords.get(name, None)
369
+ if s is not None:
370
+ found.append((k, s))
371
+
372
+ if not found:
373
+ return None
374
+
375
+ if get_robot_version() >= (6, 0):
376
+ if len(found) > 1:
377
+ found = self._select_best_matches(found)
378
+ if len(found) > 1:
379
+ found = self._get_keyword_based_on_search_order(found)
380
+ else:
381
+ if len(found) > 1:
382
+ found = self._get_keyword_based_on_search_order(found)
383
+ if len(found) == 2:
384
+ found = self._filter_stdlib_runner(*found)
385
+
386
+ if len(found) == 1:
387
+ return found[0][1]
388
+
389
+ self.diagnostics.append(
390
+ DiagnosticsEntry(
391
+ self._create_multiple_keywords_found_message(name, found),
392
+ DiagnosticSeverity.ERROR,
393
+ Error.MULTIPLE_KEYWORDS,
394
+ )
395
+ )
396
+ raise CancelSearchError
397
+
398
+ def _filter_stdlib_runner(
399
+ self,
400
+ entry1: Tuple[Optional[LibraryEntry], KeywordDoc],
401
+ entry2: Tuple[Optional[LibraryEntry], KeywordDoc],
402
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
403
+ stdlibs_without_remote = STDLIBS - {"Remote"}
404
+ if entry1[0] is not None and entry1[0].name in stdlibs_without_remote:
405
+ standard, custom = entry1, entry2
406
+ elif entry2[0] is not None and entry2[0].name in stdlibs_without_remote:
407
+ standard, custom = entry2, entry1
408
+ else:
409
+ return [entry1, entry2]
410
+
411
+ self.diagnostics.append(
412
+ DiagnosticsEntry(
413
+ self._create_custom_and_standard_keyword_conflict_warning_message(custom, standard),
414
+ DiagnosticSeverity.WARNING,
415
+ Error.CONFLICTING_LIBRARY_KEYWORDS,
416
+ )
417
+ )
418
+
419
+ return [custom]
420
+
421
+ def _create_custom_and_standard_keyword_conflict_warning_message(
422
+ self,
423
+ custom: Tuple[Optional[LibraryEntry], KeywordDoc],
424
+ standard: Tuple[Optional[LibraryEntry], KeywordDoc],
425
+ ) -> str:
426
+ custom_with_name = standard_with_name = ""
427
+ if custom[0] is not None and custom[0].alias is not None:
428
+ custom_with_name = " imported as '%s'" % custom[0].alias
429
+ if standard[0] is not None and standard[0].alias is not None:
430
+ standard_with_name = " imported as '%s'" % standard[0].alias
431
+ return (
432
+ f"Keyword '{standard[1].name}' found both from a custom test library "
433
+ f"'{'' if custom[0] is None else custom[0].name}'{custom_with_name} "
434
+ f"and a standard library '{standard[1].name}'{standard_with_name}. "
435
+ f"The custom keyword is used. To select explicitly, and to get "
436
+ f"rid of this warning, use either "
437
+ f"'{'' if custom[0] is None else custom[0].alias or custom[0].name}.{custom[1].name}' "
438
+ f"or '{'' if standard[0] is None else standard[0].alias or standard[0].name}.{standard[1].name}'."
439
+ )
440
+
441
+ def _get_bdd_style_keyword(self, name: str) -> Optional[KeywordDoc]:
442
+ if get_robot_version() < (6, 0):
443
+ lower = name.lower()
444
+ for prefix in ["given ", "when ", "then ", "and ", "but "]:
445
+ if lower.startswith(prefix):
446
+ return self._find_keyword(name[len(prefix) :])
447
+ return None
448
+
449
+ parts = name.split()
450
+ if len(parts) < 2:
451
+ return None
452
+ for index in range(1, len(parts)):
453
+ prefix = " ".join(parts[:index]).title()
454
+ if prefix.title() in (
455
+ self.namespace.languages.bdd_prefixes if self.namespace.languages is not None else DEFAULT_BDD_PREFIXES
456
+ ):
457
+ return self._find_keyword(" ".join(parts[index:]))
458
+ return None
@@ -74,6 +74,7 @@ from robotcode.robot.diagnostics.entities import (
74
74
  )
75
75
  from robotcode.robot.utils import get_robot_version
76
76
  from robotcode.robot.utils.ast import (
77
+ cached_isinstance,
77
78
  get_variable_token,
78
79
  range_from_token,
79
80
  strip_variable_token,
@@ -247,7 +248,7 @@ class KeywordMatcher:
247
248
  return self._embedded_arguments
248
249
 
249
250
  def __eq__(self, o: object) -> bool:
250
- if isinstance(o, KeywordMatcher):
251
+ if cached_isinstance(o, KeywordMatcher):
251
252
  if self._is_namespace != o._is_namespace:
252
253
  return False
253
254
 
@@ -256,7 +257,7 @@ class KeywordMatcher:
256
257
 
257
258
  o = o.name
258
259
 
259
- if not isinstance(o, str):
260
+ if not cached_isinstance(o, str):
260
261
  return False
261
262
 
262
263
  if self.embedded_arguments:
@@ -907,7 +908,8 @@ class KeywordStore:
907
908
  return self.__matchers
908
909
 
909
910
  def __getitem__(self, key: str) -> KeywordDoc:
910
- items = [(k, v) for k, v in self._matchers.items() if k == key]
911
+ key_matcher = KeywordMatcher(key)
912
+ items = [(k, v) for k, v in self._matchers.items() if k == key_matcher]
911
913
 
912
914
  if not items:
913
915
  raise KeyError
@@ -933,8 +935,10 @@ class KeywordStore:
933
935
  multiple_keywords=[v for _, v in items],
934
936
  )
935
937
 
936
- def __contains__(self, __x: object) -> bool:
937
- return any(k == __x for k in self._matchers.keys())
938
+ def __contains__(self, _x: object) -> bool:
939
+ if not isinstance(_x, KeywordMatcher):
940
+ _x = KeywordMatcher(str(_x))
941
+ return any(k == _x for k in self._matchers.keys())
938
942
 
939
943
  def __len__(self) -> int:
940
944
  return len(self.keywords)
@@ -961,7 +965,11 @@ class KeywordStore:
961
965
  return default
962
966
 
963
967
  def get_all(self, key: str) -> List[KeywordDoc]:
964
- return [v for k, v in self._matchers.items() if k == key]
968
+ return list(self.iter_all(key))
969
+
970
+ def iter_all(self, key: str) -> Iterable[KeywordDoc]:
971
+ key_matcher = KeywordMatcher(key)
972
+ yield from (v for k, v in self._matchers.items() if k == key_matcher)
965
973
 
966
974
 
967
975
  @dataclass
@@ -1540,7 +1548,6 @@ def resolve_robot_variables(
1540
1548
  command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1541
1549
  variables: Optional[Dict[str, Optional[Any]]] = None,
1542
1550
  ) -> Any:
1543
-
1544
1551
  result: Variables = _get_default_variables().copy()
1545
1552
 
1546
1553
  for k, v in {
@@ -1554,14 +1561,16 @@ def resolve_robot_variables(
1554
1561
  result[k1] = v1
1555
1562
 
1556
1563
  if variables is not None:
1557
- vars = [_Variable(k, v) for k, v in variables.items() if v is not None and not isinstance(v, NativeValue)]
1564
+ vars = [
1565
+ _Variable(k, v) for k, v in variables.items() if v is not None and not cached_isinstance(v, NativeValue)
1566
+ ]
1558
1567
  if get_robot_version() < (7, 0):
1559
1568
  result.set_from_variable_table(vars)
1560
1569
  else:
1561
1570
  result.set_from_variable_section(vars)
1562
1571
 
1563
1572
  for k2, v2 in variables.items():
1564
- if isinstance(v2, NativeValue):
1573
+ if cached_isinstance(v2, NativeValue):
1565
1574
  result[k2] = v2.value
1566
1575
 
1567
1576
  result.resolve_delayed()
@@ -1576,7 +1585,6 @@ def resolve_variable(
1576
1585
  command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1577
1586
  variables: Optional[Dict[str, Optional[Any]]] = None,
1578
1587
  ) -> Any:
1579
-
1580
1588
  _update_env(working_dir)
1581
1589
 
1582
1590
  if contains_variable(name, "$@&%"):
@@ -2645,8 +2653,6 @@ def complete_variables_import(
2645
2653
  def get_model_doc(
2646
2654
  model: ast.AST,
2647
2655
  source: str,
2648
- model_type: str = "RESOURCE",
2649
- scope: str = "GLOBAL",
2650
2656
  append_model_errors: bool = True,
2651
2657
  ) -> LibraryDoc:
2652
2658
  errors: List[Error] = []
@@ -2777,8 +2783,8 @@ def get_model_doc(
2777
2783
  libdoc = LibraryDoc(
2778
2784
  name=lib.name or "",
2779
2785
  doc=lib.doc,
2780
- type=model_type,
2781
- scope=scope,
2786
+ type="RESOURCE",
2787
+ scope="GLOBAL",
2782
2788
  source=source,
2783
2789
  line_no=1,
2784
2790
  errors=errors,